diff --git a/android/app/build.gradle b/android/app/build.gradle
index 417937c94b..b8f3effd72 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -55,6 +55,12 @@ apply plugin: 'com.github.triplet.play'
apply plugin: 'ru.cian.huawei-publish-gradle-plugin'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'kotlin-parcelize'
+apply plugin: 'kotlin-kapt'
+apply plugin: 'com.google.dagger.hilt.android'
+
+kapt {
+ correctErrorTypes true
+}
def run(cmd) {
def stdout = new ByteArrayOutputStream()
@@ -417,6 +423,11 @@ dependencies {
implementation 'androidx.work:work-runtime:2.9.0'
implementation 'androidx.lifecycle:lifecycle-process:2.8.0'
implementation 'com.google.android.material:material:1.12.0'
+
+ // hilt
+ implementation "com.google.dagger:hilt-android:2.47"
+ kapt "com.google.dagger:hilt-compiler:2.47"
+
// Fix for app/organicmaps/util/FileUploadWorker.java:14: error: cannot access ListenableFuture
// https://github.com/organicmaps/organicmaps/issues/6106
implementation 'com.google.guava:guava:33.1.0-android'
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a8578da807..61cc18076d 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -446,6 +446,10 @@
android:foregroundServiceType="location"
android:stopWithTask="false" />
+
+
= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ DownloaderService.DOWNLOADING_CHANNEL,
+ "Download",
+ NotificationManager.IMPORTANCE_LOW
+ );
+ NotificationManager notificationManager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+
/**
* Initialize native core of application: platform and framework.
*
diff --git a/android/app/src/main/java/app/tourism/DownloaderService.kt b/android/app/src/main/java/app/tourism/DownloaderService.kt
new file mode 100644
index 0000000000..82aba967d1
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/DownloaderService.kt
@@ -0,0 +1,66 @@
+package app.tourism
+
+import android.app.Notification
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import app.tourism.di.InProgressNotificationCompatBuilder
+import app.tourism.di.NotInProgressNotificationCompatBuilder
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class DownloaderService : Service() {
+ @Inject
+ lateinit var st: String
+
+ @Inject
+ lateinit var notificationManager: NotificationManagerCompat
+
+ @Inject
+ @InProgressNotificationCompatBuilder
+ lateinit var inProgressNotificationBuilder: NotificationCompat.Builder
+
+ @Inject
+ @NotInProgressNotificationCompatBuilder
+ lateinit var notInProgressNotificationBuilder: NotificationCompat.Builder
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ when (intent?.action) {
+ Actions.START.toString() -> start()
+ Actions.STOP.toString() -> stopSelf()
+ }
+
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ private fun start() {
+ notificationManager.notificationChannels
+ val mapDownloader = MapDownloader(
+ this,
+ notificationManager,
+ inProgressNotificationBuilder,
+ notInProgressNotificationBuilder,
+ )
+ mapDownloader.downloadTjkMap()
+ val notification = inProgressNotificationBuilder.build()
+ notification.flags = Notification.FLAG_ONGOING_EVENT
+ startForeground(NOTIFICATION_ID, notification)
+ }
+
+ enum class Actions {
+ START, STOP
+ }
+
+ companion object {
+ const val NOTIFICATION_ID = 1
+ const val DOWNLOADING_CHANNEL = "Downloading channel"
+ const val NOTIFICATION_CHANNEL = "Notification channel"
+ }
+}
diff --git a/android/app/src/main/java/app/tourism/MainActivity.kt b/android/app/src/main/java/app/tourism/MainActivity.kt
index ee48163d52..d21ef006e5 100644
--- a/android/app/src/main/java/app/tourism/MainActivity.kt
+++ b/android/app/src/main/java/app/tourism/MainActivity.kt
@@ -1,6 +1,8 @@
package app.tourism
+import android.Manifest
import android.content.Intent
+import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -13,58 +15,89 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat.startActivity
import app.organicmaps.DownloadResourcesLegacyActivity
+import app.organicmaps.downloader.CountryItem
import app.tourism.data.dto.SiteLocation
import app.tourism.ui.theme.OrganicMapsTheme
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
-
+@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ 0
+ )
+ }
+
enableEdgeToEdge()
setContent {
OrganicMapsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
- )
+ Column(Modifier.padding(innerPadding)) {
+ TestingStuffs()
+ }
}
}
}
}
-}
-@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- val context = LocalContext.current;
- Column {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
+ @Composable
+ fun TestingStuffs() {
+ Column {
+ TestingNavigationToMap()
+ TestingNotifs()
+ }
+ }
+
+ @Composable
+ fun TestingNavigationToMap() {
Button(
onClick = {
- val intent = Intent(context, DownloadResourcesLegacyActivity::class.java)
+ val intent = Intent(this, DownloadResourcesLegacyActivity::class.java)
intent.putExtra(
"end_point",
SiteLocation("Name", 38.573, 68.807)
)
- startActivity(context, intent, null)
+ startActivity(this, intent, null)
},
) {
- Text(text = "navigate to Map", modifier = modifier)
+ Text(text = "navigate to Map")
+ }
+ }
+
+ @Composable
+ fun TestingNotifs() {
+ Button(
+ onClick = {
+ Intent(applicationContext, DownloaderService::class.java).also {
+ val mCurrentCountry = CountryItem.fill("Tajikistan")
+ if (!mCurrentCountry.present) {
+ it.action = DownloaderService.Actions.START.toString()
+ startService(it)
+ }
+ }
+ },
+ ) {
+ Text(text = "Start download")
+ }
+ Button(
+ onClick = {
+ Intent(applicationContext, DownloaderService::class.java).also {
+ it.action = DownloaderService.Actions.STOP.toString()
+ startService(it)
+ }
+ },
+ ) {
+ Text(text = "Stop download")
}
}
}
-
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- OrganicMapsTheme {
- Greeting("Android")
- }
-}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/MapDownloader.kt b/android/app/src/main/java/app/tourism/MapDownloader.kt
new file mode 100644
index 0000000000..3c41192471
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/MapDownloader.kt
@@ -0,0 +1,165 @@
+package app.tourism
+
+import android.Manifest.permission
+import android.content.Context
+import android.content.pm.PackageManager
+import android.text.TextUtils
+import android.widget.Toast
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import app.organicmaps.R
+import app.organicmaps.downloader.CountryItem
+import app.organicmaps.downloader.MapManager
+import app.organicmaps.location.LocationHelper
+import app.organicmaps.util.Config
+import app.organicmaps.util.ConnectionState
+import app.organicmaps.util.log.Logger
+import kotlin.math.roundToInt
+
+class MapDownloader(
+ private val context: Context,
+ private val notificationManager: NotificationManagerCompat,
+ private val inProgressNotificationBuilder: NotificationCompat.Builder,
+ private val notInProgressNotificationBuilder: NotificationCompat.Builder,
+) {
+ private var mStorageSubscriptionSlot = 0
+ private var progress = 0
+ private var mCurrentCountry = CountryItem.fill("Tajikistan")
+
+ private val mStorageCallback: MapManager.StorageCallback = object :
+ MapManager.StorageCallback {
+ override fun onStatusChanged(data: List) {
+ for (item in data) {
+ if (!item.isLeafNode) continue
+
+ if (item.newStatus == CountryItem.STATUS_FAILED)
+ Toast.makeText(context, "failure", Toast.LENGTH_SHORT).show();
+
+ if (mCurrentCountry.id == item.countryId) {
+ updateProgressState()
+ return
+ }
+ }
+ }
+
+ override fun onProgress(countryId: String, localSize: Long, remoteSize: Long) {
+ if (mCurrentCountry.id == countryId) {
+ updateProgressState()
+ }
+ }
+ }
+
+ private fun updateProgressState() {
+ updateStateInternal()
+ }
+
+ private fun updateStateInternal() {
+ val inProgress = (mCurrentCountry.status == CountryItem.STATUS_PROGRESS ||
+ mCurrentCountry.status == CountryItem.STATUS_APPLYING)
+ val failed = (mCurrentCountry.status == CountryItem.STATUS_FAILED)
+ val success = (mCurrentCountry.status == CountryItem.STATUS_DONE)
+
+ if (success) {
+ notInProgressNotificationBuilder.setContentTitle(context.getString(R.string.map_downloaded))
+ if (ActivityCompat.checkSelfPermission(
+ context,
+ permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ notificationManager.notify(
+ DownloaderService.NOTIFICATION_ID,
+ notInProgressNotificationBuilder.build()
+ )
+ }
+ }
+
+ if (failed) {
+ notInProgressNotificationBuilder.setContentTitle(context.getString(R.string.map_download_error))
+ if (ActivityCompat.checkSelfPermission(
+ context,
+ permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ notificationManager.notify(
+ DownloaderService.NOTIFICATION_ID,
+ notInProgressNotificationBuilder.build()
+ )
+ }
+ }
+
+ if (inProgress) {
+ val progress = mCurrentCountry.progress.roundToInt()
+ Logger.d("progress", progress.toString())
+ inProgressNotificationBuilder.setProgress(100, progress, false)
+ if (
+ ActivityCompat.checkSelfPermission(context, permission.POST_NOTIFICATIONS)
+ == PackageManager.PERMISSION_GRANTED
+ ) {
+ notificationManager.notify(
+ DownloaderService.NOTIFICATION_ID,
+ inProgressNotificationBuilder.build()
+ )
+ }
+ } else {
+ if (Config.isAutodownloadEnabled() &&
+ !sAutodownloadLocked &&
+ !failed &&
+ ConnectionState.INSTANCE.isWifiConnected
+ ) {
+ val loc = LocationHelper.from(context).savedLocation
+ if (loc != null) {
+ val country =
+ MapManager.nativeFindCountry(loc.latitude, loc.longitude)
+ if (TextUtils.equals(mCurrentCountry.id, country) &&
+ MapManager.nativeHasSpaceToDownloadCountry(country)
+ ) {
+ MapManager.nativeDownload(mCurrentCountry.id)
+ }
+ }
+ }
+ }
+ }
+
+ public fun downloadTjkMap() {
+ if (mCurrentCountry.present) return
+
+ download()
+ }
+
+ public fun download() {
+ onResume()
+ val failed = mCurrentCountry.status == CountryItem.STATUS_FAILED
+ if (failed) {
+ MapManager.nativeRetry(mCurrentCountry.id)
+ } else {
+ MapManager.nativeDownload(mCurrentCountry.id)
+ }
+ }
+
+ public fun cancel() {
+ MapManager.nativeCancel(mCurrentCountry.id)
+ setAutodownloadLocked(true)
+ }
+
+ fun onPause() {
+ if (mStorageSubscriptionSlot > 0) {
+ MapManager.nativeUnsubscribe(mStorageSubscriptionSlot)
+ mStorageSubscriptionSlot = 0
+ }
+ }
+
+ fun onResume() {
+ if (mStorageSubscriptionSlot == 0) {
+ mStorageSubscriptionSlot = MapManager.nativeSubscribe(mStorageCallback)
+ }
+ }
+
+ companion object {
+ private var sAutodownloadLocked = false
+
+ fun setAutodownloadLocked(locked: Boolean) {
+ sAutodownloadLocked = locked
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/di/NotificationsModule.kt b/android/app/src/main/java/app/tourism/di/NotificationsModule.kt
new file mode 100644
index 0000000000..01070f2413
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/di/NotificationsModule.kt
@@ -0,0 +1,80 @@
+package app.tourism.di
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.VISIBILITY_PRIVATE
+import androidx.core.app.NotificationManagerCompat
+import app.organicmaps.R
+import app.tourism.DownloaderService
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Qualifier
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NotificationModule {
+
+ @Provides
+ @NotInProgressNotificationCompatBuilder
+ fun provideNotInProgressNotificationBuilder(
+ @ApplicationContext context: Context
+ ): NotificationCompat.Builder {
+ return NotificationCompat.Builder(context, DownloaderService.NOTIFICATION_CHANNEL)
+ .setContentTitle("Welcome")
+ .setSmallIcon(R.mipmap.ic_launcher_round)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setVisibility(VISIBILITY_PRIVATE)
+ }
+
+ @Provides
+ @InProgressNotificationCompatBuilder
+ fun provideInProgressNotificationBuilder(
+ @ApplicationContext context: Context
+ ): NotificationCompat.Builder {
+ return NotificationCompat.Builder(context, DownloaderService.DOWNLOADING_CHANNEL)
+ .setOngoing(true)
+ .setContentTitle(context.getString(R.string.map_downloading))
+ .setSmallIcon(R.mipmap.ic_launcher_round)
+ .setSilent(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ }
+
+ @Provides
+ fun provideNotificationManager(
+ @ApplicationContext context: Context
+ ): NotificationManagerCompat {
+ val notificationManager = NotificationManagerCompat.from(context)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ DownloaderService.NOTIFICATION_CHANNEL,
+ DownloaderService.NOTIFICATION_CHANNEL,
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ val channel2 = NotificationChannel(
+ DownloaderService.DOWNLOADING_CHANNEL,
+ DownloaderService.DOWNLOADING_CHANNEL,
+ NotificationManager.IMPORTANCE_LOW
+ )
+ notificationManager.createNotificationChannel(channel)
+ notificationManager.createNotificationChannel(channel2)
+ }
+ return notificationManager
+ }
+
+ @Provides
+ fun provideString(): String = "sdklfjsdalkfj"
+}
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class NotInProgressNotificationCompatBuilder
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class InProgressNotificationCompatBuilder
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/map_downloader.xml b/android/app/src/main/res/layout/map_downloader.xml
new file mode 100644
index 0000000000..ab494d5805
--- /dev/null
+++ b/android/app/src/main/res/layout/map_downloader.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml
index f2484a4e40..d6acf431c1 100644
--- a/android/app/src/main/res/values-ru/strings.xml
+++ b/android/app/src/main/res/values-ru/strings.xml
@@ -2160,5 +2160,8 @@
Место проведения мероприятий
Аукцион
Коллекции
- Комитет по туризму при Правительстве Республики Таджикистан
+ Комитет по туризму при Правительстве Республики Таджикистан
+ Карта Таджикистана скачивается
+ Карта Таджикистана была успешно скачана
+ Не удалось скачать карту
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index f6f6edb4f4..d5da3cba93 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -2203,4 +2203,7 @@
//todo
Комитет по туризму при Правительстве Республики Таджикистан
MainActivity
+ Map of Tajikistan is being downloaded
+ Tajikistan map was successfully downloaded
+ Failed to download map
diff --git a/android/build.gradle b/android/build.gradle
index d0b021fd56..4445cd5dec 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -3,4 +3,5 @@ plugins {
id 'com.android.application' version '8.4.1' apply false
id 'com.android.library' version '8.4.1' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
+ id 'com.google.dagger.hilt.android' version '2.47' apply false
}
diff --git a/android/gradle.properties b/android/gradle.properties
index 434fae6668..a3f991ccc6 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -10,14 +10,14 @@
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
-#Thu Jun 06 15:38:47 TJT 2024
+#Mon Jun 10 23:17:53 TJT 2024
android.native.buildOutput=verbose
android.nonFinalResIds=false
android.nonTransitiveRClass=true
android.useAndroidX=true
enableVulkanDiagnostics=OFF
org.gradle.caching=true
-org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1024M" -Xms256m
+org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx1536M" -Xms256m
propCompileSdkVersion=34
propMinSdkVersion=21
propTargetSdkVersion=34