Compare commits
88 commits
master
...
github/for
Author | SHA1 | Date | |
---|---|---|---|
|
039b71798e | ||
|
77f098acb6 | ||
|
a7d7d80a8a | ||
|
7aa4f4dc03 | ||
|
c92a138130 | ||
|
f9a6bc5c00 | ||
|
e0d98091ed | ||
|
3d9ebab8c2 | ||
|
f25019b8f8 | ||
|
f592cb23dd | ||
|
b34607bd04 | ||
|
a3d2a22f5b | ||
|
451d628bfd | ||
|
661807cde8 | ||
|
fa4e191fab | ||
|
ca2167658b | ||
|
2183b7a53c | ||
|
36f60c9d64 | ||
|
dae8c92ea2 | ||
|
76e66c31ec | ||
|
2cc11db7b2 | ||
|
549ca85c43 | ||
|
eb14807513 | ||
|
64ed84bfa9 | ||
|
fef69f5eec | ||
|
fb493aceee | ||
|
ded2bfe91d | ||
|
04418bc015 | ||
|
babef10504 | ||
|
e32a9d2665 | ||
|
7950027ddf | ||
|
97e162707b | ||
|
93c0970275 | ||
|
de69678a96 | ||
|
fff086dcf4 | ||
|
1077efd56c | ||
|
2d9820c745 | ||
|
beb457603a | ||
|
c59c102f62 | ||
|
90a6f01752 | ||
|
5ae27cbccb | ||
|
52bf2acb91 | ||
|
bcf18422b5 | ||
|
d728b8c7d6 | ||
|
d1bf71528e | ||
|
c5e3417af0 | ||
|
7166577cd1 | ||
|
7720b166c6 | ||
|
6ea723b0f5 | ||
|
91c544bfeb | ||
|
6be8ce1933 | ||
|
7d4e760778 | ||
|
a85dcd0efc | ||
|
0b5a4bf665 | ||
|
e62d6ef2d0 | ||
|
2d9a1ee57f | ||
|
0d0b43ae7e | ||
|
d30678a9b4 | ||
|
207bc0cd02 | ||
|
09d92aec73 | ||
|
d0a5911fd1 | ||
|
63e3de24a9 | ||
|
0b99a3572b | ||
|
7cb1b6e51f | ||
|
dc9c67480c | ||
|
dcb0d40816 | ||
|
e78bbe397e | ||
|
dc0523cbb3 | ||
|
104f02b987 | ||
|
2400c21819 | ||
|
bba8edbf48 | ||
|
b7eeeb2ed7 | ||
|
1d6e96e1fe | ||
|
ab8677439f | ||
|
ab44d68eac | ||
|
6f0c27d73a | ||
|
45a8da004b | ||
|
15b3613363 | ||
|
c3c3736f07 | ||
|
58b81266e8 | ||
|
8917001a27 | ||
|
184cd72db2 | ||
|
dce2313723 | ||
|
ba0f2bb041 | ||
|
7326924a23 | ||
|
cc9e6c9dc5 | ||
|
3a6166bae9 | ||
|
13719ee316 |
|
@ -1 +1 @@
|
|||
Subproject commit 9cbdb916de2a7bd1aa649e55efc38d2426680359
|
||||
Subproject commit 74d91febb0995b7c6706dfd4eed2d39fb1694421
|
|
@ -1 +1 @@
|
|||
Subproject commit 680f521746a3bd6a86f25f25ee50a62d88b489cf
|
||||
Subproject commit 6af11aa609f3fdf735cab5fdc051cd840960186b
|
|
@ -1 +0,0 @@
|
|||
See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md)
|
102
CONTRIBUTORS
|
@ -1,102 +0,0 @@
|
|||
This file contains a list of people who have contributed to the
|
||||
public version of MAPS.ME and Organic Maps.
|
||||
|
||||
Original MAPS.ME (MapsWithMe) design and implementation:
|
||||
Yury Melnichek <yury@melnichek.com>
|
||||
Alexander Borsuk <me@alex.bio>
|
||||
Viktor Govako <viktor.govako@gmail.com>
|
||||
Siarhei Rachytski <siarhei.rachytski@gmail.com>
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Organic Maps contributions:
|
||||
--------------------------------------------------------------------------------
|
||||
Alexander Borsuk <me@alex.bio>
|
||||
Roman Tsisyk <roman@tsisyk.com>
|
||||
Viktor Govako <viktor.govako@gmail.com>
|
||||
Caspar Nuël <casparnuel@yandex.com>
|
||||
Konstantin Pastbin
|
||||
Nishant Bhandari <nishantbhandari0019@gmail.com>
|
||||
Sebastiao Sousa <sebastiao.sousa@tecnico.ulisboa.pt>
|
||||
Harry Bond <me@hbond.xyz>
|
||||
|
||||
Organic Maps translations:
|
||||
Karina Kordon
|
||||
Konstantin Pastbin
|
||||
Metehan Özyürek
|
||||
Joan Montané
|
||||
Luna Rose
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
MAPS.ME contributions (before Organic Maps was forked in 2020-2021):
|
||||
--------------------------------------------------------------------------------
|
||||
Code contributions:
|
||||
Dmitry Yunitski
|
||||
Lev Dragunov
|
||||
Sergey Yershov <syershov@gmail.com>
|
||||
Vladimir Byko-Ianko <bykoianko@gmail.com>
|
||||
Yuri Gorshenin
|
||||
Maxim Pimenov <m@maps.me>
|
||||
Roman Kuznetsov
|
||||
Konstantin Shalnev <k.shalnev@gmail.com>
|
||||
Ilja Zverev <zverik@textual.ru>
|
||||
Vlad Mihaylenko
|
||||
Ilya Grechuhin
|
||||
Alexander Marchuk
|
||||
Sergey Magidovich
|
||||
Yury Rakhuba
|
||||
Kirill Zhdanovich
|
||||
Dmitry Kunin
|
||||
Denis Koronchik
|
||||
Darafei Praliaskouski <me@komzpa.net>
|
||||
Igor Khmurets
|
||||
Timur Bernikowich
|
||||
Roman Sorokin
|
||||
Alexander Gusak
|
||||
Alexei Vitenko
|
||||
Artem Polkovnikov <artyom.polkovnikov@gmail.com>
|
||||
Alex Gontmakher <gsasha@gmail.com>
|
||||
Dima Korolev <dmitry.korolev@gmail.com>
|
||||
Max Grigorev <forwidur@gmail.com>
|
||||
|
||||
Porting to Tizen platform:
|
||||
Sergey Pisarchik
|
||||
|
||||
Testing and automation:
|
||||
Timofey Danshin
|
||||
|
||||
Design and styles:
|
||||
Igor Tomko <igor.n.tomko@gmail.com>
|
||||
Urbica http://urbica.co
|
||||
Vasiliy Cherkasov
|
||||
Maksim Okala-Kulak <kaenoru@gmail.com>
|
||||
|
||||
Strings and translations:
|
||||
Nataliya Yakavenka
|
||||
Daria Terentieva
|
||||
Satoshi Iida
|
||||
Mathias Wittwer
|
||||
R3gi <regiprogi@gmail.com>
|
||||
Hidde Wieringa
|
||||
Vasily Korotkevich
|
||||
Mark N. Kuramochi
|
||||
Lidia Vasiljeva
|
||||
|
||||
Project management:
|
||||
Alexander Matveenko
|
||||
|
||||
Marketing & support:
|
||||
Sergey Ermilov
|
||||
Anna Mozheiko
|
||||
Alexander Bobko
|
||||
Marat Mukhamedov
|
||||
Alena Miranovich
|
||||
Polina Kovalchuk
|
||||
Ekaterina Sazonova
|
||||
Alesya Serada
|
||||
|
||||
Special thanks to:
|
||||
Yauheniya Melnichek
|
||||
Yuri Gurski
|
||||
Dmitry Matveev
|
||||
Anna Yakovleva
|
5
NOTICE
|
@ -1,5 +1,4 @@
|
|||
Copyright 2020 My.com B.V. (Mail.Ru Group)
|
||||
Copyright 2021 Organic Maps Contributors
|
||||
Copyright 2024 rebus.tj (Rebus Group) Not really Rebus will be updated
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -13,10 +12,10 @@ 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.
|
||||
|
||||
The app is based on Organic maps.
|
||||
Most libraries in the following directories made by other people and
|
||||
organizations and licensed in different ways:
|
||||
* `3party`
|
||||
* `tools`
|
||||
Please refer to their LICENCE, COPYING or NOTICE files for terms of use.
|
||||
Some icons files may be copyrighted by (C) 2020 My.com B.V. (Mail.Ru Group)
|
||||
See also `data/copyright.html` file for a full list of copyright notices.
|
||||
|
|
201
README.md
|
@ -1,38 +1,45 @@
|
|||
# Organic Maps
|
||||
# Tourism Map Tajikistan
|
||||
|
||||
<a name="install"/>
|
||||
This app is for Tajikistan tourists. It's based on open source app Organic map.
|
||||
|
||||
[Organic Maps](https://organicmaps.app) is a free Android & iOS offline maps app for travellers, tourists, drivers, hikers, and cyclists.
|
||||
It uses crowd-sourced [OpenStreetMap](https://www.openstreetmap.org) data and is developed with love by the creators of **MapsWithMe** (later renamed to **Maps.Me**) and by our community.
|
||||
No ads, no tracking, no data collection, no crapware. Your [donations](https://organicmaps.app/donate/) and positive reviews motivate and inspire us, thanks ❤️!
|
||||
## Please Read all of this before continuing
|
||||
|
||||
<p float="left">
|
||||
<a href="https://apps.apple.com/app/organic-maps/id1567437057">
|
||||
<img alt="Download on the App Store" src="docs/badges/apple-appstore.png" width="180">
|
||||
</a>
|
||||
<a href="https://play.google.com/store/apps/details?id=app.organicmaps">
|
||||
<img alt="Get it on Google Play" src="docs/badges/google-play.png" width="180">
|
||||
</a>
|
||||
<a href="https://appgallery.huawei.com/#/app/C104325611">
|
||||
<img alt="Explore it on AppGallery" src="docs/badges/huawei-appgallery.png" width="180">
|
||||
</a>
|
||||
<a href="https://f-droid.org/en/packages/app.organicmaps/">
|
||||
<img alt="Get it on F-Droid" src="docs/badges/fdroid.png" width="180">
|
||||
</a>
|
||||
</p>
|
||||
Don't forget make changes to this file, when you make significant changes to navigation.
|
||||
version of Android Studio I used: android-studio-2024.1.1.13-mac_arm.dmg
|
||||
version of XCode I used: 16.1
|
||||
|
||||
<p float="left">
|
||||
<img src="android/app/src/fdroid/play/listings/en-US/graphics/phone-screenshots/1.jpg" width="400" />
|
||||
<img src="android/app/src/fdroid/play/listings/en-US/graphics/phone-screenshots/2.jpg" width="400" />
|
||||
<img src="android/app/src/fdroid/play/listings/en-US/graphics/phone-screenshots/3.jpg" width="400" />
|
||||
<img src="android/app/src/fdroid/play/listings/en-US/graphics/phone-screenshots/4.jpg" width="400" />
|
||||
</p>
|
||||
## Navigation on Android
|
||||
|
||||
## Features
|
||||
The first activity app starts is SplashActivity.java. There it navigates to MainActivity
|
||||
where all of the data about Tajikistan places are located,
|
||||
it has its own navigation system on Jetpack compose.
|
||||
There we navigateToAuthIfNotAuthed() and then navigateToMapToDownloadIfNotPresent().
|
||||
In AuthActivity we move Tajikistan map to app's internal storage and if it is successful,
|
||||
it won't navigate to map to download.
|
||||
When map download is finished it will go to MainActivity.
|
||||
When you sign in or up, it will navigate to MainActivity.
|
||||
|
||||
## Navigation on iOS
|
||||
|
||||
The first screen to be shown is Map screen (see MapsAppDelegate.mm). I (Emin) couldn't change it.
|
||||
There's such logic in MapsAppDelegate.mm:
|
||||
|
||||
```
|
||||
if (Tajikistan is loaded) {
|
||||
if (token is nil) navigate to Auth (note: token is cleared when user signs out)
|
||||
else navigate to TourismMain (Home)
|
||||
}
|
||||
```
|
||||
|
||||
In Auth when user signs in or up, it navigates to TourismMain
|
||||
In TourismMain goes to auth if not authorized
|
||||
|
||||
## Features of their map
|
||||
|
||||
Organic Maps is the ultimate companion app for travellers, tourists, hikers, and cyclists:
|
||||
|
||||
- Detailed offline maps with places that don't exist on other maps, thanks to [OpenStreetMap](https://openstreetmap.org)
|
||||
- Detailed offline maps with places that don't exist on other maps, thanks
|
||||
to [OpenStreetMap](https://openstreetmap.org)
|
||||
- Cycling routes, hiking trails, and walking paths
|
||||
- Contour lines, elevation profiles, peaks, and slopes
|
||||
- Turn-by-turn walking, cycling, and car navigation with voice guidance
|
||||
|
@ -42,145 +49,9 @@ Organic Maps is the ultimate companion app for travellers, tourists, hikers, and
|
|||
- Countries and regions don't take a lot of space
|
||||
- Free and open-source
|
||||
|
||||
## Why Organic?
|
||||
|
||||
Organic Maps is pure and organic, made with love:
|
||||
|
||||
- Respects your privacy
|
||||
- Saves your battery
|
||||
- No unexpected mobile data charges
|
||||
|
||||
Organic Maps is free from trackers and other bad stuff:
|
||||
|
||||
- No ads
|
||||
- No tracking
|
||||
- No data collection
|
||||
- No phoning home
|
||||
- No annoying registration
|
||||
- No mandatory tutorials
|
||||
- No noisy email spam
|
||||
- No push notifications
|
||||
- No crapware
|
||||
- ~~No pesticides~~ Purely organic!
|
||||
|
||||
The Android application is verified by the <a href="https://reports.exodus-privacy.eu.org/en/reports/app.organicmaps/latest/">Exodus Privacy Project:
|
||||
|
||||
<img src="docs/privacy/exodus.png" width="400">
|
||||
</a>
|
||||
|
||||
The iOS application is verified by <a href="https://ios.trackercontrol.org/analysis/app.organicmaps">TrackerControl for iOS:
|
||||
|
||||
<img src="docs/privacy/trackercontrol-ios.png" width="400">
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
Organic Maps doesn't request excessive permissions to spy on you:
|
||||
|
||||
<p float="left">
|
||||
<img src="docs/privacy/om.jpg" width="400">
|
||||
<img src="docs/privacy/mm.jpg" width="400">
|
||||
</p>
|
||||
|
||||
At Organic Maps, we believe that privacy is a fundamental human right:
|
||||
|
||||
- Organic Maps is an indie community-driven open-source project
|
||||
- We protect your privacy from Big Tech's prying eyes
|
||||
- Stay safe no matter where you are
|
||||
|
||||
Reject surveillance - embrace your freedom.
|
||||
|
||||
[**Give Organic Maps a try!**](#install)
|
||||
|
||||
## Who is paying for the development?
|
||||
|
||||
The app is free for everyone, so we rely on donations. Please donate at [organicmaps.app/donate](https://organicmaps.app/donate) to support us!
|
||||
|
||||
Beloved institutional sponsors below have provided targeted grants to cover some infrastructure costs and fund development of new selected features:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://nlnet.nl/"><img src="docs/sponsors/nlnet.svg" alt="The NLnet Foundation" width="200px"></a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://github.com/organicmaps/organicmaps/milestone/7">The Search & Fonts improvement project</a> has been <a href="https://nlnet.nl/project/OrganicMaps/">funded</a> through NGI0 Entrust Fund. <a href="https://nlnet.nl/entrust/">NGI0 Entrust Fund</a> is established by the <a href="https://nlnet.nl/">NLnet Foundation</a> with financial support from the European Commission's <a href="https://www.ngi.eu/">Next Generation Internet programme</a>, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 101069594.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://summerofcode.withgoogle.com/"><img src="docs/sponsors/gsoc.svg" alt="Google Summer of Code" width="200px"></a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://summerofcode.withgoogle.com/">Google</a> backed 5 student's projects in the Google Summer of Code program during <a href="https://summerofcode.withgoogle.com/programs/2022/organizations/organic-maps">2022</a> and <a href="https://summerofcode.withgoogle.com/programs/2023/organizations/organic-maps">2023</a> programs. Noteworthy projects included Android Auto and Wikipedia Dump Extractor.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.mythic-beasts.com/"><img src="docs/sponsors/mythic-beasts.png" alt="Mythic Beasts" width="200px"></a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.mythic-beasts.com/">Mythic Beasts</a> ISP <a href="https://www.mythic-beasts.com/blog/2021/10/06/improving-the-world-bit-by-expensive-bit/">provides us</a> two virtual servers with 400 TB/month of free bandwidth to host and serve maps downloads and updates.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://futo.org"><img src="docs/sponsors/futo.svg" alt="FUTO" width="200px"></a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://futo.org">FUTO</a> has <a href="https://www.youtube.com/watch?v=fJJclgBHrEw">awarded $1000 micro-grant</a> to Organic Maps in February 2023.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
The majority of all expenses have been funded by founders of the project since its inception. The project is far from achieving any sort of financial sustainability. The current level of voluntary donations falls significantly short of covering efforts needed to sustain the app. Any new developments of features are beyond the scope of possibility due to the absence of the necessary financial resources.
|
||||
|
||||
Please consider [donating](https://organicmaps.app/donate) if you want to see this open-source project thriving, not dying. There are [other ways how to support the project](#contributing). No coding skills required.
|
||||
|
||||
## Copyrights
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See
|
||||
[LICENSE](https://github.com/organicmaps/organicmaps/blob/master/LICENSE),
|
||||
[NOTICE](https://github.com/organicmaps/organicmaps/blob/master/NOTICE)
|
||||
and [data/copyright.html](http://htmlpreview.github.io/?https://github.com/organicmaps/organicmaps/blob/master/data/copyright.html)
|
||||
[LICENSE](https://github.com/Ohpleaseman/tourism/blob/master/LICENSE),
|
||||
[NOTICE](https://github.com/Ohpleaseman/tourism/blob/master/NOTICE)
|
||||
for more information.
|
||||
|
||||
## Governance
|
||||
|
||||
See [docs/GOVERNANCE.md](docs/GOVERNANCE.md).
|
||||
|
||||
<a name="contributing">
|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to build the project, check [docs/INSTALL.md](docs/INSTALL.md). If you want to help the project,
|
||||
see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). You can [help in many ways](https://organicmaps.app/support-us/), the ability to code is not necessary.
|
||||
|
||||
## Beta
|
||||
|
||||
Please join our beta program, suggest your features, and report bugs:
|
||||
|
||||
- [iOS Beta (TestFlight)](https://testflight.apple.com/join/lrKCl08I)
|
||||
- [Android Beta (Firebase)](https://appdistribution.firebase.dev/i/2f0fee463107b137)
|
||||
|
||||
## Feedback
|
||||
|
||||
- **Rate us on the [App Store](https://apps.apple.com/app/organic-maps/id1567437057)
|
||||
and [Google Play](https://play.google.com/store/apps/details?id=app.organicmaps)**.
|
||||
- **Star us on GitHub**.
|
||||
- Report bugs or issues to [the issue tracker](https://github.com/organicmaps/organicmaps/issues).
|
||||
- [Discuss](https://github.com/organicmaps/organicmaps/discussions/categories/ideas) ideas or propose feature requests.
|
||||
- Subscribe to our [Telegram Channel](https://t.me/OrganicMapsApp) or to the [[matrix] space](https://matrix.to/#/#organicmaps:matrix.org) for updates.
|
||||
- Join our [Telegram Group](https://t.me/OrganicMaps) to discuss with other users.
|
||||
- Присоединяйтесь к нашей [русскоязычной группе в Telegram](https://t.me/OrganicMapsRu) для обратной связи и помощи.
|
||||
- Diğer kullanıcılarla tartışmak için [Telegram Grubumuza](https://t.me/OrganicMapsTR) katılın.
|
||||
- Rejoignez notre groupe [Telegram](https://t.me/OrganicMapsFR) pour obtenir de l'aide.
|
||||
- Contact us by [email](mailto:hello@organicmaps.app).
|
||||
- Follow our updates in
|
||||
[Mastodon](https://fosstodon.org/@organicmaps),
|
||||
[Facebook](https://facebook.com/OrganicMaps),
|
||||
[Twitter](https://twitter.com/OrganicMapsApp),
|
||||
[Instagram](https://instagram.com/organicmaps.app/).
|
||||
- Güncellemelerimizi [Instagram](https://instagram.com/organicmapstr/) üzerinden takip edin.
|
||||
|
||||
The Organic Maps community abides by the CNCF [code of conduct](https://github.com/organicmaps/organicmaps/blob/master/docs/CODE_OF_CONDUCT.md).
|
||||
|
|
3
android/app/.gitignore
vendored
|
@ -31,3 +31,6 @@
|
|||
|
||||
# ignore autogenerated metadata (see prepareGoogleReleaseListing in build.gradle)
|
||||
/src/google/play/listings
|
||||
|
||||
# ignore google releases
|
||||
/google/release
|
||||
|
|
|
@ -12,14 +12,22 @@ buildscript {
|
|||
def taskName = getGradle().getStartParameter().getTaskRequests().toString().toLowerCase()
|
||||
def isFdroid = taskName.contains('fdroid')
|
||||
def isBeta = taskName.contains('beta')
|
||||
def isRelease = taskName.contains('release')
|
||||
|
||||
// Firebase Crashlytics compile-time feature flag: -Pfirebase=true|false
|
||||
def googleFirebaseServicesFlag = findProperty('firebase')
|
||||
// Enable Firebase for all beta flavors except fdroid only if google-services.json exists.
|
||||
def googleFirebaseServicesDefault = isBeta && !isFdroid && file("$projectDir/google-services.json").exists()
|
||||
ext.googleFirebaseServicesEnabled = googleFirebaseServicesFlag != null ?
|
||||
googleFirebaseServicesFlag == '' || googleFirebaseServicesFlag.toBoolean() :
|
||||
googleFirebaseServicesDefault
|
||||
|
||||
/*
|
||||
We want to use Firebase Crashlytics for Tourism. We can't use it for debug,
|
||||
because debug version has its own package_name, release doesn't, so I (Emin) changed
|
||||
the condition for ext.googleFirebaseServicesEnabled
|
||||
*/
|
||||
// ext.googleFirebaseServicesEnabled = googleFirebaseServicesFlag != null ?
|
||||
// googleFirebaseServicesFlag == '' || googleFirebaseServicesFlag.toBoolean() :
|
||||
// googleFirebaseServicesDefault
|
||||
ext.googleFirebaseServicesEnabled = isRelease
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.4.1'
|
||||
|
@ -53,6 +61,11 @@ if (googleFirebaseServicesEnabled) {
|
|||
}
|
||||
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: 'org.jetbrains.kotlin.plugin.serialization'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.google.dagger.hilt.android'
|
||||
|
||||
def run(cmd) {
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
|
@ -81,8 +94,8 @@ def getCommitMessage() {
|
|||
|
||||
def osName = System.properties['os.name'].toLowerCase()
|
||||
|
||||
project.ext.appId = 'app.organicmaps'
|
||||
project.ext.appName = 'Organic Maps'
|
||||
project.ext.appId = 'tj.tourism.rebus'
|
||||
project.ext.appName = 'Tourism Map Tajikistan'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
|
@ -96,6 +109,7 @@ android {
|
|||
buildFeatures {
|
||||
dataBinding = true
|
||||
buildConfig = true
|
||||
compose true
|
||||
}
|
||||
// All properties are read from gradle.properties file
|
||||
compileSdk propCompileSdkVersion.toInteger()
|
||||
|
@ -105,10 +119,10 @@ android {
|
|||
defaultConfig {
|
||||
// Default package name is taken from the manifest and should be app.organicmaps
|
||||
def ver = getVersion()
|
||||
versionCode = ver.V1
|
||||
versionName = ver.V2
|
||||
println('Version: ' + versionName)
|
||||
println('VersionCode: ' + versionCode)
|
||||
versionCode = 4
|
||||
versionName = "1.0.1"
|
||||
// println('Version: ' + versionName)
|
||||
// println('VersionCode: ' + versionCode)
|
||||
minSdk propMinSdkVersion.toInteger()
|
||||
targetSdk propTargetSdkVersion.toInteger()
|
||||
applicationId project.ext.appId
|
||||
|
@ -166,6 +180,9 @@ android {
|
|||
}
|
||||
|
||||
setProperty('archivesBaseName', appName.replaceAll('\\s','') + '-' + defaultConfig.versionCode)
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += 'default'
|
||||
|
@ -344,11 +361,81 @@ android {
|
|||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.5.14'
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
|
||||
implementation 'androidx.activity:activity-compose:1.9.3'
|
||||
implementation platform('androidx.compose:compose-bom:2024.12.01')
|
||||
implementation 'androidx.compose.ui:ui'
|
||||
implementation 'androidx.compose.ui:ui-graphics'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
androidTestImplementation platform('androidx.compose:compose-bom:2024.12.01')
|
||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
debugImplementation 'androidx.compose.ui:ui-test-manifest'
|
||||
|
||||
// hilt
|
||||
def hilt = '2.51.1'
|
||||
implementation "com.google.dagger:hilt-android:$hilt"
|
||||
kapt "com.google.dagger:hilt-compiler:$hilt"
|
||||
kapt "androidx.hilt:hilt-compiler:1.2.0"
|
||||
implementation 'androidx.hilt:hilt-navigation-compose:1.2.0'
|
||||
|
||||
// navigation
|
||||
implementation 'androidx.navigation:navigation-compose:2.8.5'
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
|
||||
// countries
|
||||
implementation 'com.hbb20:ccp:2.7.3'
|
||||
// webview
|
||||
implementation "androidx.webkit:webkit:1.11.0"
|
||||
// compress
|
||||
implementation 'id.zelory:compressor:3.0.1'
|
||||
// restart app
|
||||
implementation 'com.jakewharton:process-phoenix:3.0.0'
|
||||
|
||||
//Background processing
|
||||
def coroutines = '1.8.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
|
||||
// Coroutine Lifecycle Scopes
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7'
|
||||
|
||||
// region Network
|
||||
// Retrofit
|
||||
def retrofit = '2.11.0'
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit"
|
||||
def okhttp = '5.0.0-alpha.14'
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp"
|
||||
implementation 'com.google.code.gson:gson:2.11.0'
|
||||
def coil_version = '2.7.0'
|
||||
implementation("io.coil-kt:coil-compose:$coil_version")
|
||||
implementation("io.coil-kt:coil-svg:$coil_version")
|
||||
// endregion
|
||||
|
||||
// Room
|
||||
def room = '2.6.1'
|
||||
implementation "androidx.room:room-ktx:$room"
|
||||
implementation "androidx.room:room-runtime:$room"
|
||||
kapt "androidx.room:room-compiler:$room"
|
||||
|
||||
// Google Play Location Services
|
||||
//
|
||||
// Please add symlinks to google/java/app/organicmaps/location for each new gms-enabled flavor below:
|
||||
|
@ -402,6 +489,10 @@ dependencies {
|
|||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||
}
|
||||
|
||||
kapt {
|
||||
correctErrorTypes true
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << '-Xlint:unchecked' << '-Xlint:deprecation'
|
||||
}
|
||||
|
|
22
android/app/proguard-rules.pro
vendored
|
@ -28,3 +28,25 @@
|
|||
# R8 crypts the source line numbers in all log messages.
|
||||
# https://github.com/organicmaps/organicmaps/issues/6559#issuecomment-1812039926
|
||||
-dontoptimize
|
||||
|
||||
# For some unknown reason we couldn't find out, requests are not working properly
|
||||
# when the app is shrinked and/or minified, so we keep all of these things out from R8 effects.
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||
|
||||
-if interface * { @retrofit2.http.* public *** *(...); }
|
||||
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
|
||||
|
||||
-keep class app.tourism.data.remote.** { *; }
|
||||
|
||||
-keep public class app.tourism.data.dto.** {
|
||||
public void set*(***);
|
||||
public *** get*();
|
||||
public protected private *;
|
||||
}
|
||||
-keep public class app.tourism.domain.models.** {
|
||||
public void set*(***);
|
||||
public *** get*();
|
||||
public protected private *;
|
||||
}
|
||||
|
|
BIN
android/app/src/debug/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 59 KiB |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 8.4 KiB |
BIN
android/app/src/debug/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 3.1 KiB |
BIN
android/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.9 KiB |
BIN
android/app/src/debug/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 976 B |
Before Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 13 KiB |
BIN
android/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 4.5 KiB |
BIN
android/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 24 KiB |
BIN
android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 8 KiB |
BIN
android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 8 KiB |
Before Width: | Height: | Size: 37 KiB |
BIN
android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 12 KiB |
BIN
android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#000000</color>
|
||||
</resources>
|
|
@ -1,30 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
<!-- Requiring "android.hardware.touchscreen" here breaks DeX mode -->
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true"/>
|
||||
android:glEsVersion="0x00020000"
|
||||
android:required="true" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.wifi"
|
||||
android:required="false"/>
|
||||
android:name="android.hardware.wifi"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location"
|
||||
android:required="false"/>
|
||||
android:name="android.hardware.location"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.network"
|
||||
android:required="false"/>
|
||||
android:name="android.hardware.location.network"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<!--
|
||||
https://developer.android.com/reference/androidx/core/app/JobIntentService:
|
||||
When running on Android O, the JobScheduler will take care of wake locks
|
||||
|
@ -33,146 +33,158 @@
|
|||
versions of the platform, this wake lock handling is emulated in the
|
||||
class here by directly calling the PowerManager; this means
|
||||
the application must request the Manifest.permission.WAKE_LOCK permission.
|
||||
//-->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||
//
|
||||
-->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<!--
|
||||
Android 13 (API level 33) and higher supports a runtime permission for sending non-exempt (including Foreground
|
||||
Services (FGS)) notifications from an app.
|
||||
//-->
|
||||
//
|
||||
-->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
|
||||
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
|
||||
<!-- <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES" />-->
|
||||
<!-- <uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />-->
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE"/>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<supports-screens
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true" />
|
||||
|
||||
<!-- "android:resizeableActivity" allows free-form and split-screen modes -->
|
||||
<application
|
||||
android:name=".MwmApplication"
|
||||
android:allowBackup="true"
|
||||
android:backupInForeground="true"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:dataExtractionRules="@xml/backup_content_v31"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:theme="@style/MwmTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="33">
|
||||
android:name=".MwmApplication"
|
||||
android:allowBackup="true"
|
||||
android:backupInForeground="true"
|
||||
android:dataExtractionRules="@xml/backup_content_v31"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/MwmTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="33">
|
||||
<receiver
|
||||
android:name="app.tourism.data.remote.WifiReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.wifi.STATE_CHANGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name="app.tourism.AuthActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustResize|adjustPan"
|
||||
android:theme="@style/MwmTheme" />
|
||||
<!-- Allows for config and orientation change without killing/restarting main activity -->
|
||||
<activity
|
||||
android:name="app.organicmaps.SplashActivity"
|
||||
android:name=".SplashActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="geo"/>
|
||||
<data android:scheme="ge0"/>
|
||||
<data android:scheme="om"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="ge0.me"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="@bool/autoVerify">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="omaps.app"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:scheme="geo" />
|
||||
<data android:scheme="ge0" />
|
||||
<data android:scheme="om" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="ge0.me" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="@bool/autoVerify">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="omaps.app" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<!-- #map=$zoom/$lat/$lon -->
|
||||
<data android:host="www.openstreetmap.org"/>
|
||||
<data android:path="/"/>
|
||||
<data android:path="/search"/>
|
||||
<data android:host="www.openstreetmap.org" />
|
||||
<data android:path="/" />
|
||||
<data android:path="/search" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="mapsme"/>
|
||||
<data android:scheme="mapsme" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="data"/>
|
||||
<data android:host="*"/>
|
||||
<data android:mimeType="application/vnd.google-earth.kml+xml"/>
|
||||
<data android:mimeType="application/vnd.google-earth.kmz"/>
|
||||
<data android:mimeType="application/gpx"/>
|
||||
<data android:mimeType="application/gpx+xml"/>
|
||||
<data android:mimeType="application/vnd.google-earth.kmz+xml"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="data" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="application/vnd.google-earth.kml+xml" />
|
||||
<data android:mimeType="application/vnd.google-earth.kmz" />
|
||||
<data android:mimeType="application/gpx" />
|
||||
<data android:mimeType="application/gpx+xml" />
|
||||
<data android:mimeType="application/vnd.google-earth.kmz+xml" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/vnd.google-earth.kml+xml"/>
|
||||
<data android:mimeType="application/vnd.google-earth.kmz"/>
|
||||
<data android:mimeType="application/gpx"/>
|
||||
<data android:mimeType="application/gpx+xml"/>
|
||||
<data android:mimeType="application/vnd.google-earth.kmz+xml"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/vnd.google-earth.kml+xml" />
|
||||
<data android:mimeType="application/vnd.google-earth.kmz" />
|
||||
<data android:mimeType="application/gpx" />
|
||||
<data android:mimeType="application/gpx+xml" />
|
||||
<data android:mimeType="application/vnd.google-earth.kmz+xml" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="data"/>
|
||||
<data android:host="*"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="data" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="*/*" />
|
||||
<!-- See http://stackoverflow.com/questions/3400072/pathpattern-to-match-file-extension-does-not-work-if-a-period-exists-elsewhere-i -->
|
||||
<data android:pathPattern="/.*\\.kmz" />
|
||||
<data android:pathPattern="/.*\\.KMZ" />
|
||||
|
@ -222,7 +234,7 @@
|
|||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.GPX" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.GPX" />
|
||||
<!-- Old MAPS.ME binary format //-->
|
||||
<!-- Old MAPS.ME binary format // -->
|
||||
<data android:pathPattern="/.*\\.kmb" />
|
||||
<data android:pathPattern="/.*\\.KMB" />
|
||||
<data android:pathPattern="/.*\\..*\\.kmb" />
|
||||
|
@ -241,16 +253,20 @@
|
|||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.KMB" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Duplicates the intent-filter above except it doesn't have mimeType, see
|
||||
https://stackoverflow.com/questions/1733195/android-intent-filter-for-a-particular-file-extension/31028507#31028507 -->
|
||||
<!--
|
||||
Duplicates the intent-filter above except it doesn't have mimeType, see
|
||||
https://stackoverflow.com/questions/1733195/android-intent-filter-for-a-particular-file-extension/31028507#31028507
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="data"/>
|
||||
<data android:host="*"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="data" />
|
||||
<data android:host="*" />
|
||||
<!-- See http://stackoverflow.com/questions/3400072/pathpattern-to-match-file-extension-does-not-work-if-a-period-exists-elsewhere-i -->
|
||||
<data android:pathPattern="/.*\\.kmz" />
|
||||
<data android:pathPattern="/.*\\.KMZ" />
|
||||
|
@ -300,7 +316,7 @@
|
|||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.GPX" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.GPX" />
|
||||
<!-- Old MAPS.ME binary format //-->
|
||||
<!-- Old MAPS.ME binary format // -->
|
||||
<data android:pathPattern="/.*\\.kmb" />
|
||||
<data android:pathPattern="/.*\\.KMB" />
|
||||
<data android:pathPattern="/.*\\..*\\.kmb" />
|
||||
|
@ -322,134 +338,135 @@
|
|||
<!-- Catches .gpx and .gpx.xml files opened from Google Files app -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/xml" />
|
||||
<data android:mimeType="text/xml" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="app.organicmaps.DownloadResourcesLegacyActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity-alias
|
||||
android:name="app.organicmaps.DownloadResourcesActivity"
|
||||
android:name=".DownloadResourcesActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:targetActivity="app.organicmaps.SplashActivity"
|
||||
android:exported="true">
|
||||
android:targetActivity=".SplashActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name="app.organicmaps.MwmActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="uiMode"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustPan"/>
|
||||
|
||||
android:name="app.tourism.MainActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="false"
|
||||
android:theme="@style/MwmTheme" />
|
||||
<activity
|
||||
android:name="app.organicmaps.downloader.DownloaderActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/download_maps"
|
||||
android:parentActivityName="app.organicmaps.MwmActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
android:name=".DownloadResourcesLegacyActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize" />
|
||||
<activity
|
||||
android:name="app.organicmaps.search.SearchActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/search_map"
|
||||
android:parentActivityName="app.organicmaps.MwmActivity"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
|
||||
android:name=".MwmActivity"
|
||||
android:configChanges="uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustPan" />
|
||||
<activity
|
||||
android:name="app.organicmaps.settings.SettingsActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/settings"
|
||||
android:parentActivityName="app.organicmaps.MwmActivity" />
|
||||
|
||||
android:name=".downloader.DownloaderActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/download_maps"
|
||||
android:parentActivityName=".MwmActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="app.organicmaps.help.HelpActivity"
|
||||
android:label="@string/about_menu_title"
|
||||
android:parentActivityName="app.organicmaps.MwmActivity"
|
||||
android:exported="false">
|
||||
android:name=".search.SearchActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/search_map"
|
||||
android:parentActivityName=".MwmActivity"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/settings"
|
||||
android:parentActivityName=".MwmActivity" />
|
||||
<activity
|
||||
android:name=".help.HelpActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/about_menu_title"
|
||||
android:parentActivityName=".MwmActivity">
|
||||
<intent-filter>
|
||||
<action android:name="app.organicmaps.help.HelpActivity" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="app.organicmaps.bookmarks.BookmarkCategoriesActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/bookmarks_and_tracks"
|
||||
android:parentActivityName="app.organicmaps.MwmActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
android:name=".bookmarks.BookmarkCategoriesActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/bookmarks_and_tracks"
|
||||
android:parentActivityName=".MwmActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="app.organicmaps.bookmarks.BookmarkListActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/bookmarks"
|
||||
android:parentActivityName="app.organicmaps.bookmarks.BookmarkCategoriesActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
android:name=".bookmarks.BookmarkListActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/bookmarks"
|
||||
android:parentActivityName=".bookmarks.BookmarkCategoriesActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="app.organicmaps.editor.EditorActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/edit_place"
|
||||
android:parentActivityName="app.organicmaps.MwmActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
android:name=".editor.EditorActivity"
|
||||
android:configChanges="orientation|screenLayout|screenSize"
|
||||
android:label="@string/edit_place"
|
||||
android:parentActivityName=".MwmActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="app.organicmaps.editor.ProfileActivity"
|
||||
android:parentActivityName="app.organicmaps.settings.SettingsActivity" />
|
||||
|
||||
android:name=".editor.ProfileActivity"
|
||||
android:parentActivityName=".settings.SettingsActivity" />
|
||||
<activity
|
||||
android:name="app.organicmaps.editor.FeatureCategoryActivity"
|
||||
android:parentActivityName="app.organicmaps.MwmActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
android:name=".editor.FeatureCategoryActivity"
|
||||
android:parentActivityName=".MwmActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="app.organicmaps.editor.ReportActivity"
|
||||
android:parentActivityName="app.organicmaps.MwmActivity" />
|
||||
|
||||
android:name=".editor.ReportActivity"
|
||||
android:parentActivityName=".MwmActivity" />
|
||||
<activity
|
||||
android:name="app.organicmaps.editor.OsmLoginActivity"
|
||||
android:parentActivityName="app.organicmaps.MwmActivity" />
|
||||
|
||||
android:name=".editor.OsmLoginActivity"
|
||||
android:parentActivityName=".MwmActivity" />
|
||||
<activity
|
||||
android:name="app.organicmaps.bookmarks.BookmarkCategorySettingsActivity"
|
||||
android:name=".bookmarks.BookmarkCategorySettingsActivity"
|
||||
android:label="@string/edit"
|
||||
android:windowSoftInputMode="stateVisible"/>
|
||||
android:windowSoftInputMode="stateVisible" />
|
||||
<activity
|
||||
android:name="app.organicmaps.widget.placepage.PlaceDescriptionActivity"
|
||||
android:label="@string/place_description_title"/>
|
||||
android:name=".widget.placepage.PlaceDescriptionActivity"
|
||||
android:label="@string/place_description_title" />
|
||||
<activity
|
||||
android:name="app.organicmaps.settings.DrivingOptionsActivity"
|
||||
android:label="@string/driving_options_title"/>
|
||||
<activity
|
||||
android:name="app.organicmaps.MapPlaceholderActivity"/>
|
||||
android:name=".settings.DrivingOptionsActivity"
|
||||
android:label="@string/driving_options_title" />
|
||||
<activity android:name=".MapPlaceholderActivity" />
|
||||
|
||||
<!-- <service-->
|
||||
<!-- android:name=".car.CarAppService"-->
|
||||
<!-- android:exported="true"-->
|
||||
<!-- android:foregroundServiceType="location">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="androidx.car.app.CarAppService" />-->
|
||||
|
||||
<!-- <category android:name="androidx.car.app.category.NAVIGATION" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </service>-->
|
||||
<service
|
||||
android:name="app.organicmaps.car.CarAppService"
|
||||
android:foregroundServiceType="location"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.car.app.CarAppService" />
|
||||
<category android:name="androidx.car.app.category.NAVIGATION" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
android:name=".routing.NavigationService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="location"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<service
|
||||
android:name=".routing.NavigationService"
|
||||
android:foregroundServiceType="location"
|
||||
android:name="app.tourism.ImagesDownloadService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"
|
||||
android:enabled="true"
|
||||
android:stopWithTask="false"/>
|
||||
/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
@ -458,26 +475,27 @@
|
|||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths">
|
||||
</meta-data>
|
||||
</provider>
|
||||
|
||||
<!-- Disable Google's checks for visited sites and loaded URLs in bookmarks description. -->
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" android:value="false" />
|
||||
<!-- Disable Google's anonymous stats collection -->
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
|
||||
|
||||
<!-- Version >= 3.0. Dex Dual Mode support for compatible Samsung devices.
|
||||
See the documentation: https://developer.samsung.com/samsung-dex/modify-optimizing.html //-->
|
||||
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true" />
|
||||
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider> <!-- Disable Google's checks for visited sites and loaded URLs in bookmarks description. -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc"/>
|
||||
|
||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" /> <!-- Disable Google's anonymous stats collection -->
|
||||
<meta-data
|
||||
android:name="androidx.car.app.minCarApiLevel"
|
||||
android:value="5"/>
|
||||
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
<!--
|
||||
Version >= 3.0. Dex Dual Mode support for compatible Samsung devices.
|
||||
See the documentation: https://developer.samsung.com/samsung-dex/modify-optimizing.html //
|
||||
-->
|
||||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="com.google.android.gms.car.application"-->
|
||||
<!-- android:resource="@xml/automotive_app_desc" />-->
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="androidx.car.app.minCarApiLevel"-->
|
||||
<!-- android:value="5" />-->
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
</manifest>
|
BIN
android/app/src/main/assets/tajikistan.mwm
Normal file
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 58 KiB |
|
@ -1,5 +1,6 @@
|
|||
package app.organicmaps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
|
@ -10,6 +11,9 @@ import app.organicmaps.base.BaseMwmFragmentActivity;
|
|||
import app.organicmaps.display.DisplayChangedListener;
|
||||
import app.organicmaps.display.DisplayManager;
|
||||
import app.organicmaps.display.DisplayType;
|
||||
import app.tourism.data.prefs.Language;
|
||||
import app.tourism.data.prefs.UserPreferences;
|
||||
import app.tourism.utils.LocaleHelper;
|
||||
|
||||
public class MapPlaceholderActivity extends BaseMwmFragmentActivity implements DisplayChangedListener
|
||||
{
|
||||
|
@ -18,6 +22,17 @@ public class MapPlaceholderActivity extends BaseMwmFragmentActivity implements D
|
|||
private DisplayManager mDisplayManager;
|
||||
private boolean mRemoveDisplayListener = true;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
Language language = new UserPreferences(newBase).getLanguage();
|
||||
if(language != null) {
|
||||
String languageCode = language.getCode();
|
||||
super.attachBaseContext(LocaleHelper.localeUpdateResources(newBase, languageCode));
|
||||
} else {
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSafeCreate(@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
|
|
|
@ -12,8 +12,11 @@ import android.location.Location;
|
|||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
@ -50,6 +53,7 @@ import app.organicmaps.bookmarks.data.MapObject;
|
|||
import app.organicmaps.display.DisplayChangedListener;
|
||||
import app.organicmaps.display.DisplayManager;
|
||||
import app.organicmaps.display.DisplayType;
|
||||
import app.organicmaps.downloader.CountryItem;
|
||||
import app.organicmaps.downloader.DownloaderActivity;
|
||||
import app.organicmaps.downloader.DownloaderFragment;
|
||||
import app.organicmaps.downloader.MapManager;
|
||||
|
@ -81,7 +85,6 @@ import app.organicmaps.routing.NavigationService;
|
|||
import app.organicmaps.routing.RoutePointInfo;
|
||||
import app.organicmaps.routing.RoutingBottomMenuListener;
|
||||
import app.organicmaps.routing.RoutingController;
|
||||
import app.organicmaps.routing.RoutingErrorDialogFragment;
|
||||
import app.organicmaps.routing.RoutingOptions;
|
||||
import app.organicmaps.routing.RoutingPlanFragment;
|
||||
import app.organicmaps.routing.RoutingPlanInplaceController;
|
||||
|
@ -107,6 +110,8 @@ import app.organicmaps.widget.menu.MainMenu;
|
|||
import app.organicmaps.widget.placepage.PlacePageController;
|
||||
import app.organicmaps.widget.placepage.PlacePageData;
|
||||
import app.organicmaps.widget.placepage.PlacePageViewModel;
|
||||
import app.tourism.data.dto.PlaceLocation;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -119,6 +124,8 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
|||
import static app.organicmaps.location.LocationState.FOLLOW;
|
||||
import static app.organicmaps.location.LocationState.FOLLOW_AND_ROTATE;
|
||||
import static app.organicmaps.location.LocationState.LOCATION_TAG;
|
||||
import static app.tourism.ui.utils.ShowToastKt.showToast;
|
||||
import static app.tourism.utils.MapUtilsKt.isInsideTajikistan;
|
||||
|
||||
public class MwmActivity extends BaseMwmFragmentActivity
|
||||
implements PlacePageActivationListener,
|
||||
|
@ -544,6 +551,64 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
|||
*/
|
||||
if (Map.isEngineCreated())
|
||||
onRenderingInitializationFinished();
|
||||
|
||||
tjkMapDownloadingHandling();
|
||||
|
||||
PlaceLocation endPoint = getIntent().getParcelableExtra("end_point");
|
||||
if(endPoint != null)
|
||||
routeForSiteFromMainActivityHandling(endPoint);
|
||||
}
|
||||
|
||||
private void tjkMapDownloadingHandling() {
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
Runnable delayedAction = () -> {
|
||||
CountryItem mCurrentCountry = CountryItem.fill("Tajikistan");
|
||||
|
||||
if(mCurrentCountry.status != CountryItem.STATUS_DONE) {
|
||||
// navigate to Dushanbe so it automatically downloads Tajikistan map
|
||||
goToDushanbe();
|
||||
} else {
|
||||
goToTjkIfNotThere();
|
||||
}
|
||||
};
|
||||
handler.postDelayed(delayedAction, 1000);
|
||||
}
|
||||
|
||||
private void routeForSiteFromMainActivityHandling(PlaceLocation endPoint) {
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
Runnable delayedAction = () -> {
|
||||
showRouteForSiteFromMainActivity(endPoint);
|
||||
};
|
||||
handler.postDelayed(delayedAction, 1000);
|
||||
}
|
||||
|
||||
private void showRouteForSiteFromMainActivity(PlaceLocation endPoint) {
|
||||
startLocationToPoint(endPoint.toMapObject());
|
||||
}
|
||||
|
||||
private void goToTjkIfNotThere() {
|
||||
final double[] center = Framework.nativeGetScreenRectCenter();
|
||||
final double lat = center[0];
|
||||
final double lon = center[1];
|
||||
if(!isInsideTajikistan(lat, lon))
|
||||
goToTjk();
|
||||
}
|
||||
|
||||
private void goToDushanbe() {
|
||||
Framework.nativeZoomToPoint(38.5598, 68.7870, 10, false);
|
||||
}
|
||||
|
||||
public void goToTjk() {
|
||||
Framework.nativeZoomToPoint(38.5598, 68.7870, 8, false);
|
||||
}
|
||||
|
||||
public void blockScreen() {
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
|
||||
}
|
||||
|
||||
public void removeScreenBlock() {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
|
||||
}
|
||||
|
||||
private void refreshLightStatusBar()
|
||||
|
@ -1623,9 +1688,9 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
|||
@Override
|
||||
public void onCommonBuildError(int lastResultCode, @NonNull String[] lastMissingMaps)
|
||||
{
|
||||
RoutingErrorDialogFragment fragment = RoutingErrorDialogFragment.create(getSupportFragmentManager().getFragmentFactory(),
|
||||
getApplicationContext(), lastResultCode, lastMissingMaps);
|
||||
fragment.show(getSupportFragmentManager(), RoutingErrorDialogFragment.class.getSimpleName());
|
||||
// RoutingErrorDialogFragment fragment = RoutingErrorDialogFragment.create(getSupportFragmentManager().getFragmentFactory(),
|
||||
// getApplicationContext(), lastResultCode, lastMissingMaps);
|
||||
// fragment.show(getSupportFragmentManager(), RoutingErrorDialogFragment.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -45,11 +45,13 @@ import app.organicmaps.util.UiUtils;
|
|||
import app.organicmaps.util.Utils;
|
||||
import app.organicmaps.util.log.Logger;
|
||||
import app.organicmaps.util.log.LogsManager;
|
||||
import dagger.hilt.android.HiltAndroidApp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
|
||||
@HiltAndroidApp
|
||||
public class MwmApplication extends Application implements Application.ActivityLifecycleCallbacks
|
||||
{
|
||||
@NonNull
|
||||
|
|
|
@ -16,6 +16,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import app.tourism.MainActivity;
|
||||
import app.organicmaps.display.DisplayManager;
|
||||
import app.organicmaps.location.LocationHelper;
|
||||
import app.organicmaps.util.Config;
|
||||
|
@ -23,6 +24,10 @@ import app.organicmaps.util.LocationUtils;
|
|||
import app.organicmaps.util.ThemeUtils;
|
||||
import app.organicmaps.util.concurrency.UiThread;
|
||||
import app.organicmaps.util.log.Logger;
|
||||
import app.tourism.data.prefs.Language;
|
||||
import app.tourism.data.prefs.UserPreferences;
|
||||
import app.tourism.utils.LocaleHelper;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -43,6 +48,17 @@ public class SplashActivity extends AppCompatActivity
|
|||
@NonNull
|
||||
private final Runnable mInitCoreDelayedTask = this::init;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
Language language = new UserPreferences(newBase).getLanguage();
|
||||
if(language != null) {
|
||||
String languageCode = language.getCode();
|
||||
super.attachBaseContext(LocaleHelper.localeUpdateResources(newBase, languageCode));
|
||||
} else {
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
|
@ -153,7 +169,7 @@ public class SplashActivity extends AppCompatActivity
|
|||
// Re-use original intent to retain all flags and payload.
|
||||
// https://github.com/organicmaps/organicmaps/issues/6944
|
||||
final Intent intent = Objects.requireNonNull(getIntent());
|
||||
intent.setComponent(new ComponentName(this, DownloadResourcesLegacyActivity.class));
|
||||
intent.setComponent(new ComponentName(this, MainActivity.class));
|
||||
// Flags like FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_RESET_TASK_IF_NEEDED will break the cold start of the app.
|
||||
// https://github.com/organicmaps/organicmaps/pull/7287
|
||||
intent.setFlags(intent.getFlags() & (Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_GRANT_READ_URI_PERMISSION));
|
||||
|
|
|
@ -25,6 +25,9 @@ import app.organicmaps.util.RtlUtils;
|
|||
import app.organicmaps.util.ThemeUtils;
|
||||
import app.organicmaps.util.concurrency.UiThread;
|
||||
import app.organicmaps.util.log.Logger;
|
||||
import app.tourism.data.prefs.Language;
|
||||
import app.tourism.data.prefs.UserPreferences;
|
||||
import app.tourism.utils.LocaleHelper;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -51,6 +54,17 @@ public abstract class BaseMwmFragmentActivity extends AppCompatActivity
|
|||
throw new IllegalArgumentException("Attempt to apply unsupported theme: " + theme);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
Language language = new UserPreferences(newBase).getLanguage();
|
||||
if(language != null) {
|
||||
String languageCode = language.getCode();
|
||||
super.attachBaseContext(LocaleHelper.localeUpdateResources(newBase, languageCode));
|
||||
} else {
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows splash screen and initializes the core in case when it was not initialized.
|
||||
*
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
package app.organicmaps.downloader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import app.organicmaps.base.BaseMwmFragmentActivity;
|
||||
import app.organicmaps.base.OnBackPressListener;
|
||||
import app.tourism.data.prefs.Language;
|
||||
import app.tourism.data.prefs.UserPreferences;
|
||||
import app.tourism.utils.LocaleHelper;
|
||||
|
||||
public class DownloaderActivity extends BaseMwmFragmentActivity
|
||||
{
|
||||
|
@ -15,6 +20,17 @@ public class DownloaderActivity extends BaseMwmFragmentActivity
|
|||
return DownloaderFragment.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
Language language = new UserPreferences(newBase).getLanguage();
|
||||
if(language != null) {
|
||||
String languageCode = language.getCode();
|
||||
super.attachBaseContext(LocaleHelper.localeUpdateResources(newBase, languageCode));
|
||||
} else {
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed()
|
||||
{
|
||||
|
|
|
@ -1,26 +1,34 @@
|
|||
package app.organicmaps.downloader;
|
||||
|
||||
import static androidx.core.content.ContextCompat.startActivity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.location.Location;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.organicmaps.MwmActivity;
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.location.LocationHelper;
|
||||
import app.organicmaps.routing.RoutingController;
|
||||
import app.organicmaps.widget.WheelProgressView;
|
||||
import app.organicmaps.util.Config;
|
||||
import app.organicmaps.util.ConnectionState;
|
||||
import app.organicmaps.util.StringUtils;
|
||||
import app.organicmaps.util.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
import app.organicmaps.widget.WheelProgressView;
|
||||
import app.tourism.MainActivity;
|
||||
|
||||
public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
||||
{
|
||||
|
@ -28,7 +36,6 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
|
||||
private final MwmActivity mActivity;
|
||||
private final View mFrame;
|
||||
private final TextView mParent;
|
||||
private final TextView mTitle;
|
||||
private final TextView mSize;
|
||||
private final WheelProgressView mProgress;
|
||||
|
@ -36,6 +43,8 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
|
||||
private int mStorageSubscriptionSlot;
|
||||
|
||||
private boolean alreadyNavigating = false;
|
||||
|
||||
@Nullable
|
||||
private CountryItem mCurrentCountry;
|
||||
|
||||
|
@ -63,6 +72,7 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -76,6 +86,22 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
}
|
||||
};
|
||||
|
||||
private void navigationToMainActivityHandling() {
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
Runnable delayedAction = () -> {
|
||||
if(mCurrentCountry != null) {
|
||||
if(mCurrentCountry.present && !alreadyNavigating) {
|
||||
alreadyNavigating = true;
|
||||
mActivity.removeScreenBlock();
|
||||
Intent intent = new Intent(mActivity, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(mActivity, intent, null);
|
||||
}
|
||||
}
|
||||
};
|
||||
handler.postDelayed(delayedAction, 1000);
|
||||
}
|
||||
|
||||
private final MapManager.CurrentCountryChangedListener mCountryChangedListener = new MapManager.CurrentCountryChangedListener()
|
||||
{
|
||||
@Override
|
||||
|
@ -83,6 +109,7 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
{
|
||||
mCurrentCountry = (TextUtils.isEmpty(countryId) ? null : CountryItem.fill(countryId));
|
||||
updateState(true);
|
||||
stopIfNotTjk();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -91,18 +118,9 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
updateStateInternal(shouldAutoDownload);
|
||||
}
|
||||
|
||||
private static boolean isMapDownloading(@Nullable CountryItem country)
|
||||
{
|
||||
if (country == null) return false;
|
||||
|
||||
boolean enqueued = country.status == CountryItem.STATUS_ENQUEUED;
|
||||
boolean progress = country.status == CountryItem.STATUS_PROGRESS;
|
||||
boolean applying = country.status == CountryItem.STATUS_APPLYING;
|
||||
return enqueued || progress || applying;
|
||||
}
|
||||
|
||||
private void updateProgressState(boolean shouldAutoDownload)
|
||||
{
|
||||
navigationToMainActivityHandling();
|
||||
updateStateInternal(shouldAutoDownload);
|
||||
}
|
||||
|
||||
|
@ -118,26 +136,22 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
mCurrentCountry.status == CountryItem.STATUS_APPLYING);
|
||||
boolean failed = (mCurrentCountry.status == CountryItem.STATUS_FAILED);
|
||||
|
||||
|
||||
|
||||
showFrame = (enqueued || progress || failed ||
|
||||
mCurrentCountry.status == CountryItem.STATUS_DOWNLOADABLE);
|
||||
|
||||
if (showFrame)
|
||||
{
|
||||
boolean hasParent = !CountryItem.isRoot(mCurrentCountry.topmostParentId);
|
||||
|
||||
setUiForTjkDownload();
|
||||
UiUtils.showIf(progress || enqueued, mProgress);
|
||||
UiUtils.showIf(!progress && !enqueued, mButton);
|
||||
UiUtils.showIf(hasParent, mParent);
|
||||
|
||||
if (hasParent)
|
||||
mParent.setText(mCurrentCountry.topmostParentName);
|
||||
|
||||
mTitle.setText(mCurrentCountry.name);
|
||||
|
||||
String sizeText;
|
||||
|
||||
if (progress)
|
||||
{
|
||||
mActivity.blockScreen();
|
||||
mProgress.setPending(false);
|
||||
mProgress.setProgress(Math.round(mCurrentCountry.progress));
|
||||
sizeText = StringUtils.formatUsingSystemLocale("%1$s %2$.2f%%",
|
||||
|
@ -147,6 +161,7 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
{
|
||||
if (enqueued)
|
||||
{
|
||||
mActivity.blockScreen();
|
||||
sizeText = mActivity.getString(R.string.downloader_queued);
|
||||
mProgress.setPending(true);
|
||||
}
|
||||
|
@ -174,6 +189,7 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
|
||||
mButton.setText(failed ? R.string.downloader_retry
|
||||
: R.string.download);
|
||||
mActivity.removeScreenBlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,11 +200,22 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
UiUtils.showIf(showFrame, mFrame);
|
||||
}
|
||||
|
||||
public void stopIfNotTjk() {
|
||||
if(mCurrentCountry != null && !Objects.equals(mCurrentCountry.id, "Tajikistan")) {
|
||||
mActivity.goToTjk();
|
||||
Toast.makeText(mActivity, R.string.plz_dont_go_out_of_tjk, Toast.LENGTH_LONG).show();
|
||||
MapManager.nativeCancel(mCurrentCountry.id);
|
||||
}
|
||||
}
|
||||
|
||||
public void setUiForTjkDownload() {
|
||||
mTitle.setText(mActivity.getString(R.string.wait_tjk_map_downloading));
|
||||
}
|
||||
|
||||
public OnmapDownloader(MwmActivity activity)
|
||||
{
|
||||
mActivity = activity;
|
||||
mFrame = activity.findViewById(R.id.onmap_downloader);
|
||||
mParent = mFrame.findViewById(R.id.downloader_parent);
|
||||
mTitle = mFrame.findViewById(R.id.downloader_title);
|
||||
mSize = mFrame.findViewById(R.id.downloader_size);
|
||||
|
||||
|
@ -208,6 +235,7 @@ public class OnmapDownloader implements MwmActivity.LeftAnimationTrackListener
|
|||
if (mCurrentCountry == null)
|
||||
return;
|
||||
|
||||
mActivity.blockScreen();
|
||||
boolean retry = (mCurrentCountry.status == CountryItem.STATUS_FAILED);
|
||||
if (retry)
|
||||
{
|
||||
|
|
|
@ -43,6 +43,8 @@ public class MapButtonsController extends Fragment
|
|||
@Nullable
|
||||
private View mBottomButtonsFrame;
|
||||
@Nullable
|
||||
private View mBackButton;
|
||||
@Nullable
|
||||
private FloatingActionButton mToggleMapLayerButton;
|
||||
|
||||
@Nullable
|
||||
|
@ -81,6 +83,7 @@ public class MapButtonsController extends Fragment
|
|||
mInnerLeftButtonsFrame = mFrame.findViewById(R.id.map_buttons_inner_left);
|
||||
mInnerRightButtonsFrame = mFrame.findViewById(R.id.map_buttons_inner_right);
|
||||
mBottomButtonsFrame = mFrame.findViewById(R.id.map_buttons_bottom);
|
||||
mBackButton = mFrame.findViewById(R.id.back_btn);
|
||||
|
||||
final FloatingActionButton helpButton = mFrame.findViewById(R.id.help_button);
|
||||
if (helpButton != null)
|
||||
|
@ -99,8 +102,6 @@ public class MapButtonsController extends Fragment
|
|||
.setOnClickListener((v) -> mMapButtonClickListener.onMapButtonClick(MapButtons.zoomIn));
|
||||
mFrame.findViewById(R.id.nav_zoom_out)
|
||||
.setOnClickListener((v) -> mMapButtonClickListener.onMapButtonClick(MapButtons.zoomOut));
|
||||
final View bookmarksButton = mFrame.findViewById(R.id.btn_bookmarks);
|
||||
bookmarksButton.setOnClickListener((v) -> mMapButtonClickListener.onMapButtonClick(MapButtons.bookmarks));
|
||||
final View myPosition = mFrame.findViewById(R.id.my_position);
|
||||
mNavMyPosition = new MyPositionButton(myPosition, (v) -> mMapButtonClickListener.onMapButtonClick(MapButtons.myPosition));
|
||||
|
||||
|
@ -140,7 +141,6 @@ public class MapButtonsController extends Fragment
|
|||
mButtonsMap = new HashMap<>();
|
||||
mButtonsMap.put(MapButtons.zoom, zoomFrame);
|
||||
mButtonsMap.put(MapButtons.myPosition, myPosition);
|
||||
mButtonsMap.put(MapButtons.bookmarks, bookmarksButton);
|
||||
mButtonsMap.put(MapButtons.search, searchButton);
|
||||
|
||||
if (mToggleMapLayerButton != null)
|
||||
|
@ -154,6 +154,11 @@ public class MapButtonsController extends Fragment
|
|||
UiUtils.setViewInsetsPadding(view, windowInsets);
|
||||
return windowInsets;
|
||||
});
|
||||
|
||||
if(mBackButton != null) {
|
||||
mBackButton.setOnClickListener((v) -> activity.getOnBackPressedDispatcher().onBackPressed());
|
||||
}
|
||||
|
||||
return mFrame;
|
||||
}
|
||||
|
||||
|
|
|
@ -527,18 +527,10 @@ public class PlacePageController extends Fragment implements
|
|||
// And move the bookmark button at the end
|
||||
if (needToShowRoutingButtons && RoutingController.get().isStopPointAllowed())
|
||||
buttons.add(PlacePageButtons.ButtonType.ROUTE_ADD);
|
||||
else
|
||||
buttons.add(mapObject.isBookmark()
|
||||
? PlacePageButtons.ButtonType.BOOKMARK_DELETE
|
||||
: PlacePageButtons.ButtonType.BOOKMARK_SAVE);
|
||||
|
||||
if (needToShowRoutingButtons)
|
||||
{
|
||||
buttons.add(PlacePageButtons.ButtonType.ROUTE_TO);
|
||||
if (RoutingController.get().isStopPointAllowed())
|
||||
buttons.add(mapObject.isBookmark()
|
||||
? PlacePageButtons.ButtonType.BOOKMARK_DELETE
|
||||
: PlacePageButtons.ButtonType.BOOKMARK_SAVE);
|
||||
}
|
||||
}
|
||||
mViewModel.setCurrentButtons(buttons);
|
||||
|
|
|
@ -423,19 +423,6 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
|
|||
|
||||
// showTaxiOffer(mapObject);
|
||||
|
||||
if (RoutingController.get().isNavigating() || RoutingController.get().isPlanning())
|
||||
{
|
||||
UiUtils.hide(mEditPlace, mAddOrganisation, mAddPlace, mEditTopSpace);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiUtils.showIf(Editor.nativeShouldShowEditPlace(), mEditPlace);
|
||||
UiUtils.showIf(Editor.nativeShouldShowAddBusiness(), mAddOrganisation);
|
||||
UiUtils.showIf(Editor.nativeShouldShowAddPlace(), mAddPlace);
|
||||
UiUtils.showIf(UiUtils.isVisible(mEditPlace)
|
||||
|| UiUtils.isVisible(mAddOrganisation)
|
||||
|| UiUtils.isVisible(mAddPlace), mEditTopSpace);
|
||||
}
|
||||
updateLinksView();
|
||||
updateOpeningHoursView();
|
||||
updateWikipediaView();
|
||||
|
|
64
android/app/src/main/java/app/tourism/AuthActivity.kt
Normal file
|
@ -0,0 +1,64 @@
|
|||
package app.tourism
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.organicmaps.R
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.repositories.PlacesRepository
|
||||
import app.tourism.ui.screens.auth.AuthNavigation
|
||||
import app.tourism.ui.theme.OrganicMapsTheme
|
||||
import app.tourism.utils.LocaleHelper
|
||||
import app.tourism.utils.MapFileMover
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AuthActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var placesRepository: PlacesRepository
|
||||
|
||||
@Inject
|
||||
lateinit var userPreferences: UserPreferences
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
val languageCode = UserPreferences(newBase).getLanguage()?.code
|
||||
super.attachBaseContext(LocaleHelper.localeUpdateResources(newBase, languageCode ?: "ru"))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
lifecycleScope.launch {
|
||||
if (!userPreferences.getIsEverythingSetup()) {
|
||||
if (!MapFileMover().main(this@AuthActivity)) return@launch
|
||||
userPreferences.setIsEverythingSetup(true)
|
||||
}
|
||||
}
|
||||
|
||||
val blackest = resources.getColor(R.color.button_text) // yes, I know
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.dark(blackest),
|
||||
navigationBarStyle = SystemBarStyle.dark(blackest)
|
||||
)
|
||||
setContent {
|
||||
OrganicMapsTheme() {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Column(modifier = Modifier.padding(innerPadding)) {
|
||||
AuthNavigation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
78
android/app/src/main/java/app/tourism/Constants.kt
Normal file
|
@ -0,0 +1,78 @@
|
|||
package app.tourism
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.organicmaps.R
|
||||
import app.tourism.ui.theme.getBorderColor
|
||||
|
||||
const val TAG = "GLOBAL_TAG"
|
||||
const val BASE_URL = "https://tourismmap.tj"
|
||||
const val DEBUG_BASE_URL = "https://test.tourismmap.tj/"
|
||||
|
||||
object Constants {
|
||||
// UI
|
||||
val SCREEN_PADDING = 16.dp
|
||||
val USUAL_WINDOW_INSETS = WindowInsets(
|
||||
left = SCREEN_PADDING,
|
||||
right = SCREEN_PADDING,
|
||||
bottom = 0.dp,
|
||||
top = 0.dp
|
||||
)
|
||||
|
||||
// image loading
|
||||
const val IMAGE_URL_EXAMPLE =
|
||||
"https://img.freepik.com/free-photo/young-woman-hiker-taking-photo-with-smartphone-on-mountains-peak-in-winter_335224-427.jpg?w=2000"
|
||||
const val THUMBNAIL_URL_EXAMPLE =
|
||||
"https://render.fineartamerica.com/images/images-profile-flow/400/images-medium-large-5/awesome-solitude-bess-hamiti.jpg"
|
||||
const val LOGO_URL_EXAMPLE = "https://brandeps.com/logo-download/O/OSCE-logo-vector-01.svg"
|
||||
|
||||
// data
|
||||
val categories = mapOf(
|
||||
"sights" to R.string.sights,
|
||||
"restaurants" to R.string.restaurants,
|
||||
"hotels_tourism" to R.string.hotels_tourism,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.applyAppBorder() = this
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = getBorderColor(),
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
|
||||
@Composable
|
||||
fun Modifier.drawOverlayForTextBehind() =
|
||||
this.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.9f),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun Modifier.drawDarkContainerBehind(): Modifier {
|
||||
val alpha = 0.4f
|
||||
return this
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Black.copy(alpha = alpha),
|
||||
Color.Black.copy(alpha = alpha)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
172
android/app/src/main/java/app/tourism/ImagesDownloadService.kt
Normal file
|
@ -0,0 +1,172 @@
|
|||
package app.tourism
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.organicmaps.R
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.repositories.PlacesRepository
|
||||
import app.tourism.domain.models.resource.DownloadProgress
|
||||
import app.tourism.utils.LocaleHelper
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val STOP_SERVICE_ACTION = "app.tourism.STOP_SERVICE"
|
||||
private const val CHANNEL_ID = "images_download_channel"
|
||||
private const val PROGRESS_NOTIFICATION_ID = 1
|
||||
private const val SUMMARY_NOTIFICATION_ID = 2
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ImagesDownloadService : LifecycleService() {
|
||||
|
||||
|
||||
@Inject
|
||||
lateinit var placesRepository: PlacesRepository
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
val languageCode = UserPreferences(newBase).getLanguage()?.code
|
||||
super.attachBaseContext(LocaleHelper.localeUpdateResources(newBase, languageCode ?: "ru"))
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
// Stops the service when cancel button is clicked
|
||||
if (intent?.action == STOP_SERVICE_ACTION) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
// downloading all images
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
placesRepository.downloadAllImages().collectLatest { progress ->
|
||||
updateNotification(progress)
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val name = getString(R.string.channel_name)
|
||||
val descriptionText = getString(R.string.channel_description)
|
||||
val importance = NotificationManager.IMPORTANCE_HIGH
|
||||
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
|
||||
description = descriptionText
|
||||
}
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification(downloadProgress: DownloadProgress) {
|
||||
var shouldStopSelf = false
|
||||
|
||||
val stopIntent = Intent(this, ImagesDownloadService::class.java).apply {
|
||||
action = STOP_SERVICE_ACTION
|
||||
}
|
||||
val stopPendingIntent = PendingIntent.getService(
|
||||
this, 0, stopIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_download)
|
||||
.setContentTitle(getString(R.string.downloading_images))
|
||||
.setSilent(true)
|
||||
.addAction(R.drawable.ic_cancel, getString(R.string.cancel), stopPendingIntent)
|
||||
|
||||
val groupKey = "images_download_group"
|
||||
val summaryNotification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_download)
|
||||
.setContentTitle(getString(R.string.downloading_images))
|
||||
.setStyle(NotificationCompat.InboxStyle())
|
||||
.setGroup(groupKey)
|
||||
.setGroupSummary(true)
|
||||
|
||||
when (downloadProgress) {
|
||||
is DownloadProgress.Loading -> {
|
||||
downloadProgress.stats?.let { stats ->
|
||||
val statsInString =
|
||||
"${stats.filesDownloaded}/${stats.filesTotalNum} (${stats.percentagesCompleted}%)"
|
||||
|
||||
builder.setContentText("${getString(R.string.images_downloaded)}: $statsInString")
|
||||
builder.setProgress(100, stats.percentagesCompleted, false)
|
||||
}
|
||||
}
|
||||
|
||||
is DownloadProgress.Finished -> {
|
||||
downloadProgress.stats?.let { stats ->
|
||||
val statsInString =
|
||||
"${stats.filesDownloaded}/${stats.filesTotalNum} (${stats.percentagesCompleted}%)"
|
||||
|
||||
if (stats.percentagesCompleted == 100) {
|
||||
summaryNotification.setContentTitle("${getString(R.string.all_images_were_downloaded)}: $statsInString")
|
||||
summaryNotification.setContentText(null)
|
||||
} else if (stats.percentagesCompleted >= 95) {
|
||||
summaryNotification.setContentTitle("${getString(R.string.most_images_were_downloaded)}: $statsInString")
|
||||
summaryNotification.setContentText(null)
|
||||
} else {
|
||||
summaryNotification.setContentTitle("${getString(R.string.not_all_images_were_downloaded)}: $statsInString")
|
||||
summaryNotification.setContentText(null)
|
||||
}
|
||||
}
|
||||
notificationManager.cancel(PROGRESS_NOTIFICATION_ID)
|
||||
shouldStopSelf = true
|
||||
}
|
||||
|
||||
is DownloadProgress.Error -> {
|
||||
summaryNotification.setContentTitle(
|
||||
downloadProgress.message ?: getString(R.string.smth_went_wrong)
|
||||
)
|
||||
summaryNotification.setContentText("")
|
||||
summaryNotification.setProgress(0, 0, false)
|
||||
|
||||
notificationManager.cancel(PROGRESS_NOTIFICATION_ID)
|
||||
shouldStopSelf = true
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val notificationPermission =
|
||||
ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
if (notificationPermission != PackageManager.PERMISSION_GRANTED)
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldStopSelf) {
|
||||
lifecycleScope.launch {
|
||||
notificationManager.notify(SUMMARY_NOTIFICATION_ID, summaryNotification.build())
|
||||
delay(1000L) // Delay to ensure notification is shown
|
||||
stopSelf()
|
||||
}
|
||||
} else {
|
||||
notificationManager.notify(PROGRESS_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
}
|
121
android/app/src/main/java/app/tourism/MainActivity.kt
Normal file
|
@ -0,0 +1,121 @@
|
|||
package app.tourism
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.organicmaps.DownloadResourcesLegacyActivity
|
||||
import app.organicmaps.R
|
||||
import app.organicmaps.downloader.CountryItem
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.remote.WifiReceiver
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import app.tourism.ui.screens.main.MainSection
|
||||
import app.tourism.ui.screens.main.ThemeViewModel
|
||||
import app.tourism.ui.screens.main.profile.profile.ProfileViewModel
|
||||
import app.tourism.ui.theme.OrganicMapsTheme
|
||||
import app.tourism.utils.LocaleHelper
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val wifiReceiver = WifiReceiver()
|
||||
|
||||
@Inject
|
||||
lateinit var userPreferences: UserPreferences
|
||||
|
||||
private val themeVM: ThemeViewModel by viewModels<ThemeViewModel>()
|
||||
private val profileVM: ProfileViewModel by viewModels<ProfileViewModel>()
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
val languageCode = UserPreferences(newBase).getLanguage()?.code
|
||||
super.attachBaseContext(LocaleHelper.localeUpdateResources(newBase, languageCode ?: "ru"))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val intentFilter = IntentFilter()
|
||||
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION)
|
||||
registerReceiver(wifiReceiver, intentFilter)
|
||||
|
||||
navigateToAuthIfNotAuthed()
|
||||
navigateToMapToDownloadIfNotPresent()
|
||||
|
||||
val blackest = resources.getColor(R.color.button_text) // yes, I know
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.dark(blackest),
|
||||
navigationBarStyle = SystemBarStyle.dark(blackest)
|
||||
)
|
||||
|
||||
setContent {
|
||||
val isDark = themeVM.theme.collectAsState().value?.code == "dark"
|
||||
|
||||
OrganicMapsTheme(darkTheme = isDark) {
|
||||
MainSection(themeVM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToMapToDownloadIfNotPresent() {
|
||||
if (userPreferences.getIsEverythingSetup()) {
|
||||
val mCurrentCountry = CountryItem.fill("Tajikistan")
|
||||
if (!mCurrentCountry.present) {
|
||||
val intent = Intent(this, DownloadResourcesLegacyActivity::class.java)
|
||||
startActivity(this, intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToAuthIfNotAuthed() {
|
||||
val token = userPreferences.getToken()
|
||||
if (token.isNullOrEmpty()) {
|
||||
navigateToAuth()
|
||||
return
|
||||
}
|
||||
|
||||
profileVM.getPersonalData()
|
||||
lifecycleScope.launch {
|
||||
profileVM.profileDataResource.collectLatest {
|
||||
if (it is Resource.Success) {
|
||||
it.data?.apply {
|
||||
language?.let { lang ->
|
||||
userPreferences.setLanguage(lang)
|
||||
}
|
||||
theme?.let { theme ->
|
||||
themeVM.setTheme(theme)
|
||||
}
|
||||
userPreferences.setUserId(id.toString())
|
||||
}
|
||||
}
|
||||
if (it is Resource.Error) {
|
||||
if (it.message?.contains("unauth", ignoreCase = true) == true)
|
||||
navigateToAuth()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToAuth() {
|
||||
val intent = Intent(this, AuthActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(this, intent, null)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterReceiver(wifiReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
13
android/app/src/main/java/app/tourism/data/db/Converters.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package app.tourism.data.db
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun fromList(value : List<String>) = Json.encodeToString(value)
|
||||
|
||||
@TypeConverter
|
||||
fun toList(value: String) = Json.decodeFromString<List<String>>(value)
|
||||
}
|
39
android/app/src/main/java/app/tourism/data/db/Database.kt
Normal file
|
@ -0,0 +1,39 @@
|
|||
package app.tourism.data.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import app.tourism.data.db.dao.CurrencyRatesDao
|
||||
import app.tourism.data.db.dao.HashesDao
|
||||
import app.tourism.data.db.dao.ImagesToDownloadDao
|
||||
import app.tourism.data.db.dao.PlacesDao
|
||||
import app.tourism.data.db.dao.ReviewsDao
|
||||
import app.tourism.data.db.entities.CurrencyRatesEntity
|
||||
import app.tourism.data.db.entities.FavoriteSyncEntity
|
||||
import app.tourism.data.db.entities.HashEntity
|
||||
import app.tourism.data.db.entities.ImageToDownloadEntity
|
||||
import app.tourism.data.db.entities.PlaceEntity
|
||||
import app.tourism.data.db.entities.ReviewEntity
|
||||
import app.tourism.data.db.entities.ReviewPlannedToPostEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
PlaceEntity::class,
|
||||
ReviewEntity::class,
|
||||
ReviewPlannedToPostEntity::class,
|
||||
FavoriteSyncEntity::class,
|
||||
HashEntity::class,
|
||||
CurrencyRatesEntity::class,
|
||||
ImageToDownloadEntity::class
|
||||
],
|
||||
version = 2,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class Database : RoomDatabase() {
|
||||
abstract val currencyRatesDao: CurrencyRatesDao
|
||||
abstract val placesDao: PlacesDao
|
||||
abstract val hashesDao: HashesDao
|
||||
abstract val reviewsDao: ReviewsDao
|
||||
abstract val imagesToDownloadDao: ImagesToDownloadDao
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package app.tourism.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import app.tourism.data.db.entities.CurrencyRatesEntity
|
||||
|
||||
@Dao
|
||||
interface CurrencyRatesDao {
|
||||
|
||||
@Query("SELECT * FROM currency_rates")
|
||||
suspend fun getCurrencyRates(): CurrencyRatesEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun updateCurrencyRates(entity: CurrencyRatesEntity)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package app.tourism.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import app.tourism.data.db.entities.HashEntity
|
||||
|
||||
@Dao
|
||||
interface HashesDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertHash(hash: HashEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertHashes(hashes: List<HashEntity>)
|
||||
|
||||
@Query("SELECT * FROM hashes WHERE categoryId = :id")
|
||||
suspend fun getHash(id: Long): HashEntity?
|
||||
|
||||
@Query("SELECT * FROM hashes")
|
||||
suspend fun getHashes(): List<HashEntity>
|
||||
|
||||
@Query("DELETE FROM hashes")
|
||||
suspend fun deleteHashes()
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package app.tourism.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import app.tourism.data.db.entities.ImageToDownloadEntity
|
||||
|
||||
@Dao
|
||||
interface ImagesToDownloadDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertImages(places: List<ImageToDownloadEntity>)
|
||||
|
||||
@Query("UPDATE images_to_download SET downloaded = :downloaded WHERE url = :url")
|
||||
suspend fun markAsDownloaded(url: String, downloaded: Boolean)
|
||||
|
||||
@Query("UPDATE images_to_download SET downloaded = 0")
|
||||
suspend fun markAllImagesAsNotDownloaded()
|
||||
|
||||
@Query("SELECT * FROM images_to_download")
|
||||
suspend fun getAllImages(): List<ImageToDownloadEntity>
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package app.tourism.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import app.tourism.data.db.entities.FavoriteSyncEntity
|
||||
import app.tourism.data.db.entities.PlaceEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface PlacesDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertPlaces(places: List<PlaceEntity>)
|
||||
|
||||
@Query("DELETE FROM places WHERE id IN (:idsList)")
|
||||
suspend fun deletePlaces(idsList: List<Long>)
|
||||
|
||||
@Query("DELETE FROM places")
|
||||
suspend fun deleteAllPlaces()
|
||||
|
||||
@Query("DELETE FROM places WHERE categoryId = :categoryId AND language =:language")
|
||||
suspend fun deleteAllPlacesByCategory(categoryId: Long, language: String)
|
||||
|
||||
@Query("SELECT * FROM places WHERE UPPER(name) LIKE UPPER(:q) AND language =:language")
|
||||
fun search(q: String = "", language: String): Flow<List<PlaceEntity>>
|
||||
|
||||
@Query("SELECT * FROM places WHERE categoryId = :categoryId AND language =:language ORDER BY rating DESC, name ASC")
|
||||
fun getSortedPlacesByCategoryIdFlow(categoryId: Long, language: String): Flow<List<PlaceEntity>>
|
||||
|
||||
@Query("SELECT * FROM places WHERE categoryId = :categoryId AND language =:language")
|
||||
fun getPlacesByCategoryIdNotFlow(categoryId: Long, language: String): List<PlaceEntity>
|
||||
|
||||
@Query("SELECT * FROM places WHERE categoryId =:categoryId AND language =:language ORDER BY rating DESC LIMIT 15")
|
||||
fun getTopPlacesByCategoryId(categoryId: Long, language: String): Flow<List<PlaceEntity>>
|
||||
|
||||
@Query("SELECT * FROM places WHERE id = :placeId AND language =:language")
|
||||
fun getPlaceById(placeId: Long, language: String): Flow<PlaceEntity?>
|
||||
|
||||
@Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q) AND language =:language")
|
||||
fun getFavoritePlacesFlow(q: String = "", language: String): Flow<List<PlaceEntity>>
|
||||
|
||||
@Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q) AND language =:language")
|
||||
fun getFavoritePlaces(q: String = "", language: String): List<PlaceEntity>
|
||||
|
||||
@Query("UPDATE places SET isFavorite = :isFavorite WHERE id = :placeId")
|
||||
suspend fun setFavorite(placeId: Long, isFavorite: Boolean)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun addFavoriteSync(favoriteSyncEntity: FavoriteSyncEntity)
|
||||
|
||||
@Query("DELETE FROM favorites_to_sync WHERE placeId = :placeId")
|
||||
suspend fun removeFavoriteSync(placeId: Long)
|
||||
|
||||
@Query("DELETE FROM favorites_to_sync WHERE placeId in (:placeId)")
|
||||
suspend fun removeFavoriteSync(placeId: List<Long>)
|
||||
|
||||
@Query("SELECT * FROM favorites_to_sync")
|
||||
fun getFavoriteSyncData(): List<FavoriteSyncEntity>
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package app.tourism.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import app.tourism.data.db.entities.ReviewEntity
|
||||
import app.tourism.data.db.entities.ReviewPlannedToPostEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface ReviewsDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertReview(review: ReviewEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertReviews(reviews: List<ReviewEntity>)
|
||||
|
||||
@Query("DELETE FROM reviews WHERE id = :id")
|
||||
suspend fun deleteReview(id: Long)
|
||||
|
||||
@Query("DELETE FROM reviews WHERE id in (:idsList)")
|
||||
suspend fun deleteReviews(idsList: List<Long>)
|
||||
|
||||
@Query("DELETE FROM reviews")
|
||||
suspend fun deleteAllReviews()
|
||||
|
||||
@Query("DELETE FROM reviews WHERE placeId = :placeId")
|
||||
suspend fun deleteAllPlaceReviews(placeId: Long)
|
||||
|
||||
@Query("SELECT * FROM reviews WHERE placeId = :placeId")
|
||||
fun getReviewsForPlace(placeId: Long): Flow<List<ReviewEntity>>
|
||||
|
||||
@Query("UPDATE reviews SET deletionPlanned = :deletionPlanned WHERE id = :id")
|
||||
fun markReviewForDeletion(id: Long, deletionPlanned: Boolean = true)
|
||||
|
||||
@Query("SELECT * FROM reviews WHERE deletionPlanned = 1")
|
||||
fun getReviewsPlannedForDeletion(): List<ReviewEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertReviewPlannedToPost(review: ReviewPlannedToPostEntity)
|
||||
|
||||
@Query("DELETE FROM reviews_planned_to_post WHERE placeId = :placeId")
|
||||
suspend fun deleteReviewPlannedToPost(placeId: Long)
|
||||
|
||||
@Query("SELECT * FROM reviews_planned_to_post")
|
||||
fun getReviewsPlannedToPost(): List<ReviewPlannedToPostEntity>
|
||||
|
||||
@Query("SELECT * FROM reviews_planned_to_post WHERE placeId = :placeId")
|
||||
fun getReviewsPlannedToPostFlow(placeId: Long): Flow<List<ReviewPlannedToPostEntity>>
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package app.tourism.data.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import app.tourism.domain.models.profile.CurrencyRates
|
||||
|
||||
@Entity(tableName = "currency_rates")
|
||||
data class CurrencyRatesEntity(
|
||||
@PrimaryKey
|
||||
val id: Long,
|
||||
val usd: Double,
|
||||
val eur: Double,
|
||||
val rub: Double,
|
||||
) {
|
||||
fun toCurrencyRates() = CurrencyRates(usd, eur, rub)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package app.tourism.data.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "favorites_to_sync")
|
||||
data class FavoriteSyncEntity(
|
||||
@PrimaryKey val placeId: Long,
|
||||
val isFavorite: Boolean,
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
package app.tourism.data.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "hashes")
|
||||
data class HashEntity(
|
||||
@PrimaryKey val categoryId: Long,
|
||||
val value: String,
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
package app.tourism.data.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "images_to_download")
|
||||
data class ImageToDownloadEntity(
|
||||
@PrimaryKey
|
||||
val url: String,
|
||||
val downloaded: Boolean
|
||||
)
|
|
@ -0,0 +1,51 @@
|
|||
package app.tourism.data.db.entities
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import app.tourism.data.dto.PlaceLocation
|
||||
import app.tourism.domain.models.common.PlaceShort
|
||||
import app.tourism.domain.models.details.PlaceFull
|
||||
|
||||
@Entity(tableName = "places", primaryKeys = ["id", "language"] )
|
||||
data class PlaceEntity(
|
||||
val id: Long,
|
||||
val categoryId: Long,
|
||||
val name: String,
|
||||
val excerpt: String,
|
||||
val description: String,
|
||||
val cover: String,
|
||||
val gallery: List<String>,
|
||||
@Embedded val coordinates: CoordinatesEntity?,
|
||||
val rating: Double,
|
||||
val isFavorite: Boolean,
|
||||
val language: String,
|
||||
) {
|
||||
fun toPlaceFull() = PlaceFull(
|
||||
id = id,
|
||||
name = name,
|
||||
rating = rating,
|
||||
excerpt = excerpt,
|
||||
description = description,
|
||||
placeLocation = coordinates?.toPlaceLocation(name),
|
||||
cover = cover,
|
||||
pics = gallery,
|
||||
isFavorite = isFavorite,
|
||||
language = language
|
||||
)
|
||||
|
||||
fun toPlaceShort() = PlaceShort(
|
||||
id = id,
|
||||
name = name,
|
||||
cover = cover,
|
||||
rating = rating,
|
||||
excerpt = excerpt,
|
||||
isFavorite = isFavorite
|
||||
)
|
||||
}
|
||||
|
||||
data class CoordinatesEntity(
|
||||
val latitude: Double,
|
||||
val longitude: Double
|
||||
) {
|
||||
fun toPlaceLocation(name: String) = PlaceLocation(name, latitude, longitude)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package app.tourism.data.db.entities
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import app.tourism.domain.models.details.Review
|
||||
|
||||
@Entity(tableName = "reviews")
|
||||
data class ReviewEntity(
|
||||
@PrimaryKey val id: Long,
|
||||
val placeId: Long,
|
||||
@Embedded val user: JustUser,
|
||||
val comment: String,
|
||||
val date: String,
|
||||
val rating: Int,
|
||||
val images: List<String>,
|
||||
val deletionPlanned: Boolean = false,
|
||||
) {
|
||||
fun toReview() = Review(
|
||||
id = id,
|
||||
placeId = placeId,
|
||||
rating = rating,
|
||||
user = user.toUser(),
|
||||
date = date,
|
||||
comment = comment,
|
||||
picsUrls = images,
|
||||
deletionPlanned = deletionPlanned,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package app.tourism.data.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import app.tourism.domain.models.details.ReviewToPost
|
||||
import java.io.File
|
||||
|
||||
@Entity(tableName = "reviews_planned_to_post")
|
||||
data class ReviewPlannedToPostEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long? = null,
|
||||
val placeId: Long,
|
||||
val comment: String,
|
||||
val rating: Int,
|
||||
val images: List<String>,
|
||||
) {
|
||||
fun toReviewsPlannedToPostDto(): ReviewToPost {
|
||||
val imageFiles = images.map { File(it) }
|
||||
|
||||
return ReviewToPost(
|
||||
placeId, comment, rating, imageFiles
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package app.tourism.data.db.entities
|
||||
|
||||
import app.tourism.domain.models.details.User
|
||||
|
||||
data class JustUser(
|
||||
val userId: Long,
|
||||
val fullName: String,
|
||||
val avatar: String?,
|
||||
val country: String
|
||||
) {
|
||||
fun toUser() = User(id = userId, name = fullName, pfpUrl = avatar, countryCodeName = country)
|
||||
}
|
16
android/app/src/main/java/app/tourism/data/dto/AllDataDto.kt
Normal file
|
@ -0,0 +1,16 @@
|
|||
package app.tourism.data.dto
|
||||
|
||||
import app.tourism.data.dto.place.PlaceDto
|
||||
|
||||
data class AllDataDto(
|
||||
val attractions_ru: List<PlaceDto>,
|
||||
val attractions_en: List<PlaceDto>,
|
||||
val restaurants_ru: List<PlaceDto>,
|
||||
val restaurants_en: List<PlaceDto>,
|
||||
val accommodations_ru: List<PlaceDto>,
|
||||
val accommodations_en: List<PlaceDto>,
|
||||
val attractions_hash: String,
|
||||
val restaurants_hash: String,
|
||||
val accommodations_hash: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package app.tourism.data.dto
|
||||
|
||||
import app.tourism.data.dto.place.PlaceDto
|
||||
|
||||
data class CategoryDto(
|
||||
val hash: String,
|
||||
val ru: List<PlaceDto>,
|
||||
val en: List<PlaceDto>,
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package app.tourism.data.dto
|
||||
|
||||
import app.tourism.data.dto.place.PlaceDto
|
||||
|
||||
data class FavoritesDto(
|
||||
val ru: List<PlaceDto>,
|
||||
val en: List<PlaceDto>,
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
package app.tourism.data.dto
|
||||
|
||||
data class FavoritesIdsDto(
|
||||
val marks: List<Long>
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.data.dto
|
||||
|
||||
data class HashDto(val hash: String)
|
|
@ -0,0 +1,16 @@
|
|||
package app.tourism.data.dto
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.organicmaps.bookmarks.data.FeatureId
|
||||
import app.organicmaps.bookmarks.data.MapObject
|
||||
import app.tourism.data.db.entities.CoordinatesEntity
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class PlaceLocation(val name: String, val lat: Double, val lon: Double) : Parcelable {
|
||||
fun toMapObject() = MapObject.createMapObject(
|
||||
FeatureId.EMPTY, MapObject.POI, name, "", lat, lon
|
||||
);
|
||||
|
||||
fun toCoordinatesEntity() = CoordinatesEntity(lat, lon)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package app.tourism.data.dto.auth
|
||||
|
||||
import app.tourism.domain.models.auth.AuthResponse
|
||||
|
||||
data class AuthResponseDto(
|
||||
val token: String,
|
||||
) {
|
||||
fun toAuthResponse() = AuthResponse(
|
||||
token = token
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.data.dto.auth
|
||||
|
||||
data class EmailBodyDto(val email: String)
|
|
@ -0,0 +1,11 @@
|
|||
package app.tourism.data.dto.currency
|
||||
|
||||
import app.tourism.data.db.entities.CurrencyRatesEntity
|
||||
import app.tourism.domain.models.profile.CurrencyRates
|
||||
|
||||
data class CurrencyRatesDataDto(val data: CurrencyRatesDto) {
|
||||
fun toCurrencyRates() = CurrencyRates(data.usd, data.eur, data.rub)
|
||||
fun toCurrencyRatesEntity() = CurrencyRatesEntity(1, data.usd, data.eur, data.rub)
|
||||
}
|
||||
|
||||
data class CurrencyRatesDto(val usd: Double, val eur: Double, val rub: Double)
|
|
@ -0,0 +1,21 @@
|
|||
package app.tourism.data.dto.place
|
||||
|
||||
import app.tourism.data.dto.PlaceLocation
|
||||
|
||||
data class CoordinatesDto(
|
||||
val latitude: String?,
|
||||
val longitude: String?
|
||||
) {
|
||||
fun toPlaceLocation(name: String): PlaceLocation? {
|
||||
try {
|
||||
return PlaceLocation(
|
||||
name,
|
||||
latitude!!.toDouble(),
|
||||
longitude!!.toDouble()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package app.tourism.data.dto.place
|
||||
|
||||
import app.tourism.domain.models.details.PlaceFull
|
||||
|
||||
data class PlaceDto(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val coordinates: CoordinatesDto?,
|
||||
val cover: String,
|
||||
val feedbacks: List<ReviewDto>?,
|
||||
val gallery: List<String>,
|
||||
val rating: String,
|
||||
val short_description: String,
|
||||
val long_description: String,
|
||||
) {
|
||||
fun toPlaceFull(isFavorite: Boolean, language: String) = PlaceFull(
|
||||
id = id,
|
||||
name = name,
|
||||
rating = rating.toDouble(),
|
||||
excerpt = short_description,
|
||||
description = long_description,
|
||||
placeLocation = coordinates?.toPlaceLocation(name),
|
||||
cover = cover,
|
||||
pics = gallery,
|
||||
isFavorite = isFavorite,
|
||||
reviews = feedbacks?.map { it.toReview() } ?: emptyList(),
|
||||
language = language,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package app.tourism.data.dto.place
|
||||
|
||||
import app.tourism.domain.models.details.Review
|
||||
|
||||
data class ReviewDto(
|
||||
val id: Long,
|
||||
val mark_id: Long,
|
||||
val images: List<String>,
|
||||
val message: String,
|
||||
val points: Int,
|
||||
val created_at: String,
|
||||
val user: UserDto
|
||||
) {
|
||||
fun toReview() = Review(
|
||||
id = id,
|
||||
placeId = mark_id,
|
||||
rating = points,
|
||||
user = user.toUser(),
|
||||
date = created_at,
|
||||
comment = message,
|
||||
picsUrls = images
|
||||
)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package app.tourism.data.dto.place
|
||||
|
||||
data class ReviewIdsDto(
|
||||
val feedbacks: List<Long>,
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.data.dto.place
|
||||
|
||||
data class ReviewsDto(val data: List<ReviewDto>)
|
|
@ -0,0 +1,18 @@
|
|||
package app.tourism.data.dto.place
|
||||
|
||||
import app.tourism.domain.models.details.User
|
||||
|
||||
|
||||
data class UserDto(
|
||||
val id: Long,
|
||||
val avatar: String,
|
||||
val country: String,
|
||||
val full_name: String,
|
||||
) {
|
||||
fun toUser() = User(
|
||||
id = id,
|
||||
name = full_name,
|
||||
countryCodeName = country,
|
||||
pfpUrl = avatar,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.data.dto.profile
|
||||
|
||||
data class LanguageDto(val language: String)
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.data.dto.profile
|
||||
|
||||
data class ThemeDto(val theme: String)
|
|
@ -0,0 +1,24 @@
|
|||
package app.tourism.data.dto.profile
|
||||
|
||||
import app.tourism.domain.models.profile.PersonalData
|
||||
|
||||
data class User(
|
||||
val id: Long,
|
||||
val avatar: String?,
|
||||
val country: String,
|
||||
val full_name: String,
|
||||
val email: String,
|
||||
val language: String?,
|
||||
val theme: String?,
|
||||
val username: String
|
||||
) {
|
||||
fun toPersonalData() = PersonalData(
|
||||
id = id,
|
||||
fullName = full_name,
|
||||
country = country,
|
||||
pfpUrl = avatar,
|
||||
email = email,
|
||||
language = language,
|
||||
theme = theme,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.data.dto.profile
|
||||
|
||||
data class UserData(val data: User)
|
|
@ -0,0 +1,53 @@
|
|||
package app.tourism.data.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import app.organicmaps.R
|
||||
|
||||
class UserPreferences(context: Context) {
|
||||
private var sharedPref: SharedPreferences =
|
||||
context.getSharedPreferences("user", Context.MODE_PRIVATE)
|
||||
|
||||
val languages = listOf(
|
||||
Language(code = "ru", name = "Русский"),
|
||||
Language(code = "en", name = "English")
|
||||
)
|
||||
|
||||
val themes = listOf(
|
||||
Theme(code = "dark", name = context.getString(R.string.dark_theme)),
|
||||
Theme(code = "light", name = context.getString(R.string.light_theme)),
|
||||
)
|
||||
|
||||
fun getLanguage(): Language? {
|
||||
val languageCode = sharedPref.getString("language", null)
|
||||
return languages.firstOrNull() { it.code == languageCode }
|
||||
}
|
||||
|
||||
fun setLanguage(value: String) = sharedPref.edit { putString("language", value) }
|
||||
|
||||
fun getTheme(): Theme? {
|
||||
val themeCode = sharedPref.getString("theme", "")
|
||||
return themes.firstOrNull() { it.code == themeCode }
|
||||
}
|
||||
|
||||
fun setTheme(value: String?) = sharedPref.edit { putString("theme", value) }
|
||||
|
||||
fun getToken() = sharedPref.getString("token", "")
|
||||
fun setToken(value: String?) = sharedPref.edit { putString("token", value) }
|
||||
|
||||
fun getUserId() = sharedPref.getString("user_id", "")
|
||||
fun setUserId(value: String?) = sharedPref.edit { putString("user_id", value) }
|
||||
|
||||
fun getIsEverythingSetup() = sharedPref.getBoolean("is_everything_setup", false)
|
||||
fun setIsEverythingSetup(value: Boolean) =
|
||||
sharedPref.edit { putBoolean("is_everything_setup", value) }
|
||||
|
||||
fun getShouldSyncLanguage() = sharedPref.getBoolean("should_sync_language", false)
|
||||
fun setShouldSyncLanguage(value: Boolean) =
|
||||
sharedPref.edit { putBoolean("should_sync_language", value) }
|
||||
}
|
||||
|
||||
data class Language(val code: String, val name: String)
|
||||
|
||||
data class Theme(val code: String, val name: String)
|
|
@ -0,0 +1,11 @@
|
|||
package app.tourism.data.remote
|
||||
|
||||
import app.tourism.data.dto.currency.CurrencyRatesDataDto
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface CurrencyApi {
|
||||
|
||||
@GET("currency")
|
||||
suspend fun getCurrency(): Response<CurrencyRatesDataDto>
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package app.tourism.data.remote
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import app.organicmaps.R
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONException
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
suspend inline fun <T, reified Re> FlowCollector<Resource<Re>>.handleGenericCall(
|
||||
call: () -> Response<T>,
|
||||
mapper: (T) -> Re,
|
||||
context: Context,
|
||||
emitLoadingStatusBeforeCall: Boolean = true
|
||||
) {
|
||||
if (emitLoadingStatusBeforeCall) emit(Resource.Loading())
|
||||
try {
|
||||
val response = call()
|
||||
val body = response.body()?.let { mapper(it) }
|
||||
if (response.isSuccessful) emit(Resource.Success(body))
|
||||
else emit(response.parseError())
|
||||
} catch (e: HttpException) {
|
||||
e.printStackTrace()
|
||||
emit(Resource.Error(context.getString(R.string.smth_went_wrong)))
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
emit(Resource.Error(context.getString(R.string.no_network)))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
emit(Resource.Error(context.getString(R.string.smth_went_wrong)))
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <reified T> handleResponse(
|
||||
call: () -> Response<T>,
|
||||
context: Context,
|
||||
): Resource<T> {
|
||||
try {
|
||||
val response = call()
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body()!!
|
||||
return Resource.Success(body)
|
||||
} else return response.parseError()
|
||||
} catch (e: HttpException) {
|
||||
e.printStackTrace()
|
||||
return Resource.Error(context.getString(R.string.smth_went_wrong))
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return Resource.Error(context.getString(R.string.no_network))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return Resource.Error(context.getString(R.string.smth_went_wrong))
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, reified R> Response<T>.parseError(): Resource<R> {
|
||||
return try {
|
||||
val response = Gson()
|
||||
.fromJson(
|
||||
errorBody()?.string().toString(),
|
||||
SimpleResponse::class.java
|
||||
)
|
||||
|
||||
Resource.Error(message = response?.message ?: "")
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
Resource.Error(e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toFormDataRequestBody() = this.toRequestBody("multipart/form-data".toMediaTypeOrNull())
|
||||
|
||||
fun isOnline(context: Context): Boolean {
|
||||
val cm =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
|
||||
val netInfo = cm!!.activeNetworkInfo
|
||||
return netInfo != null && netInfo.isConnected()
|
||||
}
|
120
android/app/src/main/java/app/tourism/data/remote/TourismApi.kt
Normal file
|
@ -0,0 +1,120 @@
|
|||
package app.tourism.data.remote
|
||||
|
||||
import app.tourism.data.dto.AllDataDto
|
||||
import app.tourism.data.dto.CategoryDto
|
||||
import app.tourism.data.dto.FavoritesDto
|
||||
import app.tourism.data.dto.FavoritesIdsDto
|
||||
import app.tourism.data.dto.auth.AuthResponseDto
|
||||
import app.tourism.data.dto.auth.EmailBodyDto
|
||||
import app.tourism.data.dto.place.ReviewDto
|
||||
import app.tourism.data.dto.place.ReviewIdsDto
|
||||
import app.tourism.data.dto.place.ReviewsDto
|
||||
import app.tourism.data.dto.profile.LanguageDto
|
||||
import app.tourism.data.dto.profile.ThemeDto
|
||||
import app.tourism.data.dto.profile.UserData
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.HTTP
|
||||
import retrofit2.http.Multipart
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Part
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface TourismApi {
|
||||
// region auth
|
||||
@FormUrlEncoded
|
||||
@POST("login")
|
||||
suspend fun signIn(
|
||||
@Field("email") email: String,
|
||||
@Field("password") password: String,
|
||||
): Response<AuthResponseDto>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("register")
|
||||
suspend fun signUp(
|
||||
@Field("full_name") fullName: String,
|
||||
@Field("email") email: String,
|
||||
@Field("password") password: String,
|
||||
@Field("password_confirmation") passwordConfirmation: String,
|
||||
@Field("country") country: String,
|
||||
): Response<AuthResponseDto>
|
||||
|
||||
@POST("logout")
|
||||
suspend fun signOut(): Response<SimpleResponse>
|
||||
|
||||
@POST("forgot-password")
|
||||
suspend fun sendEmailForPasswordReset(@Body emailBody: EmailBodyDto): Response<SimpleResponse>
|
||||
// endregion auth
|
||||
|
||||
// region profile
|
||||
@GET("user")
|
||||
suspend fun getUser(): Response<UserData>
|
||||
|
||||
@Multipart
|
||||
@POST("profile")
|
||||
suspend fun updateProfile(
|
||||
@Part("full_name") fullName: RequestBody? = null,
|
||||
@Part("email") email: RequestBody? = null,
|
||||
@Part("country") country: RequestBody? = null,
|
||||
@Part("language") language: RequestBody? = null,
|
||||
@Part("theme") theme: RequestBody? = null,
|
||||
@Part("_method") _method: RequestBody? = "PUT".toFormDataRequestBody(),
|
||||
@Part avatar: MultipartBody.Part? = null
|
||||
): Response<UserData>
|
||||
|
||||
@PUT("profile/lang")
|
||||
suspend fun updateLanguage(@Body language: LanguageDto): Response<UserData>
|
||||
|
||||
@PUT("profile/theme")
|
||||
suspend fun updateTheme(@Body theme: ThemeDto): Response<UserData>
|
||||
// endregion profile
|
||||
|
||||
// region places
|
||||
@GET("marks/{id}")
|
||||
suspend fun getPlacesByCategory(
|
||||
@Path("id") id: Long,
|
||||
@Query("hash") hash: String
|
||||
): Response<CategoryDto>
|
||||
|
||||
@GET("marks/all")
|
||||
suspend fun getAllPlaces(): Response<AllDataDto>
|
||||
// endregion places
|
||||
|
||||
// region favorites
|
||||
@GET("favourite-marks")
|
||||
suspend fun getFavorites(): Response<FavoritesDto>
|
||||
|
||||
@POST("favourite-marks")
|
||||
suspend fun addFavorites(@Body ids: FavoritesIdsDto): Response<SimpleResponse>
|
||||
|
||||
@HTTP(method = "DELETE", path = "favourite-marks", hasBody = true)
|
||||
suspend fun removeFromFavorites(@Body ids: FavoritesIdsDto): Response<SimpleResponse>
|
||||
// endregion favorites
|
||||
|
||||
// region reviews
|
||||
@GET("feedbacks/{id}")
|
||||
suspend fun getReviewsByPlaceId(@Path("id") id: Long): Response<ReviewsDto>
|
||||
|
||||
@Multipart
|
||||
@POST("feedbacks")
|
||||
suspend fun postReview(
|
||||
@Part("message") comment: RequestBody? = null,
|
||||
@Part("mark_id") placeId: RequestBody? = null,
|
||||
@Part("points") points: RequestBody? = null,
|
||||
@Part images: List<MultipartBody.Part>? = null
|
||||
): Response<ReviewDto>
|
||||
|
||||
@HTTP(method = "DELETE", path = "feedbacks", hasBody = true)
|
||||
suspend fun deleteReviews(
|
||||
@Body feedbacks: ReviewIdsDto,
|
||||
): Response<SimpleResponse>
|
||||
// endregion reviews
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package app.tourism.data.remote
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.NetworkInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.repositories.PlacesRepository
|
||||
import app.tourism.data.repositories.ProfileRepository
|
||||
import app.tourism.data.repositories.ReviewsRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WifiReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var reviewsRepository: ReviewsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var placesRepository: PlacesRepository
|
||||
|
||||
@Inject
|
||||
lateinit var profileRepository: ProfileRepository
|
||||
|
||||
@Inject
|
||||
lateinit var userPreferences: UserPreferences
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val info: NetworkInfo? = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO)
|
||||
|
||||
if (info != null && info.isConnected) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(2000L) // to avoid errors
|
||||
CoroutineScope(Dispatchers.IO).launch { reviewsRepository.syncReviews() }
|
||||
CoroutineScope(Dispatchers.IO).launch { placesRepository.syncFavorites() }
|
||||
CoroutineScope(Dispatchers.IO).launch { profileRepository.syncLanguageIfNecessary() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import app.tourism.data.dto.auth.EmailBodyDto
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import app.tourism.data.remote.handleGenericCall
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.auth.AuthResponse
|
||||
import app.tourism.domain.models.auth.RegistrationData
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class AuthRepository(private val api: TourismApi, private val context: Context) {
|
||||
fun signIn(email: String, password: String): Flow<Resource<AuthResponse>> = flow {
|
||||
handleGenericCall(
|
||||
call = { api.signIn(email, password) },
|
||||
mapper = { it.toAuthResponse() },
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
fun signUp(registrationData: RegistrationData) = flow {
|
||||
handleGenericCall(
|
||||
call = {
|
||||
api.signUp(
|
||||
registrationData.fullName,
|
||||
registrationData.email,
|
||||
registrationData.password,
|
||||
registrationData.passwordConfirmation,
|
||||
registrationData.country
|
||||
)
|
||||
},
|
||||
mapper = { it.toAuthResponse() },
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
fun signOut(): Flow<Resource<SimpleResponse>> = flow {
|
||||
handleGenericCall(
|
||||
call = { api.signOut() },
|
||||
mapper = { it },
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
fun sendEmailForPasswordReset(email: String) = flow {
|
||||
handleGenericCall(
|
||||
call = { api.sendEmailForPasswordReset(EmailBodyDto(email)) },
|
||||
mapper = { it },
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import app.tourism.data.db.Database
|
||||
import app.tourism.data.remote.CurrencyApi
|
||||
import app.tourism.data.remote.handleGenericCall
|
||||
import app.tourism.domain.models.profile.CurrencyRates
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class CurrencyRepository(
|
||||
private val api: CurrencyApi,
|
||||
private val db: Database,
|
||||
val context: Context
|
||||
) {
|
||||
val currenciesDao = db.currencyRatesDao
|
||||
|
||||
suspend fun getCurrency(): Flow<Resource<CurrencyRates>> = flow {
|
||||
currenciesDao.getCurrencyRates()?.let {
|
||||
emit(Resource.Success(it.toCurrencyRates()))
|
||||
}
|
||||
|
||||
handleGenericCall(
|
||||
call = { api.getCurrency() },
|
||||
mapper = {
|
||||
db.currencyRatesDao.updateCurrencyRates(it.toCurrencyRatesEntity())
|
||||
it.toCurrencyRates()
|
||||
},
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import app.organicmaps.R
|
||||
import app.tourism.data.db.Database
|
||||
import app.tourism.data.db.entities.FavoriteSyncEntity
|
||||
import app.tourism.data.db.entities.HashEntity
|
||||
import app.tourism.data.db.entities.ImageToDownloadEntity
|
||||
import app.tourism.data.db.entities.PlaceEntity
|
||||
import app.tourism.data.db.entities.ReviewEntity
|
||||
import app.tourism.data.dto.FavoritesIdsDto
|
||||
import app.tourism.data.dto.place.PlaceDto
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import app.tourism.data.remote.handleGenericCall
|
||||
import app.tourism.data.remote.handleResponse
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.categories.PlaceCategory
|
||||
import app.tourism.domain.models.common.PlaceShort
|
||||
import app.tourism.domain.models.details.PlaceFull
|
||||
import app.tourism.domain.models.resource.DownloadProgress
|
||||
import app.tourism.domain.models.resource.DownloadStats
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import coil.imageLoader
|
||||
import coil.request.ErrorResult
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.SuccessResult
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class PlacesRepository(
|
||||
private val api: TourismApi,
|
||||
db: Database,
|
||||
val userPreferences: UserPreferences,
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val placesDao = db.placesDao
|
||||
private val reviewsDao = db.reviewsDao
|
||||
private val hashesDao = db.hashesDao
|
||||
private val imagesToDownloadDao = db.imagesToDownloadDao
|
||||
|
||||
private val language = userPreferences.getLanguage()?.code ?: "ru"
|
||||
|
||||
fun downloadAllData(): Flow<Resource<SimpleResponse>> = flow {
|
||||
// this is for test
|
||||
// hashesDao.deleteHashes()
|
||||
val hashes = hashesDao.getHashes()
|
||||
val favoritesResponse = handleResponse(call = { api.getFavorites() }, context)
|
||||
|
||||
if (hashes.isEmpty()) {
|
||||
handleGenericCall(
|
||||
call = { api.getAllPlaces() },
|
||||
mapper = { data ->
|
||||
// get data
|
||||
val favoritesEn =
|
||||
if (favoritesResponse is Resource.Success)
|
||||
favoritesResponse.data?.en?.map {
|
||||
it.toPlaceFull(true, language = "en")
|
||||
} else null
|
||||
|
||||
val reviewsEntities = mutableListOf<ReviewEntity>()
|
||||
|
||||
fun PlaceDto.toEntity(
|
||||
placeCategory: PlaceCategory,
|
||||
language: String
|
||||
): PlaceEntity {
|
||||
var placeFull = this.toPlaceFull(false, language)
|
||||
placeFull =
|
||||
placeFull.copy(
|
||||
isFavorite = favoritesEn?.any { it.id == placeFull.id } ?: false
|
||||
)
|
||||
|
||||
placeFull.reviews?.let { it1 ->
|
||||
reviewsEntities.addAll(it1.map { it.toReviewEntity() })
|
||||
}
|
||||
return placeFull.toPlaceEntity(placeCategory.id)
|
||||
}
|
||||
|
||||
val sightsEntitiesEn = data.attractions_en.map { placeDto ->
|
||||
placeDto.toEntity(PlaceCategory.Sights, "en")
|
||||
}
|
||||
val restaurantsEntitiesEn = data.restaurants_en.map { placeDto ->
|
||||
placeDto.toEntity(PlaceCategory.Restaurants, "en")
|
||||
}
|
||||
val hotelsEntitiesEn = data.accommodations_en.map { placeDto ->
|
||||
placeDto.toEntity(PlaceCategory.Hotels, "en")
|
||||
}
|
||||
val sightsEntitiesRu = data.attractions_ru.map { placeDto ->
|
||||
placeDto.toEntity(PlaceCategory.Sights, "ru")
|
||||
}
|
||||
val restaurantsEntitiesRu = data.restaurants_ru.map { placeDto ->
|
||||
placeDto.toEntity(PlaceCategory.Restaurants, "ru")
|
||||
}
|
||||
val hotelsEntitiesRu = data.accommodations_ru.map { placeDto ->
|
||||
placeDto.toEntity(PlaceCategory.Hotels, "ru")
|
||||
}
|
||||
|
||||
val allPlacesEntities =
|
||||
sightsEntitiesEn + restaurantsEntitiesEn + hotelsEntitiesEn + sightsEntitiesRu + restaurantsEntitiesRu + hotelsEntitiesRu
|
||||
|
||||
// add all images urls to download
|
||||
val imagesToDownload = mutableListOf<ImageToDownloadEntity>()
|
||||
allPlacesEntities.forEach { placeEntity ->
|
||||
val gallery = placeEntity.gallery.filter { it.isNotBlank() }
|
||||
.map { ImageToDownloadEntity(it, false) }
|
||||
imagesToDownload.addAll(gallery)
|
||||
if (placeEntity.cover.isNotBlank()) {
|
||||
val cover = ImageToDownloadEntity(placeEntity.cover, false)
|
||||
imagesToDownload.add(cover)
|
||||
}
|
||||
}
|
||||
reviewsEntities.forEach { reviewEntity ->
|
||||
val images = reviewEntity.images.filter { it.isNotBlank() }
|
||||
.map { ImageToDownloadEntity(it, false) }
|
||||
imagesToDownload.addAll(images)
|
||||
|
||||
reviewEntity.user.avatar?.let {
|
||||
if (it.isNotBlank()) {
|
||||
val userPfp = ImageToDownloadEntity(it, false)
|
||||
imagesToDownload.add(userPfp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imagesToDownloadDao.insertImages(imagesToDownload)
|
||||
|
||||
// update places
|
||||
placesDao.deleteAllPlaces()
|
||||
placesDao.insertPlaces(allPlacesEntities)
|
||||
|
||||
// update reviews
|
||||
reviewsDao.deleteAllReviews()
|
||||
reviewsDao.insertReviews(reviewsEntities)
|
||||
|
||||
// update favorites
|
||||
favoritesEn?.forEach {
|
||||
placesDao.setFavorite(it.id, it.isFavorite)
|
||||
}
|
||||
|
||||
// update hashes
|
||||
hashesDao.insertHashes(
|
||||
listOf(
|
||||
HashEntity(PlaceCategory.Sights.id, data.attractions_hash),
|
||||
HashEntity(PlaceCategory.Restaurants.id, data.restaurants_hash),
|
||||
HashEntity(PlaceCategory.Hotels.id, data.accommodations_hash),
|
||||
)
|
||||
)
|
||||
|
||||
// return response
|
||||
SimpleResponse(message = context.getString(R.string.download_successful))
|
||||
},
|
||||
context
|
||||
)
|
||||
} else {
|
||||
emit(Resource.Success(SimpleResponse(message = context.getString(R.string.download_successful))))
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadAllImages(): Flow<DownloadProgress> = flow {
|
||||
try {
|
||||
val imagesToDownload = imagesToDownloadDao.getAllImages()
|
||||
val notDownloadedImages = imagesToDownload.filter { !it.downloaded }
|
||||
|
||||
val filesTotalNum = imagesToDownload.size
|
||||
val filesDownloaded = filesTotalNum - notDownloadedImages.size
|
||||
val downloadStats = DownloadStats(
|
||||
filesTotalNum,
|
||||
filesDownloaded,
|
||||
0
|
||||
)
|
||||
|
||||
if (downloadStats.percentagesCompleted >= 90) return@flow
|
||||
|
||||
notDownloadedImages.forEach {
|
||||
try {
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(it.url)
|
||||
.build()
|
||||
val result = context.imageLoader.execute(request)
|
||||
|
||||
when (result) {
|
||||
is SuccessResult -> {
|
||||
downloadStats.filesDownloaded++
|
||||
imagesToDownloadDao.markAsDownloaded(it.url, true)
|
||||
}
|
||||
|
||||
is ErrorResult -> {
|
||||
downloadStats.filesFailedToDownload++
|
||||
Log.d("", "Url failed to download: ${it.url}")
|
||||
}
|
||||
}
|
||||
|
||||
downloadStats.updatePercentage()
|
||||
Log.d("", "downloadStats: $downloadStats")
|
||||
|
||||
if (downloadStats.isAllFilesProcessed()) {
|
||||
emit(DownloadProgress.Finished(downloadStats))
|
||||
} else {
|
||||
emit(DownloadProgress.Loading(downloadStats))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
downloadStats.filesFailedToDownload++
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
emit(DownloadProgress.Error(message = context.getString(R.string.smth_went_wrong)))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun markAllImagesAsNotDownloadedIfCacheWasCleared() {
|
||||
// if coil cache is less than 10 MB,
|
||||
// then most likely it was cleared and data needs to be downloaded again
|
||||
// so we mark all images as not downloaded
|
||||
context.imageLoader.diskCache?.let {
|
||||
if (it.size < 10000000) {
|
||||
imagesToDownloadDao.markAllImagesAsNotDownloaded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun shouldDownloadImages(): Boolean {
|
||||
val imagesToDownload = imagesToDownloadDao.getAllImages()
|
||||
val notDownloadedImages = imagesToDownload.filter { !it.downloaded }
|
||||
|
||||
val filesTotalNum = imagesToDownload.size
|
||||
val filesDownloaded = filesTotalNum - notDownloadedImages.size
|
||||
val percentage = (filesDownloaded * 100) / filesTotalNum
|
||||
|
||||
return percentage < 90
|
||||
}
|
||||
|
||||
fun search(q: String): Flow<Resource<List<PlaceShort>>> = channelFlow {
|
||||
placesDao.search("%$q%", language).collectLatest { placeEntities ->
|
||||
val places = placeEntities.map { it.toPlaceShort() }
|
||||
send(Resource.Success(places))
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlacesByCategoryFromDbFlow(id: Long): Flow<Resource<List<PlaceShort>>> = channelFlow {
|
||||
placesDao.getSortedPlacesByCategoryIdFlow(categoryId = id, language)
|
||||
.collectLatest { placeEntities ->
|
||||
send(Resource.Success(placeEntities.map { it.toPlaceShort() }))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPlacesByCategoryFromApiIfThereIsChange(id: Long) {
|
||||
val hash = hashesDao.getHash(id)
|
||||
|
||||
val favorites = placesDao.getFavoritePlaces("%%", language)
|
||||
val resource =
|
||||
handleResponse(call = { api.getPlacesByCategory(id, hash?.value ?: "") }, context)
|
||||
|
||||
if (hash != null && resource is Resource.Success) {
|
||||
resource.data?.let { categoryDto ->
|
||||
if (categoryDto.hash.isBlank()) return
|
||||
// update places
|
||||
val placesEn = categoryDto.en.map { placeDto ->
|
||||
var placeFull = placeDto.toPlaceFull(false, "en")
|
||||
placeFull =
|
||||
placeFull.copy(isFavorite = favorites.any { it.id == placeFull.id })
|
||||
placeFull
|
||||
}
|
||||
val placesRu = categoryDto.ru.map { placeDto ->
|
||||
var placeFull = placeDto.toPlaceFull(false, "ru")
|
||||
placeFull =
|
||||
placeFull.copy(isFavorite = favorites.any { it.id == placeFull.id })
|
||||
placeFull
|
||||
}
|
||||
|
||||
val oldCacheRu = placesDao.getPlacesByCategoryIdNotFlow(id, "ru")
|
||||
val oldCacheEn = placesDao.getPlacesByCategoryIdNotFlow(id, "en")
|
||||
val oldCache = oldCacheEn + oldCacheRu
|
||||
|
||||
val allPlaces = mutableListOf<PlaceFull>()
|
||||
allPlaces.addAll(placesEn)
|
||||
allPlaces.addAll(placesRu)
|
||||
|
||||
val placesRemovedFromApi =
|
||||
oldCache
|
||||
.filter { oldCachePlace -> !allPlaces.any { oldCachePlace.id == it.id } }
|
||||
.map { it.id }
|
||||
|
||||
placesDao.deletePlaces(placesRemovedFromApi)
|
||||
placesDao.insertPlaces(allPlaces.map { it.toPlaceEntity(id) })
|
||||
|
||||
// update reviews
|
||||
val reviewsEntities = mutableListOf<ReviewEntity>()
|
||||
allPlaces.forEach { place ->
|
||||
reviewsDao.deleteAllPlaceReviews(place.id)
|
||||
place.reviews?.map { review -> review.toReviewEntity() }
|
||||
?.also { reviewEntity -> reviewsEntities.addAll(reviewEntity) }
|
||||
}
|
||||
reviewsDao.insertReviews(reviewsEntities)
|
||||
|
||||
// update hash
|
||||
hashesDao.insertHash(hash.copy(value = categoryDto.hash))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTopPlaces(id: Long): Flow<Resource<List<PlaceShort>>> = channelFlow {
|
||||
placesDao.getTopPlacesByCategoryId(categoryId = id, language = language)
|
||||
.collectLatest { placeEntities ->
|
||||
send(Resource.Success(placeEntities.map { it.toPlaceShort() }))
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlaceById(id: Long): Flow<Resource<PlaceFull>> = channelFlow {
|
||||
placesDao.getPlaceById(id, language).collectLatest { placeEntity ->
|
||||
if (placeEntity != null)
|
||||
send(Resource.Success(placeEntity.toPlaceFull()))
|
||||
else
|
||||
send(Resource.Error(message = "Не найдено"))
|
||||
}
|
||||
}
|
||||
|
||||
fun getFavorites(q: String): Flow<Resource<List<PlaceShort>>> = channelFlow {
|
||||
placesDao.getFavoritePlacesFlow("%$q%", language)
|
||||
.collectLatest { placeEntities ->
|
||||
send(Resource.Success(placeEntities.map { it.toPlaceShort() }))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setFavorite(placeId: Long, isFavorite: Boolean) {
|
||||
placesDao.setFavorite(placeId, isFavorite)
|
||||
|
||||
val favoritesIdsDto = FavoritesIdsDto(marks = listOf(placeId))
|
||||
|
||||
val favoriteSyncEntity = FavoriteSyncEntity(placeId, isFavorite)
|
||||
placesDao.addFavoriteSync(favoriteSyncEntity)
|
||||
val response: Resource<SimpleResponse> = if (isFavorite)
|
||||
handleResponse(call = { api.addFavorites(favoritesIdsDto) }, context)
|
||||
else
|
||||
handleResponse(call = { api.removeFromFavorites(favoritesIdsDto) }, context)
|
||||
|
||||
if (response is Resource.Success)
|
||||
placesDao.removeFavoriteSync(favoriteSyncEntity.placeId)
|
||||
else if (response is Resource.Error)
|
||||
placesDao.addFavoriteSync(favoriteSyncEntity)
|
||||
}
|
||||
|
||||
suspend fun syncFavorites() {
|
||||
val favoritesToSyncEntities = placesDao.getFavoriteSyncData()
|
||||
|
||||
val favoritesToAdd = favoritesToSyncEntities.filter { it.isFavorite }.map { it.placeId }
|
||||
val favoritesToRemove = favoritesToSyncEntities.filter { !it.isFavorite }.map { it.placeId }
|
||||
|
||||
if (favoritesToAdd.isNotEmpty()) {
|
||||
val responseToAddFavs =
|
||||
handleResponse(
|
||||
call = { api.addFavorites(FavoritesIdsDto(favoritesToAdd)) },
|
||||
context
|
||||
)
|
||||
if (responseToAddFavs is Resource.Success) {
|
||||
placesDao.removeFavoriteSync(favoritesToAdd)
|
||||
}
|
||||
}
|
||||
|
||||
if (favoritesToRemove.isNotEmpty()) {
|
||||
val responseToRemoveFavs =
|
||||
handleResponse(
|
||||
call = { api.removeFromFavorites(FavoritesIdsDto(favoritesToRemove)) },
|
||||
context
|
||||
)
|
||||
if (responseToRemoveFavs is Resource.Success) {
|
||||
placesDao.removeFavoriteSync(favoritesToRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import app.tourism.data.dto.profile.LanguageDto
|
||||
import app.tourism.data.dto.profile.ThemeDto
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import app.tourism.data.remote.handleGenericCall
|
||||
import app.tourism.data.remote.handleResponse
|
||||
import app.tourism.data.remote.toFormDataRequestBody
|
||||
import app.tourism.domain.models.profile.PersonalData
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import java.io.File
|
||||
|
||||
class ProfileRepository(
|
||||
private val api: TourismApi,
|
||||
private val userPreferences: UserPreferences,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
fun getPersonalData(): Flow<Resource<PersonalData>> = flow {
|
||||
handleGenericCall(
|
||||
call = { api.getUser() },
|
||||
mapper = {
|
||||
it.data.toPersonalData()
|
||||
},
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
fun updateProfile(
|
||||
fullName: String,
|
||||
country: String,
|
||||
email: String?,
|
||||
pfpFile: File?
|
||||
): Flow<Resource<PersonalData>> =
|
||||
flow {
|
||||
var pfpMultipart: MultipartBody.Part? = null
|
||||
if (pfpFile != null) {
|
||||
val requestBody = pfpFile.asRequestBody("image/*".toMediaType())
|
||||
pfpMultipart =
|
||||
MultipartBody.Part.createFormData("avatar", pfpFile.name, requestBody)
|
||||
}
|
||||
|
||||
val language = userPreferences.getLanguage()?.code
|
||||
val theme = userPreferences.getTheme()?.code
|
||||
|
||||
handleGenericCall(
|
||||
call = {
|
||||
api.updateProfile(
|
||||
fullName = fullName.toFormDataRequestBody(),
|
||||
email = email?.toFormDataRequestBody(),
|
||||
country = country.toFormDataRequestBody(),
|
||||
language = language.toString().toFormDataRequestBody(),
|
||||
theme = theme.toString().toFormDataRequestBody(),
|
||||
avatar = pfpMultipart
|
||||
)
|
||||
},
|
||||
mapper = { it.data.toPersonalData() },
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateLanguage(code: String) {
|
||||
try {
|
||||
val resource = handleResponse(
|
||||
call = { api.updateLanguage(language = LanguageDto(code)) },
|
||||
context
|
||||
)
|
||||
if (resource is Resource.Success) {
|
||||
userPreferences.setShouldSyncLanguage(false)
|
||||
} else if (resource is Resource.Error) {
|
||||
userPreferences.setShouldSyncLanguage(true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
userPreferences.setShouldSyncLanguage(true)
|
||||
println(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun syncLanguageIfNecessary() {
|
||||
userPreferences.getLanguage()?.code?.let {
|
||||
if (userPreferences.getShouldSyncLanguage()) {
|
||||
updateLanguage(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateTheme(code: String) {
|
||||
try {
|
||||
api.updateTheme(theme = ThemeDto(code))
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import app.organicmaps.R
|
||||
import app.tourism.data.db.Database
|
||||
import app.tourism.data.dto.place.ReviewIdsDto
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import app.tourism.data.remote.handleResponse
|
||||
import app.tourism.data.remote.isOnline
|
||||
import app.tourism.data.remote.toFormDataRequestBody
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.details.Review
|
||||
import app.tourism.domain.models.details.ReviewToPost
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import app.tourism.utils.compress
|
||||
import app.tourism.utils.saveToInternalStorage
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import java.io.File
|
||||
|
||||
class ReviewsRepository(
|
||||
@ApplicationContext val context: Context,
|
||||
private val api: TourismApi,
|
||||
private val db: Database,
|
||||
) {
|
||||
private val reviewsDao = db.reviewsDao
|
||||
|
||||
fun getReviewsForPlace(id: Long): Flow<Resource<List<Review>>> = channelFlow {
|
||||
reviewsDao.getReviewsForPlace(id).collectLatest { reviewsEntities ->
|
||||
val reviews = reviewsEntities.map { it.toReview() }
|
||||
send(Resource.Success(reviews))
|
||||
}
|
||||
}
|
||||
|
||||
fun isThereReviewPlannedToPublish(placeId: Long): Flow<Boolean> = channelFlow {
|
||||
reviewsDao.getReviewsPlannedToPostFlow(placeId).collectLatest { reviewsEntities ->
|
||||
send(reviewsEntities.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getReviewsFromApi(id: Long) {
|
||||
val getReviewsResponse = handleResponse(call = { api.getReviewsByPlaceId(id) }, context)
|
||||
if (getReviewsResponse is Resource.Success) {
|
||||
reviewsDao.deleteAllPlaceReviews(id)
|
||||
getReviewsResponse.data?.data?.map { it.toReview().toReviewEntity() }
|
||||
?.let { reviewsDao.insertReviews(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun postReview(review: ReviewToPost): Flow<Resource<SimpleResponse>> = flow {
|
||||
emit(Resource.Loading())
|
||||
val imageFiles = mutableListOf<File>()
|
||||
review.images.forEach { imageFiles.add(compress(it, context)) }
|
||||
|
||||
if (isOnline(context)) {
|
||||
val postReviewResponse = handleResponse(
|
||||
call = {
|
||||
api.postReview(
|
||||
placeId = review.placeId.toString().toFormDataRequestBody(),
|
||||
comment = review.comment.toFormDataRequestBody(),
|
||||
points = review.rating.toString().toFormDataRequestBody(),
|
||||
images = getMultipartFromImageFiles(imageFiles)
|
||||
)
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
if (postReviewResponse is Resource.Success) {
|
||||
updateReviewsForDb(review.placeId)
|
||||
emit(Resource.Success(SimpleResponse(context.getString(R.string.review_was_published))))
|
||||
} else if (postReviewResponse is Resource.Error) {
|
||||
emit(Resource.Error(postReviewResponse.message ?: ""))
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
saveToInternalStorage(imageFiles, context)
|
||||
reviewsDao.insertReviewPlannedToPost(review.toReviewPlannedToPostEntity(imageFiles))
|
||||
emit(Resource.Error(context.getString(R.string.review_will_be_published_when_online)))
|
||||
} catch (e: OutOfMemoryError) {
|
||||
e.printStackTrace()
|
||||
emit(Resource.Error(context.getString(R.string.smth_went_wrong)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteReview(id: Long): Flow<Resource<SimpleResponse>> =
|
||||
flow {
|
||||
if (isOnline(context)) {
|
||||
val deleteReviewResponse =
|
||||
handleResponse(
|
||||
call = { api.deleteReviews(ReviewIdsDto(listOf(id))) },
|
||||
context,
|
||||
)
|
||||
|
||||
if (deleteReviewResponse is Resource.Success) {
|
||||
reviewsDao.deleteReview(id)
|
||||
}
|
||||
emit(deleteReviewResponse)
|
||||
} else {
|
||||
reviewsDao.markReviewForDeletion(id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun syncReviews() {
|
||||
deleteReviewsThatWereNotDeletedOnTheServer()
|
||||
publishReviewsThatWereNotPublished()
|
||||
}
|
||||
|
||||
private suspend fun deleteReviewsThatWereNotDeletedOnTheServer() {
|
||||
val reviews = reviewsDao.getReviewsPlannedForDeletion()
|
||||
if (reviews.isNotEmpty()) {
|
||||
val reviewsIds = reviews.map { it.id }
|
||||
val response =
|
||||
handleResponse(call = { api.deleteReviews(ReviewIdsDto(reviewsIds)) }, context)
|
||||
if (response is Resource.Success) {
|
||||
reviewsDao.deleteReviews(reviewsIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun publishReviewsThatWereNotPublished() {
|
||||
val reviewsPlannedToPostEntities = reviewsDao.getReviewsPlannedToPost()
|
||||
if (reviewsPlannedToPostEntities.isNotEmpty()) {
|
||||
val reviews = reviewsPlannedToPostEntities.map { it.toReviewsPlannedToPostDto() }
|
||||
reviews.forEach {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = handleResponse(
|
||||
call = {
|
||||
api.postReview(
|
||||
placeId = it.placeId.toString().toFormDataRequestBody(),
|
||||
comment = it.comment.toFormDataRequestBody(),
|
||||
points = it.rating.toString().toFormDataRequestBody(),
|
||||
images = getMultipartFromImageFiles(it.images)
|
||||
)
|
||||
},
|
||||
context,
|
||||
)
|
||||
if (response is Resource.Success) {
|
||||
try {
|
||||
updateReviewsForDb(it.placeId)
|
||||
reviewsDao.deleteReviewPlannedToPost(it.placeId)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
} else if (response is Resource.Error) {
|
||||
try {
|
||||
reviewsDao.deleteReviewPlannedToPost(it.placeId)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateReviewsForDb(id: Long) {
|
||||
val getReviewsResponse = handleResponse(
|
||||
call = { api.getReviewsByPlaceId(id) },
|
||||
context
|
||||
)
|
||||
if (getReviewsResponse is Resource.Success) {
|
||||
reviewsDao.deleteAllPlaceReviews(id)
|
||||
val reviews =
|
||||
getReviewsResponse.data?.data?.map { it.toReview().toReviewEntity() } ?: listOf()
|
||||
reviewsDao.insertReviews(reviews)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMultipartFromImageFiles(imageFiles: List<File>): MutableList<MultipartBody.Part> {
|
||||
val imagesMultipart = mutableListOf<MultipartBody.Part>()
|
||||
imageFiles.forEach {
|
||||
val requestBody = it.asRequestBody("image/*".toMediaType())
|
||||
val imageMultipart =
|
||||
MultipartBody.Part.createFormData("images[]", it.name, requestBody)
|
||||
imagesMultipart.add(imageMultipart)
|
||||
}
|
||||
return imagesMultipart
|
||||
}
|
||||
}
|
26
android/app/src/main/java/app/tourism/di/DatabaseModule.kt
Normal file
|
@ -0,0 +1,26 @@
|
|||
package app.tourism.di
|
||||
|
||||
import android.app.Application
|
||||
import androidx.room.Room
|
||||
import app.tourism.data.db.Database
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(app: Application): Database {
|
||||
return Room.databaseBuilder(
|
||||
app, Database::class.java, "tourism_database"
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
}
|
||||
}
|