Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.iml
*.log
.gradle
/local.properties
/.idea/caches
Expand Down
26 changes: 23 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,42 @@ android {
}
buildFeatures {
viewBinding true
buildConfig true
}
buildscript {
dependencies {
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
}
}
}

secrets {
// To add your Maps API key to this project:
// 1. If the secrets.properties file does not exist, create it in the same folder as the local.properties file.
// 2. Add this line, where YOUR_API_KEY is your API key:
// MAPS_API_KEY=YOUR_API_KEY
propertiesFileName = "secrets.properties"

// A properties file containing default secret values. This file can be
// checked in version control.
defaultPropertiesFileName = "local.properties"
}

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 'com.google.android.gms:play-services-maps:19.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
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 'com.google.android.gms:play-services-location:21.3.0'
implementation 'androidx.exifinterface:exifinterface:1.3.7'
implementation 'androidx.exifinterface:exifinterface:1.4.0'
implementation 'com.github.bumptech.glide:glide:4.15.1' // или актуальная версия
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
Expand Down
26 changes: 22 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-feature
android:name="android.hardware.camera"
android:required="true" />

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
tools:ignore="SelectedPhotoAccess" />

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" android:maxSdkVersion="32" />

<uses-permission android:name="android.permission.CAMERA" />


<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -11,10 +30,9 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.OTUSLocationMapsHW"
tools:targetApi="31">
tools:targetApi="31"
>
<!--
TODO: Before you run your application, you need a Google Maps API key.

To get one, follow the directions here:

https://developers.google.com/maps/documentation/android-sdk/get-api-key
Expand All @@ -25,7 +43,7 @@
-->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY" />
android:value="${MAPS_API_KEY}" />

<activity
android:name=".MapsActivity"
Expand Down
158 changes: 148 additions & 10 deletions app/src/main/java/com/sample/otuslocationmapshw/MapsActivity.kt
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
package com.sample.otuslocationmapshw

import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback
import androidx.core.content.ContextCompat
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.Glide
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.sample.otuslocationmapshw.camera.CameraActivity
import com.sample.otuslocationmapshw.data.utils.LocationDataUtils
import com.sample.otuslocationmapshw.databinding.ActivityMapsBinding
import java.io.File

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private const val REQUEST_CODE_PERMISSIONS = 1001

class MapsActivity : AppCompatActivity(), OnMapReadyCallback, OnRequestPermissionsResultCallback {

private lateinit var map: GoogleMap
private lateinit var binding: ActivityMapsBinding
Expand All @@ -31,7 +42,8 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == CameraActivity.SUCCESS_RESULT_CODE) {
// TODO("Обновить точки на карте при получении результата от камеры")
// Обновить точки на карте при получении результата от камеры
showPreviewsOnMap()
}
}

Expand All @@ -41,9 +53,51 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
binding = ActivityMapsBinding.inflate(layoutInflater)
setContentView(binding.root)

// Проверяем разрешения
checkAndRequestPermissions()

val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
// TODO("Вызвать инициализацию карты")
// Вызвать инициализацию карты
mapFragment.getMapAsync(this)
}

// Запрашиваем разрешения
private fun checkAndRequestPermissions() {
val permissions = mutableListOf<String>()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+
permissions.add(android.Manifest.permission.READ_MEDIA_IMAGES)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13
permissions.add(android.Manifest.permission.READ_MEDIA_IMAGES)
} else {
permissions.add(android.Manifest.permission.READ_EXTERNAL_STORAGE)
}

if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.ACCESS_FINE_LOCATION)
}

if (permissions.isNotEmpty()) {
ActivityCompat.requestPermissions(this, permissions.toTypedArray(), REQUEST_CODE_PERMISSIONS)
}
}

// Обрабатываем результат запроса
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
val deniedPermissions = permissions.zip(grantResults.toList()).filter { it.second != PackageManager.PERMISSION_GRANTED }

if (deniedPermissions.isNotEmpty()) {
Toast.makeText(this,
getString(R.string.permissions_must_be_provided), Toast.LENGTH_LONG).show()
} else {
Toast.makeText(this,
getString(R.string.permissions_have_been_received), Toast.LENGTH_SHORT).show()
showPreviewsOnMap()
}
}
}

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
Expand All @@ -66,29 +120,113 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap

// Включаем кнопки масштабирования
map.uiSettings.isZoomControlsEnabled = true

// Включаем жесты масштабирования (pinch-to-zoom)
map.uiSettings.isZoomGesturesEnabled = true

// Включаем кнопки компаса и вращение карты
map.uiSettings.isCompassEnabled = true
map.uiSettings.isRotateGesturesEnabled = true
map.uiSettings.isMapToolbarEnabled = true

if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
map.isMyLocationEnabled = true // Включаем кнопку "Моё местоположение"
}

showPreviewsOnMap()
}

private fun showPreviewsOnMap() {
map.clear()

if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this,
getString(R.string.there_is_no_permission_to_read_images), Toast.LENGTH_SHORT).show()
return
}

val folder = File("${filesDir.absolutePath}/photos/")
folder.listFiles()?.forEach {
val exifInterface = ExifInterface(it)
var lastLocation: LatLng? = null // Для хранения местоположения первого фото

// Получаем список файлов и сортируем их по дате изменения (новейшие в конце)
val files = folder.listFiles()
?.sortedBy { it.lastModified() } // Сортируем по последней дате изменения


files?.forEach { file ->
val exifInterface = ExifInterface(file)
val location = locationDataUtils.getLocationFromExif(exifInterface)
val point = LatLng(location.latitude, location.longitude)

val density = resources.displayMetrics.density
val markerSize = (64 * density).toInt() // 64 dp в пикселях

// Создаём Bitmap для маркера
val pinBitmap = Bitmap.createScaledBitmap(
BitmapFactory.decodeFile(
it.path,
file.path,
BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.ARGB_8888
}), 64, 64, false
}), markerSize, markerSize, false
)
// TODO("Указать pinBitmap как иконку для маркера")
map.addMarker(

// Создаём BitmapDescriptor
val bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(pinBitmap)

// Добавляем маркер с иконкой
val marker = map.addMarker(
MarkerOptions()
.position(point)
.icon(bitmapDescriptor) // Используем превью фотографии как иконку
)
// TODO("Передвинуть карту к местоположению последнего фото")

marker?.tag = file.absolutePath

// Запоминаем местоположение первого файла
lastLocation = point
}

// Устанавливаем обработчик кликов на маркер
map.setOnMarkerClickListener { marker ->
val photoPath = marker.tag as? String
photoPath?.let { showPhotoDialog(it) }
true
}

// Если есть хотя бы одно фото, перемещаем камеру к первому фото
lastLocation?.let { location ->
// Применяем moveCamera для перемещения камеры
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(location, 15f)
//map.moveCamera(cameraUpdate)

// Или используйте animateCamera для анимации камеры
map.animateCamera(cameraUpdate)
}
}

private fun showPhotoDialog(photoPath: String) {

// Используем MaterialAlertDialogBuilder для создания диалога
val dialogView = layoutInflater.inflate(R.layout.dialog_photo, null)
val imageView = dialogView.findViewById<ImageView>(R.id.dialogImageView)

val width = imageView.width* 2
Glide.with(this)
.load(photoPath)
.override(width) // Устанавливаем ширину изображения, равную ширине контейнера
.fitCenter() // Масштабируем изображение по ширине контейнера
.into(imageView)

// Создаём MDC диалог
val dialog = MaterialAlertDialogBuilder(this)
.setView(dialogView) // Устанавливаем наш кастомный layout
.setCancelable(true) // Позволяет закрыть диалог
.create()

dialog.show()
}


}
Loading