Compare commits

...

162 Commits

Author SHA1 Message Date
Andrea Busi bdfcb7a5c4 release: Increase version for release 2023-09-14 17:02:58 +02:00
Andrea Busi 40fcb4707f fix: Be sure to download updated data when country is changed 2023-09-14 17:02:17 +02:00
Andrea Busi 37f9a856b1 feat: Filter modify notification settings by default 2023-09-14 17:02:17 +02:00
Andrea Busi f42b9f1b53 refactor: Migrate some UserDefaults constants 2023-09-14 17:02:17 +02:00
Andrea Busi 4fd9966435 release: Update changelog 2023-09-14 17:02:17 +02:00
Andrea Busi df2c0a94a4 refactor: Remove unused import 2023-09-14 17:02:17 +02:00
Andrea Busi cdd1a8d875 refactor: Remove deprecated method, useless with new BGTask implementation 2023-09-14 17:02:17 +02:00
Andrea Busi 31f1cb5f35 refactor: Can enable/disable background position debug 2023-09-14 17:02:17 +02:00
Andrea Busi d426f15c8e refactor: Remove deprecated code 2023-09-14 17:02:17 +02:00
Andrea Busi 037a74061d feat: Perform WS request to upload user position 2023-09-14 17:02:17 +02:00
Andrea Busi ea6172226d feat: Add a debug screen for saved background positions 2023-09-11 15:52:27 +02:00
Andrea Busi ec94db29b9 feat: Add background task to get user location 2023-09-11 15:52:27 +02:00
Andrea Busi 91a9bce03c release: Update changelog 2023-08-16 12:03:09 +02:00
Andrea Busi f54f4a2312 refactor: Remove no longer needed available checks 2023-08-16 12:02:32 +02:00
Andrea Busi a959df7cd9 dependency: Update Pods & SPM 2023-08-16 12:02:18 +02:00
Andrea Busi e95a93ff2c feat: Increase target to iOS 13 2023-08-16 11:57:49 +02:00
Andrea Busi b7c1f7379d refactor: Migrate some user default constants 2023-08-16 11:54:52 +02:00
Andrea Busi 0f71e0fea9 refactor: Disable monitoring background logic 2023-08-14 16:33:54 +02:00
Andrea Busi 92de4c534c release: Increase version for release 2023-08-10 16:50:50 +02:00
Andrea Busi 3c237c5b18 docs: Add push payload for AR 2023-08-10 16:17:38 +02:00
Andrea Busi 16dc2410bc fix: Get user location if manager one is null 2023-08-10 15:58:59 +02:00
Andrea Busi 3c83cb97cb release: Increase version for release 2023-08-04 16:20:13 +02:00
Andrea Busi 4796e3d5a7 fix: Update AR string 2023-08-04 16:09:08 +02:00
Andrea Busi 56f53550da release: Increase version for release 2023-07-31 07:19:45 +02:00
Andrea Busi d52b980959 release: Increase version for release 2023-07-27 17:35:16 +02:00
Andrea Busi b9f87c130d fix: Add missing AR strings 2023-07-27 17:35:16 +02:00
Andrea Busi b7acbc70df feat: Add arabic language 2023-07-27 17:35:09 +02:00
Andrea Busi 093b6471e8 release: Increase version for release 2023-07-27 17:06:41 +02:00
Andrea Busi 8ca814561b fix: Improve colors in home 2023-07-27 17:06:33 +02:00
Andrea Busi 359667b659 release: Increase version for release 2023-07-24 17:31:39 +02:00
Andrea Busi 468659ee9f refactor: Align card colors with Android app 2023-07-24 17:30:28 +02:00
Andrea Busi ff50abd58a fix: Solve glitch in allerte table 2023-07-24 17:20:27 +02:00
Andrea Busi 4770578ae3 release: Increase version for release 2023-07-21 08:08:55 +02:00
Andrea Busi f0fe102901 feat: Use intensity color for alert card 2023-07-21 08:08:55 +02:00
Andrea Busi 9dacb33736 release: Increase version for release 2023-07-14 17:26:16 +02:00
Andrea Busi aed78e44cd feat: Add background animation in realtime alert 2023-07-14 16:59:58 +02:00
Andrea Busi e531088c86 feat: Add helper class for debug purpose 2023-07-14 16:17:26 +02:00
Andrea Busi 7e0112bf94 refactor: Use new extension method to evaluate difference between dates 2023-07-14 16:17:26 +02:00
Andrea Busi 5d12a86cfe refactor: Move alert expiration login inside new EQNRealtimePushNotification class 2023-07-14 16:17:26 +02:00
Andrea Busi 09e7786e65 feat: Handle realtime alert notification message based on intensity value 2023-07-14 16:17:26 +02:00
Andrea Busi 8671533e91 feat: Store lastLocation in appGroup instead of app user defaults 2023-07-14 16:17:15 +02:00
Andrea Busi 9bd94def0f refactor: Migrate UserDefaults key for app migration 2023-07-14 16:17:15 +02:00
Andrea Busi 29c325b7e2 refactor: Migrate UserDefaults key for user data informations 2023-07-14 16:17:15 +02:00
Andrea Busi 8366f2eabb refactor: Use new UserDefaults constants in NotificationService 2023-07-14 16:17:15 +02:00
Andrea Busi 7e26fee45b refactor: Migrate some UserDefaults from Macro to new constants 2023-07-14 16:17:15 +02:00
Andrea Busi 76d551b847 refactor: Create Swift constants file for UserDefaults 2023-07-14 16:17:15 +02:00
Andrea Busi 8f55553759 refactor: Print notification payload 2023-07-14 16:17:15 +02:00
Andrea Busi b44a0a2e27 refactor: Create a model to handle realtime push notification 2023-07-14 16:17:15 +02:00
Andrea Busi 2b98a4e292 dependency: Update Pods 2023-07-13 17:20:56 +02:00
Andrea Busi 44a27536ad refactor: Remove PurchasePro controller 2023-05-19 17:07:12 +02:00
Andrea Busi 76dcabdc5e fix: Use localized name for time zone in position cell 2023-05-19 17:07:03 +02:00
Andrea Busi 2405be895c refactor: Remove radius and intensity filters for real time notifications
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/61
2023-05-19 17:06:54 +02:00
Andrea Busi 89ca785864 refactor: Remove unused logic for Do not disturb notifications 2023-05-19 17:06:32 +02:00
Andrea Busi 60678d0839 refactor: Use proper way to manage plural in localized strings 2023-05-18 12:02:01 +02:00
Andrea Busi 88b36a501d refactor: Align strings with Android app and some reorder 2023-05-18 12:01:26 +02:00
Andrea Busi c5b4448830 refactor: Let Xcode reorganize storyboard 2023-05-18 12:00:43 +02:00
Andrea Busi 58b8960e21 refactor: Remove unused "Convert to pro" cell in allerts 2023-05-18 12:00:43 +02:00
Andrea Busi 49d210eca1 refactor: Remove unused strings 2023-05-12 10:46:14 +02:00
Andrea Busi 068e457297 release: Increase version for release 2023-03-28 18:10:53 +02:00
Andrea Busi 9796a40e0e release: Update changelg 2023-03-28 18:09:40 +02:00
Andrea Busi 01e8996572 refactor: Use new request parameters for network downloads endpoint 2023-03-28 18:09:40 +02:00
Andrea Busi 19c6b3d642 refactor: Move selected seismis networks management to EQNUserData 2023-03-28 18:09:40 +02:00
Andrea Busi 0c63a59f19 feat: Add UOA as seismic network 2023-03-28 18:09:40 +02:00
Andrea Busi f0c56584b8 chore: Update gitignore 2023-03-28 18:09:40 +02:00
Andrea Busi e9961af792 fix: Try to improve ObjC-Swift interoperability 2023-03-24 09:54:39 +01:00
Andrea Busi 806b4b67bf feat: Migrate to new network downloads endpoint and remove weather feature 2023-03-24 09:54:27 +01:00
Andrea Busi 851ece0a3b feat: Migrate smartphone download and subscription counter to new cached endpoints 2023-03-24 09:53:10 +01:00
Andrea Busi 0a76768f88 feat: Migrate user report endpoint to cache 2023-03-24 09:52:21 +01:00
Andrea Busi b15efe83e0 dependency: Rework Firebase configuration, as per official documentation 2023-03-21 18:06:18 +01:00
Andrea Busi d84dc8657a dependency: Update Shogun 2023-03-21 17:58:07 +01:00
Andrea Busi bac5e909bb dependency: Update Pods 2023-03-21 17:55:22 +01:00
Andrea Busi 15088b744f release: Increase version for release 2022-12-30 00:18:07 +01:00
Andrea Busi ac03a0cccb refactor: Update translations for tracking permission 2022-12-30 00:17:57 +01:00
Andrea Busi 76a26e3100 release: Increase version for release 2022-12-07 17:56:52 +01:00
Andrea Busi cac6ed67ac feat: Configure AppTracking and Facebook required flags 2022-12-07 09:26:37 +01:00
Andrea Busi 094c682dbd refactor: Reorganize Firebase configuration 2022-12-07 09:26:03 +01:00
Andrea Busi c44f46ca46 refactor: Reorganize push notification configuration 2022-12-07 09:25:38 +01:00
Andrea Busi 5e8c3d0796 release: Increase version fo release 2022-12-06 09:12:53 +01:00
Andrea Busi 61ce27ed4b feat: Enable Facebook SDK in app 2022-12-02 17:33:42 +01:00
Andrea Busi 3aea60e560 dependency: Add Facebook SDK 2022-12-02 15:18:18 +01:00
Andrea Busi 84b61fd7e2 release: Increase version for release 2022-11-28 22:21:51 +01:00
Andrea Busi a5a8c6f5c5 fix: Solve missing critical alerts after notification service migration 2022-11-28 10:33:30 +01:00
Andrea Busi 61587a0341 release: Increase version for release 2022-11-24 11:08:22 +01:00
Andrea Busi a56a04a4ad refactor: Recreate NotificationService (in Swift) to solve unpredictable behaviour
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/54
2022-11-24 11:08:22 +01:00
Andrea Busi 7173dc7031 dependency: Update Shogun 2022-11-24 10:52:39 +01:00
Andrea Busi 35dbdbab28 fix: Reduce space 2022-11-18 10:57:08 +01:00
Andrea Busi 14614267d4 feat: Add color in user report total 2022-11-18 10:50:24 +01:00
Andrea Busi 32833010ed dependency: Update Shogun 2022-11-18 10:50:08 +01:00
Andrea Busi ecc77e9f2b fix: Solve runtime warning 2022-11-18 08:34:41 +01:00
Andrea Busi e69747f133 refactor: Migrate preference to AppPreferences class 2022-11-18 08:33:35 +01:00
Andrea Busi 09685fd4a7 feat: Store user selection for compact/expanded view in suer report 2022-11-17 22:21:59 +01:00
Andrea Busi 3e122240dc refactor: Rework image creation 2022-11-17 22:13:18 +01:00
Andrea Busi 95a214403b release: Increase version for release 2022-11-17 15:44:07 +01:00
Andrea Busi a7db3c6fa1 docs: Add some example payloads 2022-11-17 15:42:32 +01:00
Andrea Busi 4805c79ed6 refactor: Merge icons and colors in a single asset, to solve issue with notification extensions
For some reasons, notification extension doesn't support multiple xcasset
2022-11-17 15:42:32 +01:00
Andrea Busi c6ec20e180 refactor: Migrate some extensions usage to Shogun 2022-11-17 15:42:32 +01:00
Andrea Busi f6dfd4a761 dependency: Add Shogun library 2022-11-17 15:42:32 +01:00
Andrea Busi 0212d0a15f fix: Minor UI tweaks in user reports map 2022-11-17 15:42:32 +01:00
Andrea Busi 3176bde5ed release: Increase version for release 2022-11-17 15:42:32 +01:00
Andrea Busi dfdbbd0ed4 feat: Update new user report data in notification extension 2022-11-17 15:42:00 +01:00
Andrea Busi e9986e0fe1 feat: Align user report map to Android app 2022-11-15 22:47:42 +01:00
Andrea Busi beb264f95e refactor: Move create snapshot to an extension method 2022-11-15 22:41:04 +01:00
Andrea Busi 217b5edbcf fix: Properly evaluate if ads is visible 2022-11-15 22:37:32 +01:00
Andrea Busi e13f95aa5d feat: Add color assets for user report 2022-11-15 12:28:54 +01:00
Andrea Busi fc5bdbcc92 fix: Solve issue with new registration logic 2022-11-15 12:28:54 +01:00
Andrea Busi d0ee637449 refactor: Remove send comment in manual report 2022-11-15 12:28:54 +01:00
Andrea Busi 66e9d7035e fix: Increase size for new report earthquake section buttons 2022-11-15 12:28:34 +01:00
Andrea Busi 7db48e381f release: Update changelog 2022-11-15 12:28:34 +01:00
Andrea Busi f9bdb84ad9 feat: Align 'last 24h user reports' card to Android 2022-11-11 15:16:16 +01:00
Andrea Busi bee18f6407 refactor: Use existing category for dictionary parsing 2022-11-11 15:16:16 +01:00
Andrea Busi 96f3a44db4 feat: Reset Firebase token to force a new server registration 2022-11-11 15:16:16 +01:00
Andrea Busi e394259f24 feat: New report earthquake section
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/51
2022-11-11 15:16:13 +01:00
Andrea Busi 3b53350969 refactor: Store lastLocation in EQNUserData 2022-11-11 15:15:47 +01:00
Andrea Busi f3c3c19e39 refactor: Store userId in EQNUserData 2022-11-11 15:15:47 +01:00
Andrea Busi 8404d72d8f refactor: Rework user registration to solve double registration
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/50
2022-11-11 15:15:40 +01:00
Andrea Busi a3b0499ed3 dependency: Update Pods 2022-10-28 12:21:29 +02:00
Andrea Busi d923b37fbd refactor: Update to Xcode 14.1 recommended settings 2022-10-28 12:12:26 +02:00
Andrea Busi 282803cf98 release: Increase version for release 2022-07-18 12:54:07 +02:00
Andrea Busi 039e7b82a1 fix: Don't show debug view when tap 5 times on Settings 2022-07-18 12:53:57 +02:00
Andrea Busi b098caf2ef chore: Update push payloads 2022-06-23 11:33:50 +02:00
Andrea Busi 73caf9647c release: Increase version for release 2022-06-22 16:16:24 +02:00
Andrea Busi 8700e200f9 chore: Update push payloads 2022-06-22 16:15:50 +02:00
Andrea Busi e99845ff1b fix: Add missing check for null alert 2022-06-22 09:27:47 +02:00
Andrea Busi a0161e8f4c release: Increase version for release 2022-06-17 14:42:47 +02:00
Andrea Busi 1b9944a7ca fix: Solve retain cycle and crashes 2022-06-17 14:42:47 +02:00
Andrea Busi 4c00e4ef6a refactor: Review time to show full screen view or card only 2022-06-17 14:42:47 +02:00
Andrea Busi 63592e6cfb refactor: Create a model for the realtime alert 2022-06-17 14:42:47 +02:00
Andrea Busi 2877dff23c refactor: Increase card interspace 2022-06-17 10:09:04 +02:00
Andrea Busi f2386a1abb refactor: Don't show countdown in expanded card 2022-06-17 10:09:04 +02:00
Andrea Busi 5e4a500f03 dependency: Update Pods 2022-06-17 10:09:04 +02:00
Andrea Busi 2b8f2db7c5 feat: Add new realtime alert screen 2022-06-17 10:09:04 +02:00
Andrea Busi 11d994696d release: Increase version for release 2022-06-09 22:20:56 +02:00
Andrea Busi cd6e20c1b2 fix: Solve issue with EMSC networks 2022-06-09 22:20:48 +02:00
Andrea Busi af5371571c release: Increase version for release 2022-06-03 17:44:54 +02:00
Andrea Busi 6291b22df0 fix: Add a delay before calling the completion, to let iOS remove already posted notification 2022-06-03 17:44:32 +02:00
Andrea Busi 2a7cfd3079 release: Increase version for release 2022-06-01 18:10:01 +02:00
Andrea Busi cae5fee992 feat: Remove same type posted notifications
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/48
2022-05-30 16:02:46 +02:00
Andrea Busi 53b8c0fab4 release: Increase version for release 2022-05-29 22:16:19 +02:00
Andrea Busi 8751d3c8f2 fix: Solve crazy behaviour with iOS 15 2022-05-29 22:14:06 +02:00
Andrea Busi 2d333f993b feat: Make slider with step values 2022-05-29 22:13:46 +02:00
Andrea Busi 22d421f1cc refactor: Add debug log 2022-05-26 21:51:30 +02:00
Andrea Busi cec8b39fc7 fix: Be sure to update push token in Firebase 2022-05-26 21:51:08 +02:00
Andrea Busi 5ceaa4a8be fix: Save user position after server registration 2022-05-26 18:06:37 +02:00
Andrea Busi 94bdb9dbe1 fix: Don't try to parse nil data 2022-05-26 18:06:27 +02:00
Andrea Busi ee1b762032 fix: Call callback for position endpoint 2022-05-26 18:06:13 +02:00
Andrea Busi 5a6c5a5cfc release: Increase version for release 2022-05-22 21:31:58 +02:00
Andrea Busi d55b2ec98f fix: Solve wrong value save in settings 2022-05-22 21:31:50 +02:00
Andrea Busi 761ebc1d17 release: Increase version for release 2022-05-22 11:54:06 +02:00
Andrea Busi dd5e8862ed fix: Don't reload entire table view in settings, to avoid strange behaviour with iOS 15 2022-05-22 11:53:23 +02:00
Andrea Busi ceca3ed50d fix: Remove wrong translatesAutoresizingMaskIntoConstraints for cell views 2022-05-22 11:28:07 +02:00
Andrea Busi 6cce448acf release: Increase version for release 2022-05-20 18:37:45 +02:00
Andrea Busi 5ddb8da902 chore: Add project icon for SourceTree 2022-05-20 18:36:31 +02:00
Andrea Busi 644002a792 fix: Solve layout issue in seismic card 2022-05-20 18:35:26 +02:00
Andrea Busi e902969c9a release: Increase version for release 2022-05-19 11:26:56 +02:00
Andrea Busi 57c2ca5ae9 refactor: Disable ads
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/46
2022-05-19 11:26:50 +02:00
Andrea Busi 107d4c7600 fix: Remove broken reference 2022-05-19 11:26:33 +02:00
Andrea Busi 6a41f4558a dependency: Update Pods
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/45
2022-05-19 11:26:33 +02:00
Andrea Busi 995a6011d5 chore: Update certificates and profiles 2022-05-19 11:26:33 +02:00
Andrea Busi fcb3ab5be1 release: Update changelog 2022-05-19 11:26:33 +02:00
Andrea Busi f8c7edf588 refactor: Hide pro version section in home
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/43
2022-05-19 11:26:28 +02:00
Andrea Busi 4bb77e2921 refactor: Use new icon
Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/43
2022-05-19 11:26:22 +02:00
Andrea Busi d399e0b37b refactor: Replace deprecated 'class' in protocol definition
Also remove not needed @objc declaration

Resolves: https://gitlab.steamware.net/eqn/eqn.ios/-/issues/44
2022-05-19 11:25:22 +02:00
575 changed files with 6371 additions and 3452 deletions
+3
View File
@@ -1,3 +1,6 @@
# MacOS files
.DS_Store
# Exclude Pods
Sources/Pods
+86
View File
@@ -1,5 +1,91 @@
# Changelog
## Versione 5.7
- Aumentato target ad iOS 13
- Disattivata logica calibrazione/monitoraggio
- Aggiunto invio posizione in background
## Versione 5.6
- Aggiunta lingua araba
## Versione 5.5
- Aggiornata integrazione Firebase
- Migrati alcuni endpooint a cache.earthquakenetwork.it
- Rimossa funzionalità meteo
- Aggiunta rete UOA
- Rimosse stringhe non utilizzate
- Rimosse sezioni non utilizzate (es PRO)
- Aggiunta gestione localizzata plurali
- Rimossi filtri intensità da notifiche in tempo reale
- Rivista gestione notifiche push allerte
- Migrate costanti in nuova struttura Swift
## Versione 5.4
- Aggiunto SDK Facebook
## Versione 5.3.2
- Corretto problema con notifiche critiche
## Versione 5.3.1
- Ricreato progetto NotificationService
- Correzioni minori
## Versione 5.3
- Rivista gestione registrazione utente
- Modifica in segnalazioni utente
- Allineata mappa segnalazioni utente ad Android
## Versione 5.2.1
- Disattivata schermata di debug
## Versione 5.2
- Nuova schermata per allerta in tempo reale
## Versione 5.1
### Build (91)
- Corretto problema con selezioni reti EMSC
### Build (90)
- Rimozione notifiche dello stesso tipo
## Versione 5.0.3
### Build (89)
- Corretto problema con ultima posizione non salvata in app
- Forzato aggiornamento Firebase
- Corretti metodi deprecati in salvataggio dati
- Corretti comportamento filtri con iOS 15
- Slider con valori a step
## Versione 5.0.2
### Build (88)
- Corretto errato salvataggio raggio sismi forti in allerte tempo reale
## Versione 5.0.1
### Build (87)
- Corretto problema con slider in impostazioni
## Versione 5.0
### Build (86)
- Corretto problema layout in schede sismi
### Build (85)
- Nuova icona
- Nascosta sezione 'pro'
- Aggiornamento ad Xcode 13
- Disabilitati ads
## Versione 4.2
### Build (84)
- Corretti errori in nuovi sviluppi
+31
View File
@@ -0,0 +1,31 @@
{
"magnitude_range" : "0",
"provider" : "SGC",
"google.c.a.e" : "1",
"google.c.fid" : "d3PS1dEvrUA-tmLLpl5E5f",
"preliminary" : "0",
"longitude" : "-75.5157",
"gcm.message_id" : "1668682445010677",
"latitude" : "4.35306",
"type" : "official",
"google.c.sender.id" : "899482329945",
"difference" : "6",
"data" : "2022-11-17 11:48:00",
"depth" : "26",
"aps" : {
"content-available" : 1,
"alert" : {
"loc-key" : "Sisma rilevato a",
"title-loc-key" : "Segnalazione da rete sismica",
"loc-args" : [
"Cajamarca - Tolima, Colombia - M2.2"
]
},
"mutable-content" : 1,
"sound" : "default"
},
"magnitude" : "2.2",
"magnitude_type" : "M",
"place" : "Cajamarca - Tolima, Colombia",
"pop100" : "6622"
}
@@ -0,0 +1,34 @@
{
"Simulator Target Bundle": "com.finazzi.distquake",
"aps": {
"alert": {
"loc-args": [
"2 km da Foligno"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
},
"category": "notifica_con_mappa",
"content-available": 1,
"mutable-content": 1,
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2022-06-22 19:12:00",
"delay": 4,
"detection_latitude": "45.64664",
"detection_longitude": "9.188540",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"magnitude": 20,
"latitude": "42.958",
"location": "2 km da Foligno",
"longitude": "12.702",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "manual",
"wave_speed": "4.7",
"critical": true
}
@@ -0,0 +1,34 @@
{
"Simulator Target Bundle": "com.finazzi.distquake",
"aps": {
"alert": {
"loc-args": [
"14 km Al Qunfudhah"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
},
"category": "notifica_con_mappa",
"content-available": 1,
"mutable-content": 1,
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2023-08-10 15:57:00",
"delay": 4,
"detection_latitude": "18.65",
"detection_longitude": "42.76",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "18.65",
"location": "14 km Al Qunfudhah",
"longitude": "42.76",
"peak": "1.2",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
}
+10 -9
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"15 χλμ από το Αίγιο"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,20 +14,21 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:34:50",
"datetime": "2022-06-23 10:10:00",
"delay": 4,
"detection_latitude": "37.983810",
"detection_longitude": "23.727539",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "37.683810",
"location": "150 km (Test)",
"longitude": "23.327539",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "38.19",
"location": "15 χλμ από το Αίγιο",
"longitude": "22.26",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
}
}
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"14 km from California City"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-16 12:41:20",
"datetime": "2022-06-23 07:55:00",
"delay": 4,
"detection_latitude": "37.3229978",
"detection_longitude": "-122.0321823",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "37.9229978",
"location": "150 km (Test)",
"longitude": "-121.0321823",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "35.15",
"location": "14 km from California City",
"longitude": "-117.78",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"5 km de Las Cujas"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 20:39:00",
"datetime": "2022-06-23 10:19:00",
"delay": 4,
"detection_latitude": "-34.603722",
"detection_longitude": "-58.381592",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "-34.103722",
"location": "150 km (Test)",
"longitude": "-58.781592",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "-32.57",
"location": "5 km de Las Cujas",
"longitude": "-71.46",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"15 km de Dieppe"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:37:40",
"datetime": "2022-06-23 10:07:00",
"delay": 4,
"detection_latitude": "48.856614",
"detection_longitude": "2.8522219",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "48.456614",
"location": "150 km (Test)",
"longitude": "2.3522219",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "49.77",
"location": "15 km de Dieppe",
"longitude": "1.05",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"5 km od Novog"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:39:50",
"datetime": "2022-06-23 10:02:00",
"delay": 4,
"detection_latitude": "45.813177",
"detection_longitude": "15.977048",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "45.413177",
"location": "150 km (Test)",
"longitude": "15.677048",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "45.09",
"location": "5 km od Novog",
"longitude": "14.87",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"35 km dari Sindanbarang"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:42:40",
"datetime": "2022-06-23 10:13:00",
"delay": 4,
"detection_latitude": "-2.548926",
"detection_longitude": "118.0148634",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "-2.948926",
"location": "150 km (Test)",
"longitude": "118.6148634",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "-7.38",
"location": "35 km dari Sindanbarang",
"longitude": "106.83",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"2 km da Foligno"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 20:32:20",
"datetime": "2022-06-22 19:12:00",
"delay": 4,
"detection_latitude": "45.64664",
"detection_longitude": "9.188540",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "45.164664",
"location": "150 km (Test)",
"longitude": "9.788540",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "42.958",
"location": "2 km da Foligno",
"longitude": "12.702",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+9 -8
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"45 km de Puebla"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,19 +14,20 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-16 12:39:40",
"datetime": "2022-06-23 10:16:00",
"delay": 4,
"detection_latitude": "19.432608",
"detection_longitude": "-99.133209",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "19.932608",
"location": "150 km (Test)",
"longitude": "-98.033209",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "18.72",
"location": "45 km de Puebla",
"longitude": "-97.90",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
+10 -9
View File
@@ -3,7 +3,7 @@
"aps": {
"alert": {
"loc-args": [
"150 km (Test)"
"Bandırma'ya 25 km"
],
"loc-key": "Rilevato sisma forte a",
"title-loc-key": "Allerta sismica in tempo reale"
@@ -14,20 +14,21 @@
"sound": "alert_star_trek.wav"
},
"counter": 10,
"datetime": "2021-04-15 21:44:30",
"datetime": "2022-06-23 10:22:00",
"delay": 4,
"detection_latitude": "41.008238",
"detection_longitude": "28.978359",
"gcm.message_id": "1614708857742608",
"google.c.a.e": 1,
"google.c.sender.id": "899482329945",
"intensity": 2,
"latitude": "41.608238",
"location": "150 km (Test)",
"longitude": "28.678359",
"peak": "-1",
"randcode": 0,
"test": 1,
"latitude": "40.33",
"location": "Bandırma'ya 25 km",
"longitude": "27.66",
"peak": "0.4",
"randcode": 100,
"test": 0,
"type": "eqn",
"wave_speed": "4.7",
"critical": true
}
}
@@ -41,6 +41,7 @@
[super viewDidLoad];
[self.mapView registerClass:[EQNCustomAnnotationView class] forAnnotationViewWithReuseIdentifier:EQNCustomAnnotationView.SingleLineIdentifier];
[self.mapView registerClass:[EQNCustomAnnotationView class] forAnnotationViewWithReuseIdentifier:EQNCustomAnnotationView.SmallIdentifier];
}
- (void)didReceiveNotification:(UNNotification *)notification
@@ -67,9 +68,8 @@
[self.mapView addAnnotation:annotation];
} else if ([userInfo[@"type"] isEqualToString:@"manual"]){
EQNMapAnnotationUserReport *annotation = [[EQNMapAnnotationUserReport alloc] initWithTitle:@""
coordinate:coordinate.coordinate
magnitude:[userInfo[@"magnitudo"] intValue]];
EQNMapAnnotationUserReport *annotation = [[EQNMapAnnotationUserReport alloc] initWithMagnitude:[userInfo[@"magnitude"] intValue]
coordinate:coordinate.coordinate];
[self.mapView addAnnotation:annotation];
}
@@ -94,7 +94,7 @@
NSTimeInterval difference = MAX([self.userSeismicTimestamp timeIntervalSinceDate:now], 0);
NSInteger seconds = (int)lround(difference);
self.waveLabel.text = [NSString stringWithFormat:NSLocalizedString(@"alert_wave", @""), seconds];
self.waveLabel.text = [NSString localizedStringWithFormat:NSLocalizedString(@"alert_wave", @""), seconds];
if (difference <= 0) {
// stop the countdown
@@ -118,9 +118,9 @@
} else if ([annotation isKindOfClass:[EQNMapAnnotationUserReport class]]) {
EQNMapAnnotationUserReport *report = (EQNMapAnnotationUserReport *)annotation;
EQNCustomAnnotationView *annotationView = (EQNCustomAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:EQNCustomAnnotationView.SingleLineIdentifier];
annotationView.image = report.image;
annotationView.title = report.title;
EQNCustomAnnotationView *annotationView = (EQNCustomAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:EQNCustomAnnotationView.SmallIdentifier];
annotationView.image = [report imageWithHeight:EQNCustomAnnotationView.SmallViewImageHeight];
annotationView.title = report.timeDifference;
return annotationView;
}
return nil;
@@ -1,4 +0,0 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

+1 -24
View File
@@ -2,35 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>EQNNotificationService</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>NotificationService</string>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>
@@ -1,13 +0,0 @@
//
// NotificationService.h
// EQNNotificationService
//
// Refactored by Andrea Busi
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import <UserNotifications/UserNotifications.h>
@interface NotificationService : UNNotificationServiceExtension
@end
@@ -1,176 +0,0 @@
//
// NotificationService.m
// EQNNotificationService
//
// Refactored by Andrea Busi
// Copyright © 2020 Earthquake Network. All rights reserved.
//
#import "NotificationService.h"
#import "EQNAllertaSismica.h"
#import "Costanti.h"
@interface NotificationService ()
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@end
@implementation NotificationService
static NSString * const EQNSoundNotificationEQN = @"alert_sound.wav";
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent *_Nonnull))contentHandler
{
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// Configure the notification's payload.
UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init];
content.title = [NSString localizedUserNotificationStringForKey:request.content.title arguments:nil];
content.body = [NSString localizedUserNotificationStringForKey:request.content.body arguments:nil];
self.bestAttemptContent.title = content.title;
self.bestAttemptContent.body = content.body;
// check for required informations
NSDictionary *userInfo = request.content.userInfo;
NSString *notificationType = [userInfo objectForKey:@"type"];
if (userInfo == nil || notificationType == nil) {
[self contentComplete];
return;
}
NSString *iconName = @"";
if ([notificationType isEqualToString:@"eqn"]) {
// check if user has enabled critical alerts
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:EQNUserDefaultAppGroupSuite];
BOOL criticalAlertsEnabled = [sharedDefaults boolForKey:NOTIFICHE_ALLERA_SISMICA_CRITICAL_ALERTS];
// !!WORKAROUND
// this is a workaround to use critical alerts with legacy FCM api
// when the server implementation will be migrated to Firebase v1 APIs, this could be removed
BOOL isCritical = [[userInfo objectForKey:@"critical"] boolValue];
if (isCritical && criticalAlertsEnabled) {
self.bestAttemptContent.sound = [UNNotificationSound criticalSoundNamed:EQNSoundNotificationEQN withAudioVolume:1.0];
} else {
self.bestAttemptContent.sound = [UNNotificationSound soundNamed:EQNSoundNotificationEQN];
}
NSString *intensity = [userInfo objectForKey:@"intensity"];
switch ([intensity intValue]) {
case 0:
iconName = @"star_realtime_white.png";
break;
case 1:
iconName = @"star_realtime_lightblue.png";
break;
case 2:
iconName = @"star_realtime_blue.png";
break;
default:
break;
}
} else if ([notificationType isEqualToString:@"manual"]) {
NSString *intensity = [userInfo objectForKey:@"magnitude"];
switch ([intensity intValue]) {
case 0:
iconName = @"star_green.png";
break;
case 1:
iconName = @"star_yellow.png";
break;
case 2:
iconName = @"star_red.png";
break;
default:
break;
}
} else if ([notificationType isEqualToString:@"official"]) {
NSString *provaider = [userInfo objectForKey:@"provider"];
double intensity = [[userInfo objectForKey:@"magnitude"] doubleValue];
NSString *colore = @"";
if (intensity < 2.0) {
colore = @"_white";
} else if (intensity < 3.5) {
colore = @"_green";
} else if (intensity < 4.5) {
colore = @"_yellow";
} else if (intensity < 5.5) {
colore = @"_red";
} else {
colore = @"_purple";
}
if ([provaider isEqualToString:@"USGS"]) {
iconName = [NSString stringWithFormat:@"star%@.png", colore];
} else if ([provaider isEqualToString:@"SGC"]) {
iconName = [NSString stringWithFormat:@"star3%@.png", colore];
} else if ([provaider isEqualToString:@"CSN"]) {
iconName = [NSString stringWithFormat:@"star3f%@.png", colore];
} else if ([provaider isEqualToString:@"SSN"]) {
iconName = [NSString stringWithFormat:@"star4%@.png", colore];
} else if ([provaider isEqualToString:@"INPRES"]) {
iconName = [NSString stringWithFormat:@"star4r%@.png", colore];
} else if ([provaider isEqualToString:@"FUNVISIS"]) {
iconName = [NSString stringWithFormat:@"star6%@.png", colore];
} else if ([provaider isEqualToString:@"Ineter"]) {
iconName = [NSString stringWithFormat:@"triangle%@.png", colore];
} else if ([provaider isEqualToString:@"RSN"]) {
iconName = [NSString stringWithFormat:@"triangle2%@.png", colore];
} else if ([provaider isEqualToString:@"PHIVOLCS"]) {
iconName = [NSString stringWithFormat:@"triround_inner%@.png", colore];
} else if ([provaider isEqualToString:@"IGEPN"]) {
iconName = [NSString stringWithFormat:@"triround%@.png", colore];
} else if ([provaider isEqualToString:@"INGV"]) {
iconName = [NSString stringWithFormat:@"circle%@.png", colore];
} else if ([provaider isEqualToString:@"EMSC"]) {
iconName = [NSString stringWithFormat:@"dyamond%@.png", colore];
} else if ([provaider isEqualToString:@"IGP"]) {
iconName = [NSString stringWithFormat:@"dyamond_round%@.png", colore];
} else if ([provaider isEqualToString:@"JMA"]) {
iconName = [NSString stringWithFormat:@"esa%@.png", colore];
} else if ([provaider isEqualToString:@"GEONET"]) {
iconName = [NSString stringWithFormat:@"oct%@.png", colore];
} if ([provaider isEqualToString:@"CSI"]) {
iconName = [NSString stringWithFormat:@"penta%@.png", colore];
} else if ([provaider isEqualToString:@"IGN"]) {
iconName = [NSString stringWithFormat:@"square%@.png", colore];
} else if ([provaider isEqualToString:@"UASD"] || [provaider isEqualToString:@"BDTIM"] || [provaider isEqualToString:@"NCS"]) {
iconName = [NSString stringWithFormat:@"thick_star%@.png", colore];
} else if ([provaider isEqualToString:@"RSPR"]) {
iconName = [NSString stringWithFormat:@"star6f%@.png", colore];
}
}
if (![iconName isEqualToString:@""]) {
iconName = [iconName stringByReplacingOccurrencesOfString:@".png" withString:@""];
NSURL *imageUrl = [NSBundle.mainBundle URLForResource:iconName withExtension:@"png"];
NSError *attachmentError = nil;
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:iconName
URL:imageUrl
options:nil
error:&attachmentError];
if (!attachmentError) {
self.bestAttemptContent.attachments = @[attachment];
}
}
[self contentComplete];
}
- (void)serviceExtensionTimeWillExpire
{
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
[self contentComplete];
}
- (void)contentComplete
{
self.contentHandler(self.bestAttemptContent);
}
@end
@@ -0,0 +1,212 @@
//
// NotificationService.swift
// EQNNotificationService
//
// Created by Andrea Busi on 24/11/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import UserNotifications
import CoreLocation
import Shogun
class NotificationService: UNNotificationServiceExtension {
private static let EQNSoundNotification = UNNotificationSoundName("alert_sound.wav")
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let bestAttemptContent = bestAttemptContent else {
print("[NotificationService] Unable to get notification content")
contentComplete()
return
}
let userInfo = request.content.userInfo
guard let notificationType = userInfo.string(forKey: "type") else {
print("[NotificationService] Unable to get notification type")
contentComplete()
return
}
var iconName = ""
switch notificationType.lowercased() {
case "eqn":
// check if user has enabled critical alerts
let criticalAlertsEnabled = UserDefaults.appGroup?.bool(forKey: UserDefaults.AllertaSismicaCriticalAlerts) ?? false
// !!WORKAROUND
// this is a workaround to use critical alerts with legacy FCM api
// when the server implementation will be migrated to Firebase v1 APIs, this could be removed
let isCritical = userInfo.integer(forKey: "critical", orDefault: 0) == 1
if isCritical && criticalAlertsEnabled {
bestAttemptContent.sound = UNNotificationSound.criticalSoundNamed(Self.EQNSoundNotification)
} else {
bestAttemptContent.sound = UNNotificationSound(named: Self.EQNSoundNotification)
}
let intensity = userInfo.integer(forKey: "intensity")
switch intensity {
case 0:
iconName = "star_realtime_white.png"
case 1:
iconName = "star_realtime_lightblue.png"
case 2:
iconName = "star_realtime_blue.png"
default:
break
}
// evaluate intensity and get proper string to display
guard let latitude = userInfo.double(forKey: "latitude"),
let longitude = userInfo.double(forKey: "longitude"),
let peak = userInfo.double(forKey: "peak") else {
print("[NotificationService] Unable to get base info for intensity calculation")
return
}
let location = CLLocation(latitude: latitude, longitude: longitude)
guard let distance = EQNUserData.shared.lastLocation?.distance(from: location) else {
print("[NotificationService] Unable to calculate distance or get last location")
return
}
let distanceKm = distance / 1_000
let intensita = peak * exp(-distanceKm/peak/250)
let stringSuffix: String
if intensita < 0.004 {
stringSuffix = "no_shaking"
} else if intensita < 0.30 {
stringSuffix = "mild"
} else if intensita < 0.70 {
stringSuffix = "moderate"
} else {
stringSuffix = "strong"
}
bestAttemptContent.body = "alert_intensity_\(stringSuffix)".localized
case "manual":
// there are 12 levels, so a customized icon doesn't make sense
// use a generic warning icon instead
iconName = "warning_yellow.png"
case "official":
let provider = userInfo.string(forKey: "provider", orDefault: "")
let intensity = userInfo.double(forKey: "magnitude", orDefault: 0)
let color: String
if intensity < 2.0 {
color = "_white"
} else if intensity < 3.5 {
color = "_green"
} else if intensity < 4.5 {
color = "_yellow"
} else if intensity < 5.5 {
color = "_red"
} else {
color = "_purple"
}
iconName = manualIconName(for: provider, color: color)
default:
break
}
// add the icon as notification attachment
if !iconName.isEmpty {
iconName = iconName.replacingOccurrences(of: ".png", with: "")
if let imageUrl = Bundle(for: NotificationService.self).url(forResource: iconName, withExtension: "png"),
let attachment = try? UNNotificationAttachment(identifier: iconName, url: imageUrl) {
bestAttemptContent.attachments = [ attachment ]
}
}
// remove same type posted notification
removeNotifications(for: notificationType) { [weak self] in
self?.contentComplete()
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
// MARK: - Private
private func contentComplete() {
if let bestAttemptContent {
contentHandler?(bestAttemptContent)
}
}
private func removeNotifications(
for type: String?,
completion: @escaping() -> Void
) {
guard let type = type else {
completion()
return
}
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.getDeliveredNotifications { notifications in
let sameTypeNotifications = notifications.filter { notification in
let payload = notification.request.content.userInfo
if let notificationType = payload["type"] as? String {
return notificationType == type
}
return false
}
let identifiers = sameTypeNotifications.map { $0.request.identifier }
notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers)
// !! Note: this is a known issue/bug
// we need to add a delay before invoking the completion, otherwise the notification will not be remved
// ref: https://stackoverflow.com/questions/53697279/why-are-notifications-not-removed-with-removedeliverednotifications
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completion()
}
}
}
// MARK: - Helpers
private func manualIconName(for provider: String, color: String) -> String {
switch provider.uppercased() {
case "USGS": return "star\(color).png"
case "SGC": return "star3\(color).png"
case "CSN": return "star3f\(color).png"
case "SSN": return "star4\(color).png"
case "INPRES": return "star4r\(color).png"
case "FUNVISIS": return "star6\(color).png"
case "INETER": return "triangle\(color).png"
case "RSN": return "triangle2\(color).png"
case "PHIVOLCS": return "triround_inner\(color).png"
case "IGEPN": return "triround\(color).png"
case "INGV": return "circle\(color).png"
case "EMSC": return "dyamond\(color).png"
case "IGP": return "dyamond_round\(color).png"
case "JMA": return "esa\(color).png"
case "GEONET": return "oct\(color).png"
case "CSI": return "penta\(color).png"
case "IGN": return "square\(color).png"
case "UASD", "BDTIM", "NCS": return "thick_star\(color).png"
case "RSPR": return "star6f\(color).png"
case "UOA": return "triangle\(color).png"
default: return ""
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
LastUpgradeVersion = "1410"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
@@ -74,6 +74,7 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
LastUpgradeVersion = "1410"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
@@ -74,6 +74,7 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "shogun",
"kind" : "remoteSourceControl",
"location" : "https://github.com/andreabusi-it/Shogun",
"state" : {
"revision" : "c164595fdd5d0771a6a24cbff85a7582f0f07311",
"version" : "1.3.0"
}
}
],
"version" : 2
}
+96 -51
View File
@@ -8,21 +8,22 @@
#import "AppDelegate.h"
#import "Costanti.h"
#import "ServerRequest.h"
#import "EQNGeneratoreURLServer.h"
#import "EQNUser.h"
#import "EQNAccelerometroManager.h"
#import "EQNManager.h"
#import "EQNUtility.h"
#import "EQNAllertaSismica.h"
#import "EQNNotificheSegnalazioniUtente.h"
#import "EQNNotificheReteSismiche.h"
#import "EQNMainTabBarController.h"
#import "NSDictionary+EQNExtensions.h"
@import Firebase;
@import FirebaseCrashlytics;
@import UserNotifications;
@import AppTrackingTransparency;
@import FirebaseCore;
@import FirebaseMessaging;
@import FirebaseCrashlytics;
@import GoogleMobileAds;
@import FBSDKCoreKit;
@interface AppDelegate () <FIRMessagingDelegate, UNUserNotificationCenterDelegate>
@property (strong, nonatomic) NSArray<id<EQNCommandProtocol>> *commands;
@@ -36,34 +37,32 @@
// Test ads on specific devices
GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers = @[ @"81392581e1790d4fbc6eff919815088d" ];
#endif
#if ADS_ENABLED
// start 3rd party SDKs and custom managers
[GADMobileAds.sharedInstance startWithCompletionHandler:^(GADInitializationStatus *status) {
NSLog(@"[AppDelegate] GADMobileAds started with status: %@", status);
}];
[EQNUser defaultUser];
[EQNManager defaultManager];
[FIRApp configure];
[FIRMessaging messaging].delegate = self;
// add some generic logs for Crashlytics
NSString *language = [[NSLocale preferredLanguages] firstObject];
[[FIRCrashlytics crashlytics] setCustomValue:language forKey:@"lang"];
#endif
// execute commands
EQNStartupCommandsBuilder *builder = [[EQNStartupCommandsBuilder alloc] init];
self.commands = [builder build];
[builder executeWithCommands:self.commands];
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
[[UNUserNotificationCenter currentNotificationCenter]
requestAuthorizationWithOptions:authOptions
completionHandler:^(BOOL granted, NSError * _Nullable error) {
[self registraNotifica];
}];
[EQNUser defaultUser];
[EQNManager defaultManager];
[self configureFirebase];
[self configureFacebookSDKWithApplication:application andOptions:launchOptions];
// schedule background tasks
[BackgroundTaskManager.shared registerTasks];
[BackgroundTaskManager.shared scheduleUpdateServerPosition];
// add some generic logs for Crashlytics
NSString *language = [[NSLocale preferredLanguages] firstObject];
[[FIRCrashlytics crashlytics] setCustomValue:language forKey:@"lang"];
[self configurePushNotifications];
[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
[application registerForRemoteNotifications];
return YES;
@@ -84,12 +83,14 @@
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
[[EQNManager defaultManager] avviaManager];
[[EQNAccelerometroManager sharedInstance] startUpdatingLocationBackground];
NSUInteger counter = [[NSUserDefaults standardUserDefaults] integerForKey:EQNUserDefaultProDiscountOpenCounter];
// disabilitiamo logica calibrazione/monitoraggio perchè attualmente non utilizzata dal server
//[[EQNManager defaultManager] avviaManager];
//[[EQNAccelerometroManager sharedInstance] startUpdatingLocationBackground];
NSUInteger counter = [[NSUserDefaults standardUserDefaults] integerForKey:NSUserDefaults.UserDataProDiscountOpenCounter];
counter += 1;
[[NSUserDefaults standardUserDefaults] setInteger:counter forKey:EQNUserDefaultProDiscountOpenCounter];
[[NSUserDefaults standardUserDefaults] setInteger:counter forKey:NSUserDefaults.UserDataProDiscountOpenCounter];
}
@@ -122,7 +123,10 @@
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSString *token = [self stringWithDeviceToken:deviceToken];
[[NSUserDefaults standardUserDefaults] setObject:token forKey:EQNUserDefaultPushToken];
NSLog(@"[DEBUG] push token: %@", token);
[[NSUserDefaults standardUserDefaults] setObject:token forKey:NSUserDefaults.UserDataPushToken];
FIRMessaging.messaging.APNSToken = deviceToken;
}
- (NSString *)stringWithDeviceToken:(NSData *)deviceToken
@@ -144,8 +148,8 @@
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
// execute common logic and refresh the proper tab
NSDictionary *userInfo = notification.request.content.userInfo;
[self handlePushNotificationWithUserInfo:userInfo];
UNNotificationContent *content = notification.request.content;
[self handlePushNotificationWithNotificationContent:content];
// Change this to your preferred presentation option
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound);
@@ -154,37 +158,35 @@
// Handle notification messages after display notification is tapped by the user.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler
{
NSLog(@"[DEBUG] push payload\n%@", [response.notification.request.content.userInfo eqn_jsonStringWithPrettyPrint:YES]);
// execute common logic and refresh the proper tab
NSDictionary *userInfo = response.notification.request.content.userInfo;
[self handlePushNotificationWithUserInfo:userInfo];
UNNotificationContent *content = response.notification.request.content;
[self handlePushNotificationWithNotificationContent:content];
completionHandler();
}
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSURL *url = [EQNGeneratoreURLServer urlPosizione];
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:url richiesta:EQNTipoChiamataPosizione success:^(id result) {
completionHandler(UIBackgroundFetchResultNewData);
} failure:^(NSError *error) {
completionHandler(UIBackgroundFetchResultFailed);
}];
}
#pragma mark - Private
- (void)handlePushNotificationWithUserInfo:(NSDictionary *)userInfo
- (void)handlePushNotificationWithNotificationContent:(UNNotificationContent *)content
{
NSString *type = content.userInfo[@"type"];
EQNTabBarSection section = EQNTabBarSectionAllerte;
if ([userInfo[@"type"] isEqualToString:@"eqn"]) {
NSDate *dataRicezione = [NSDate date];
[[NSUserDefaults standardUserDefaults] setObject:dataRicezione forKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
[EQNUtility storeDictionary:userInfo toUserDefaultForKey:NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA];
if ([type isEqualToString:@"eqn"]) {
// Store both original payload and modified title/body
// This will be usefull to avoid to re-evaluate logic for title display.
NSDictionary *notification = @{
@"title": content.title,
@"body": content.body,
@"userInfo": content.userInfo
};
[EQNRealtimePushNotification storeNotificationWithPayload:notification];
section = EQNTabBarSectionAllerte;
} else if([userInfo[@"type"] isEqualToString:@"manual"]) {
} else if([type isEqualToString:@"manual"]) {
section = EQNTabBarSectionSegnalazioni;
} else if([userInfo[@"type"] isEqualToString:@"official"]) {
} else if([type isEqualToString:@"official"]) {
section = EQNTabBarSectionRetiSismiche;
}
@@ -192,19 +194,62 @@
[self.mainTabBarController selectSection:section];
}
#pragma mark - Configurations
- (void)configurePushNotifications
{
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) {
[self registraNotifica];
[self configureAppTracking];
}];
}
- (void)configureAppTracking
{
if (@available(iOS 14, *)) {
// add a delay otherwise the alert will not be displayed
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
if (status == ATTrackingManagerAuthorizationStatusAuthorized) {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = NO;
}
}];
});
} else {
FBSDKSettings.sharedSettings.isAdvertiserTrackingEnabled = YES;
}
}
- (void)configureFirebase
{
[FIRApp configure];
[FIRMessaging messaging].delegate = self;
}
- (void)configureFacebookSDKWithApplication:(UIApplication *)application andOptions:(NSDictionary *)launchOptions
{
[FBSDKApplicationDelegate.sharedInstance application:application didFinishLaunchingWithOptions:launchOptions];
[FBSDKSettings.sharedSettings setIsAdvertiserIDCollectionEnabled:YES];
[FBSDKSettings.sharedSettings setIsAdvertiserIDCollectionEnabled:YES];
}
#pragma mark - FIRMessagingDelegate
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken
{
NSLog(@"[Firebase] fcmToken %@", fcmToken);
if (![[NSUserDefaults standardUserDefaults] objectForKey:EQNUserDefaultUserFirebaseToken]) {
if (EQNUserData.sharedData.isFirstStart) {
// save default values for notification settings
[EQNAllertaSismica saveDefaultValues];
[EQNNotificheSegnalazioniUtente saveDefaultValues];
[EQNNotificheReteSismiche saveDefaultValues];
}
[EQNUser defaultUser].tokenUser = fcmToken;
[EQNUser.defaultUser registerUserIfNeededWithFirebaseToken:fcmToken];
}
@end
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -24,12 +24,12 @@
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" red="0.50766238939999997" green="0.75272958739999996" blue="0.98132258650000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Dzo-29-2FJ" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="LUF-7j-Kmc"/>
<constraint firstItem="Dzo-29-2FJ" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="MuU-xU-Ic2"/>
</constraints>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
@@ -38,6 +38,6 @@
</scene>
</scenes>
<resources>
<image name="eq_icon" width="166" height="166"/>
<image name="eq_icon" width="200" height="200"/>
</resources>
</document>
@@ -0,0 +1,98 @@
//
// Constants.swift
// Earthquake Network
//
// Created by Andrea Busi on 14/07/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
@objc
extension UserDefaults {
// UserDefaults condivisi con l'AppGroup
@objc(appGroupUserDefaults)
static let appGroup = UserDefaults(suiteName: "group.com.finazzi.distquake")
// Impostazioni della sezione `Allerta in tempo reale`
static let AllertaSismicaAbilitato = "NOTIFICHE_ALLERA_SISMICA_ABILITATO"
static let AllertaSismicaCriticalAlerts = "NOTIFICHE_ALLERA_SISMICA_CRITICAL_ALERTS"
static let AllertaSismicaSismiDaNotificare = "NOTIFICHE_ALLERA_SISMICA_SISMI_DA_NOTIFICARE"
static let AllertaSismicaRaggioSismiLievi = "NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_LIEVI"
static let AllertaSismicaRaggioSismiForti = "NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_FORTI"
static let AllertaSismicaImpostaVolume = "NOTIFICHE_ALLERA_SISMICA_IMPOSTA_VOLUME"
static let AllertaSismicaTestaAllarma = "NOTIFICHE_ALLERA_SISMICA_TESTA_ALLARME"
static let AllertaSismicaAbilitaIntervallo = "NOTIFICHE_ALLERA_SISMICA_ABILITA_INTERVALLO"
static let AllertaSismicaOraInizio = "NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO"
static let AllertaSismicaOraFine = "NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO"
// Impostazioni della sezione `Notifiche da reti sismiche`
static let NotificheRetiSismicheAbilitato = "NOTIFICHE_ATTIVA_RETI_SISMICHE"
static let NotificheRetiSismicheViciniAbilitato = "NOTIFICHE_ATTIVA_RETI_SISMICHE_VICINE"
static let NotificheRetiSismicheTerremotiFortiAbilitato = "NOTIFICHE_ATTIVA_RETI_TERREMOTI_FORTI"
static let NotificheRetiSismicheDistanzaPosizione = "NOTIFICHE_DISTANZA_POSIZIONE_RETI_SISMICHE"
static let NotificheRetiSismicheEnergiaSisma = "NOTIFICHE_ATTIVA_RETI_ENERGIA_SISMI"
static let NotificheRetiSismicheEnergiaTerremotiForti = "NOTIFICHE_ATTIVA_RETI_ENERGIA_FORTI"
static let NotificheRetiSismicheListaEnti = "NOTIFICHE_ATTIVA_RETI_LISTA_ENTI"
// Impostazioni della sezione `Notifiche segnalazioni utente`
static let NotificheSegnalazioniUtenteAbilitato = "NOTIFICHE_SU_ATTIVA_SEGNALAZIONE_UTENTE"
static let NotificheSegnalazioniUtenteDistanzaPosizione = "NOTIFICHE_SU_DISTANZA_POSIZIONE"
// Messaggio in segnalazione utente
static let UserReportMessage = "DATA_MESSAGE_EQN"
static let UserReportCodeStatus = "CODE_MESSAGE_EQN"
// Proprietà e preferenze dell'utente
/// Ultima posizione conosciuta dell'utente
static let UserDataLastLocation = "EQNLast_Location"
/// Token Firebase dell'utente corrente
static let UserDataFirebaseToken = "EQNToken_User"
/// Server user ID dell'utente corrente
static let UserDataUserId = "EQNUSER_ID"
/// Reti sismiche selezionate
static let UserDataSelectedSeismicNetworks = "IMPOSTAZIONE_ENTI_RETI_SISMICHEI"
/// Token delle notifiche push
static let UserDataPushToken = "EQNetwork.PushToken"
/// Numero di aperture dell'app per sbloccare la versione Pro scontata
static let UserDataProDiscountOpenCounter = "CONTEGGIO_APERTURE_PER_SCONTO"
/// Prezzo scontato per la versione pro scaduto
static let UserDataProDiscountExpired = "PREZZO_SCONTATO_SCADUTO"
/// Se `true` visualizza il tempo nelle annotazioni della mappa segnalazioni utente
static let UserReportExpandedView = "EQNData.UserReportExpandedView"
/// Se `true` visualizza le opzioni nella singole card in reti sismiche
static let AlertsShowCardOptions = "EQNetwork.AlertsShowAllCards"
// Migrazioni
static let AppMigrationV5_3 = "EQNUserDefaultMigrationV5_3"
static let AppMigrationV5_4 = "EQNUserDefaultMigrationV5_4"
// Notifica allerta salvata
static let RealTimeAlertPayload = "EQNData.RealtimePushNotificationPayload"
static let RealTimeAlertDate = "EQNData.RealtimeAlertDate"
// Filtri sezioni reti sismiche
static let SeismicMagnitudoMinima = "EQN_MAGNITUDO_MINIMA"
static let SeismicDistanzaMassima = "EQN_DISTANZA_MASSIMA"
static let SeismicEtaMassima = "EQN_ETA_MASSIMA"
static let SeismicSismiFortiAbilitati = "EQN_SISMI_FORTI_ABILITATI"
static let SeismicSismiForti = "EQN_SISMI_FORTI"
static let SeismicSismiQualsiasiMagnitudo = "EQN_SISMI_QUALSIASI_MAGNITUDO"
static let SeismicModificaImpostazioni = "EQN_SISMI_MODIFICA_IMPOSTAZIONI"
}
extension UserDefaults {
/// Get a generic stored values
/// - Parameters:
/// - key: A key in the current users defaults database.
/// - defaultValue: Default value to return if the key is not found
/// - Returns: The object associated with the specified key, or `defaultValue` if the key was not found.
func object<T>(forKey key: String, or defaultValue: T) -> T {
if let value = UserDefaults.standard.object(forKey: key) as? T {
return value
}
return defaultValue
}
}
@@ -16,7 +16,6 @@
@import StoreKit;
@import SafariServices;
@import CoreLocation;
@import GoogleMobileAds;
@interface AllerteViewController () <UITableViewDelegate, UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UIBarButtonItem *expandeCollapseButton;
@@ -27,7 +26,6 @@
@implementation AllerteViewController
static NSString * const SegueIdentifierProVersion = @"ShowProVersion";
static NSString * const SegueIdentifierPrioritySubscriptions = @"ShowPrioritySubscriptions";
/// Sections inside the app
@@ -37,7 +35,6 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
AllerteTableRowAllertePassate,
AllerteTableRowReteSmartphone,
AllerteTableRowServizioPriorita,
AllerteTableRowVersionePro,
AllerteTableRowDatiPosizione
};
@@ -107,6 +104,10 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
self.title = [NSLocalizedString(@"tab_network", nil) capitalizedString];
self.tableView.estimatedRowHeight = 200.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
if (EQNBackgroundPositionDebugHelper.shared.isEnabled) {
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemBookmarks target:self action:@selector(backgroundPositionDebugTapped:)];
}
}
- (void)refreshUI
@@ -114,18 +115,22 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
[super refreshUI];
// `AllerteTableRowReteSmartphone` and `AllerteTableRowDatiPosizione` are hidden bu default, user can show them
BOOL showAllCards = [[NSUserDefaults standardUserDefaults] boolForKey:EQNUserDefaultKeyAlertsShowAllCards];
BOOL showAllCards = AppPreferences.shared.alertsShowAllCards;
self.expandeCollapseButton.image = showAllCards ? [UIImage imageNamed:@"navbar-icon-arrow-collapse"] : [UIImage imageNamed:@"navbar-icon-arrow-expand"];
NSDate *date = [[NSUserDefaults standardUserDefaults] objectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
if (date) {
if ([EQNUtility getDifferenceMinute:date] < TEMPO_VISUALIZZAZIONE_NOTIFICA)
self.isNotificaAttiva = YES;
else{
self.isNotificaAttiva = NO;
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
[[NSUserDefaults standardUserDefaults] synchronize];
// controlliamo se c'è una notifica in tempo reale da mostrare
EQNRealtimePushNotification *notification = [EQNRealtimePushNotification storedNotification];
if (notification) {
self.isNotificaAttiva = YES;
// mostriamo la schermata solo se il countdown non è a zero
if (![notification isCountdownExpired]) {
RealtimeAlertViewController *controller = [[RealtimeAlertViewController alloc] initWithNotification:notification];
controller.modalInPresentation = YES;
[self presentViewController:controller animated:YES completion:nil];
}
} else {
[self resetRealtimeAlert];
}
[self.tableItems removeAllObjects];
@@ -140,10 +145,7 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
if (CLLocationManager.authorizationStatus != kCLAuthorizationStatusAuthorizedAlways) {
[self.tableItems addObject:@(AllerteTableRowLocationPermission)];
}
// pro cell is visible only if user hasn't already bought the app
if ([EQNPurchaseUtility isProVersionEnabled] == NO) {
[self.tableItems addObject:@(AllerteTableRowVersionePro)];
}
// location data visible only if last position is known
if ([EQNUser defaultUser].lastPosition != nil && showAllCards) {
[self.tableItems addObject:@(AllerteTableRowDatiPosizione)];
@@ -156,6 +158,12 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
[self.tableView reloadData];
}
- (void)resetRealtimeAlert
{
[EQNRealtimePushNotification removeStoredNotification];
self.isNotificaAttiva = NO;
}
#pragma mark - Actions
- (IBAction)refreshDataTapped:(id)sender
@@ -166,20 +174,21 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
- (IBAction)collapseExpandTapped:(id)sender
{
// toggle saved value
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL showAll = [defaults boolForKey:EQNUserDefaultKeyAlertsShowAllCards];
[defaults setBool:!showAll forKey:EQNUserDefaultKeyAlertsShowAllCards];
AppPreferences.shared.alertsShowAllCards = !AppPreferences.shared.alertsShowAllCards;
[self refreshUI];
}
- (IBAction)backgroundPositionDebugTapped:(id)sender
{
EQNBackgroundPositionDebugViewController *controller = [[EQNBackgroundPositionDebugViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller];
[self presentViewController:navController animated:YES completion:nil];
}
- (void)actionCloseNotification
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA];
[[NSUserDefaults standardUserDefaults] synchronize];
self.isNotificaAttiva = NO;
[self resetRealtimeAlert];
[self.tableView reloadData];
}
@@ -262,8 +271,8 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
} else if (tableRow == AllerteTableRowSismiRilevati) {
if (self.isNotificaAttiva) {
AlertsSeismicNotificationExpandedTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SeismicNotificationExpandedCell" forIndexPath:indexPath];
NSDictionary *info = [EQNUtility loadDictionaryFromUserDefaultsForKey:NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA];
cell.notification = info;
EQNRealtimePushNotification *notification = [EQNRealtimePushNotification storedNotification];
cell.notification = notification;
__weak AllerteViewController *weakSelf = self;
cell.onTapClose = ^{
@@ -322,10 +331,6 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
cell.smartphoneNetwork = [EQNManager defaultManager].rete_smartphone;
return cell;
} else if (tableRow == AllerteTableRowVersionePro) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ProVersionCell" forIndexPath:indexPath];
return cell;
} else if (tableRow == AllerteTableRowDatiPosizione) {
AlertsPositionDataTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PositionDataCell" forIndexPath:indexPath];
cell.position = [EQNUser defaultUser].lastPosition;
@@ -344,9 +349,6 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
case AllerteTableRowServizioPriorita:
[self performSegueWithIdentifier:SegueIdentifierPrioritySubscriptions sender:nil];
break;
case AllerteTableRowVersionePro:
[self performSegueWithIdentifier:SegueIdentifierProVersion sender:nil];
break;
default:
break;
}
@@ -43,11 +43,12 @@ class AlertsPositionDataTableViewCell: EQNBaseTableViewCell {
positionLabel.text = EQNUtility.coordinateString(coordinate: position.coordinate)
if let solar = Solar(coordinate: position.coordinate) {
let timeZone = TimeZone.current.localizedName(for: .generic, locale: .current) ?? TimeZone.current.identifier
if let sunrise = solar.sunrise {
sunriseTimeLabel.text = dateFormatter.string(from: sunrise) + " (\(TimeZone.current.identifier))"
sunriseTimeLabel.text = dateFormatter.string(from: sunrise) + " \(timeZone)"
}
if let sunset = solar.sunset {
sunsetTimeLabel.text = dateFormatter.string(from: sunset) + " (\(TimeZone.current.identifier))"
sunsetTimeLabel.text = dateFormatter.string(from: sunset) + " \(timeZone)"
}
}
}
@@ -40,7 +40,24 @@ class AlertsPriorityServiceTableViewCell: EQNBaseTableViewCell {
private func updateUI() {
guard let smartphoneNetwork = smartphoneNetwork else { return }
let formattedTime = EQNUtility.formattedString(forTimeDifference: smartphoneNetwork.lastSubscriptionDiff)
lastSubscriptionLabel.text = String(format: NSLocalizedString("inapp_adv_time", comment: ""), formattedTime)
lastSubscriptionLabel.text = subscriptionText(for: smartphoneNetwork.lastSubscriptionDiff)
}
private func subscriptionText(for time: Int) -> String {
var format = ""
var finalValue = time
// check for minutes, hours or days
if time < 60 {
format = NSLocalizedString("inapp_adv_minutes", comment: "")
} else if time < 1440 {
finalValue = time / 60
format = NSLocalizedString("inapp_adv_hours", comment: "")
} else {
finalValue = time / 1440
format = NSLocalizedString("inapp_adv_days", comment: "")
}
return String.localizedStringWithFormat(format, finalValue)
}
}
@@ -1,30 +0,0 @@
//
// AlertsProVersionTableViewCell.swift
// Earthquake Network
//
// Created by Andrea Busi on 05/04/21.
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import UIKit
class AlertsProVersionTableViewCell: EQNBaseTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
// MARK: - View Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
localizeUI()
}
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("network_pro", comment: "")
descriptionLabel.text = NSLocalizedString("network_topro", comment: "")
}
}
@@ -8,14 +8,14 @@
import UIKit
import MapKit
import Shogun
class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMapViewDelegate {
typealias DefaultCompletion = () -> Void
@objc var notification: [String: Any]? {
@objc var notification: EQNRealtimePushNotification? {
didSet {
startCountdown()
updateUI()
}
}
@@ -29,6 +29,7 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
@IBOutlet weak var notificationTitleLabel: UILabel!
@IBOutlet weak var notificationDescriptionLabel: UILabel!
@IBOutlet weak var notificationIntensityLabel: UILabel!
@IBOutlet weak var waveTimeLabel: UILabel!
@IBOutlet weak var mapView: MKMapView! {
didSet {
@@ -52,6 +53,7 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
super.awakeFromNib()
localizeUI()
setUI()
}
// MARK: - Private
@@ -64,15 +66,11 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
descriptionLabel.text = NSLocalizedString("map_smartphone_magnitude", comment: "")
}
private func startCountdown() {
guard let notification = notification else { return }
// calculate the impact timestamp and start a timer for the countdown label
if let impactTimestamp = EQNUtility.calculateUserSeismicTimestamp(fromUserInfo: notification) {
self.impactTimestamp = impactTimestamp
countdownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(countdownTimerFired(_:)), userInfo: nil, repeats: true)
countdownTimer?.fire()
}
private func setUI() {
shareButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
rateAppButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
viewOnTwitterButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
closeButton.layer.borderColor = AppTheme.Colors.darkGray.cgColor
}
private func updateUI() {
@@ -81,50 +79,33 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
notificationDescriptionLabel.text = ""
mapView.removeAnnotations(mapView.annotations)
guard let notification = notification,
let aps = notification["aps"] as? [String: Any],
let alert = aps["alert"] as? [String: Any] else { return }
guard let notification = notification else { return }
let intensity = notification.eqn_intValue(for: "intensity") ?? 0
containerView.backgroundColor = color(for: intensity)
containerView.backgroundColor = backgroundColor(for: notification.relativeIntensity())
notificationTitleLabel.text = notification.title
notificationIntensityLabel.text = notification.displayBody
notificationIntensityLabel.textColor = notification.relativeIntensityColor
if let title = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first {
notificationTitleLabel.text = String(format: NSLocalizedString(title, comment: ""), arg)
}
// get coordinate
var coordinate: CLLocation?
if let latitude = notification.eqn_doubleValue(for: "latitude"),
let longitude = notification.eqn_doubleValue(for: "longitude") {
if let date = notification.dateTime {
coordinate = CLLocation(latitude: latitude, longitude: longitude)
}
if let coordinate = coordinate,
let counter = notification["counter"],
let dateString = notification["datetime"] as? String,
let date = EQNUtility.getDateFrom(dateString) {
let distance = EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0
let distance = EQNUser.default().lastPosition?.distance(from: notification.coordinate) ?? 0.0
let distanceRound = Int(round(distance / 1_000))
let difference = Int(NSDate().timeIntervalSince(date) / 60.0)
notificationDescriptionLabel.text = ""
+ NSLocalizedString("official_card_distance", comment: "") + " \(distanceRound) km"
+ " - " + EQNUtility.formattedString(forTimeDifference: difference)
+ "\n" + String(format: NSLocalizedString("map_number", comment: ""), "\(counter)")
+ "\n" + String(format: NSLocalizedString("map_number", comment: ""), "\(notification.counter)")
}
if let coordinate = coordinate {
let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5)
let region = MKCoordinateRegion(center: coordinate.coordinate, span: span)
mapView.setCenter(coordinate.coordinate, animated: false)
mapView.setRegion(region, animated: true)
let annotation = EQNMapAnnotationPastquake(title: "", coordinate: coordinate.coordinate, intensity: intensity)
mapView.addAnnotation(annotation)
}
let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5)
let region = MKCoordinateRegion(center: notification.coordinate.coordinate, span: span)
mapView.setCenter(notification.coordinate.coordinate, animated: false)
mapView.setRegion(region, animated: true)
let annotation = EQNMapAnnotationPastquake(title: "", coordinate: notification.coordinate.coordinate, intensity: notification.intensity)
mapView.addAnnotation(annotation)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
@@ -137,32 +118,9 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
annotationView.title = annotation.title
return annotationView
}
private func color(for intensity: Int) -> UIColor {
switch intensity {
case 0: return UIColor(red: 208.0/255.0, green: 234.0/255.0, blue: 201.0/255.0, alpha:1.0)
case 1: return UIColor(red: 254.0/255.0, green: 252.0/255.0, blue: 203.0/255.0, alpha:1.0)
case 2: return UIColor(red: 254.0/255.0, green: 186.0/255.0, blue: 186.0/255.0, alpha:1.0)
default: return UIColor.white
}
}
// MARK: - Actions
@objc private func countdownTimerFired(_ sender: Timer) {
guard let impactTimestamp = impactTimestamp else { return }
let now = Date()
let difference = lround(max(impactTimestamp.timeIntervalSince(now), 0))
waveTimeLabel.text = String(format: NSLocalizedString("alert_wave", comment: ""), difference)
if difference <= 0 {
// stop the countdown
countdownTimer?.invalidate()
countdownTimer = nil
}
}
@IBAction private func shareAppTapped(_ sender: UIButton) {
onTapShareApp?()
}
@@ -178,4 +136,19 @@ class AlertsSeismicNotificationExpandedTableViewCell: EQNBaseTableViewCell, MKMa
@IBAction private func closeTapped(_ sender: UIButton) {
onTapClose?()
}
// MARK: - Helpers
private func backgroundColor(for intensity: Double) -> UIColor {
switch intensity {
case _ where intensity < 0.004:
return UIColor(named: "Gray (card background)")!
case _ where intensity < 0.30:
return UIColor(named: "Green (card background)")!
case _ where intensity < 0.70:
return UIColor(named: "Yellow (card background)")!
default:
return UIColor(named: "Red (card background)")!
}
}
}
@@ -65,6 +65,8 @@ class PasquakesMapViewController: EQNBaseMapViewController {
}
override func didTapAnnotation(_ annotation: MKAnnotation) {
mapView.deselectAnnotation(annotation, animated: true)
guard let annotation = annotation as? EQNMapAnnotationPastquake, let pastquake = annotation.pastquake else {
return
}
@@ -0,0 +1,84 @@
//
// EQNBackgrounPositionDebugViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 14/08/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import UIKit
import CoreLocation
class EQNBackgroundPositionDebugViewController: UITableViewController {
private var positions = [EQNBackgroundPosition]()
private let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .medium
formatter.dateStyle = .medium
return formatter
}()
private let helper = EQNBackgroundPositionDebugHelper()
// MARK: - Init
convenience init() {
self.init(style: .insetGrouped)
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
loadData()
}
private func configureUI() {
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 60.0
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(onTapDeleteButton(_:)))
}
private func loadData() {
positions = helper.loadPosition()
tableView.reloadData()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
positions.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let position = positions[indexPath.row]
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "PositionCell")
cell.textLabel?.text = formatter.string(from: position.date)
cell.detailTextLabel?.text = String(format: "Lat: %.4f - Lon: %.4f", position.coordinate.latitude, position.coordinate.longitude)
var imageView: UIImageView?
switch position.request {
case .none:
imageView = nil
case .some(true):
imageView = .init(image: .init(systemName: "checkmark.circle.fill"))
imageView?.tintColor = AppTheme.Colors.green
case .some(false):
imageView = .init(image: .init(systemName: "x.circle.fill"))
imageView?.tintColor = AppTheme.Colors.red
}
cell.accessoryView = imageView
return cell
}
// MARK: - Actions
@objc private func onTapDeleteButton(_ sender: UIBarButtonItem) {
helper.resetPositions()
loadData()
}
}
@@ -57,8 +57,8 @@ class EQNDebugViewController: UIViewController {
}
private func loadDebugData() {
let firebaseToken = UserDefaults.standard.string(forKey: EQNUserDefaultUserFirebaseToken) ?? ""
let pushToken = UserDefaults.standard.string(forKey: EQNUserDefaultPushToken) ?? ""
let firebaseToken = UserDefaults.standard.string(forKey: UserDefaults.UserDataFirebaseToken) ?? ""
let pushToken = UserDefaults.standard.string(forKey: UserDefaults.UserDataPushToken) ?? ""
let text =
"""
@@ -29,7 +29,7 @@
[[EQNManager defaultManager] avviaManager];
[[EQNAccelerometroManager sharedInstance] startUpdatingLocationBackground];
self.testo = [NSString stringWithFormat:@" LOG ID UTENTE %@\n\nTOKEN FIREBASE:\n%@\n\n", [EQNUser defaultUser].user_ID, [EQNUser defaultUser].tokenUser];
self.testo = [NSString stringWithFormat:@" LOG ID UTENTE %@\n\nTOKEN FIREBASE:\n%@\n\n", [EQNUser defaultUser].user_ID, EQNUserData.sharedData.firebaseToken];
self.logView.text = self.testo;
}
@@ -21,7 +21,11 @@
- (BOOL)isBannerVisible
{
#if ADS_ENABLED
return ![EQNPurchaseUtility isProVersionEnabled];
#else
return NO;
#endif
}
#pragma mark - View Lifecycle
@@ -44,7 +48,11 @@
{
[super viewWillAppear:animated];
#if ADS_ENABLED
[self handleAdBanner];
#else
[self hideAdBanner];
#endif
}
#pragma mark - Public
@@ -84,12 +92,9 @@
}
// Determine the view width to use for the ad width.
CGRect frame = self.view.frame;
// Here safe area is taken into account, hence the view frame is used after
// the view has been laid out.
if (@available(iOS 11.0, *)) {
frame = UIEdgeInsetsInsetRect(self.view.frame, self.view.safeAreaInsets);
}
CGRect frame = UIEdgeInsetsInsetRect(self.view.frame, self.view.safeAreaInsets);
CGFloat viewWidth = frame.size.width;
// Step 3 - Get Adaptive GADAdSize and set the ad view.
@@ -63,9 +63,9 @@ static NSString * const SegueIdentifierLogs = @"ShowLogs";
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"retry", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
// retry server registration
[[EQNUser defaultUser] verificaRegistrazione];
[[EQNUser defaultUser] retryUserRegistration];
}]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"options_cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"status_cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
});
}
@@ -136,7 +136,7 @@ static NSString * const SegueIdentifierLogs = @"ShowLogs";
}
// tap 5 times on "Settings" to open debug view
if ([controller isKindOfClass:[SettingsViewController class]]) {
if ([controller isKindOfClass:[SettingsViewController class]] && EQNEnableDebugView) {
self.debugTapCounter += 1;
if (self.debugTapCounter == 5) {
self.debugTapCounter = 0;
@@ -1,181 +0,0 @@
//
// PurchaseProVersionViewController.swift
// Earthquake Network
//
// Created by Busi Andrea on 29/07/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import UIKit
import SafariServices
import StoreKit
class PurchaseProVersionViewController: UIViewController {
@IBOutlet private weak var containerView: UIView!
@IBOutlet private weak var titleLabel: UILabel!
@IBOutlet private weak var subtitleLabel: UILabel!
@IBOutlet private weak var discountTextLabel: UILabel!
@IBOutlet private weak var descriptionTextLabel: UILabel!
@IBOutlet private weak var openPrivacyButton: UIButton!
@IBOutlet private weak var openTermsButton: UIButton!
@IBOutlet private weak var payingLabel: UILabel!
@IBOutlet private weak var priceLabel: UILabel!
@IBOutlet private weak var purchaseButton: UIButton!
// MARK: - Internal
private var products = [SKProduct]()
private var proProduct: SKProduct?
private var restoreTapped = false
/// Time remaining (in hours) for discounted price. If zero, no discount available
private var discountTimeRemaining: Int = 0
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(handlePurchaseNotification(_:)),
name: .EQNInAppPurchaseDidComplete,
object: nil)
configureUI()
loadProducts()
checkDiscountPrice()
}
// MARK: - Private
private func configureUI() {
let restoreButton = UIBarButtonItem(title: NSLocalizedString("purchase_pro_restore", comment: ""),
style: .plain,
target: self,
action: #selector(restoreTapped(_:)))
navigationItem.rightBarButtonItem = restoreButton
purchaseButton.isEnabled = false
titleLabel.text = NSLocalizedString("network_pro", comment: "")
subtitleLabel.text = NSLocalizedString("network_pro_subtitle", comment: "")
descriptionTextLabel.text = NSLocalizedString("purchase_pro_description", comment: "")
discountTextLabel.text = NSLocalizedString("purchase_pro_discount", comment: "")
discountTextLabel.isHidden = true
openPrivacyButton.setTitle(NSLocalizedString("network_pro_privacy_disclaimer", comment: ""), for: .normal)
openTermsButton.setTitle(NSLocalizedString("network_pro_terms_conditions", comment: ""), for: .normal)
payingLabel.text = NSLocalizedString("network_pro_paying", comment: "")
purchaseButton.setTitle(NSLocalizedString("network_pro_convert", comment: "").uppercased(), for: .normal)
containerView.eqn_applyShadowAndRoundedCorners()
}
private func updateUI() {
// search for the Pro product
let isDiscountEnabled = discountTimeRemaining > 0
let identifier = isDiscountEnabled ? VersioneProProducts.Identifier.ProVersionDiscounted : VersioneProProducts.Identifier.ProVersionFullPrice
guard let proProduct = products.first(where: { $0.productIdentifier.lowercased() == identifier.lowercased() }) else {
return
}
self.proProduct = proProduct
priceFormatter.locale = proProduct.priceLocale
if isDiscountEnabled {
discountTextLabel.isHidden = false
let string = NSLocalizedString("purchase_pro_discount", comment: "")
discountTextLabel.text = String(format: string, discountTimeRemaining)
}
priceLabel.text = priceFormatter.string(from: proProduct.price)
purchaseButton.isEnabled = true
if UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.ProVersionFullPrice) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.ProVersionDiscounted) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.Subscription10kYearly) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.Subscription10kYearlyDiscounted) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.Subscription100kYearly) ||
UserDefaults.standard.bool(forKey: VersioneProProducts.Identifier.Subscription100kYearlyDiscounted) {
purchaseButton.isEnabled = false
priceLabel.text = "-"
}
}
private func loadProducts() {
VersioneProProducts.store.requestProducts { [weak self] success, products in
guard let self = self, let products = products, success == true else { return }
self.products = products
self.updateUI()
}
}
private func checkDiscountPrice() {
EQNPurchaseUtility.offerTimeRemaining { (timeRemaining) in
DispatchQueue.main.async {
self.discountTimeRemaining = timeRemaining
self.updateUI()
}
}
}
// MARK: - Actions
@objc func restoreTapped(_ sender: Any) {
restoreTapped = true
VersioneProProducts.store.restorePurchases()
}
@IBAction func purchaseTapped(_ sender: UIButton) {
guard let product = proProduct else { return }
VersioneProProducts.store.buyProduct(product)
}
@IBAction func openExternalLinkTapped(_ sender: UIButton) {
var linkUrl: URL?
if sender == openPrivacyButton {
linkUrl = URL(string: "\(EQNWebsiteAddress)/privacy/")
} else if sender == openTermsButton {
linkUrl = URL(string: "\(EQNWebsiteAddress)/terms-conditions/")
}
if let url = linkUrl {
let controller = SFSafariViewController(url: url)
present(controller, animated: true, completion: nil)
}
}
// MARK: - Notifications
@objc func handlePurchaseNotification(_ notification: Notification) {
guard let productId = notification.object as? String,
products.contains(where: { $0.productIdentifier == productId }) else {
print("[PurchasePro] Unable to find the product")
return
}
if restoreTapped {
restoreTapped = false
let alert = UIAlertController(title: NSLocalizedString("purchase_pro_restore_alert_title", comment: ""),
message: NSLocalizedString("purchase_pro_restore_alert_message", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "ok", style: .default) { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
})
present(alert, animated: true)
} else {
navigationController?.popViewController(animated: true)
}
}
// MARK: - Helper
private var priceFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.formatterBehavior = .behavior10_4
formatter.numberStyle = .currency
return formatter
}()
}
@@ -8,6 +8,7 @@
import UIKit
import StoreKit
import Shogun
class SubscriptionsViewController: UITableViewController {
@@ -0,0 +1,223 @@
//
// RealtimeAlertView.swift
// Earthquake Network
//
// Created by Andrea Busi on 15/06/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
class RealtimeAlertContainerView: UIView {
lazy var alertView: RealtimeAlertView = {
let view = RealtimeAlertView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var animationColor: UIColor = .white
// MARK: - Init
convenience init() {
self.init(frame: .zero)
configureUI()
}
// MARK: - Private
private func configureUI() {
backgroundColor = .white
addSubview(alertView)
let margin: CGFloat = 24
alertView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: margin).isActive = true
alertView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -margin).isActive = true
alertView.topAnchor.constraint(equalTo: topAnchor, constant: margin).isActive = true
alertView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -margin).isActive = true
}
// MARK: - Public
func startBackgroundAnimation() {
let animation = CABasicAnimation(keyPath: "backgroundColor")
animation.fromValue = UIColor.white.cgColor
animation.toValue = animationColor.cgColor
animation.duration = 2.0
animation.beginTime = CACurrentMediaTime() + 1
animation.autoreverses = true
animation.repeatCount = .infinity
layer.add(animation, forKey: "backgroundColor")
}
}
class RealtimeAlertView: UIView {
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title2)
label.text = NSLocalizedString("app_name", comment: "")
return label
}()
let descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
let waveTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title2)
label.textColor = AppTheme.Colors.red
label.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), 0)
label.textAlignment = .center
label.isHidden = true
return label
}()
let intensityLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .title3)
label.textColor = AppTheme.Colors.red
label.textAlignment = .center
label.numberOfLines = 2
return label
}()
lazy var mapView: MKMapView = {
let map = MKMapView()
map.translatesAutoresizingMaskIntoConstraints = false
map.delegate = self
map.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
map.showsUserLocation = true
return map
}()
let closeButton: EQNBlurredCloseButton = {
let button = EQNBlurredCloseButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(.darkGray, for: .normal)
return button
}()
// MARK: - Init
convenience init() {
self.init(frame: .zero)
configureUI()
}
// MARK: - Private
private func configureUI() {
backgroundColor = AppTheme.Colors.lightGray
addSubview(closeButton)
addSubview(titleLabel)
addSubview(descriptionLabel)
addSubview(mapView)
closeButton.addDefaultConstraint(to: self)
titleLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true
titleLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -10.0).isActive = true
titleLabel.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 10.0).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor).isActive = true
descriptionLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor).isActive = true
descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20.0).isActive = true
let stackView = UIStackView(arrangedSubviews: [waveTimeLabel, intensityLabel])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.spacing = 20.0
addSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: descriptionLabel.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 20.0).isActive = true
mapView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
mapView.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 20.0).isActive = true
mapView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
// MARK: - Public
func addMapCircle(
center: CLLocationCoordinate2D,
radius: CLLocationDistance,
overlayId: String
) {
// remove any other existing overlays
let overlays = mapView.overlays.filter { $0.title == overlayId }
mapView.removeOverlays(overlays)
// add new overlay
let circle = MKCircle(center: center, radius: radius)
circle.title = overlayId
mapView.addOverlay(circle)
}
func addMapLine(
coordinates: [CLLocationCoordinate2D]
) {
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)
}
func addMapAnnotation(
title: String = "",
center: CLLocationCoordinate2D,
intensity: Int
) {
let annotation = EQNMapAnnotationPastquake(title: title, coordinate: center, intensity: intensity)
mapView.addAnnotation(annotation)
}
}
extension RealtimeAlertView: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
switch overlay {
case let circle as MKCircle:
let circleRenderer = MKCircleRenderer(overlay: circle)
circleRenderer.strokeColor = AppTheme.Colors.red
circleRenderer.fillColor = AppTheme.Colors.red.withAlphaComponent(0.2)
circleRenderer.lineWidth = 3.0
return circleRenderer
case let polyline as MKPolyline:
let polylineRenderer = MKPolylineRenderer(polyline: polyline)
polylineRenderer.strokeColor = .blue
polylineRenderer.lineWidth = 2.0
return polylineRenderer
default:
return MKOverlayRenderer(overlay: overlay)
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? EQNMapAnnotationPastquake else {
return nil
}
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
return annotationView
}
}
@@ -0,0 +1,196 @@
//
// RealtimeAlertViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 15/06/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import UIKit
import MapKit
class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
@objc var onClose: () -> Void = {}
// MARK: - Internal
private let containerView = RealtimeAlertContainerView()
private var notificationView: RealtimeAlertView {
containerView.alertView
}
/// Alert to display
private let realtimeAlert: EQNRealtimePushNotification
/// Timer to constantly update countdown label
private var countdownTimer: Timer?
/// Refresh time for wave animation
private let waveAnimationRefreshRate = 0.1
/// Current radius of the wave animation on the map
private var waveAnimationCurrentRadius: CLLocationDistance = 0
private var waveAnimationVelocity: Double = 1_000
/// Timer to simulate animation for the wave
private var waveAnimationTimer: Timer?
// MARK: - Init
@objc
init(notification: EQNRealtimePushNotification) {
self.realtimeAlert = notification
super.init(nibName: nil, bundle: nil)
self.waveAnimationCurrentRadius = currentWavePosition()
self.waveAnimationVelocity = evaluateWaveAnimationVelocity()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
// importante togliere il delegato, altrimenti causa crash
notificationView.mapView.delegate = nil
}
// MARK: - View Lifecycle
override func loadView() {
view = containerView
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
updateUI()
startCountdown()
startWaveAnimation()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
containerView.startBackgroundAnimation()
}
// MARK: - Private
private func configureUI() {
notificationView.closeButton.addTarget(self, action: #selector(onTapClose(_:)), for: .touchUpInside)
// configure color for animation
containerView.animationColor = realtimeAlert.relativeIntensityColor
}
private func updateUI() {
notificationView.descriptionLabel.text = realtimeAlert.title
// update title with distance from earthquake
let distanceRound = Int(round(realtimeAlert.distanceFromUser() / 1_000))
notificationView.descriptionLabel.text = (notificationView.descriptionLabel.text ?? "")
+ ".\n"
+ String(format: NSLocalizedString("official_distance", comment: ""), distanceRound)
notificationView.intensityLabel.text = realtimeAlert.displayBody
notificationView.intensityLabel.textColor = realtimeAlert.relativeIntensityColor
// center map on the earthquake coordinate
let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5)
let region = MKCoordinateRegion(center: realtimeAlert.coordinate.coordinate, span: span)
notificationView.mapView.setCenter(realtimeAlert.coordinate.coordinate, animated: false)
notificationView.mapView.setRegion(region, animated: true)
// aggiungiamo annotation con epicentro sisma
notificationView.addMapAnnotation(center: realtimeAlert.coordinate.coordinate, intensity: realtimeAlert.intensity)
// simuliamo animazione dell'onda sismica
notificationView.addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
// aggiungiamo un segmento tra la posizione del sisma e quella dell'utente
if let lastPosition = EQNUser.default().lastPosition {
notificationView.addMapLine(coordinates: [realtimeAlert.coordinate.coordinate, lastPosition.coordinate])
}
}
private func startCountdown() {
// show countdown only if time is less than 300 seconds
if realtimeAlert.currentCountdown() < 300 {
// start a timer for the countdown label
notificationView.waveTimeLabel.isHidden = false
countdownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(countdownTimerFired(_:)), userInfo: nil, repeats: true)
countdownTimer?.fire()
}
}
private func startWaveAnimation() {
waveAnimationTimer = Timer.scheduledTimer(timeInterval: waveAnimationRefreshRate, target: self, selector: #selector(mapWaveAnimationFired(_:)), userInfo: nil, repeats: true)
waveAnimationTimer?.fire()
}
// MARK: - Action
@objc private func onTapClose(_ sender: UIButton) {
// invalidiamo i timer, altri
countdownTimer?.invalidate()
countdownTimer = nil
waveAnimationTimer?.invalidate()
waveAnimationTimer = nil
onClose()
dismiss(animated: true)
}
// MARK: - Timer
@objc private func countdownTimerFired(_ sender: Timer) {
let countdown = realtimeAlert.currentCountdown()
notificationView.waveTimeLabel.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), countdown)
notificationView.waveTimeLabel.textColor = waveTimeTextColor(for: countdown)
if countdown <= 0 {
// stop the countdown
countdownTimer?.invalidate()
countdownTimer = nil
}
}
@objc private func mapWaveAnimationFired(_ sender: Timer) {
waveAnimationCurrentRadius += waveAnimationVelocity
notificationView.addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
}
// MARK: - Helpers
/// Evaluate current position for the wave
/// Used to define initial position for the wave circle
/// - Returns: Distance of the wave from the original earthquake point
private func currentWavePosition() -> Double {
// distanza tra utente e terremoto
let distance = realtimeAlert.distanceFromUser()
// calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo
let remainingDistance = realtimeAlert.waveSpeed * Double(realtimeAlert.currentCountdown())
return distance - remainingDistance
}
/// Evaluate wave velocity based on push notification data
/// - Returns: Wave velocity, used for animation
private func evaluateWaveAnimationVelocity() -> Double {
let velocity = realtimeAlert.waveSpeed
return velocity * waveAnimationRefreshRate
}
/// Returns the text color based on impact countdown
private func waveTimeTextColor(for countdown: Int) -> UIColor {
switch countdown {
case _ where countdown > 15:
return UIColor(red: 255.0/255.0, green: 140.0/255.0, blue: 0.0, alpha: 1.0)
case _ where countdown > 5:
return UIColor(red: 255.0/255.0, green: 100.0/255.0, blue: 0.0, alpha: 1.0)
default:
return UIColor(red: 255.0/255.0, green: 0.0/255.0, blue: 0.0, alpha: 1.0)
}
}
}
@@ -7,13 +7,13 @@
//
import UIKit
import Shogun
class SegnalazioniLast24HoursCell: EQNBaseTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var mildReportsLabel: UILabel!
@IBOutlet private weak var strongReportsLabel: UILabel!
@IBOutlet private weak var veryStrongReportsLabel: UILabel!
@IBOutlet private weak var reportsLabel: UILabel!
@IBOutlet private weak var reportsDescriptionLabel: UILabel!
@IBOutlet private weak var twitterButton: UIButton! {
didSet {
@@ -38,11 +38,11 @@ class SegnalazioniLast24HoursCell: EQNBaseTableViewCell {
// MARK: - Private
private func localizeUI() {
headerLabel.text = NSLocalizedString("main_map", comment: "")
headerLabel.text = NSLocalizedString("tab_manual", comment: "").capitalized
reportsDescriptionLabel.text = NSLocalizedString("main_map", comment: "")
mapButton.setTitle(NSLocalizedString("official_button_map", comment: ""), for: .normal)
}
// MARK: - Public
@objc func updateUI(for smartphoneNetwork: EQNReteSmartphone?) {
@@ -50,8 +50,15 @@ class SegnalazioniLast24HoursCell: EQNBaseTableViewCell {
return
}
self.mildReportsLabel.text = "\(smartphoneNetwork.manualGreen)"
self.strongReportsLabel.text = "\(smartphoneNetwork.manualYellow)"
self.veryStrongReportsLabel.text = "\(smartphoneNetwork.manualRed)"
let reports = smartphoneNetwork.manual
self.reportsLabel.text = "\(reports)"
if reports < 100 {
self.reportsLabel.textColor = UIColor(hex6: 0x01b400)
} else if reports < 1000 {
self.reportsLabel.textColor = UIColor(hex6: 0xe8da00)
} else {
self.reportsLabel.textColor = UIColor(hex6: 0xff0000)
}
}
}
@@ -8,6 +8,7 @@
import Foundation
import MapKit
import Shogun
class SegnalazioniMapViewController: EQNBaseMapViewController {
@@ -16,11 +17,74 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
let circle: MKCircle
}
override var isCloseButtonVisible: Bool {
false
}
private let appPreferences = AppPreferences.shared
/// Contains circles and related colors to draw overlays on the map
private var mapCircles = [MapCircle]()
/// Reports currently showned on the map
private var filteredReports = [EQNSegnalazione]()
// MARK: - UI
// app icon and name displayed on the screenshot
private lazy var watermarkView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = true
let logo = UIImageView(image: .init(named: "eq_icon_transparent"))
logo.translatesAutoresizingMaskIntoConstraints = false
logo.contentMode = .scaleAspectFit
view.addSubview(logo)
logo.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
logo.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
logo.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
logo.widthAnchor.constraint(equalTo: logo.heightAnchor).isActive = true
let title = UILabel()
title.translatesAutoresizingMaskIntoConstraints = false
title.text = NSLocalizedString("app_name", comment: "") + " App"
title.textColor = AppTheme.Colors.red
title.font = .preferredFont(forTextStyle: .title3, weight: .semibold)
view.addSubview(title)
title.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 10.0).isActive = true
title.centerYAnchor.constraint(equalTo: logo.centerYAnchor).isActive = true
title.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
return view
}()
private lazy var magnitudeLegendView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.distribution = .fillEqually
[20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120].forEach { magnitude in
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = (magnitude / 10).romanNumber()
label.backgroundColor = UIColor(named: "Mercalli \(magnitude)")
label.textAlignment = .center
label.font = .preferredFont(forTextStyle: .callout)
label.textColor = magnitude >= 100 ? .white : .black
stackView.addArrangedSubview(label)
}
view.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
return view
}()
// MARK: - View Lifecycle
override func viewDidLoad() {
@@ -29,8 +93,31 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
// MARK: - Public
override func extraUI() {
view.addSubview(magnitudeLegendView)
view.addSubview(watermarkView)
magnitudeLegendView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
magnitudeLegendView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
magnitudeLegendView.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
magnitudeLegendView.topAnchor.constraint(equalTo: mapView.topAnchor).isActive = true
watermarkView.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 10.0).isActive = true
watermarkView.leadingAnchor.constraint(equalTo: mapView.leadingAnchor, constant: 10.0).isActive = true
watermarkView.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
}
override func configureUI() {
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTapCloseButton(_:)))
navigationItem.rightBarButtonItems = [
UIBarButtonItem(image: UIImage(named: "navbar-icon-screenshot"), style: .plain, target: self, action: #selector(onTapScreenshotButton(_:))),
UIBarButtonItem(image: UIImage(named: "navbar-icon-pin-arrow"), style: .plain, target: self, action: #selector(onTapMapDetailStyleButton(_:)))
]
}
override func registerMapAnnotationViews() {
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SmallIdentifier)
}
override func loadDataSource() {
@@ -96,27 +183,41 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
}
}
override func didTapAnnotation(_ annotation: MKAnnotation) {
guard let annotation = annotation as? EQNMapAnnotationUserReport, let report = annotation.report else {
return
// MARK: - Actions
@objc private func onTapCloseButton(_ sender: Any) {
dismiss(animated: true)
}
@objc private func onTapMapDetailStyleButton(_ sender: Any) {
appPreferences.userReportExpandedView.toggle()
loadDataSource()
}
@objc private func onTapScreenshotButton(_ sender: Any) {
let snapshot = createSnapshot()
let controller = UIActivityViewController(activityItems: [snapshot], applicationActivities: [])
present(controller, animated: true)
}
public func createSnapshot() -> UIImage {
// mostriamo il watermark e nascondiamo la legenda
watermarkView.isHidden = false
magnitudeLegendView.isHidden = true
// riduciamo la porzione da salvare alla sola mappa (eliminiamo i filtri)
let size = CGSize(width: view.bounds.width, height: mapView.bounds.maxY)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { ctx in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
let difference = Int(Date().timeIntervalSince(report.date) / 60.0)
let title = EQNUtility.formattedString(forTimeDifference: difference) + " - \(report.intensity.description)"
// torniamo allo stato originale
watermarkView.isHidden = true
magnitudeLegendView.isHidden = false
var message = ""
+ "🏢 " + report.address
+ "\n" + EQNUtility.formattedDate(from: report.date) + " \(NSLocalizedString("share_yourtime", comment: ""))"
if !report.message.isEmpty {
message += "\n💬 \(report.message)"
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("main_share", comment: ""), style: .default, handler: { [unowned self] _ in
self.openShareActivity(for: report)
}))
alert.addAction(UIAlertAction(title: NSLocalizedString("official_close", comment: ""), style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
return image
}
// MARK: - Private
@@ -125,7 +226,7 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
let vector_latitude = reports.map { $0.coordinate.coordinate.latitude }
let vector_longitude = reports.map { $0.coordinate.coordinate.longitude }
let vector_date = reports.map { $0.date }
let vector_state = reports.map { $0.intensity.rawValue }
let vector_state = reports.map { $0.intensity }
let minutes: TimeInterval = filter.minutes
@@ -200,21 +301,7 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
}
let circles = Array(0..<cluster_code).map { (i) -> MapCircle in
var value_distance = max_distance[i] / 20.0
if value_distance > 1.0 {
value_distance = 1.0
}
let value_intensity = (cluster_intensity[i]-1.0) / 2.0
let value_reference = max(value_distance, value_intensity)
let color: UIColor
if value_reference <= 0.5 {
let red = round(value_reference * 510)
color = UIColor(red: CGFloat(red / 255.0), green: 230.0/255.0, blue: 0.0, alpha: 1.0)
} else {
let green = round(230 - (value_reference - 0.5) * 460)
color = UIColor(red: 255.0, green: CGFloat(green / 255.0), blue: 0.0, alpha: 1.0)
}
let color: UIColor = AppTheme.Colors.darkGray
let centre = CLLocation(latitude: lat_centre[i], longitude: lon_centre[i])
let farest = CLLocation(latitude: lat_farest[i], longitude: lon_farest[i])
@@ -244,25 +331,6 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
Date().timeIntervalSince(date) / 60.0
}
private func openShareActivity(for report: EQNSegnalazione) {
// create message to share
let intensity = report.intensity.description
let difference = Int(Date().timeIntervalSince(report.date) / 60.0)
let time = EQNUtility.formattedString(forTimeDifference: difference)
let message = [
NSLocalizedString("share_hashtag", comment: ""),
intensity,
NSLocalizedString("share_felt", comment: ""),
report.address,
time + ".",
NSLocalizedString("share_notified", comment: "")
].joined(separator: " ")
let controller = UIActivityViewController(activityItems: [message], applicationActivities: [])
present(controller, animated: true)
}
// MARK: - Map
override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
@@ -270,10 +338,17 @@ class SegnalazioniMapViewController: EQNBaseMapViewController {
return nil
}
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
let identifier = appPreferences.userReportExpandedView ? EQNCustomAnnotationView.SingleLineIdentifier : EQNCustomAnnotationView.SmallIdentifier
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
let size = appPreferences.userReportExpandedView ? EQNCustomAnnotationView.SingleLineImageHeight : EQNCustomAnnotationView.SmallViewImageHeight
annotationView.image = annotation.image(with: size)
annotationView.title = annotation.timeDifference
annotationView.canShowCallout = true
// Psizioniamo più in alto le segnalazioni con intensità maggiore.
// Valori maggiori di anchorPointZ mettono la view più in basso,
// quindi invertiamo il valore dell'intensità
annotationView.layer.anchorPointZ = (1000 - CGFloat(annotation.report?.intensity ?? 0))
return annotationView
}
@@ -10,15 +10,23 @@ import UIKit
class SegnalazioniSendReportCell: EQNBaseTableViewCell {
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var descriptionLabel: UILabel!
@IBOutlet private weak var mildLabel: UILabel!
@IBOutlet private weak var mildButton: UIButton!
@IBOutlet private weak var strongLabel: UILabel!
@IBOutlet private weak var strongButton: UIButton!
@IBOutlet private weak var veryStrongLabel: UILabel!
@IBOutlet private weak var veryStrongButton: UIButton!
@objc var onTapReport: (_ magnitude: Int) -> Void = { _ in }
// MARK: - UI
@IBOutlet private weak var headerLabel: UILabel!
@IBOutlet private weak var reportMercalli2: UILabel!
@IBOutlet private weak var reportMercalli3: UILabel!
@IBOutlet private weak var reportMercalli4: UILabel!
@IBOutlet private weak var reportMercalli5: UILabel!
@IBOutlet private weak var reportMercalli6: UILabel!
@IBOutlet private weak var reportMercalli7: UILabel!
@IBOutlet private weak var reportMercalli8: UILabel!
@IBOutlet private weak var reportMercalli9: UILabel!
@IBOutlet private weak var reportMercalli10: UILabel!
@IBOutlet private weak var reportMercalli11: UILabel!
@IBOutlet private weak var reportMercalli12: UILabel!
// MARK: - View Lifecycle
override func awakeFromNib() {
@@ -31,21 +39,23 @@ class SegnalazioniSendReportCell: EQNBaseTableViewCell {
private func localizeUI() {
headerLabel.text = NSLocalizedString("main_feel", comment: "")
descriptionLabel.text = NSLocalizedString("manual_usebutton", comment: "")
mildLabel.text = NSLocalizedString("manual_mild", comment: "")
strongLabel.text = NSLocalizedString("manual_strong", comment: "")
veryStrongLabel.text = NSLocalizedString("manual_verystrong", comment: "")
reportMercalli2.text = NSLocalizedString("mercalli_II", comment: "")
reportMercalli3.text = NSLocalizedString("mercalli_III", comment: "")
reportMercalli4.text = NSLocalizedString("mercalli_IV", comment: "")
reportMercalli5.text = NSLocalizedString("mercalli_V", comment: "")
reportMercalli6.text = NSLocalizedString("mercalli_VI", comment: "")
reportMercalli7.text = NSLocalizedString("mercalli_VII", comment: "")
reportMercalli8.text = NSLocalizedString("mercalli_VIII", comment: "")
reportMercalli9.text = NSLocalizedString("mercalli_IX", comment: "")
reportMercalli10.text = NSLocalizedString("mercalli_X", comment: "")
reportMercalli11.text = NSLocalizedString("mercalli_XI", comment: "")
reportMercalli12.text = NSLocalizedString("mercalli_XII", comment: "")
}
override func layoutSubviews() {
super.layoutSubviews()
let labels: [UILabel] = [mildLabel, strongLabel, veryStrongLabel]
labels.forEach { (label) in
label.layer.borderWidth = AppTheme.shared.buttonBorderWidth
label.layer.borderColor = AppTheme.shared.buttonBorderColor.cgColor
label.layer.cornerRadius = AppTheme.shared.buttonCornerRadius
label.clipsToBounds = true
}
// MARK: - Actions
@IBAction private func onTapReportButton(_ sender: UIButton) {
let magnitude = sender.tag
onTapReport(magnitude)
}
}
@@ -46,11 +46,11 @@
{
[super refreshUI];
if ([self.userDefoult objectForKey:DATA_MESSAGE_EQN]){
NSDate *dateMessage = [self.userDefoult objectForKey:DATA_MESSAGE_EQN];
if ([EQNUtility getDifferenceMinute:dateMessage] >= EQNSendReportDelayBetweenComments){
[self.userDefoult removeObjectForKey:DATA_MESSAGE_EQN];
[self.userDefoult removeObjectForKey:CODE_MESSAGE_EQN];
if ([self.userDefoult objectForKey:NSUserDefaults.UserReportMessage]){
NSDate *dateMessage = [self.userDefoult objectForKey:NSUserDefaults.UserReportMessage];
if (![dateMessage isBeforeInterval:EQNSendReportDelayBetweenComments]) {
[self.userDefoult removeObjectForKey:NSUserDefaults.UserReportMessage];
[self.userDefoult removeObjectForKey:NSUserDefaults.UserReportCodeStatus];
}
}
@@ -70,14 +70,6 @@
return 2;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
switch (indexPath.row) {
case 0: return 140;
default: return UITableViewAutomaticDimension;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0) {
@@ -87,7 +79,10 @@
return cell;
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ReportEarthquakeCell" forIndexPath:indexPath];
SegnalazioniSendReportCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ReportEarthquakeCell" forIndexPath:indexPath];
cell.onTapReport = ^(NSInteger magnitude) {
[self sendReportTappedWithMagnitude:magnitude];
};
return cell;
}
@@ -96,7 +91,8 @@
- (IBAction)openMapTapped:(id)sender
{
SegnalazioniMapViewController *controller = [[SegnalazioniMapViewController alloc] init];
[self presentViewController:controller animated:YES completion:nil];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller];
[self presentViewController:navController animated:YES completion:nil];
}
- (IBAction)openTwitterTapped:(id)sender
@@ -117,12 +113,12 @@
[[EQNManager defaultManager] sincronizza];
}
- (IBAction)sendReportTapped:(UIButton *)sender
- (void)sendReportTappedWithMagnitude:(NSInteger)magnitude
{
// check to avoid multiple consecutive reports
if ([self.userDefoult objectForKey:CODE_MESSAGE_EQN]) {
NSDate *dateMessage = [self.userDefoult objectForKey:DATA_MESSAGE_EQN];
if ([EQNUtility getDifferenceMinute:dateMessage] <= EQNSendReportDelayBetweenMessages){
if ([self.userDefoult objectForKey:NSUserDefaults.UserReportCodeStatus]) {
NSDate *dateMessage = [self.userDefoult objectForKey:NSUserDefaults.UserReportMessage];
if (![dateMessage isBeforeInterval:EQNSendReportDelayBetweenMessages]) {
NSString *message = NSLocalizedString(@"manual_wait", @"");
[self showErrorAlertWithMessage:message];
[self performSelectorOnMainThread:@selector(sincronizzazione) withObject:nil waitUntilDone:YES];
@@ -133,9 +129,9 @@
// ask for user confirmation
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"attention", @"") message:NSLocalizedString(@"manual_sure", nil) preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"manual_yes", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[self executeSendReportWithMagnitude:sender.tag];
[self executeSendReportWithMagnitude:magnitude];
}]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"options_cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"status_cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
@@ -158,16 +154,14 @@
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:url richiesta:EQNTipoChiamataSegnalazioneTerremoto success:^(id result) {
[self.userDefoult setObject:result forKey:CODE_MESSAGE_EQN];
[self.userDefoult setObject:[NSDate date] forKey:DATA_MESSAGE_EQN];
[self.userDefoult setObject:result forKey:NSUserDefaults.UserReportCodeStatus];
[self.userDefoult setObject:[NSDate date] forKey:NSUserDefaults.UserReportMessage];
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"report", @"")
message:NSLocalizedString(@"manual_ok", @"")
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ok", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[self sendComment];
}]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ok", nil) style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
});
} failure:^(NSError * error) {
@@ -177,42 +171,6 @@
}];
}
- (void)sendComment
{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"manual_sendmessage_button" , @"")
message:NSLocalizedString(@"manual_sendmessage", @"")
preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
}];
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ok" ,@"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
UITextField * messaggio = alertController.textFields.firstObject;
NSURL *url = [EQNGeneratoreURLServer urlInvioCommentoTerremoto:messaggio.text codeMessage:[self.userDefoult objectForKey:CODE_MESSAGE_EQN]];
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:url richiesta:EQNTipoChiamataCommentoTerremoto success:^(id result) {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"manual_sendmessage_button" , @"")
message:NSLocalizedString(@"manual_message_received", @"")
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ok", nil) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
});
} failure:^(NSError * error) {
[self showErrorAlertWithMessage:error.localizedDescription];
}];
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshUI];
});
}]];
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"options_cancel", @"") style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alertController animated:YES completion:nil];
}
#pragma mark - Private
- (void)showErrorAlertWithMessage:(NSString *)errorMessage
@@ -9,12 +9,12 @@
import UIKit
import MapKit
import CoreLocation
import Shogun
protocol SeismicNetworkTableViewCellDelegate: class {
protocol SeismicNetworkTableViewCellDelegate: AnyObject {
func seismicNetworkCellDidTapShare(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapMapDetail(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapWeather(_ cell: SeismicNetworkTableViewCell, hasValidWeatherData: Bool)
func seismicNetworkCellDidTapCalendar(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapSettings(_ cell: SeismicNetworkTableViewCell)
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkTableViewCell)
@@ -44,8 +44,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
case normal
/// Cell with map visible
case mapExpanded
/// Cell with weather info visible
case weatherExpanded
}
/// Delegate
@@ -55,7 +53,7 @@ class SeismicNetworkTableViewCell: UITableViewCell {
private static let DefaultVerticalSpacing: CGFloat = 6.0
private static let DefaultBodyFont = UIFont.preferredFont(forTextStyle: .body)
private static let DefaultBodyFontLight = UIFont.preferredFont(for: .body, weight: .light)
private static let DefaultBodyFontLight = UIFont.preferredFont(forTextStyle: .body, weight: .light)
/// Seismic to show
private var seismic: EQNSisma?
@@ -89,8 +87,8 @@ class SeismicNetworkTableViewCell: UITableViewCell {
private lazy var placeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.preferredFont(for: .title2, weight: .semibold)
label.numberOfLines = 2
label.font = UIFont.preferredFont(forTextStyle: .title2, weight: .semibold)
label.numberOfLines = 3
return label
}()
@@ -173,20 +171,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
return mapView
}()
private lazy var weatherImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var weatherInfoLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.font = Self.DefaultBodyFontLight
return label
}()
// MARK: - Init
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
@@ -203,7 +187,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
private func setupUI() {
selectionStyle = .default
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .clear
// container view
@@ -256,7 +239,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
titleImageView.widthAnchor.constraint(equalTo: titleImageView.heightAnchor).isActive = true
networkLabel.heightAnchor.constraint(equalToConstant: 34.0).isActive = true
networkLabel.setContentHuggingPriority(.init(800), for: .horizontal)
networkLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
placeLabel.setContentHuggingPriority(.init(200), for: .horizontal)
placeLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
shareButton.widthAnchor.constraint(equalToConstant: titleComponentsHeight).isActive = true
shareButton.widthAnchor.constraint(equalTo: shareButton.heightAnchor).isActive = true
@@ -343,8 +328,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
let buttonMap = createRoundedButton(title: "🗺", action: #selector(mapTapped(_:)))
stackViewButtons.addArrangedSubview(buttonMap)
let buttonWeather = createRoundedButton(title: "🌤", action: #selector(weatherTapped(_:)))
stackViewButtons.addArrangedSubview(buttonWeather)
let buttonCalendar = createRoundedButton(title: "📆", action: #selector(calendarTapped(_:)))
stackViewButtons.addArrangedSubview(buttonCalendar)
let buttonSettings = createRoundedButton(title: "🔧", action: #selector(settingsTapped(_:)))
@@ -367,32 +350,9 @@ class SeismicNetworkTableViewCell: UITableViewCell {
mapView.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
previousView = mapView
} else if displayType == .weatherExpanded {
let weatherTitleLabel = UILabel()
weatherTitleLabel.translatesAutoresizingMaskIntoConstraints = false
weatherTitleLabel.text = NSLocalizedString("weather_weather", comment: "")
weatherTitleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
containerView.addSubview(weatherTitleLabel)
weatherTitleLabel.topAnchor.constraint(equalTo: previousView.bottomAnchor, constant: Self.DefaultVerticalSpacing).isActive = true
weatherTitleLabel.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
weatherTitleLabel.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
containerView.addSubview(weatherInfoLabel)
containerView.addSubview(weatherImageView)
weatherImageView.heightAnchor.constraint(equalToConstant: 60.0).isActive = true
weatherImageView.widthAnchor.constraint(equalTo: weatherImageView.heightAnchor).isActive = true
weatherImageView.leadingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.leadingAnchor).isActive = true
weatherImageView.trailingAnchor.constraint(equalTo: weatherInfoLabel.leadingAnchor, constant: -8.0).isActive = true
weatherImageView.centerYAnchor.constraint(equalTo: weatherInfoLabel.centerYAnchor).isActive = true
weatherInfoLabel.topAnchor.constraint(equalTo: weatherTitleLabel.bottomAnchor, constant: 4.0).isActive = true
weatherInfoLabel.trailingAnchor.constraint(equalTo: containerView.layoutMarginsGuide.trailingAnchor).isActive = true
previousView = weatherInfoLabel
}
if (displayType == .mapExpanded || displayType == .weatherExpanded) {
if (displayType == .mapExpanded) {
let buttonClose = createRoundedButton(title: NSLocalizedString("official_close", comment: "").uppercased(), action: #selector(closeTapped(_:)))
containerView.addSubview(buttonClose)
@@ -461,14 +421,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(mapDetailTapped(_:)))
mapView.addGestureRecognizer(tapRecognizer)
} else if displayType == .weatherExpanded {
weatherInfoLabel.text = ""
+ String(format: NSLocalizedString("weather_temperature", comment: ""), seismic.weatherTemperature.doubleValue - EQNMathKelvin) + "\n"
+ String(format: NSLocalizedString("weather_pressure", comment: ""), seismic.weatherPressure) + "\n"
+ String(format: NSLocalizedString("weather_windspeed", comment: ""), seismic.weatherWindSpeed) + "\n"
+ String(format: NSLocalizedString("weather_humidity", comment: ""), seismic.weatherHumidity) + "\n"
+ String(format: NSLocalizedString("weather_clouds", comment: ""), seismic.weatherCloud)
weatherImageView.image = UIImage(named: "weather_\(seismic.weatherIcon).png")
}
}
@@ -503,16 +455,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
updateUI()
}
/// Creates a snapshot of the current cell
/// - Returns: Image with the snapshot of the cell
public func createSnapshot() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: contentView.bounds.size)
let image = renderer.image { ctx in
contentView.drawHierarchy(in: contentView.bounds, afterScreenUpdates: true)
}
return image
}
// MARK: - Actions
@objc func shareTapped(_ sender: UIButton) {
@@ -524,13 +466,6 @@ class SeismicNetworkTableViewCell: UITableViewCell {
delegate?.seismicNetworkCellDidTapMap(self)
}
}
@objc func weatherTapped(_ sender: UIButton) {
if displayType != .weatherExpanded {
let validData = seismic?.weatherCode != nil
delegate?.seismicNetworkCellDidTapWeather(self, hasValidWeatherData: validData)
}
}
@objc func calendarTapped(_ sender: UIButton) {
delegate?.seismicNetworkCellDidTapCalendar(self)
@@ -7,8 +7,9 @@
//
import UIKit
import Shogun
@objc protocol SeismicFiltersViewControllerDelegate: class {
protocol SeismicFiltersViewControllerDelegate: AnyObject {
func seismicFiltersControllerDidUpdateFilters(_ controller: SeismicFiltersViewController)
}
@@ -24,7 +25,7 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
case modificaImpostazioni
}
@objc weak var delegate: SeismicFiltersViewControllerDelegate?
weak var delegate: SeismicFiltersViewControllerDelegate?
/// Tells if delegate needs to redownload data when filter is dismissed
@objc private(set) var needsDataUpdate = false
@@ -37,7 +38,7 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
private var settings = [
SettingItem(type: .slider, title: NSLocalizedString("filter_magnitude", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("Distanza massima", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("filter_distance", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("filter_timeframe", comment: "")),
SettingItem(type: .enable, title: NSLocalizedString("filter_strong", comment: "")),
SettingItem(type: .slider, title: NSLocalizedString("options_strong_magnitude", comment: "")),
@@ -66,7 +67,8 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
super.viewDidLoad()
setupUI()
updateUI()
loadDataSource()
tableView.reloadData()
}
private func setupUI() {
@@ -83,7 +85,7 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
closeButton.setLocalizedTitle(key: "official_close", uppercased: false)
}
private func updateUI() {
private func loadDataSource() {
currentMagnitudoMinima = EQNData.magitudoDebole(for: EQNSeismic.shared.magnitudoMinima)
if initialMagnitudoMinima == nil {
initialMagnitudoMinima = currentMagnitudoMinima
@@ -97,7 +99,6 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
initialQualsiasiMagnitudo = currentSismiQualsiasiMagnitudo
}
currentModificaImpostazioni = EQNSeismic.shared.modificaImpostazioniAbilitato
tableView.reloadData()
}
// MARK: - Table view delegate and data source
@@ -111,7 +112,7 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
switch setting.type {
case .slider:
let cell = tableView.dequeueReusableCell(withIdentifier: SettingSliderTableViewCell.Identifier, for: indexPath) as! SettingSliderTableViewCell
let cell = SettingSliderTableViewCell(style: .default, reuseIdentifier: nil)
cell.titleLabel.text = setting.displayTitle
if indexPath.row == RowIdentifier.magnitudoMinima.rawValue {
@@ -120,7 +121,6 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
currentMagnitudoMinima = value
EQNSeismic.shared.magnitudoMinima = value.value
EQNSeismic.shared.saveFilters()
updateUI()
}
cell.dragEnded = { [unowned self] in
showWarningAlertIfNeeded(for: currentMagnitudoMinima)
@@ -131,7 +131,6 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
currentDistanzaMassima = value
EQNSeismic.shared.distanzaMassima = value.value
EQNSeismic.shared.saveFilters()
updateUI()
}
} else if indexPath.row == RowIdentifier.periodoTemporale.rawValue {
cell.configureSlider(with: dataSourcePeriodoTemporale, current: currentPeriodoTemporale)
@@ -139,7 +138,6 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
currentPeriodoTemporale = value
EQNSeismic.shared.periodoTemporale = value.value
EQNSeismic.shared.saveFilters()
updateUI()
}
} else if indexPath.row == RowIdentifier.sismiFortiDistanza.rawValue {
cell.isDisabled = !currentSismiFortiAbilitati
@@ -148,13 +146,12 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
currentSismiFortiDistanza = value
EQNSeismic.shared.sismiFortiMagnitudo = value.value
EQNSeismic.shared.saveFilters()
updateUI()
}
}
return cell
case .enable:
let cell = tableView.dequeueReusableCell(withIdentifier: SettingEnableTableViewCell.Identifier, for: indexPath) as! SettingEnableTableViewCell
let cell = SettingEnableTableViewCell(style: .default, reuseIdentifier: nil)
cell.titleLabel.text = setting.displayTitle
cell.detailTextLabel?.text = setting.subtitle
@@ -164,7 +161,9 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
currentSismiFortiAbilitati = value
EQNSeismic.shared.sismiFortiAbilitati = value
EQNSeismic.shared.saveFilters()
updateUI()
loadDataSource()
tableView.reloadData()
}
} else if indexPath.row == RowIdentifier.sismiQualsiasiMagnitudo.rawValue {
cell.toggleSwitch.isOn = currentSismiQualsiasiMagnitudo
@@ -172,7 +171,6 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
currentSismiQualsiasiMagnitudo = value
EQNSeismic.shared.sismiQualsiasiAbilitati = value
EQNSeismic.shared.saveFilters()
updateUI()
}
} else if indexPath.row == RowIdentifier.modificaImpostazioni.rawValue {
cell.toggleSwitch.isOn = currentModificaImpostazioni
@@ -180,7 +178,6 @@ class SeismicFiltersViewController: UIViewController, UITableViewDelegate, UITab
currentModificaImpostazioni = value
EQNSeismic.shared.modificaImpostazioniAbilitato = value
EQNSeismic.shared.saveFilters()
updateUI()
}
}
@@ -8,7 +8,7 @@
import UIKit
protocol SeismicCardSettingsViewControllerDelegate: class {
protocol SeismicCardSettingsViewControllerDelegate: AnyObject {
func seismicCardSettingsDidComplete(_ controller: SeismicCardSettingsViewController)
}
@@ -52,7 +52,7 @@ struct SeismicNetworkViewModel {
// distance
let distanceRounded = Int(round(seismic.userDistance))
self.distance = String(format: NSLocalizedString("timer_message2_other", comment: ""), distanceRounded)
self.distance = String(format: NSLocalizedString("official_distance", comment: ""), distanceRounded)
let coordinateText = EQNUtility.coordinateString(coordinate: seismic.coordinate.coordinate)
self.coordinate = "\(coordinateText)"
@@ -8,8 +8,9 @@
import UIKit
import MapKit
import Shogun
protocol SeismicNetworksMapDetailViewControllerDelegate: class {
protocol SeismicNetworksMapDetailViewControllerDelegate: AnyObject {
func seismicNetworksMapDetailControllerWillUpdateData(_ controller: SeismicNetworksMapDetailViewController, needsDataUpdate: Bool)
}
@@ -107,6 +108,8 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
}
override func didTapAnnotation(_ annotation: MKAnnotation) {
mapView.deselectAnnotation(annotation, animated: true)
guard let annotation = annotation as? EQNMapAnnotationSeismic else { return }
let viewModel = SeismicNetworkViewModel(seismic: annotation.seismic)
@@ -162,7 +165,7 @@ class SeismicNetworksMapDetailViewController: EQNBaseMapViewController {
// MARK: - Actions
@objc override func filtersTapped(_ sender: UIGestureRecognizer) {
let controller = SeismicFiltersViewController.makeController()
let controller = SeismicFiltersViewController.makeViewController()
controller.delegate = self
controller.modalPresentationStyle = .overCurrentContext
controller.modalTransitionStyle = .crossDissolve
@@ -9,6 +9,7 @@
import UIKit
import EventKitUI
import DZNEmptyDataSet
import Shogun
class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
@@ -43,8 +44,6 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
private var informations = [SeismicNetworkTableViewCell.InformationType]()
/// Index path of row with map expanded
private var openMapIndexPath: IndexPath?
/// Index path of row with weather expanded
private var openWeatherIndexPath: IndexPath?
// MARK: - View Lifecycle
@@ -116,7 +115,6 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
@objc func didReceiveDownloadCompleteNotification(_ sender: Notification) {
self.openMapIndexPath = nil
self.openWeatherIndexPath = nil
DispatchQueue.main.async {
self.refreshUI()
@@ -155,10 +153,12 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
let filteredSeismics = EQNSeismic.shared.filterSeismicList(allSeismics ?? [])
rows = filteredSeismics.map { .seismic($0) }
#if ADS_ENABLED
// if is not a pro user, show an advertise
if !EQNPurchaseUtility.isProVersionEnabled() {
loadAd()
}
#endif
// if a map detail is presented, update its data
if let mapController = currentMapController {
@@ -216,8 +216,6 @@ class SeismicNetworksViewController: UIViewController, UITableViewDelegate, UITa
var type = SeismicNetworkTableViewCell.DisplayType.normal
if openMapIndexPath == indexPath {
type = .mapExpanded
} else if openWeatherIndexPath == indexPath {
type = .weatherExpanded
}
cell.configure(with: seismic, type: type, informations: informations)
@@ -315,7 +313,7 @@ extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
guard let index = tableView?.indexPath(for: cell), case let .seismic(seismic) = rows[index.row] else { return }
// create a snapshot of the cell and share with default share sheet
let snapshot = cell.createSnapshot()
let snapshot = cell.contentView.createSnapshot()
// text to share with the snapshot
let shareHashtag = NSLocalizedString("share_hashtag", comment: "")
@@ -328,32 +326,12 @@ extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
present(controller, animated: true)
}
func seismicNetworkCellDidTapWeather(_ cell: SeismicNetworkTableViewCell, hasValidWeatherData: Bool) {
guard let index = tableView?.indexPath(for: cell) else { return }
if !hasValidWeatherData {
let alert = UIAlertController(title: NSLocalizedString("attention", comment: ""),
message: NSLocalizedString("weather_nodata", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .default))
present(alert, animated: true)
return
}
let indexToReloads = [openMapIndexPath, openWeatherIndexPath, index].compactMap { $0 }
openWeatherIndexPath = index
openMapIndexPath = nil
tableView?.reloadRows(at: indexToReloads, with: .automatic)
}
func seismicNetworkCellDidTapMap(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell) else { return }
let indexToReloads = [openMapIndexPath, openWeatherIndexPath, index].compactMap { $0 }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
openMapIndexPath = index
openWeatherIndexPath = nil
tableView?.reloadRows(at: indexToReloads, with: .automatic)
}
@@ -376,10 +354,9 @@ extension SeismicNetworksViewController: SeismicNetworkTableViewCellDelegate {
func seismicNetworkCellDidTapClose(_ cell: SeismicNetworkTableViewCell) {
guard let index = tableView?.indexPath(for: cell) else { return }
let indexToReloads = [openMapIndexPath, openWeatherIndexPath, index].compactMap { $0 }
let indexToReloads = [openMapIndexPath, index].compactMap { $0 }
openMapIndexPath = nil
openWeatherIndexPath = nil
tableView?.reloadRows(at: indexToReloads, with: .automatic)
}
}
@@ -400,7 +377,8 @@ extension SeismicNetworksViewController: SeismicFiltersViewControllerDelegate {
extension SeismicNetworksViewController: SeismicSettingsViewControllerDelegate {
func seismicSettingsControllerDidComplete(_ controller: SeismicSettingsViewController) {
refreshUI()
// riscarichiamo i dati, le reti selezionate potrebbero essere cambiate
loadData(forced: true)
}
func seismicSettingsControllerWillOpenProviders(_ controller: SeismicSettingsViewController) {
@@ -410,7 +388,8 @@ extension SeismicNetworksViewController: SeismicSettingsViewControllerDelegate {
extension SeismicNetworksViewController: SeismicSettingsNetworksViewControllerDelegate {
func seismicSettingsNetworksControllerDidComplete(_ controller: SeismicSettingsNetworksViewController) {
refreshUI()
// riscarichiamo i dati, le reti selezionate potrebbero essere cambiate
loadData(forced: true)
}
}
@@ -9,13 +9,13 @@
import UIKit
@objc protocol SeismicSettingsNetworksViewControllerDelegate: class {
protocol SeismicSettingsNetworksViewControllerDelegate: AnyObject {
func seismicSettingsNetworksControllerDidComplete(_ controller: SeismicSettingsNetworksViewController)
}
class SeismicSettingsNetworksViewController: UITableViewController {
@objc weak var delegate: SeismicSettingsNetworksViewControllerDelegate?
weak var delegate: SeismicSettingsNetworksViewControllerDelegate?
// MARK: - Private
@@ -39,7 +39,8 @@ class SeismicSettingsNetworksViewController: UITableViewController {
networks = EQNData.seismicNetworks().sorted(by: { $0.acronym < $1.acronym })
// load saved selected networks or fill with all available networks
if let savedNetworks = UserDefaults.standard.object(forKey: IMPOSTAZIONE_ENTI_RETI_SISMICHEI) as? [String] {
let savedNetworks = EQNUserData.shared.seismicNetworksSelected()
if !savedNetworks.isEmpty {
self.savedNetworks = savedNetworks
} else {
self.savedNetworks = EQNData.seismicNetworkAcronyms()
@@ -86,7 +87,12 @@ class SeismicSettingsNetworksViewController: UITableViewController {
savedNetworks.append(network.acronym)
}
tableView.reloadRows(at: [indexPath], with: .automatic)
// reload all rows with the given acronym
let indexes = networks
.enumerated()
.filter { $0.element.acronym == network.acronym }
.map { IndexPath(row: $0.offset, section: 0) }
tableView.reloadRows(at: indexes, with: .automatic)
}
// MARK: - Actions
@@ -97,7 +103,7 @@ class SeismicSettingsNetworksViewController: UITableViewController {
@IBAction func saveTapped(_ sender: Any) {
// save selected networks
UserDefaults.standard.set(savedNetworks, forKey: IMPOSTAZIONE_ENTI_RETI_SISMICHEI)
EQNUserData.shared.saveSelectedSeismicNetworks(savedNetworks)
// se solo un'ente è selezionato, salviamolo anche come nazione
if savedNetworks.count == 1 {
@@ -9,7 +9,7 @@
import Foundation
@objc protocol SeismicSettingsViewControllerDelegate: class {
protocol SeismicSettingsViewControllerDelegate: AnyObject {
func seismicSettingsControllerDidComplete(_ controller: SeismicSettingsViewController)
func seismicSettingsControllerWillOpenProviders(_ controller: SeismicSettingsViewController)
}
@@ -17,7 +17,7 @@ import Foundation
class SeismicSettingsViewController: UIViewController {
@objc weak var delegate: SeismicSettingsViewControllerDelegate?
weak var delegate: SeismicSettingsViewControllerDelegate?
// MARK: - Private
@@ -54,7 +54,7 @@ class SeismicSettingsViewController: UIViewController {
confirmButton.setLocalizedTitle(key: "official_select_confirm", uppercased: false)
otherwiseLabel.text = NSLocalizedString("official_select_or", comment: "")
manageNetworksButton.setLocalizedTitle(key: "official_select_networks", uppercased: false)
cancelButton.setLocalizedTitle(key: "options_cancel", uppercased: false)
cancelButton.setLocalizedTitle(key: "status_cancel", uppercased: false)
// load saved country (if exists)
let savedCountry = UserDefaults.standard.object(forKey: IMPOSTAZIONE_NAZIONE_RETI_SISMICHE) as? String
@@ -82,7 +82,7 @@ class SeismicSettingsViewController: UIViewController {
// gli enti selezionati conterranno solo l'ente della nazione selezionata
let selectedNetworks = [network.acronym]
UserDefaults.standard.set(selectedNetworks, forKey: IMPOSTAZIONE_ENTI_RETI_SISMICHEI)
EQNUserData.shared.saveSelectedSeismicNetworks(selectedNetworks)
// aggiorniamo le impostazioni di notifica
EQNNotificheReteSismiche.shared().listaEnti = selectedNetworks
@@ -112,7 +112,7 @@ class SeismicSettingsViewController: UIViewController {
let alert = UIAlertController(title: NSLocalizedString("attention", comment: ""),
message: NSLocalizedString("official_select_message", comment: ""),
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("options_cancel", comment: ""), style: .cancel))
alert.addAction(UIAlertAction(title: NSLocalizedString("status_cancel", comment: ""), style: .cancel))
alert.addAction(UIAlertAction(title: NSLocalizedString("official_select_confirm", comment: ""), style: .default, handler: { [unowned self] (action) in
self.performSave(for: network)
}))
@@ -89,8 +89,6 @@ class SettingDateTableViewCell: UITableViewCell {
// MARK: - Private
private func setupUI() {
translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(valuesLabel)
@@ -62,7 +62,6 @@ class SettingEnableTableViewCell: UITableViewCell {
private func setupUI() {
selectionStyle = .none
translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
@@ -54,8 +54,6 @@ class SettingMultivaluesTableViewCell: UITableViewCell {
// MARK: - Private
private func setupUI() {
translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
@@ -55,7 +55,6 @@ class SettingSegmentedTableViewCell: UITableViewCell {
private func setupUI() {
selectionStyle = .none
translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
@@ -66,7 +66,6 @@ class SettingSliderTableViewCell: UITableViewCell {
private func setupUI() {
selectionStyle = .none
translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
@@ -99,6 +98,12 @@ class SettingSliderTableViewCell: UITableViewCell {
let index = Int(sender.value)
let item = items[index]
// make slider with step values
let step: Float = 1
let roundedValue = round(sender.value / step) * step
sender.value = roundedValue
// update value
valueLabel.text = item.display
valueChanged?(item)
}
@@ -14,35 +14,17 @@
@property (nonatomic, strong) NSArray<SettingItem *> *settings;
@property (strong, nonatomic) NSArray<EQNGenericValue *> *dataSourceSismi;
@property (nonatomic, strong) NSArray<EQNGenericValue *> *dataSourceRaggioSisma;
@property (nonatomic, strong) EQNGenericValue *currentSeismicToNotify;
@property (strong, nonatomic) EQNGenericValue *currentLowSeismicRadius;
@property (strong, nonatomic) EQNGenericValue *currentStrongSeismicRadius;
@property (strong, nonatomic) NSDate *currentStartTime;
@property (nonatomic) BOOL isStartTimeExpanded;
@property (strong, nonatomic) NSDate *currentEndTime;
@property (nonatomic) BOOL isEndTimeExpanded;
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
@property (nonatomic, assign) BOOL notificationEnabled;
@property (nonatomic, assign) BOOL criticalAlertsEnabled;
@property (nonatomic, assign) BOOL doNotDisturbEnabled;
@end
@implementation SettingsRealTimeAlertsViewController
typedef NS_ENUM(NSInteger, RowIdentifier) {
RowIdentifierAbilitaNotifiche = 0,
RowIdentifierAbilitaCriticalAlerts,
RowIdentifierSismiDaNotificare,
RowIdentifierRaggioSismiLievi,
RowIndntifierRaggioSismiForti,
RowIdentifierNonDisturbare,
RowIdentifierNonDisturbareOraInizio,
RowIdentifierNonDisturbareOraFine
RowIdentifierAbilitaCriticalAlerts
};
#pragma mark - Accessories
@@ -66,16 +48,11 @@ typedef NS_ENUM(NSInteger, RowIdentifier) {
self.settings = @[
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"options_notification_enable_alarm", @"")],
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"critical_alerts_setting", @"")],
[[SettingItem alloc] initWithType:SettingTypeSegmented title:NSLocalizedString(@"options_notification_eqn_intensity", @"") subtitle:NSLocalizedString(@"", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_radius_mild", @"")],
[[SettingItem alloc] initWithType:SettingTypeSlider title:NSLocalizedString(@"options_radius_strong", @"")]
[[SettingItem alloc] initWithType:SettingTypeEnable title:NSLocalizedString(@"critical_alerts_setting", @"")]
];
self.dataSourceSismi = [EQNData seismicToNotify];
self.dataSourceRaggioSisma = [EQNData raggioSismi];
[self updateUI];
[self loadDataSource];
[self.tableView reloadData];
}
#pragma mark - Private
@@ -94,34 +71,12 @@ typedef NS_ENUM(NSInteger, RowIdentifier) {
[self.tableView registerClass:[SettingDateTableViewCell class] forCellReuseIdentifier:SettingDateTableViewCell.Identifier];
}
- (void)updateUI
- (void)loadDataSource
{
self.notificationEnabled = [EQNAllertaSismica sharedInstance].isAbilitato;
self.criticalAlertsEnabled = [EQNAllertaSismica sharedInstance].isCriticalAlertsEnabled;
self.doNotDisturbEnabled = [EQNAllertaSismica sharedInstance].isintervalloAllarme;
// sismi da notificare
EQNGenericValue *sismiDaNotificare = [EQNData seismicToNotifyFor:[EQNAllertaSismica sharedInstance].sismiDaNotificare];
self.currentSeismicToNotify = sismiDaNotificare;
// raggio sismi lievi
EQNGenericValue *raggioSismiLievi = [EQNData raggioSismaFor:[EQNAllertaSismica sharedInstance].raggioSismiLievi];
self.currentLowSeismicRadius = raggioSismiLievi;
// raggio sismi forti
EQNGenericValue *raggioSismiForti = [EQNData raggioSismaFor:[EQNAllertaSismica sharedInstance].raggioSismiForti];
self.currentStrongSeismicRadius = raggioSismiForti;
// non disturbare, orari
NSDate *startTime = [EQNData doNotDisturbEndDateFrom:[EQNAllertaSismica sharedInstance].oraioInizio];
self.currentStartTime = startTime;
NSDate *endTime = [EQNData doNotDisturbEndDateFrom:[EQNAllertaSismica sharedInstance].orarioFine];
self.currentEndTime = endTime;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self.tableView reloadData];
}
@@ -173,124 +128,30 @@ typedef NS_ENUM(NSInteger, RowIdentifier) {
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
} else if (indexPath.row == RowIdentifierNonDisturbare) {
cell.toggleSwitch.on = self.doNotDisturbEnabled;
cell.isDisabled = !self.notificationEnabled;
cell.valueChanged = ^(BOOL enabled) {
self.doNotDisturbEnabled = enabled;
[EQNAllertaSismica sharedInstance].isintervalloAllarme = self.doNotDisturbEnabled;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self.tableView reloadData];
};
}
return cell;
} else if (setting.type == SettingTypeSegmented) {
SettingSegmentedTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSegmentedTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
if (indexPath.row == RowIdentifierSismiDaNotificare) {
cell.isDisabled = !self.notificationEnabled;
[cell configureControlWith:self.dataSourceSismi current:self.currentSeismicToNotify];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateSismicToNotify:item];
};
}
return cell;
} else if (setting.type == SettingTypeSlider) {
SettingSliderTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingSliderTableViewCell.Identifier forIndexPath:indexPath];
cell.titleLabel.text = setting.displayTitle;
if (indexPath.row == RowIdentifierRaggioSismiLievi) {
cell.isDisabled = !self.notificationEnabled;
[cell configureSliderWith:self.dataSourceRaggioSisma current:self.currentLowSeismicRadius];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateLowSeismicRadius:item];
};
} else if (indexPath.row == RowIndntifierRaggioSismiForti) {
cell.isDisabled = !self.notificationEnabled;
[cell configureSliderWith:self.dataSourceRaggioSisma current:self.currentStrongSeismicRadius];
cell.valueChanged = ^(EQNGenericValue *item) {
[self updateStrongSeismicRadius:item];
};
}
return cell;
} else if (setting.type == SettingTypeDate) {
SettingDateTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SettingDateTableViewCell.Identifier forIndexPath:indexPath];
cell.isDisabled = !self.doNotDisturbEnabled || !self.notificationEnabled;
cell.userInteractionEnabled = self.doNotDisturbEnabled && self.notificationEnabled;
cell.titleLabel.text = setting.title;
if (indexPath.row == RowIdentifierNonDisturbareOraInizio) {
cell.isPickerVisible = self.isStartTimeExpanded;
[cell updateDate:self.currentStartTime];
cell.valuesLabel.text = [self.dateFormatter stringFromDate:self.currentStartTime];
cell.valueChanged = ^(NSDate *date) {
[self updateStartTime:date];
};
} else if (indexPath.row == RowIdentifierNonDisturbareOraFine) {
cell.isPickerVisible = self.isEndTimeExpanded;
[cell updateDate:self.currentEndTime];
cell.valuesLabel.text = [self.dateFormatter stringFromDate:self.currentEndTime];
cell.valueChanged = ^(NSDate *date) {
[self updateEndTime:date];
};
}
return cell;
}
return nil;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == RowIdentifierNonDisturbareOraInizio) {
self.isStartTimeExpanded = !self.isStartTimeExpanded;
} else if (indexPath.row == RowIdentifierNonDisturbareOraFine) {
self.isEndTimeExpanded = !self.isEndTimeExpanded;
}
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - Private
- (void)updateSismicToNotify:(EQNGenericValue *)seismic
{
[EQNAllertaSismica sharedInstance].sismiDaNotificare = seismic.value;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
}
- (void)updateLowSeismicRadius:(EQNGenericValue *)radius
{
[EQNAllertaSismica sharedInstance].raggioSismiLievi = radius.value;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
}
- (void)updateStrongSeismicRadius:(EQNGenericValue *)radius
{
[EQNAllertaSismica sharedInstance].raggioSismiForti = radius.value;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
}
- (void)updateStartTime:(NSDate *)date
{
[EQNAllertaSismica sharedInstance].oraioInizio = date;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
}
- (void)updateEndTime:(NSDate *)date
{
[EQNAllertaSismica sharedInstance].orarioFine = date;
[[EQNAllertaSismica sharedInstance] saveUserInfo];
[self updateUI];
[self loadDataSource];
}
- (void)askForCriticalAlertsPermission
@@ -62,14 +62,16 @@ typedef NS_ENUM(NSInteger, RowIdentifier) {
self.dataSourceRaggioSisma = [EQNData raggioSismi];
self.dataSourceMagnitudoForti = [EQNData magitudoForti];
[self updateUI];
[self loadDataSource];
[self.tableView reloadData];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self updateUI];
[self loadDataSource];
[self.tableView reloadData];
}
#pragma mark - Private
@@ -86,7 +88,7 @@ typedef NS_ENUM(NSInteger, RowIdentifier) {
[self.tableView registerClass:[SettingMultivaluesTableViewCell class] forCellReuseIdentifier:SettingMultivaluesTableViewCell.Identifier];
}
- (void)updateUI
- (void)loadDataSource
{
self.notificationEnabled = [EQNNotificheReteSismiche sharedInstance].isAbilitato;
self.notificationNearEarthquakeEnabled = [EQNNotificheReteSismiche sharedInstance].isAbilitaVicini;
@@ -110,8 +112,6 @@ typedef NS_ENUM(NSInteger, RowIdentifier) {
}
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self.tableView reloadData];
}
@@ -229,7 +229,7 @@ typedef NS_ENUM(NSInteger, RowIdentifier) {
[EQNNotificheReteSismiche sharedInstance].distanzaPosizione = radius.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self updateUI];
[self loadDataSource];
}
- (void)updateSeismicEnergy:(EQNGenericValue *)energy
@@ -237,7 +237,7 @@ typedef NS_ENUM(NSInteger, RowIdentifier) {
[EQNNotificheReteSismiche sharedInstance].energiaSisma = energy.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self updateUI];
[self loadDataSource];
}
- (void)updateStrongEarthquakeEnergy:(EQNGenericValue *)energy
@@ -245,7 +245,7 @@ typedef NS_ENUM(NSInteger, RowIdentifier) {
[EQNNotificheReteSismiche sharedInstance].energiaTerremotiForti = energy.value;
[[EQNNotificheReteSismiche sharedInstance] saveUserInfo];
[self updateUI];
[self loadDataSource];
}
- (NSString *)stringOfSelectedNetworks
@@ -27,6 +27,11 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
!availableFilters.isEmpty
}
/// If `true` the close button will be shown on top of the map view
var isCloseButtonVisible: Bool {
true
}
// MARK: - Internal
/// Annotations displayed on the map
@@ -116,13 +121,14 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
private func setupUI() {
view.backgroundColor = .white
view.addSubview(mapView)
view.addSubview(closeButton)
view.addSubview(containerView)
if isFilterViewVisible {
view.addSubview(filtersView)
}
closeButton.addDefaultConstraint(to: view)
if isCloseButtonVisible {
view.addSubview(closeButton)
closeButton.addDefaultConstraint(to: view)
}
containerView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
@@ -145,6 +151,8 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: (isFilterViewVisible ? filtersView : containerView).topAnchor).isActive = true
extraUI()
}
// MARK: - View Lifecycle
@@ -152,6 +160,7 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
registerMapAnnotationViews()
loadDataSource()
}
@@ -167,6 +176,16 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
// MARK: - Public
/// Add extra UI not available in the base class
func extraUI() {
// nope, subclass will implement some logic
}
/// Configure UI after view initialization
func configureUI() {
// nope, subclass will implement some logic
}
/// Load data to display on the map
func loadDataSource() {
// nope, subclass will implement some logic
@@ -249,7 +268,7 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
self.applyFilter(filter)
}))
}
sheet.addAction(UIAlertAction(title: NSLocalizedString("options_cancel", comment: ""), style: .cancel, handler: nil))
sheet.addAction(UIAlertAction(title: NSLocalizedString("status_cancel", comment: ""), style: .cancel, handler: nil))
present(sheet, animated: true, completion: nil)
}
@@ -277,7 +296,5 @@ class EQNBaseMapViewController: EQNBaseViewController, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation else { return }
didTapAnnotation(annotation)
mapView.deselectAnnotation(annotation, animated: true)
}
}
@@ -8,6 +8,7 @@
import UIKit
import MapKit
import Shogun
class AlertSimulatorViewController: UIViewController, MKMapViewDelegate {
@@ -243,7 +244,7 @@ class AlertSimulatorViewController: UIViewController, MKMapViewDelegate {
}
private func navigateToSubscriptions() {
let controller = SubscriptionsViewController.makeController()
let controller = SubscriptionsViewController.makeViewController()
let navigationController = UINavigationController(rootViewController: controller)
present(navigationController, animated: true)
}
@@ -1,21 +0,0 @@
//
// Costanti+Extensions.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/03/21.
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import Foundation
extension EQNPastquakeIntensity {
var description: String {
switch self {
case .mild: return NSLocalizedString("widget_mild", comment: "")
case .strong: return NSLocalizedString("widget_strong", comment: "")
case .veryStrong: return NSLocalizedString("widget_verystring", comment: "")
}
}
}
+10 -70
View File
@@ -13,6 +13,8 @@
/// Stampa le risposte delle chiamate al server
static BOOL const EQNDebugPrintResponse = NO;
/// Visualizza schermata con info di debug (ex. push token)
static BOOL const EQNEnableDebugView = NO;
#pragma mark - Urls
@@ -31,11 +33,9 @@ static double const EQNMathKelvin = 273.15;
#pragma mark - Server APIs
/// Download reti sismiche
static NSString * const EQNServerUrlDownloadRetiSismiche = @"https://srv.earthquakenetwork.it/distquake_download_automatic18%@.php";
static NSString * const EQNServerUrlDownloadRetiSismiche = @"https://cache.earthquakenetwork.it/distquake_download_automatic21.php%@";
/// Recupera il tempo ancora disponibile per la versione Pro scontata
static NSString * const EQNServerUrlOfferTimeRemaining = @"https://srv.earthquakenetwork.it/distquake_download_offer_time_remaining.php";
/// Recupera il numero di sottoscrizioni ancora disponibili per ogni prodotto
static NSString * const EQNServerUrlAvailableSubscriptionsCounter = @"https://srv.earthquakenetwork.it/distquake_count_top_redis.php";
/// Registra l'abbonamento acquistato dall'utente
static NSString * const EQNServerUrlRegisterSubscription = @"https://srv.earthquakenetwork.it/distquake_upload_subscription.php";
/// Carica le impostazioni delle notifiche definite dall'utente
@@ -47,15 +47,15 @@ static NSString * const EQNServerUrlUserLocation = @"https://srv.earthquakenetwo
static NSString * const EQNServerUrlCalibration = @"https://srv.earthquakenetwork.it/distquake_upload4.php";
// download rete smartphone
static NSString * const EQNServerUrlDownloadSmartphoneNetwork = @"https://srv.earthquakenetwork.it/distquake_count_redis.php";
static NSString * const EQNServerUrlDownloadSmartphoneNetwork = @"https://cache.earthquakenetwork.it/distquake_count_redis3.php";
// download area check
static NSString * const EQNServerUrlDownloadAreaCheck = @"https://srv.earthquakenetwork.it/distquake_download_areacheck.php";
// download pastquakes
static NSString * const EQNServerUrlDownloadPastQuakes = @"https://srv.earthquakenetwork.it/distquake_download_pastquakes.php";
// download segnalazioni
static NSString * const EQNServerUrlDownloadUserReports = @"https://srv.earthquakenetwork.it/distquake_download_manual.php";
static NSString * const EQNServerUrlDownloadUserReports = @"https://cache.earthquakenetwork.it/distquake_download_manual3.php";
// Invio segnalazione
static NSString * const EQNServerUrlSendUserReport = @"https://srv.earthquakenetwork.it/distquake_upload_manual3.php";
static NSString * const EQNServerUrlSendUserReport = @"https://srv.earthquakenetwork.it/distquake_upload_manual4.php";
static NSString * const EQNServerUrlSendUserReportMessage = @"https://srv.earthquakenetwork.it/distquake_upload_manual_message.php";
/// Effettua un test delle notifiche push
static NSString * const EQNServerUrlTestAlarm = @"https://srv.earthquakenetwork.it/distquake_upload_testalarm.php";
@@ -64,23 +64,9 @@ static NSString * const EQNServerUrlAlertSimulator = @"https://srv.earthquakenet
#pragma mark - UserDefaults Keys
static NSString * const EQNUserDefaultAppGroupSuite = @"group.com.finazzi.distquake";
static NSString * const EQNUserDefaultKeyAlertsShowAllCards = @"EQNetwork.AlertsShowAllCards";
static NSString * const EQNUserDefaultKeySesmicInformations = @"EQNetwork.SeismicInformations";
static NSString * const EQNUserDefaultKeyOneShotShowCountry = @"EQNetwork.OneShot.CountrySelection";
static NSString * const EQNUserDefaultLastLocation = @"EQNLast_Location";
static NSString * const EQNUserDefaultSeismicNetworkCards = @"EQNData.RetiSismiche";
/// Numero di aperture dell'app per sbloccare la versione Pro scontata
static NSString * const EQNUserDefaultProDiscountOpenCounter = @"CONTEGGIO_APERTURE_PER_SCONTO";
/// Prezzo scontato per la versione pro scaduto
static NSString * const EQNUserDefaultProDiscountExpired = @"PREZZO_SCONTATO_SCADUTO";
/// Token Firebase dell'utente corrente
static NSString * const EQNUserDefaultUserFirebaseToken = @"EQNToken_User";
/// Server user ID dell'utente corrente
static NSString * const EQNUserDefaultUserId = @"EQNUSER_ID";
/// Token delle notifiche push
static NSString * const EQNUserDefaultPushToken = @"EQNetwork.PushToken";
#pragma mark - NSNotification
@@ -102,10 +88,10 @@ static NSNotificationName const EQNDebugLogWillUpdateNotification = @"EQNDebugLo
#pragma mark - Other constants
static NSTimeInterval const EQNSeismicDataRefreshInterval = 120.0;
/// Tempo di attesa (minuti) tra l'invio di due segnalazioni
static NSTimeInterval const EQNSendReportDelayBetweenMessages = 5.0;
/// Tempo di attesa (minuti) per l'invio di due commenti
static NSTimeInterval const EQNSendReportDelayBetweenComments = 30.0;
/// Tempo di attesa (secondi) tra l'invio di due segnalazioni
static NSTimeInterval const EQNSendReportDelayBetweenMessages = 300.0; // 5 minuti
/// Tempo di attesa (secondi) per l'invio di due commenti
static NSTimeInterval const EQNSendReportDelayBetweenComments = 900.0; // 30 minuti
#ifdef DEBUG
static NSString * const EQNAdMobAppIdAdaptiveBanner = @"ca-app-pub-3940256099942544/2934735716"; // test
@@ -182,53 +168,7 @@ typedef enum : NSInteger {
nonCalibrato
} EQNStatoCal;
//////////////////////////////////////// SEGNALAZIONE MANUALE TERREMOTI ////////////////////////////////////////
#define CODE_MESSAGE_EQN @"CODE_MESSAGE_EQN"
#define DATA_MESSAGE_EQN @"DATA_MESSAGE_EQN"
/////////////////// Segnalazioni Utente ////////////////////////////
#define NOTIFICHE_SU_DISTANZA_POSIZIONE @"NOTIFICHE_SU_DISTANZA_POSIZIONE"
#define NOTIFICHE_SU_ATTIVA_SEGNALAZIONE_UTENTE @"NOTIFICHE_SU_ATTIVA_SEGNALAZIONE_UTENTE"
/////////////////// Reti sismiche ////////////////////////////
#define NOTIFICHE_DISTANZA_POSIZIONE_RETI_SISMICHE @"NOTIFICHE_DISTANZA_POSIZIONE_RETI_SISMICHE"
#define NOTIFICHE_ATTIVA_RETI_SISMICHE @"NOTIFICHE_ATTIVA_RETI_SISMICHE"
#define NOTIFICHE_ATTIVA_RETI_SISMICHE_VICINE @"NOTIFICHE_ATTIVA_RETI_SISMICHE_VICINE"
#define NOTIFICHE_ATTIVA_RETI_TERREMOTI_FORTI @"NOTIFICHE_ATTIVA_RETI_TERREMOTI_FORTI"
#define NOTIFICHE_ATTIVA_RETI_ENERGIA_SISMI @"NOTIFICHE_ATTIVA_RETI_ENERGIA_SISMI"
#define NOTIFICHE_ATTIVA_RETI_ENERGIA_FORTI @"NOTIFICHE_ATTIVA_RETI_ENERGIA_FORTI"
#define NOTIFICHE_ATTIVA_RETI_LISTA_ENTI @"NOTIFICHE_ATTIVA_RETI_LISTA_ENTI"
// Sigla della rete sismica selezionata
#define IMPOSTAZIONE_NAZIONE_RETI_SISMICHE @"IMPOSTAIONE_NAZIONE_RETI_SISMICHE"
#define IMPOSTAZIONE_ENTI_RETI_SISMICHEI @"IMPOSTAZIONE_ENTI_RETI_SISMICHEI"
/////////////////// Allera sismica ////////////////////////////
#define NOTIFICHE_ALLERA_SISMICA_ABILITATO @"NOTIFICHE_ALLERA_SISMICA_ABILITATO"
#define NOTIFICHE_ALLERA_SISMICA_CRITICAL_ALERTS @"NOTIFICHE_ALLERA_SISMICA_CRITICAL_ALERTS"
#define NOTIFICHE_ALLERA_SISMICA_SISMI_DA_NOTIFICARE @"NOTIFICHE_ALLERA_SISMICA_SISMI_DA_NOTIFICARE"
#define NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_LIEVI @"NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_LIEVI"
#define NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_FORTI @"NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_FORTI"
#define NOTIFICHE_ALLERA_SISMICA_IMPOSTA_VOLUME @"NOTIFICHE_ALLERA_SISMICA_IMPOSTA_VOLUME"
#define NOTIFICHE_ALLERA_SISMICA_TESTA_ALLARME @"NOTIFICHE_ALLERA_SISMICA_TESTA_ALLARME"
#define NOTIFICHE_ALLERA_SISMICA_ABILITA_INTERVALLO @"NOTIFICHE_ALLERA_SISMICA_ABILITA_INTERVALLO"
#define NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO @"NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO"
#define NOTIFICHE_ALLERA_SISMICA_ORA_FINE @"NOTIFICHE_ALLERA_SISMICA_ORA_INIZIO"
// NOTIFICHE RETE SMARTPHONE
#define NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA @"NOTIFICHE_RETE_SMARTPHONE_DATA_NOTIFICA"
#define NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA @"NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA"
#define TEMPO_VISUALIZZAZIONE_NOTIFICA 10800
// FILTRO ENTI
#define EQN_MAGNITUDO_MINIMA @"EQN_MAGNITUDO_MINIMA"
#define EQN_DISTANZA_MASSIMA @"EQN_DISTANZA_MASSIMA"
#define EQN_ETA_MASSIMA @"EQN_ETA_MASSIMA"
#define EQN_SISMI_FORTI_ABILITATI @"EQN_SISMI_FORTI_ABILITATI"
#define EQN_SISMI_FORTI @"EQN_SISMI_FORTI"
#define EQN_SISMI_QUALSIASI_MAGNITUDO @"EQN_SISMI_QUALSIASI_MAGNITUDO"
#define EQN_SISMI_MODIFICA_IMPOSTAZIONI @"EQN_SISMI_MODIFICA_IMPOSTAZIONI"
#endif /* Costanti_h */
@@ -8,6 +8,6 @@
#ifdef __OBJC__
#import "Earthquake_Network-Swift.h"
#import <Earthquake_Network-Swift.h>
#endif
+28 -2
View File
@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.finazzi.distquake.update_server_position</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -18,16 +22,35 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>fb1444404982546319</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<true/>
<key>FacebookAppID</key>
<string>1444404982546319</string>
<key>FacebookAutoLogAppEventsEnabled</key>
<true/>
<key>FacebookClientToken</key>
<string>46c7a338b2bbd2186b2f1c12865b4004</string>
<key>FacebookDisplayName</key>
<string>Earthquake Network</string>
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-0053870219990922~2021960172</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechromes</string>
<string>comgooglemaps</string>
<string>fbapi</string>
<string>fb-messenger-share-api</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
@@ -43,6 +66,8 @@
<string> Ci occorre la tua posizione per inviare messaggi precisi in caso di terremoto</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>L'accesso alla libreria è richiesto per poter salvare le immagini generate dall'app</string>
<key>NSUserTrackingUsageDescription</key>
<string>Il tracciamento serve a capire se la pubblicità dell'app è efficace</string>
<key>SKAdNetworkItems</key>
<array>
<dict>
@@ -55,6 +80,7 @@
<string>audio</string>
<string>fetch</string>
<string>location</string>
<string>processing</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
@@ -1,31 +0,0 @@
//
// Dictionary+EQNExtensions.swift
// Earthquake Network
//
// Created by Andrea Busi on 27/03/21.
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import Foundation
extension Dictionary {
func eqn_intValue(for key: Key) -> Int? {
if let value = self[key] as? Int {
return value
} else if let stringValue = self[key] as? String, let value = Int(stringValue) {
return value
}
return nil
}
func eqn_doubleValue(for key: Key) -> Double? {
if let value = self[key] as? Double {
return value
} else if let stringValue = self[key] as? String, let value = Double(stringValue) {
return value
}
return nil
}
}
@@ -0,0 +1,28 @@
//
// Foundation+Extensions.swift
// Earthquake Network
//
// Created by Andrea Busi on 14/07/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
extension Date {
func isBeforeInterval(
_ interval: TimeInterval
) -> Bool {
let now = Date()
return self.addingTimeInterval(interval) >= now
}
}
@objc
extension NSDate {
func isBeforeInterval(
_ interval: TimeInterval
) -> Bool {
return (self as Date).isBeforeInterval(interval)
}
}
@@ -1,19 +0,0 @@
//
// UIFont+Extensions.swift
// Earthquake Network
//
// Created by Busi Andrea on 25/09/2020.
// Copyright © 2020 Earthquake Network. All rights reserved.
//
import Foundation
extension UIFont {
static func preferredFont(for style: TextStyle, weight: Weight) -> UIFont {
let metrics = UIFontMetrics(forTextStyle: style)
let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style)
let font = UIFont.systemFont(ofSize: desc.pointSize, weight: weight)
return metrics.scaledFont(for: font)
}
}
@@ -22,3 +22,29 @@ extension UIButton {
setTitle(title, for: .normal)
}
}
extension UIImage {
class func circle(
diameter: CGFloat,
color: UIColor,
borderWidth: CGFloat = 0.0,
borderColor: UIColor = .black
) -> UIImage {
let size = CGSize(width: diameter, height: diameter)
let renderer = UIGraphicsImageRenderer(size: size)
let img = renderer.image { ctx in
ctx.cgContext.setFillColor(color.cgColor)
ctx.cgContext.setStrokeColor(borderColor.cgColor)
ctx.cgContext.setLineWidth(borderWidth)
// reduce circle size to keep space for the border
// without this, the image view is cropped
let circleDiameter = diameter - 2*borderWidth
let rectangle = CGRect(x: borderWidth, y: borderWidth, width: circleDiameter, height: circleDiameter)
ctx.cgContext.addEllipse(in: rectangle)
ctx.cgContext.drawPath(using: .fillStroke)
}
return img
}
}
@@ -1,25 +0,0 @@
//
// StoryboardInitializable.swift
//
// Created by Busi Andrea on 10/11/2020.
//
import Foundation
protocol StoryboardInitializable where Self: UIViewController {
static var storyboardName: String { get }
static var storyboardControllerId: String { get }
static func makeController() -> Self
}
extension StoryboardInitializable {
static func makeController() -> Self {
let storyboard = UIStoryboard(name: storyboardName, bundle: Bundle(for: Self.self))
guard let controller = storyboard.instantiateViewController(withIdentifier: storyboardControllerId) as? Self else {
fatalError("Unable to instantiate controller with anem \(storyboardControllerId)")
}
return controller
}
}
@@ -0,0 +1,32 @@
//
// AppPreferences.swift
// Earthquake Network
//
// Created by Andrea Busi on 17/11/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import Foundation
@objc
class AppPreferences: NSObject {
@objc
static let shared = AppPreferences()
// MARK: - Public
/// Defines if time has to be shown on map annotations in User Reports
var userReportExpandedView: Bool {
get { UserDefaults.standard.bool(forKey: UserDefaults.UserReportExpandedView) }
set { UserDefaults.standard.set(newValue, forKey: UserDefaults.UserReportExpandedView) }
}
/// Defines if options has to be shown on seismic cards
@objc
var alertsShowAllCards: Bool {
get { UserDefaults.standard.bool(forKey: UserDefaults.AlertsShowCardOptions) }
set { UserDefaults.standard.set(newValue, forKey: UserDefaults.AlertsShowCardOptions) }
}
}
@@ -0,0 +1,27 @@
//
// BackgroundTaskIdentifiable.swift
// Earthquake Network
//
// Created by Andrea Busi on 11/09/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import BackgroundTasks
protocol BackgroundTaskIdentifiable {
typealias TaskCompletion = (_ success: Bool) -> Void
static var identifier: String { get }
static var interval: TimeInterval { get }
init()
func handle(_ task: BGTask, completion: @escaping TaskCompletion)
func exipration()
}
extension BackgroundTaskIdentifiable {
func exipration() { }
}
@@ -0,0 +1,81 @@
//
// BackgroundTaskManager.swift
// Earthquake Network
//
// Created by Andrea Busi on 16/08/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import BackgroundTasks
@objc
class BackgroundTaskManager: NSObject {
@objc
static let shared = BackgroundTaskManager()
private let identifiers: [BackgroundTaskIdentifiable.Type] = [UpdateUserLocationTask.self]
// MARK: - Public
@objc
func registerTasks() {
identifiers
.forEach { taskIdentifiable in
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifiable.identifier, using: DispatchQueue.global()) { [weak self] task in
guard let appTask = task as? BGAppRefreshTask else { return }
self?.handleTask(appTask, with: taskIdentifiable)
self?.scheduleTaskRequest(for: taskIdentifiable)
}
}
}
// MARK: - Public
@objc
func scheduleUpdateServerPosition() {
scheduleTaskRequest(for: UpdateUserLocationTask.self)
}
// MARK: - Private
private func scheduleTaskRequest(
for taskIdentifiable: BackgroundTaskIdentifiable.Type
) {
let request = BGAppRefreshTaskRequest(identifier: taskIdentifiable.identifier)
// Fetch no earlier than X minutes from now
request.earliestBeginDate = Date(timeIntervalSinceNow: taskIdentifiable.interval)
//request.requiresNetworkConnectivity = true
do {
try BGTaskScheduler.shared.submit(request)
print("[BackgroundTaskManager] Background task scheduler submitted")
} catch {
print("[BackgroundTaskManager] Could not schedule background taksk. Error: \(error)")
}
}
private func handleTask(
_ appTask: BGAppRefreshTask,
with taskIdentifiable: BackgroundTaskIdentifiable.Type
) {
// create a task
let task = taskIdentifiable.init()
// Provide the background task with an expiration handler that cancels the operation.
appTask.expirationHandler = {
task.exipration()
}
// Handle workload
task.handle(appTask) { success in
// Inform the system that the background task is complete
// when the operation completes.
appTask.setTaskCompleted(success: success)
}
}
}
@@ -0,0 +1,32 @@
//
// EQNBackgroundPosition.swift
// Earthquake Network
//
// Created by Andrea Busi on 16/08/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import CoreLocation
struct EQNBackgroundPosition: Codable {
let date: Date
private let latitude: Double
private let longitude: Double
var coordinate: CLLocationCoordinate2D {
.init(latitude: latitude, longitude: longitude)
}
let request: Bool?
init(
date: Date,
coordinate: CLLocationCoordinate2D,
request: Bool?
) {
self.date = date
self.latitude = coordinate.latitude
self.longitude = coordinate.longitude
self.request = request
}
}
@@ -0,0 +1,66 @@
//
// EQNDebugHelper.swift
// Earthquake Network
//
// Created by Andrea Busi on 14/07/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
@objc
class EQNBackgroundPositionDebugHelper: NSObject {
@objc
static let shared = EQNBackgroundPositionDebugHelper()
private var timers: [String: Timer] = [:]
@objc
var isEnabled: Bool { false }
// MARK: - Public
@objc
func printPositions(
interval: TimeInterval = 2.0,
repeats: Bool = true
) {
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { timer in
let current = EQNUserData.shared.lastLocation?.coordinate
print("[EQNDebugHelper] Current | lat: \(current?.latitude ?? 0) - lon: \(current?.longitude ?? 0)")
let saved = EQNUser.default().lastPosition?.coordinate
print("[EQNDebugHelper] Saved | lat: \(saved?.latitude ?? 0) - lon: \(saved?.longitude ?? 0)")
}
timers["positions"] = timer
}
// MARK: - Class
func savePosition(
coordinate: CLLocationCoordinate2D,
requestSuccess: Bool
) {
var positions = loadPosition()
positions.append(.init(date: Date(), coordinate: coordinate, request: requestSuccess))
if let data = try? JSONEncoder().encode(positions) {
UserDefaults.standard.set(data, forKey: "BackgroundPositions")
}
}
func resetPositions() {
UserDefaults.standard.removeObject(forKey: "BackgroundPositions")
}
func loadPosition() -> [EQNBackgroundPosition] {
guard let data = UserDefaults.standard.object(forKey: "BackgroundPositions") as? Data else {
return []
}
let positions = try? JSONDecoder().decode([EQNBackgroundPosition].self, from: data)
return positions ?? []
}
}
@@ -0,0 +1,95 @@
//
// UpdateUserLocationTask.swift
// Earthquake Network
//
// Created by Andrea Busi on 16/08/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import BackgroundTasks
import CoreLocation
final class UpdateUserLocationTask: NSObject, BackgroundTaskIdentifiable {
static var identifier: String {
"com.finazzi.distquake.update_server_position"
}
static var interval: TimeInterval {
5 * 60
}
static let shared = UpdateUserLocationTask()
private let debugHelper = EQNBackgroundPositionDebugHelper()
// MARK: - Internal
private lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
manager.allowsBackgroundLocationUpdates = true
manager.pausesLocationUpdatesAutomatically = false
return manager
}()
var appTaskCompletion: BackgroundTaskIdentifiable.TaskCompletion?
func handle(_ task: BGTask, completion: @escaping (_ success: Bool) -> Void) {
self.appTaskCompletion = completion
// ricaviamo la posizione corrente dell'utente
if let location = locationManager.location {
complete(with: location)
} else {
locationManager.requestLocation()
}
}
func exipration() {
locationManager.stopUpdatingLocation()
failed()
}
// MARK: - Private
private func complete(with location: CLLocation) {
// send position to cloud
let url = EQNGeneratoreURLServer.urlPosizione(withLocation: location.coordinate)
ServerRequest.default().inviaInformazioniAlServer(with: url, richiesta: .posizione) { result in
if self.debugHelper.isEnabled {
self.debugHelper.savePosition(coordinate: location.coordinate, requestSuccess: true)
}
self.appTaskCompletion?(true)
} failure: { error in
if self.debugHelper.isEnabled {
self.debugHelper.savePosition(coordinate: location.coordinate, requestSuccess: false)
}
self.appTaskCompletion?(false)
}
}
private func failed() {
appTaskCompletion?(false)
}
}
extension UpdateUserLocationTask: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
complete(with: location)
} else {
failed()
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("[UpdateUserLocationTask] Location manager failed. Error: \(error.localizedDescription)")
// nope, but mandatory
failed()
}
}
@@ -24,30 +24,21 @@ public class EQNAppearanceCommand: EQNCommandProtocol {
private func applyAppearance() {
// UINavigationBar
let proxyNavBar = UINavigationBar.appearance(whenContainedInInstancesOf: [UINavigationController.self])
if #available(iOS 13.0, *) {
let navAppearance = UINavigationBarAppearance()
navAppearance.configureWithOpaqueBackground()
navAppearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: AppTheme.Colors.darkGray
]
navAppearance.largeTitleTextAttributes = [
NSAttributedString.Key.foregroundColor: AppTheme.Colors.darkGray
]
navAppearance.backgroundColor = AppTheme.Colors.primary
navAppearance.shadowColor = UIColor.clear
let navAppearance = UINavigationBarAppearance()
navAppearance.configureWithOpaqueBackground()
navAppearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: AppTheme.Colors.darkGray
]
navAppearance.largeTitleTextAttributes = [
NSAttributedString.Key.foregroundColor: AppTheme.Colors.darkGray
]
navAppearance.backgroundColor = AppTheme.Colors.primary
navAppearance.shadowColor = UIColor.clear
proxyNavBar.isTranslucent = false
proxyNavBar.tintColor = AppTheme.Colors.darkGray
proxyNavBar.standardAppearance = navAppearance
proxyNavBar.scrollEdgeAppearance = navAppearance
} else {
proxyNavBar.tintColor = AppTheme.Colors.darkGray
proxyNavBar.isTranslucent = false
proxyNavBar.barTintColor = AppTheme.Colors.primary
proxyNavBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: UIColor.white
]
}
proxyNavBar.isTranslucent = false
proxyNavBar.tintColor = AppTheme.Colors.darkGray
proxyNavBar.standardAppearance = navAppearance
proxyNavBar.scrollEdgeAppearance = navAppearance
let proxyTabBar = UITabBar.appearance()
proxyTabBar.tintColor = AppTheme.Colors.red
@@ -10,6 +10,6 @@ import Foundation
@objc
protocol EQNCommandProtocol: class {
protocol EQNCommandProtocol: AnyObject {
@objc func execute()
}
@@ -7,6 +7,7 @@
//
import Foundation
import CoreLocation
public class EQNUserDefaultsCommand: EQNCommandProtocol {
@@ -18,6 +19,9 @@ public class EQNUserDefaultsCommand: EQNCommandProtocol {
applyDefaultSettings()
saveMissingValues()
migrationV5_3()
migrationV5_4()
}
// MARK: - Private
@@ -33,8 +37,45 @@ public class EQNUserDefaultsCommand: EQNCommandProtocol {
private func saveMissingValues() {
// `raggio sismi forti` was not saved before v2.3
if UserDefaults.standard.object(forKey: NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_FORTI) == nil {
UserDefaults.standard.set("600", forKey: NOTIFICHE_ALLERA_SISMICA_RAGGIO_SISMI_FORTI)
if UserDefaults.standard.object(forKey: UserDefaults.AllertaSismicaRaggioSismiForti) == nil {
UserDefaults.standard.set("600", forKey: UserDefaults.AllertaSismicaRaggioSismiForti)
}
}
private func migrationV5_3() {
let migrationPerformed = UserDefaults.standard.bool(forKey: UserDefaults.AppMigrationV5_3)
if migrationPerformed {
print("[EQNUserDefaultsCommand] Migration v5.3 already performed")
return
}
// l'ultima posizione era salvata come array, la trasformiamo in valore singolo
let lastLocations = EQNUtility.loadArray(of: CLLocation.self, fromUserDefaultsForKey: UserDefaults.UserDataLastLocation) as? [CLLocation]
if let lastLocation = lastLocations?.last {
UserDefaults.standard.removeObject(forKey: UserDefaults.UserDataLastLocation)
EQNUserData.shared.saveLastLocation(lastLocation)
}
// resettiamo il Firebase token in modo da ri-eseguire la procedura di registrazione corretta
EQNUserData.shared.saveFirebaseToken(nil)
UserDefaults.standard.set(true, forKey: UserDefaults.AppMigrationV5_3)
}
private func migrationV5_4() {
let migrationPerformed = UserDefaults.standard.bool(forKey: UserDefaults.AppMigrationV5_4)
if migrationPerformed {
print("[EQNUserDefaultsCommand] Migration v5.4 already performed")
return
}
// migriamo l'ultima posizione negli user defaults condivisi
let userDefaults = UserDefaults.standard
let groupUserDefaults = UserDefaults.appGroup
if let encodedLocation = userDefaults.object(forKey: UserDefaults.UserDataLastLocation) as? Data {
groupUserDefaults?.set(encodedLocation, forKey: UserDefaults.UserDataLastLocation)
}
userDefaults.set(true, forKey: UserDefaults.AppMigrationV5_4)
}
}
@@ -14,9 +14,6 @@ import Foundation
@objc public static let DefaultRaggioSisma = EQNGenericValue(value:MaxRaggioSisma, display:"radius_any_distance")
@objc public static let DefaultMagitudoDebole = EQNGenericValue(value:"2.0", display:"official_magnitude_value_20")
@objc public static let DefaultMagitudoForte = EQNGenericValue(value:"5.5", display:"official_magnitude_value_55")
@objc public static let DefaultSeismicToNotify = EQNGenericValue(value: "0", display: "eqn_intensity_any")
@objc public static let DefaultDoNotDisturbStartTime = 8
@objc public static let DefaultDoNotDisturbEndTime = 22
@objc public static let DefaultPeriodoTemporale = EQNGenericValue(value: "1440", display: "report_timeframe_one_day")
// MARK: - Public
@@ -100,7 +97,7 @@ import Foundation
EQNSeismicNetwork(acronym: "USGS", country: NSLocalizedString("configuration_countries_united_states", comment: ""), extended: ""),
EQNSeismicNetwork(acronym: "INGV", country: NSLocalizedString("configuration_countries_italy", comment: ""), extended: ""),
EQNSeismicNetwork(acronym: "IGN", country: NSLocalizedString("configuration_countries_spain", comment: ""), extended: ""),
EQNSeismicNetwork(acronym: "EMSC", country: NSLocalizedString("configuration_countries_greece", comment: ""), extended: ""),
EQNSeismicNetwork(acronym: "UOA", country: NSLocalizedString("configuration_countries_greece", comment: ""), extended: ""),
EQNSeismicNetwork(acronym: "EMSC", country: NSLocalizedString("configuration_countries_france", comment: ""), extended: ""),
EQNSeismicNetwork(acronym: "EMSC", country: NSLocalizedString("configuration_countries_croatia", comment: ""), extended: ""),
EQNSeismicNetwork(acronym: "CSI", country: NSLocalizedString("configuration_countries_china", comment: ""), extended: ""),
@@ -136,49 +133,6 @@ import Foundation
return Self.seismicNetworks().first(where: { $0.acronym == acronym })
}
@objc class func seismicToNotify() -> [EQNGenericValue] {
[
EQNGenericValue(value:"0", display:"eqn_intensity_any"),
EQNGenericValue(value:"1", display:"eqn_intensity_strong")
]
}
@objc class func seismicToNotify(for value: String?) -> EQNGenericValue {
if let value = value, let genericValue = Self.seismicToNotify().first(where: { $0.value == value }) {
return genericValue
}
return Self.DefaultSeismicToNotify
}
@objc class func doNotDisturbStartDate(from date: Date?) -> Date {
if let date = date {
return date
}
// return default
let calendar = Calendar(identifier: .gregorian)
let units: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute]
var components = calendar.dateComponents(units, from: Date())
components.hour = Self.DefaultDoNotDisturbStartTime
components.minute = 00
return calendar.date(from: components)!
}
@objc class func doNotDisturbEndDate(from date: Date?) -> Date {
if let date = date {
return date
}
// return default
let calendar = Calendar(identifier: .gregorian)
let units: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute]
var components = calendar.dateComponents(units, from: Date())
components.hour = Self.DefaultDoNotDisturbEndTime
components.minute = 00
return calendar.date(from: components)!
}
@objc class func periodiTemporali() -> [EQNGenericValue] {
[
EQNGenericValue(value: "10", display: "10 minuti"),
+12 -19
View File
@@ -120,28 +120,21 @@
- (void)scaricaReteSismica
{
// Per ridurre i dati trasferiti tra app e server, ci sono degli endpoint dedicati
// in base alla magnitudo impostata dall'utente (per valori inferiori a 2.0).
// Se l'opzione `Mostra sismi di qualsiasi magnitudo se a meno di 50km` è attiva,
// dobbiamo utilizzare l'endpoint `_M0_0` perchè dobbiamo scaricare tutti i dati
NSString *queryString = @"";
double filterMagnitude = [[EQNSeismic shared].magnitudoMinima doubleValue];
bool filterAnyNearEarthquake = [EQNSeismic shared].sismiQualsiasiAbilitati;
if (filterMagnitude < 0.5 || filterAnyNearEarthquake) {
queryString = @"_M0_0";
} else if (filterMagnitude < 1.0) {
queryString = @"_M0_5";
} else if (filterMagnitude < 1.5) {
queryString = @"_M1_0";
} else if (filterMagnitude < 2.0) {
queryString = @"_M1_5";
// L'endpoint per lo scaricamento dei dati prende due parametri: pro per il provider selezionato, mag per la
// magnitudo minima. Se l'utente ha selezionato più di un provider, inviamo ALL. Mentre la magnitudo
// deve avere sempre una cifra decimale (es 2.0).
NSString *filterProvider = @"";
NSArray<NSString *> *networks = [EQNUserData.sharedData seismicNetworksSelected];
if (networks.count == 1) {
filterProvider = [networks firstObject];
} else {
// verrà usato l'url base
queryString = @"";
filterProvider = @"ALL";
}
NSString *filterMagnitude = [EQNSeismic shared].magnitudoMinima;
NSString *queryString = [NSString stringWithFormat:@"?pro=%@&mag=%@", filterProvider, filterMagnitude];
NSString *urlString = [NSString stringWithFormat:EQNServerUrlDownloadRetiSismiche, queryString];
NSURL *url = [NSURL URLWithString:urlString];
NSLog(@"[EQNManager] Url utilizzato per download reti sismiche: %@", url.absoluteURL);
@@ -15,6 +15,14 @@ struct EQNPurchaseAvailability {
// MARK: - Init
init(
top10kAvailable: Int,
top100kAvailable: Int
) {
self.top10kAvailable = top10kAvailable
self.top100kAvailable = top100kAvailable
}
init(data: Data) {
guard let availabilities = try? JSONSerialization.jsonObject(with: data, options: []) as? [[String: String]] else {
return
@@ -20,13 +20,13 @@ public class EQNPurchaseUtility: NSObject {
/// If zero, no discounted price is available
/// - Parameter completion: Completion
static func offerTimeRemaining(completion: @escaping (_ timeRemaining: Int) -> Void) {
let appOpenCounter = UserDefaults.standard.integer(forKey:EQNUserDefaultProDiscountOpenCounter)
let appOpenCounter = UserDefaults.standard.integer(forKey: UserDefaults.UserDataProDiscountOpenCounter)
if appOpenCounter < Self.AppOpenCountForDiscount {
completion(0)
return
}
let discountExpired = UserDefaults.standard.bool(forKey: EQNUserDefaultProDiscountExpired)
let discountExpired = UserDefaults.standard.bool(forKey: UserDefaults.UserDataProDiscountExpired)
if discountExpired {
completion(0)
return
@@ -34,7 +34,7 @@ public class EQNPurchaseUtility: NSObject {
EQNUser.default().downloadOfferTimeRemaining { (timeOffer) in
if timeOffer == 0 {
UserDefaults.standard.set(true, forKey: EQNUserDefaultProDiscountExpired)
UserDefaults.standard.set(true, forKey: UserDefaults.UserDataProDiscountExpired)
}
let timeInHours = timeOffer / 60
@@ -45,21 +45,13 @@ public class EQNPurchaseUtility: NSObject {
/// Returns availabilities for active subscriptions
/// - Parameter completion: Completion
static func availableSubscriptions(completion: @escaping (_ availability: EQNPurchaseAvailability?) -> Void) {
guard let url = URL(string: EQNServerUrlAvailableSubscriptionsCounter) else {
completion(nil)
return
}
// previously, to get this data a separated call was required
// starting from March 2023, the values are available from the EQNServerUrlDownloadSmartphoneNetwork request
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
completion(nil)
return
}
let availability = EQNPurchaseAvailability(data: data)
completion(availability)
}
task.resume()
let reteSmartPhone = EQNManager.manager().rete_smartphone
let availability = EQNPurchaseAvailability(top10kAvailable: reteSmartPhone?.top10kAvailable ?? 0,
top100kAvailable: reteSmartPhone?.top100kAvailable ?? 0)
completion(availability)
}
/// Check if user has bought pro app version
@@ -0,0 +1,264 @@
//
// EQNRealtimePushNotification.swift
// Earthquake Network
//
// Created by Andrea Busi on 13/07/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import UIKit
import CoreLocation
import Shogun
@objc
class EQNRealtimePushNotification: NSObject, Codable {
/// Tempo (in secondi) entro cui vengono mostrate allerte in tempo reale ricevute
private static let RealtimeAlertExpiration: TimeInterval = 28_800 // 8 ore
private enum CodingKeys: String, CodingKey {
case type
case intensity
case latitude
case longitude
case counter
case dateTime
case waveSpeed
case impactTimestamp
case peak
case title
case displayTitle
case displayBody
}
let type: String
/// Earthquake intensity
let intensity: Int
/// Earthquake coordinate
let latitude: Double
let longitude: Double
/// Number of smartphones that report the earthquake
let counter: Int
let dateTime: Date?
/// Earthquake wave speed
let waveSpeed: Double
/// Calculated timestamp for earthquake on user position
let impactTimestamp: Date?
let peak: Double?
// Title received inside `aps.alert`
let title: String
// Title and body elaborated in NotificationService
let displayTitle: String
let displayBody: String
var coordinate: CLLocation {
.init(latitude: latitude, longitude: longitude)
}
// MARK: - Init
init(
type: String,
intensity: Int,
latitude: Double,
longitude: Double,
counter: Int,
dateTime: Date?,
waveSpeed: Double,
impactTimestamp: Date?,
peak: Double?,
title: String,
displayTitle: String,
displayBody: String
) {
self.type = type
self.intensity = intensity
self.latitude = latitude
self.longitude = longitude
self.counter = counter
self.dateTime = dateTime
self.waveSpeed = waveSpeed
self.impactTimestamp = impactTimestamp
self.peak = peak
self.title = title
self.displayTitle = displayTitle
self.displayBody = displayBody
}
func distanceFromUser() -> CLLocationDistance {
// Usiamo la posizione salvata, che coincide con quella presnete in EQNUser.
// In questo modo non abbiamo dipendenze e possiamo includere questa classe
// anche nel target NotificationService
EQNUserData.shared.lastLocation?.distance(from: coordinate) ?? 0.0
}
/// Remaining time before earthquake gets user position
func currentCountdown() -> Int {
guard let impactTimestamp else { return 0 }
let now = Date()
let difference = lround(max(impactTimestamp.timeIntervalSince(now), 0))
return difference
}
@objc
func isCountdownExpired() -> Bool {
currentCountdown() <= 0
}
/// Intensity on user location
func relativeIntensity() -> Double {
guard distanceFromUser() > 0, let peak else { return 0 }
let distanceKm = distanceFromUser() / 1_000 // get in km
let relativeIntensity = peak * exp(-distanceKm/peak/250)
return relativeIntensity
}
// MARK: - Class
/// Remove any saved notification
@objc(removeStoredNotification)
static func removeStored() {
UserDefaults.standard.removeObject(forKey: UserDefaults.RealTimeAlertPayload)
UserDefaults.standard.removeObject(forKey: UserDefaults.RealTimeAlertDate)
}
@objc(storedNotification)
static func stored() -> EQNRealtimePushNotification? {
guard let date = UserDefaults.standard.object(forKey: UserDefaults.RealTimeAlertDate) as? Date else {
return nil
}
guard date.isBeforeInterval(Self.RealtimeAlertExpiration) else {
print("[EQNRealtimePushNotification] Saved notification expired")
return nil
}
guard let data = UserDefaults.standard.object(forKey: UserDefaults.RealTimeAlertPayload) as? Data else {
print("[EQNRealtimePushNotification] No notification saved for key '\(UserDefaults.RealTimeAlertPayload)'")
return nil
}
guard let notification = try? JSONDecoder().decode(EQNRealtimePushNotification.self, from: data) else {
print("[EQNRealtimePushNotification] Unable to decode given notification")
return nil
}
return notification
}
/// Convert and store a push notification payload.
/// Expected payload has the following structure:
/// ```
/// {
/// "title": "Allerta sismica in tempo reale",
/// "body": "Previsto uno scuotimento forte",
/// "userInfo": {
/// "datetime" : "2023-07-13 12:24:04",
/// ...
/// "aps": {
/// "alert" : {
/// "loc-key" : "Rilevato sisma forte a",
/// "title-loc-key" : "Allerta sismica in tempo reale",
/// "loc-args" : [
/// "150 km (Test)"
/// ]
/// },
/// }
/// }
/// }
/// ```
/// - Parameter payload: Notification payload
/// - Returns: `true` if save succeed, `false` otherwise
@objc(storeNotificationWithPayload:)
@discardableResult
static func store(payload: [String: Any]) -> Bool {
guard let notification = from(payload: payload) else {
print("[EQNRealtimePushNotification] Unable to convert received notification")
return false
}
guard let data = try? JSONEncoder().encode(notification) else {
print("[EQNRealtimePushNotification] Unable to encode given notification")
return false
}
UserDefaults.standard.set(data, forKey: UserDefaults.RealTimeAlertPayload)
UserDefaults.standard.set(Date(), forKey: UserDefaults.RealTimeAlertDate)
return true
}
@objc
private static func from(payload: [String: Any]) -> EQNRealtimePushNotification? {
guard let userInfo = payload["userInfo"] as? [String: Any],
let aps = userInfo["aps"] as? [String: Any],
let alert = aps["alert"] as? [String: Any] else {
print("[EQNRealtimePushNotification] Missing required info to parse push notification")
return nil
}
guard let latitude = userInfo.double(forKey: "latitude"),
let longitude = userInfo.double(forKey: "longitude") else {
print("[EQNRealtimePushNotification] Unable to get coordinate from push notification")
return nil
}
let type = userInfo.string(forKey: "type", orDefault: "")
let intensity = userInfo.integer(forKey: "intensity", orDefault: 0)
let counter = userInfo.integer(forKey: "counter", orDefault: 0)
var dateTime: Date?
if let dateString = userInfo.string(forKey: "datetime"), let date = EQNUtility.getDateFrom(dateString) {
dateTime = date
}
let waveSpeed = userInfo.double(forKey: "wave_speed", orDefault: 0.0) * 1000 // m/s
var impactTimestamp: Date?
if let timestamp = EQNUtility.calculateUserSeismicTimestamp(fromUserInfo: userInfo) {
impactTimestamp = timestamp
}
let peak = userInfo.double(forKey: "peak")
var title: String = ""
if let titleKey = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first {
title = String(format: NSLocalizedString(titleKey, comment: ""), arg)
}
let displayTitle = payload.string(forKey: "title", orDefault: "")
let displayBody = payload.string(forKey: "body", orDefault: "")
return .init(
type: type,
intensity: intensity,
latitude: latitude,
longitude: longitude,
counter: counter,
dateTime: dateTime,
waveSpeed: waveSpeed,
impactTimestamp: impactTimestamp,
peak: peak,
title: title,
displayTitle: displayTitle,
displayBody: displayBody
)
}
}
extension EQNRealtimePushNotification {
/// Returns the color based on relative intensity
var relativeIntensityColor: UIColor {
let intensity = relativeIntensity()
switch intensity {
case _ where intensity < 0.004:
return UIColor(red: 90.0/255.0, green: 90.0/255.0, blue: 90.0/255.0, alpha: 1.0)
case _ where intensity < 0.30:
return UIColor(red: 38.0/255.0, green: 100.0/255.0, blue: 38.0/255.0, alpha: 1.0)
case _ where intensity < 0.70:
return UIColor(red: 255.0/255.0, green: 140.0/255.0, blue: 0.0, alpha: 1.0)
default:
return UIColor(red: 215.0/255.0, green: 0.0, blue: 0.0, alpha: 1.0)
}
}
}
@@ -7,6 +7,7 @@
//
import Foundation
import Shogun
@objc
@@ -14,11 +15,11 @@ class EQNReteSmartphone: NSObject {
@objc let counterLastDayAlerts: Int
@objc let counterTotalAlerts: Int
@objc let counterSmartphones: Int
@objc let manualGreen: Int
@objc let manualYellow: Int
@objc let manualRed: Int
@objc let manual: Int
@objc let lastSubscriptionDiff: Int
@objc let subscriptionsDiscounted: Bool
let top10kAvailable: Int
let top100kAvailable: Int
// MARK: - Init
@@ -28,30 +29,16 @@ class EQNReteSmartphone: NSObject {
return result.merging(dictionary, uniquingKeysWith: { (_, new) in new })
}
self.counterLastDayAlerts = Self.getValue(from: allValues, for: "eq")
self.counterTotalAlerts = Self.getValue(from: allValues, for: "eq_p")
self.counterSmartphones = Self.getValue(from: allValues, for: "green")
self.manualGreen = Self.getValue(from: allValues, for: "g_man")
self.manualYellow = Self.getValue(from: allValues, for: "y_man")
self.manualRed = Self.getValue(from: allValues, for: "r_man")
self.lastSubscriptionDiff = Self.getValue(from: allValues, for: "diff")
let subscriptionsDiscounted = Self.getValue(from: allValues, for: "st")
self.counterLastDayAlerts = allValues.integer(forKey: "eq", orDefault: 0)
self.counterTotalAlerts = allValues.integer(forKey: "eq_p", orDefault: 0)
self.counterSmartphones = allValues.integer(forKey: "green", orDefault: 0)
self.manual = allValues.integer(forKey: "man", orDefault: 0)
self.lastSubscriptionDiff = allValues.integer(forKey: "diff", orDefault: 0)
let subscriptionsDiscounted = allValues.integer(forKey: "st", orDefault: 0)
self.subscriptionsDiscounted = subscriptionsDiscounted == 1
self.top10kAvailable = allValues.integer(forKey: "t10k")
self.top100kAvailable = allValues.integer(forKey: "t100k")
super.init()
}
// MARK: - Private
/// This method helps to extract an int value from the received values (where data are both strings and integers).
/// If convertion is not possible, it will return zero.
private static func getValue(from values: [String: Any], for key: String) -> Int {
if let intValue = values[key] as? Int {
return intValue
}
if let stringValue = values[key] as? String, let intValue = Int(stringValue) {
return intValue
}
return 0
}
}
@@ -9,23 +9,15 @@
@import Foundation;
@import CoreLocation;
// Intensità terremoti segnalazioni utente
typedef NS_CLOSED_ENUM(NSInteger, EQNPastquakeIntensity) {
EQNPastquakeIntensityMild = 1,
EQNPastquakeIntensityStrong = 2,
EQNPastquakeIntensityVeryStrong = 3
};
NS_ASSUME_NONNULL_BEGIN
@interface EQNSegnalazione : NSObject
@property (nonatomic, strong) CLLocation *coordinate;
@property (nonatomic, strong) NSString *address;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic) NSInteger difference;
@property (nonatomic) EQNPastquakeIntensity intensity;
@property (nonatomic, strong) NSString *message;
// values are from 20 to 120
@property (nonatomic) NSInteger intensity;
- (instancetype)initWithDictionary:(NSDictionary *)dictionary;
@@ -17,15 +17,12 @@
{
self = [super init];
if (self) {
double latitude = [dictionary[@"latitude"] doubleValue];
double longitude = [dictionary[@"longitude"] doubleValue];
double latitude = [dictionary[@"la"] doubleValue];
double longitude = [dictionary[@"lo"] doubleValue];
_coordinate = [[CLLocation alloc] initWithLatitude:latitude longitude:longitude];
_address = dictionary[@"address"];
NSDate *date = [EQNUtility getDateFromString:dictionary[@"date"]];
NSDate *date = [EQNUtility getDateFromString:dictionary[@"dt"]];
_date = date == nil ? [NSDate date] : date;
_difference = [dictionary[@"difference"] integerValue];
_intensity = [dictionary[@"magnitude"] integerValue];
_message = dictionary[@"msg"];
_intensity = [dictionary[@"ma"] integerValue];
}
return self;
}
@@ -34,24 +31,18 @@
- (void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:self.address forKey:@"address"];
[encoder encodeObject:self.date forKey:@"date"];
[encoder encodeInteger:self.difference forKey:@"difference"];
[encoder encodeObject:self.coordinate forKey:@"coordinate"];
[encoder encodeInteger:self.intensity forKey:@"intensity"];
[encoder encodeObject:self.message forKey:@"message"];
}
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self) {
self.address = [decoder decodeObjectForKey:@"address"];
self.date = [decoder decodeObjectForKey:@"date"];
self.difference = [decoder decodeIntegerForKey:@"difference"];
self.coordinate = [decoder decodeObjectForKey:@"coordinate"];
self.intensity = [decoder decodeIntegerForKey:@"intensity"];
self.message = [decoder decodeObjectForKey:@"message"];
}
return self;
}
@@ -27,14 +27,15 @@ import Foundation
override init() {
Self.migrateOldDistanza()
Self.migrateOldPeriodo()
magnitudoMinima = Self.userDefaults(for: EQN_MAGNITUDO_MINIMA, or: EQNData.DefaultMagitudoDebole.value)
distanzaMassima = Self.userDefaults(for: EQN_DISTANZA_MASSIMA, or: EQNData.DefaultRaggioSisma.value)
periodoTemporale = Self.userDefaults(for: EQN_ETA_MASSIMA, or: EQNData.DefaultPeriodoTemporale.value)
sismiFortiAbilitati = Self.userDefaults(for: EQN_SISMI_FORTI_ABILITATI, or: false)
sismiFortiMagnitudo = Self.userDefaults(for: EQN_SISMI_FORTI, or: EQNData.DefaultMagitudoForte.value)
sismiQualsiasiAbilitati = Self.userDefaults(for: EQN_SISMI_QUALSIASI_MAGNITUDO, or: false)
modificaImpostazioniAbilitato = Self.userDefaults(for: EQN_SISMI_MODIFICA_IMPOSTAZIONI, or: false)
let defaults = UserDefaults.standard
magnitudoMinima = defaults.object(forKey: UserDefaults.SeismicMagnitudoMinima, or: EQNData.DefaultMagitudoDebole.value)
distanzaMassima = defaults.object(forKey: UserDefaults.SeismicDistanzaMassima, or: EQNData.DefaultRaggioSisma.value)
periodoTemporale = defaults.object(forKey: UserDefaults.SeismicEtaMassima, or: EQNData.DefaultPeriodoTemporale.value)
sismiFortiAbilitati = defaults.object(forKey: UserDefaults.SeismicSismiFortiAbilitati, or: false)
sismiFortiMagnitudo = defaults.object(forKey: UserDefaults.SeismicSismiForti, or: EQNData.DefaultMagitudoForte.value)
sismiQualsiasiAbilitati = defaults.object(forKey: UserDefaults.SeismicSismiQualsiasiMagnitudo, or: false)
modificaImpostazioniAbilitato = defaults.object(forKey: UserDefaults.SeismicModificaImpostazioni, or: true)
super.init()
}
@@ -43,33 +44,26 @@ import Foundation
// MARK: - Public
public func saveFilters() {
UserDefaults.standard.set(magnitudoMinima, forKey:EQN_MAGNITUDO_MINIMA)
UserDefaults.standard.set(distanzaMassima, forKey:EQN_DISTANZA_MASSIMA)
UserDefaults.standard.set(periodoTemporale, forKey:EQN_ETA_MASSIMA)
UserDefaults.standard.set(sismiFortiMagnitudo, forKey:EQN_SISMI_FORTI)
UserDefaults.standard.set(sismiFortiAbilitati, forKey:EQN_SISMI_FORTI_ABILITATI)
UserDefaults.standard.set(sismiQualsiasiAbilitati, forKey:EQN_SISMI_QUALSIASI_MAGNITUDO)
UserDefaults.standard.set(modificaImpostazioniAbilitato, forKey:EQN_SISMI_MODIFICA_IMPOSTAZIONI)
UserDefaults.standard.set(magnitudoMinima, forKey: UserDefaults.SeismicMagnitudoMinima)
UserDefaults.standard.set(distanzaMassima, forKey: UserDefaults.SeismicDistanzaMassima)
UserDefaults.standard.set(periodoTemporale, forKey: UserDefaults.SeismicEtaMassima)
UserDefaults.standard.set(sismiFortiMagnitudo, forKey: UserDefaults.SeismicSismiForti)
UserDefaults.standard.set(sismiFortiAbilitati, forKey: UserDefaults.SeismicSismiFortiAbilitati)
UserDefaults.standard.set(sismiQualsiasiAbilitati, forKey: UserDefaults.SeismicSismiQualsiasiMagnitudo)
UserDefaults.standard.set(modificaImpostazioniAbilitato, forKey: UserDefaults.SeismicModificaImpostazioni)
}
// MARK: - Private
private static func userDefaults<T>(for key: String, or defaultValue: T) -> T {
if let value = UserDefaults.standard.object(forKey: key) as? T {
return value
}
return defaultValue
}
private static func migrateOldDistanza() {
guard let savedValue = UserDefaults.standard.object(forKey: EQN_DISTANZA_MASSIMA) as? String else {
guard let savedValue = UserDefaults.standard.object(forKey: UserDefaults.SeismicDistanzaMassima) as? String else {
print("[EQNSeismic] Distanza massima: nessun valore da convertire")
return
}
if savedValue.lowercased() == NSLocalizedString("radius_any_distance", comment: "").lowercased() {
print("[EQNSeismic] Distanza massima: trovato qualsiasi distanza, salvo valore")
UserDefaults.standard.set("100000", forKey: EQN_DISTANZA_MASSIMA)
UserDefaults.standard.set("100000", forKey: UserDefaults.SeismicDistanzaMassima)
} else {
print("[EQNSeismic] Distanza massima: valore da non convertire (value: \(savedValue))")
@@ -77,7 +71,7 @@ import Foundation
}
private static func migrateOldPeriodo() {
guard let savedValue = UserDefaults.standard.object(forKey: EQN_ETA_MASSIMA) as? String else {
guard let savedValue = UserDefaults.standard.object(forKey: UserDefaults.SeismicEtaMassima) as? String else {
print("[EQNSeismic] Età massima: nessun valore da convertire");
return
}
@@ -99,7 +93,7 @@ import Foundation
if let convertedValue = convertedValue {
print("[EQNSeismic] Età massima: salvo valore convertito (old: \(savedValue) - new: \(convertedValue)")
UserDefaults.standard.set(convertedValue, forKey: EQN_ETA_MASSIMA)
UserDefaults.standard.set(convertedValue, forKey: UserDefaults.SeismicEtaMassima)
} else {
print("[EQNSeismic] Età massima: valore già convertito")
}
@@ -109,10 +103,8 @@ import Foundation
@objc func filterSeismicList(_ list: [EQNSisma]) -> [EQNSisma] {
// enti abilitati
var networks: [String]
if let savedNetworks = UserDefaults.standard.object(forKey: IMPOSTAZIONE_ENTI_RETI_SISMICHEI) as? [String] {
networks = savedNetworks
} else {
var networks = EQNUserData.shared.seismicNetworksSelected()
if networks.isEmpty {
networks = EQNData.seismicNetworkAcronyms()
}
networks = networks.map { $0.lowercased() }
@@ -33,14 +33,6 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong) NSNumber *preliminary;
@property (nonatomic, strong) NSNumber *smartphoneNumber;
@property (nonatomic, strong) NSNumber *userNumber;
@property (nonatomic, strong, nullable) NSString *weatherCode;
@property (nonatomic, strong) NSString *weatherIcon;
@property (nonatomic, strong) NSNumber *weatherCloud;
@property (nonatomic, strong) NSNumber *weatherWindSpeed;
@property (nonatomic, strong) NSNumber *weatherPressure;
@property (nonatomic, strong) NSNumber *weatherHumidity;
@property (nonatomic, strong) NSNumber *weatherTemperature;
@property (nonatomic, strong) NSNumber *pictureCount;
- (instancetype)initWithInfo:(NSDictionary *)info;
@@ -42,15 +42,6 @@
self.preliminary = info[@"py"];
self.smartphoneNumber = info[@"sm"];
self.userNumber = info[@"rp"];
self.weatherCode = [info eqn_safeObjectForKey:@"wc"];
self.weatherIcon = info[@"ic"];
self.weatherCloud = info[@"cl"];
self.weatherWindSpeed = info[@"ws"];
self.weatherPressure = info[@"pe"];
self.weatherHumidity = info[@"hu"];
self.weatherTemperature = info[@"te"];
self.pictureCount = info[@"pc"];
}
return self;
}
@@ -74,14 +65,6 @@
[encoder encodeObject:self.preliminary forKey:@"preliminary"];
[encoder encodeObject:self.smartphoneNumber forKey:@"smartphoneNumber"];
[encoder encodeObject:self.userNumber forKey:@"userNumber"];
[encoder encodeObject:self.weatherCode forKey:@"weatherCode"];
[encoder encodeObject:self.weatherIcon forKey:@"weatherIcon"];
[encoder encodeObject:self.weatherCloud forKey:@"weatherCloud"];
[encoder encodeObject:self.weatherWindSpeed forKey:@"weatherWindSpeed"];
[encoder encodeObject:self.weatherPressure forKey:@"weatherPressure"];
[encoder encodeObject:self.weatherHumidity forKey:@"weatherHumidity"];
[encoder encodeObject:self.weatherTemperature forKey:@"weatherTemperature"];
[encoder encodeObject:self.pictureCount forKey:@"pictureCount"];
}
- (instancetype)initWithCoder:(NSCoder *)decoder
@@ -103,14 +86,6 @@
self.preliminary = [decoder decodeObjectForKey:@"preliminary"];
self.smartphoneNumber = [decoder decodeObjectForKey:@"smartphoneNumber"];
self.userNumber = [decoder decodeObjectForKey:@"userNumber"];
self.weatherCode = [decoder decodeObjectForKey:@"weatherCode"];
self.weatherIcon = [decoder decodeObjectForKey:@"weatherIcon"];
self.weatherCloud = [decoder decodeObjectForKey:@"weatherCloud"];
self.weatherWindSpeed = [decoder decodeObjectForKey:@"weatherWindSpeed"];
self.weatherPressure = [decoder decodeObjectForKey:@"weatherPressure"];
self.weatherHumidity = [decoder decodeObjectForKey:@"weatherHumidity"];
self.weatherTemperature = [decoder decodeObjectForKey:@"weatherTemperature"];
self.pictureCount = [decoder decodeObjectForKey:@"pictureCount"];
}
return self;
}
+2 -6
View File
@@ -15,20 +15,16 @@ NS_ASSUME_NONNULL_BEGIN
@interface EQNUser : NSObject
@property (nonatomic, strong, nullable) NSString *tokenUser;
@property (nonatomic, strong, nullable) NSString *user_ID;
@property (nonatomic, strong, nullable) CLLocation *lastPosition;
@property (nonatomic, assign) CLLocationDistance distanza;
@property (nonatomic, assign) BOOL monitorOn;
@property (nonatomic, assign) BOOL inCarica;
@property (nonatomic, assign) BOOL registrato;
+ (instancetype)defaultUser;
- (void)inviaPosizioneServer;
- (void)saveUserInfo;
- (void)removeUser;
- (void)verificaRegistrazione;
- (void)retryUserRegistration;
- (void)registerUserIfNeededWithFirebaseToken:(NSString *)firebaseToken;
- (void)downloadOfferTimeRemainingWithCompletion:(timeRemainingCompletion)completionHandler;
@end
+121 -84
View File
@@ -8,12 +8,16 @@
#import "EQNUser.h"
#import "Costanti.h"
#import "EQNAccelerometroManager.h"
#import "ServerRequest.h"
#import "EQNAccelerometroManager.h"
#import "EQNGeneratoreURLServer.h"
#import "EQNUtility.h"
#import "EQNManager.h"
@interface EQNUser ()
@property (strong, nonatomic) NSString *currentFirebaseToken;
@property (nonatomic) BOOL registrationInProgress;
@end
@implementation EQNUser
@@ -35,112 +39,61 @@
{
self = [super init];
if (self) {
_tokenUser = [[NSUserDefaults standardUserDefaults] objectForKey:EQNUserDefaultUserFirebaseToken];
id savedUserId = [[NSUserDefaults standardUserDefaults] objectForKey:EQNUserDefaultUserId];
_user_ID = [self convertUserIdIntoString:savedUserId];
self.registrationInProgress = NO;
self.user_ID = EQNUserData.sharedData.userId;
self.lastPosition = EQNUserData.sharedData.lastLocation;
NSArray *lastPosArray = [EQNUtility loadArrayOfClass:[CLLocation class] fromUserDefaultsForKey:EQNUserDefaultLastLocation];
if (lastPosArray.count > 0) {
_lastPosition = [lastPosArray lastObject];
}
_registrato = NO;
if (_user_ID) {
_registrato = YES;
}
[[EQNAccelerometroManager sharedInstance] addObserver:(id)self
forKeyPath:@"currentLocation"
options:NSKeyValueObservingOptionNew
context:nil];
[[EQNAccelerometroManager sharedInstance] startUpdatingLocationBackground];
[self registerForLocationUpdates];
}
return self;
}
#pragma mark - Accessories
- (void)setTokenUser:(NSString *)tokenUser
{
// token could be retrieved after some times
// thanks to this, we force the server registration when the token is received
_tokenUser = tokenUser;
[[NSUserDefaults standardUserDefaults] setObject:tokenUser forKey:EQNUserDefaultUserFirebaseToken];
[self verificaRegistrazione];
}
#pragma mark - Public
- (void)saveUserInfo
{
[[NSUserDefaults standardUserDefaults] setObject:self.tokenUser forKey:EQNUserDefaultUserFirebaseToken];
[[NSUserDefaults standardUserDefaults] setObject:self.user_ID forKey:EQNUserDefaultUserId];
if (self.lastPosition) {
NSArray *lastPosiArray = @[self.lastPosition];
[EQNUtility storeArray:lastPosiArray toUserDefaultForKey:EQNUserDefaultLastLocation];
}
[EQNUserData.sharedData saveFirebaseToken:self.currentFirebaseToken];
[EQNUserData.sharedData saveUserId:self.user_ID];
[EQNUserData.sharedData saveLastLocation:self.lastPosition];
}
- (void)removeUser
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:EQNUserDefaultUserFirebaseToken];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:EQNUserDefaultUserId];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:EQNUserDefaultLastLocation];
[[NSUserDefaults standardUserDefaults] synchronize];
self.tokenUser = nil;
self.currentFirebaseToken = nil;
[EQNUserData.sharedData removeAllData];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
#pragma mark - Public
- (void)retryUserRegistration
{
if ([keyPath isEqualToString:@"currentLocation"]) {
// do some stuff
if (self.lastPosition) {
self.distanza = [self.lastPosition distanceFromLocation:[EQNAccelerometroManager sharedInstance].currentLocation];
}
self.lastPosition = [EQNAccelerometroManager sharedInstance].currentLocation;
[self verificaRegistrazione];
if (![EQNManager defaultManager].isBackground)
[[EQNAccelerometroManager sharedInstance] stopUpdatingLocation];
}
[self registerUserIfNeededWithFirebaseToken:self.currentFirebaseToken];
}
- (void)verificaRegistrazione
- (void)registerUserIfNeededWithFirebaseToken:(NSString *)firebaseToken
{
if (!self.user_ID && self.tokenUser) {
[self inviaregistrazioneServer:self.tokenUser withPosition:self.lastPosition];
self.currentFirebaseToken = firebaseToken;
NSString *previousFirebaseToken = EQNUserData.sharedData.firebaseToken;
// dobbiamo effettuare la registrazione se:
// - userId non disponibile (si tratta di prima registrazione)
// - i due token di Firebase sono diversi (il token è stato aggiornato)
if (!self.user_ID) {
// prima registrazione dell'utente
NSLog(@"[EQNUser] perform first registration");
[self performServerRegistrationWithFirebaseToken:firebaseToken existingUserId:nil];
} else if (![previousFirebaseToken isEqualToString:firebaseToken]) {
// token cambiato, effettuiamo una nuova registrazione
NSLog(@"[EQNUser] firebase token is changed, update registration");
[self performServerRegistrationWithFirebaseToken:firebaseToken existingUserId:self.user_ID];
} else {
[self inviaPosizioneServer];
// non serve la registrazione, monitorniamo la posizione
NSLog(@"[EQNUser] user already registered, start location update");
[self registerForLocationUpdates];
}
}
- (void)inviaregistrazioneServer:(NSString *)token withPosition:(CLLocation *)location
{
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:[EQNGeneratoreURLServer urlRegistrazione] richiesta:EQNTipoChiamataRegistrazione success:^(id result) {
self.user_ID = [self convertUserIdIntoString:result];
[self saveUserInfo];
} failure:^(NSError *errore) {
NSLog(@"USER_ID Error %@", errore);
[[NSNotificationCenter defaultCenter] postNotificationName:EQNServerRegistrationDidFailNotification object:nil];
}];
}
- (void)inviaPosizioneServer
{
NSLog(@"URLPosizione %@", [EQNGeneratoreURLServer urlPosizione]);
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:[EQNGeneratoreURLServer urlPosizione] richiesta:EQNTipoChiamataPosizione success:^(id result) {
NSLog(@"inviato");
} failure:^(NSError *errore) {
}];
}
- (void)downloadOfferTimeRemainingWithCompletion:(_Nonnull timeRemainingCompletion)completionHandler
{
NSURL *url = [EQNGeneratoreURLServer urlDownloadOfferTimeRemaining];
@@ -160,8 +113,92 @@
}];
}
#pragma mark - Network
- (void)performServerRegistrationWithFirebaseToken:(NSString *)token existingUserId:(NSString *)userId
{
if (self.registrationInProgress) {
NSLog(@"[EQNUser] Registration already in progress");
return;
}
self.registrationInProgress = YES;
NSURL *url = [EQNGeneratoreURLServer urlRegistrazioneFirebaseToken:token existingUserId:userId];
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:url richiesta:EQNTipoChiamataRegistrazione success:^(id result) {
NSLog(@"[EQNUser] User registration completed");
// store userId
self.registrationInProgress = NO;
self.user_ID = [self convertUserIdIntoString:result];
[self saveUserInfo];
// inviamo, se già disponibile, la posizione al server
[self performServerSendLocation];
} failure:^(NSError *errore) {
NSLog(@"[EQNUser] Unable to perform user registration: %@", errore);
self.registrationInProgress = NO;
[[NSNotificationCenter defaultCenter] postNotificationName:EQNServerRegistrationDidFailNotification object:nil];
}];
}
- (void)performServerSendLocation
{
if (!self.user_ID) {
NSLog(@"[EQNUser] User id not available");
return;
}
NSURL *url = [EQNGeneratoreURLServer urlPosizione];
[[ServerRequest defaultServerConnectionSingleton] inviaInformazioniAlServerWithURL:url richiesta:EQNTipoChiamataPosizione success:^(id result) {
if ([result isKindOfClass:[NSString class]]) {
NSString *stringResult = (NSString *)result;
if ([stringResult isEqualToString:@"reg"]) {
// l'utente non è stato trovato sul server, ri-eseguiamo la registrazione
NSLog(@"[EQNUser] User not found, retry registration");
[self retryUserRegistration];
} else {
NSLog(@"[EQNUser] Position saved on server");
}
}
[self saveUserInfo];
} failure:^(NSError *error) {
NSLog(@"[EQNUser] Unable to save position. Error: %@", error.localizedDescription);
}];
}
#pragma mark - Private
- (void)registerForLocationUpdates
{
[[EQNAccelerometroManager sharedInstance] addObserver:(id)self
forKeyPath:@"currentLocation"
options:NSKeyValueObservingOptionNew
context:nil];
[[EQNAccelerometroManager sharedInstance] startUpdatingLocationBackground];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"currentLocation"]) {
NSLog(@"[EQNUser] currentLocation changed");
// do some stuff
if (self.lastPosition) {
self.distanza = [self.lastPosition distanceFromLocation:[EQNAccelerometroManager sharedInstance].currentLocation];
}
self.lastPosition = [EQNAccelerometroManager sharedInstance].currentLocation;
[self performServerSendLocation];
if (![EQNManager defaultManager].isBackground) {
[[EQNAccelerometroManager sharedInstance] stopUpdatingLocation];
}
}
}
#pragma mark - Helpers
/// user_id saved as a Number, but is used as a NSString
- (NSString *)convertUserIdIntoString:(id)userId
{
@@ -0,0 +1,107 @@
//
// EQNUserData.swift
// Earthquake Network
//
// Created by Andrea Busi on 04/11/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import Foundation
import CoreLocation
@objc class EQNUserData: NSObject {
@objc(sharedData) static let shared = EQNUserData()
// MARK: - Public
@objc
var isFirstStart: Bool {
firebaseToken == nil
}
// MARK: - Firebase Token
@objc
var firebaseToken: String? {
UserDefaults.standard.object(forKey: UserDefaults.UserDataFirebaseToken) as? String
}
@objc
func saveFirebaseToken(_ token: String?) {
if let token = token {
UserDefaults.standard.set(token, forKey: UserDefaults.UserDataFirebaseToken)
} else {
UserDefaults.standard.removeObject(forKey: UserDefaults.UserDataFirebaseToken)
}
}
// MARK: - User id
@objc
var userId: String? {
let userId = UserDefaults.standard.object(forKey: UserDefaults.UserDataUserId)
// nel corso delle versioni l'id è stato salvato in diversi modi
// per evitare problemi, cerchiamo di convertirlo in modi diveri
if let userId = userId as? String {
return userId
} else if let userId = userId as? Int {
return "\(userId)"
} else if let userId = userId as? NSNumber {
return userId.stringValue
}
return nil
}
@objc
func saveUserId(_ userId: String) {
UserDefaults.standard.set(userId, forKey: UserDefaults.UserDataUserId)
}
// MARK: - Last location
@objc
var lastLocation: CLLocation? {
guard let encodedLocation = UserDefaults.appGroup?.object(forKey: UserDefaults.UserDataLastLocation) as? Data else {
return nil
}
let location = try? NSKeyedUnarchiver.unarchivedObject(ofClass: CLLocation.self, from: encodedLocation)
return location
}
@objc
func saveLastLocation(_ location: CLLocation) {
guard let encodedLocation = try? NSKeyedArchiver.archivedData(withRootObject: location, requiringSecureCoding: false) else {
return
}
UserDefaults.appGroup?.set(encodedLocation, forKey: UserDefaults.UserDataLastLocation)
}
// MARK: - Seismic Networks
@objc
func seismicNetworksSelected() -> [String] {
if let savedNetworks = UserDefaults.standard.object(forKey: UserDefaults.UserDataSelectedSeismicNetworks) as? [String] {
return savedNetworks
}
return []
}
func saveSelectedSeismicNetworks(_ networks: [String]) {
UserDefaults.standard.set(networks, forKey: UserDefaults.UserDataSelectedSeismicNetworks)
}
// MARK: - Public
@objc
func removeAllData() {
UserDefaults.standard.removeObject(forKey: UserDefaults.UserDataFirebaseToken)
UserDefaults.standard.removeObject(forKey: UserDefaults.UserDataUserId)
UserDefaults.appGroup?.removeObject(forKey: UserDefaults.UserDataLastLocation)
}
}
+1 -14
View File
@@ -25,17 +25,13 @@ NS_ASSUME_NONNULL_BEGIN
/// Creates a string for a given time interval.
/// Some examples: 1 hour ago, 25 minutes ago, 2 days ago
/// @param timeDifference Time differnce
/// @param timeDifference Time difference, in minutes
+ (NSString *)formattedStringForTimeDifference:(NSInteger)timeDifference;
/// Clear a given string from unwanted characters
/// @param messaggio Cleaned string
+ (NSString *)clearStringMessaggi:(NSString *)messaggio;
/// Calculate time difference (in minutes) between the given date and the current timestamp
/// @param date Difference (in minutes)
+ (NSInteger)getDifferenceMinute:(NSDate *)date;
/// Store an array of custom objects to NSUserDefaults
/// @param array Array to store
/// @param keyName Key of NSUserDefault to use
@@ -46,15 +42,6 @@ NS_ASSUME_NONNULL_BEGIN
/// @param keyName Key of NSUserDefault to retrieve
+ (nullable NSArray *)loadArrayOfClass:(Class)class fromUserDefaultsForKey:(NSString* )keyName;
/// Store a dictionary to NSUserDefaults
/// @param dictionary Dictionary to store
/// @param keyName Key of NSUserDefault to use
+ (void)storeDictionary:(NSDictionary *)dictionary toUserDefaultForKey:(NSString *)keyName;
/// Retrieve a saved dictionary from NSUserDefaults.
/// @param keyName Key of NSUserDefault to retrieve
+ (nullable NSDictionary *)loadDictionaryFromUserDefaultsForKey:(NSString *)keyName;
/// Calculate impact time from a received push notification
/// @param info Payload of a received notification
/// @return Impact time
+18 -50
View File
@@ -8,9 +8,11 @@
#import "EQNUtility.h"
#import "EQNSegnalazione.h"
#import "EQNSegnalazione.h"
#import "EQNPastquakes.h"
#import "EQNSisma.h"
#if NOTIFICATION_CONTENT
#import "EQNNotificationContent-Swift.h"
#endif
@implementation EQNUtility
@@ -27,25 +29,18 @@
NSString *format = @"";
NSInteger finalValue = timeDifference;
if (timeDifference == 1) {
format = NSLocalizedString(@"minutes_one", comment: nil);
} else if (timeDifference < 60) {
format = NSLocalizedString(@"minutes_other", comment: nil);
} else if (timeDifference > 60 && timeDifference < 120) {
// check for minutes, hours or days
if (timeDifference < 60) {
format = NSLocalizedString(@"manual_minutes_ago", nil);
} else if (timeDifference < 1440) {
finalValue = timeDifference / 60.0;
format = NSLocalizedString(@"hours_one", comment: nil);
} else if (timeDifference > 60 && timeDifference < 1440) {
finalValue = timeDifference / 60.0;
format = NSLocalizedString(@"hours_other", comment: nil);
} else if (timeDifference > 1400 && timeDifference < 2800) {
finalValue = timeDifference / 1400.0;
format = NSLocalizedString(@"days_one", comment: nil);
format = NSLocalizedString(@"manual_hours_ago", nil);
} else {
finalValue = timeDifference / 1400.0;
format = NSLocalizedString(@"days_other", comment: nil);
finalValue = timeDifference / 1440.0;
format = NSLocalizedString(@"manual_days_ago", nil);
}
return [NSString stringWithFormat:format, (long)finalValue];
return [NSString localizedStringWithFormat:format, finalValue];
}
+ (NSString *)clearStringMessaggi:(NSString *)messaggio
@@ -61,18 +56,6 @@
return clearString;
}
+ (NSInteger)getDifferenceMinute:(NSDate *)date
{
NSDate *now = [NSDate date];
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSDateComponents *components = [calendar components:NSCalendarUnitMinute
fromDate:date
toDate:now
options:0];
return components.minute;
}
#pragma mark - Store/load data
+ (void)storeArray:(NSArray *)array toUserDefaultForKey:(NSString *)keyName
@@ -80,11 +63,6 @@
[self storeRootObject:array toUserDefaultForKey:keyName];
}
+ (void)storeDictionary:(NSDictionary *)dictionary toUserDefaultForKey:(NSString *)keyName
{
[self storeRootObject:dictionary toUserDefaultForKey:keyName];
}
+ (void)storeRootObject:(id)rootObject toUserDefaultForKey:(NSString *)keyName
{
NSError *error;
@@ -102,6 +80,10 @@
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults objectForKey:keyName];
if (!data) {
NSLog(@"[EQNUtility] No array saved for key '%@'", keyName);
return nil;
}
NSError *error;
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error];
@@ -115,23 +97,6 @@
return array;
}
+ (NSDictionary *)loadDictionaryFromUserDefaultsForKey:(NSString *)keyName
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults objectForKey:keyName];
NSError *error;
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error];
unarchiver.requiresSecureCoding = NO;
NSDictionary *dictionary = [unarchiver decodeObjectOfClass:[NSDictionary class] forKey:NSKeyedArchiveRootObjectKey];
if (error) {
NSLog(@"[EQNUtility] Unable to unarchive object for key '%@' (error: %@)", keyName, error.localizedDescription);
return nil;
}
return dictionary;
}
#pragma mark - Notifications
+ (nullable NSDate *)calculateUserSeismicTimestampFromUserInfo:(NSDictionary *)info
@@ -143,6 +108,9 @@
// ultima posizione nota dell'utente
CLLocationManager *manager = [[CLLocationManager alloc] init];
CLLocation *lastUserLocation = manager.location;
if (lastUserLocation == nil) {
lastUserLocation = EQNUserData.sharedData.lastLocation;
}
if (!lastUserLocation) {
return nil;
}
@@ -70,6 +70,7 @@ class EQNMapAnnotationSeismic: NSObject, MKAnnotation {
case "IGN": icon = "square"
case "UASD", "BDTIM", "NCS": icon = "thick_star"
case "RSPR": icon = "star6f"
case "UOA": icon = "triangle"
default: icon = ""
}

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