Compare commits

...
Sign in to create a new pull request.

88 commits

Author SHA1 Message Date
mamadnazar
039b71798e [ios] Load img ui fix 2025-03-18 15:30:11 +05:00
mamadnazar
77f098acb6 [ios] ui and sorting fixes 2025-03-13 21:33:34 +05:00
mamadnazar
a7d7d80a8a [ios] predownload and set tajikistan map 2025-03-13 13:28:39 +05:00
mamadnazar
7aa4f4dc03 [ios] navigation to main from sign up bug fix 2025-03-13 13:27:38 +05:00
mamadnazar
c92a138130 [ios] added internet connection check on welcome screen since ios asks for enabling wifi and/or cellular after first request, and if it is login we get error so added an internet connection check before login or registration requests 2025-03-12 01:52:21 +05:00
mamadnazar
f9a6bc5c00 [ios] pre-download Tajikistan map 2025-03-12 01:50:51 +05:00
mamadnazar
e0d98091ed [ios] fix: When user is in the map and gets out (without killing app) then returns back it shows main page instead of map 2025-03-06 19:55:08 +05:00
mamadnazar
3d9ebab8c2 ios: core data thread safety crashes fix 2025-03-02 17:38:28 +05:00
Emin
f25019b8f8 android: fix bugs 2025-02-14 21:26:40 +05:00
Emin
f592cb23dd android: Find a way to have map pre-included in the app 2025-02-13 14:43:08 +05:00
Emin
b34607bd04 android: impl smarter places updates thus fixing weird places ui updates bug 2025-02-11 17:42:59 +05:00
Emin
a3d2a22f5b android: make images offline 2025-02-11 15:28:35 +05:00
Emin
451d628bfd android: sync language 2025-02-11 09:41:57 +05:00
Emin
661807cde8 android: make translations offline 2025-02-10 01:17:15 +05:00
Emin
fa4e191fab android: fix network crashes on older devices 2025-02-07 16:31:56 +05:00
Emin
ca2167658b android: fix change language for real (android 15, 14, 12, 10 tested) 2025-02-07 16:28:49 +05:00
Emin
2183b7a53c android: fix change language 2025-02-05 20:21:25 +05:00
Emin
36f60c9d64 ios: send app for review 2025-01-09 13:38:40 +05:00
Emin
dae8c92ea2 Merge remote-tracking branch 'refs/remotes/origin/master' 2025-01-06 17:08:24 +05:00
Emin
76e66c31ec ios: add basic html parser in placesItem 2025-01-06 17:06:00 +05:00
Emin A
2cc11db7b2
Make README.md more readable
Signed-off-by: Emin A <89159161+Ohpleaseman@users.noreply.github.com>
2025-01-05 15:12:23 +05:00
Emin
549ca85c43 ios: fix some bugs 2025-01-01 20:43:00 +05:00
Emin
eb14807513 ios: fix download 2024-12-30 20:21:46 +05:00
Emin
64ed84bfa9 update readme 2024-12-30 17:54:18 +05:00
Emin
fef69f5eec android: fix crashes/bugs, adjust ui a little 2024-12-26 16:37:49 +05:00
Emin
fb493aceee android: send updates to play store, tweak map downloading 2024-10-30 16:04:02 +05:00
Emin
ded2bfe91d android: remove test crash 2024-10-30 11:53:20 +05:00
Emin
04418bc015 android: add Firebase Crashlytics 2024-10-30 10:51:52 +05:00
Emin
babef10504 ios: attempt to fix weird core data crashes, but related to hashes 2024-10-29 16:49:27 +05:00
Emin
e32a9d2665 ios: attempt to fix weird core data crashes 2024-10-23 14:30:37 +05:00
Emin
7950027ddf global: fix forgot-password 2024-10-14 11:36:27 +05:00
Emin
97e162707b global: fix forgot-password 2024-10-14 11:32:31 +05:00
Emin
93c0970275 android: replace blurriness with dark container on auth, ios: add developedByLabel on sign up screen 2024-10-11 14:24:57 +05:00
Emin
de69678a96 global: add developedByRebus label, ios: fix bugs 2024-10-10 16:06:33 +05:00
Emin
fff086dcf4 ios: make fullscreen images viewer with zoom 2024-10-02 11:45:33 +05:00
Emin
1077efd56c android: make fullscreen images viewer with zoom 2024-10-02 09:10:08 +05:00
Emin
2d9820c745 fix navigation bugs, refactor a little 2024-10-01 09:51:01 +05:00
Emin
beb457603a ios: change brand name, logo, and splash 2024-09-26 11:57:27 +05:00
Emin
c59c102f62 ios: map tasks finished (create route, download map, remove unnecessary UI), ios finished, global: change base_url, other little changes 2024-09-25 23:11:53 +05:00
Emin
90a6f01752 ios: remove unnecessary UI on map 2024-09-21 21:12:05 +05:00
Emin
5ae27cbccb android: disable car support, sent to Play Store for review; ios: backup 2024-09-20 09:38:56 +05:00
Emin
52bf2acb91 backup 2024-09-16 11:21:24 +05:00
Emin
bcf18422b5 backup 2024-09-13 09:21:18 +05:00
Emin
d728b8c7d6 backup 2024-09-13 09:20:44 +05:00
Emin
d1bf71528e backup, UI/UX finished everywhere 2024-09-10 14:56:12 +05:00
Emin
c5e3417af0 backup 2024-09-06 09:23:46 +05:00
Emin
7166577cd1 backup 2024-09-05 15:20:33 +05:00
Emin
7720b166c6 backup, home, categories, search UI/UX 2024-09-04 12:05:18 +05:00
Emin
6ea723b0f5 backup 2024-09-03 10:56:38 +05:00
LLC Rebus
91c544bfeb backup 2024-09-02 09:50:59 +05:00
LLC Rebus
6be8ce1933 backup 2024-09-02 09:34:15 +05:00
Emin
7d4e760778 push skipped SignInRequest struct 2024-08-14 10:17:16 +05:00
Emin
a85dcd0efc finish auth, ongoing: profile 2024-08-14 10:16:18 +05:00
Emin
0b5a4bf665 ios: adding skipped files 2024-08-07 16:02:02 +05:00
Emin
e62d6ef2d0 ios: lil refactor CountryPickerView 2024-08-06 17:20:34 +05:00
Emin
2d9a1ee57f ios: do auth UI/UI, do l10n 2024-08-06 17:09:21 +05:00
Emin
0d0b43ae7e ios: add translations 2024-08-05 09:45:00 +05:00
Emin
d30678a9b4 ios forgot fonts 2024-08-04 23:16:27 +05:00
Emin
207bc0cd02 ios do theme, text styles 2024-08-04 23:14:53 +05:00
Emin
09d92aec73 tried some programmatic UI 2024-08-01 17:11:45 +05:00
Emin
d0a5911fd1 ios - remove swiftui stuff, decrease ios min to 12 2024-07-31 11:06:02 +05:00
Emin
63e3de24a9 ios Tried some swiftui integraton (failed) 2024-07-31 09:47:26 +05:00
Emin
0b99a3572b make some empty screens, navigate to tourism main, set ios min to 13 2024-07-29 17:19:52 +05:00
Emin
7cb1b6e51f android: adjust UI 2024-07-26 14:21:47 +05:00
Emin
dc9c67480c android: set domain, adjustments, fix bugs, optimize imports 2024-07-22 17:58:16 +05:00
Emin
dcb0d40816 android: lil adjustments, ios: prepared to launch 2024-07-19 14:19:19 +05:00
Emin
e78bbe397e adjustments and bug fixes 2024-07-17 14:50:15 +05:00
Emin
dc0523cbb3 get currencies from tourism api instead 2024-07-16 11:31:47 +05:00
Emin
104f02b987 api/cache/sync finished 2024-07-15 11:15:14 +05:00
Emin
2400c21819 backup 2024-07-12 10:39:40 +05:00
Emin
bba8edbf48 backup, ongoing: api/cache/sync 2024-07-08 15:51:31 +05:00
Emin
b7eeeb2ed7 adjust auth, profile, do currency, ongoing: places API/DB 2024-07-03 11:23:57 +05:00
Emin
1d6e96e1fe do place details (including reviews), favorites UI/UX 2024-06-26 16:37:33 +05:00
Emin
ab8677439f do home, categories, search UI/UX 2024-06-24 14:04:48 +05:00
Emin
ab44d68eac do profile imageUpload feature 2024-06-21 15:30:06 +05:00
Emin
6f0c27d73a do profile UI/UX 2024-06-21 11:47:54 +05:00
Emin
45a8da004b do profile UI/UX 2024-06-21 10:53:58 +05:00
Emin
15b3613363 set up more libs, utils, and res; finish auth; ongoing: profile 2024-06-20 16:04:58 +05:00
Emin
c3c3736f07 set up themes, localization, auth UI 2024-06-15 00:29:23 +05:00
Emin
58b81266e8 download map after launch but in Map screen 2024-06-11 17:19:32 +05:00
Emin
8917001a27 Revert "failed attempt to download map after launch"
This reverts commit 184cd72db2.
2024-06-11 15:15:01 +05:00
Emin
184cd72db2 failed attempt to download map after launch 2024-06-11 15:14:39 +05:00
Emin
dce2313723 show route on map from another screen 2024-06-07 20:15:38 +05:00
Emin
ba0f2bb041 remove unnecessary ui elements from the map, other small stuff 2024-06-06 16:16:47 +05:00
Emin
7326924a23 remove unnecessary buttons from the map, changed appId 2024-06-06 11:34:49 +05:00
Emin
cc9e6c9dc5 try using kotlin (successful) 2024-06-06 10:14:44 +05:00
Emin
3a6166bae9 change logo and splash 2024-06-05 17:08:17 +05:00
Emin
13719ee316 remove unnecessary docs, adjust legal docs 2024-06-04 14:27:06 +05:00
620 changed files with 28236 additions and 5646 deletions

@ -1 +1 @@
Subproject commit 9cbdb916de2a7bd1aa649e55efc38d2426680359
Subproject commit 74d91febb0995b7c6706dfd4eed2d39fb1694421

@ -1 +1 @@
Subproject commit 680f521746a3bd6a86f25f25ee50a62d88b489cf
Subproject commit 6af11aa609f3fdf735cab5fdc051cd840960186b

View file

@ -1 +0,0 @@
See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md)

View file

@ -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
View file

@ -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
View file

@ -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).

View file

@ -31,3 +31,6 @@
# ignore autogenerated metadata (see prepareGoogleReleaseListing in build.gradle)
/src/google/play/listings
# ignore google releases
/google/release

View file

@ -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'
}

View file

@ -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 *;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -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>

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

View file

@ -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>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -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)
{

View file

@ -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

View file

@ -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

View file

@ -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));

View file

@ -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.
*

View file

@ -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()
{

View file

@ -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)
{

View file

@ -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;
}

View file

@ -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);

View file

@ -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();

View 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()
}
}
}
}
}
}

View 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)
)
)
)
}

View 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())
}
}
}

View 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()
}
}

View 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)
}

View 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
}

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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>
}

View file

@ -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>
}

View file

@ -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>>
}

View file

@ -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)
}

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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
)

View file

@ -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)
}

View file

@ -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,
)
}

View file

@ -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
)
}
}

View file

@ -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)
}

View 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,
)

View file

@ -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>,
)

View file

@ -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>,
)

View file

@ -0,0 +1,5 @@
package app.tourism.data.dto
data class FavoritesIdsDto(
val marks: List<Long>
)

View file

@ -0,0 +1,3 @@
package app.tourism.data.dto
data class HashDto(val hash: String)

View file

@ -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)
}

View file

@ -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
)
}

View file

@ -0,0 +1,3 @@
package app.tourism.data.dto.auth
data class EmailBodyDto(val email: String)

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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,
)
}

View file

@ -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
)
}

View file

@ -0,0 +1,5 @@
package app.tourism.data.dto.place
data class ReviewIdsDto(
val feedbacks: List<Long>,
)

View file

@ -0,0 +1,3 @@
package app.tourism.data.dto.place
data class ReviewsDto(val data: List<ReviewDto>)

View file

@ -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,
)
}

View file

@ -0,0 +1,3 @@
package app.tourism.data.dto.profile
data class LanguageDto(val language: String)

View file

@ -0,0 +1,3 @@
package app.tourism.data.dto.profile
data class ThemeDto(val theme: String)

View file

@ -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,
)
}

View file

@ -0,0 +1,3 @@
package app.tourism.data.dto.profile
data class UserData(val data: User)

View file

@ -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)

View file

@ -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>
}

View file

@ -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()
}

View 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
}

View file

@ -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() }
}
}
}
}

View file

@ -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
)
}
}

View file

@ -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
)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View 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()
}
}

Some files were not shown because too many files have changed in this diff Show more