223 Commits

Author SHA1 Message Date
7121afab08 Fix [TestQuotaExceededNoPro, TestQuotaExceededPro] 2023-06-17 20:16:02 +02:00
d9a4c4ffd6 Fix [TestChannelMessageCounter] 2023-06-17 20:08:39 +02:00
fb826919a6 added linter 2023-06-14 15:03:37 +02:00
22720169a2 Tests[TestUserMessageCounter, TestTokenKeysMessageCounter, TestChannelMessageCounter] 2023-06-10 03:41:54 +02:00
7fefd251db Update TODO.md 2023-06-10 00:15:42 +02:00
5de4f67344 use nil-safe json renderer in ginresp 2023-06-09 21:37:30 +02:00
d396a12d68 fix missing error on missing json header 2023-06-09 20:14:30 +02:00
3888c91a6b Switch to multi-stage Dockerbuild 2023-06-06 18:52:03 +02:00
562bac6987 Move enum-generate to goext 2023-06-05 13:37:49 +02:00
e825b4dd85 Update PrimaryHash3 2023-05-30 14:25:55 +02:00
08587b7a7a Tests[**Subscription**] 2023-05-29 01:51:51 +02:00
0daca2cf8f added UpdateClient route 2023-05-28 23:25:18 +02:00
3a9b15c2be Use sq.InsertSingle to insert entities 2023-05-28 22:27:38 +02:00
e9b4db0f1c Validate db schema before startup 2023-05-28 19:59:57 +02:00
312a31ce9e Fix missing certificates in docker 2023-05-28 17:54:53 +02:00
d4a8a2e720 Tests[SendWithAdminKey, SendWithSendKey, SendWithReadKey, SendWithPermissionSendKey] 2023-05-28 17:38:19 +02:00
dcb4f253d8 fix swagger errors 2023-05-28 17:04:44 +02:00
d0a04bae84 Move to multistage Dockerfile 2023-05-28 16:03:14 +02:00
34ac96edd7 Tests[...]:
- SearchMessageFTSMulti
 - ListMessagesFilteredChannels
 - ListMessagesFilteredChannelIDs
 - ListMessagesFilteredSenders
 - ListMessagesFilteredTime
 - ListMessagesFilteredPriority
 - ListMessagesFilteredKeyTokens
2023-05-28 15:46:46 +02:00
b42ce84c3e Added [Channels, ChannelIDs, Senders, TimeBefore, TimeAfter, Priority, KeyTokens] filter to ListMessages 2023-05-28 14:05:53 +02:00
2db779b44f split api.go into multiple files 2023-05-28 13:39:20 +02:00
397bfe78aa Remove Channel/Username normalization (except TrimSpace) 2023-05-28 13:14:05 +02:00
efaad3f97c Fix RequestLogCollectorJob sometimes not properly shutting down 2023-05-28 12:31:14 +02:00
624c613bd1 Tests[ListChannelMessagesOfUnsubscribed, ListChannelMessagesOfUnconfirmed1, ListChannelMessagesOfUnconfirmed2, ListChannelMessagesOfRevokedConfirmation] 2023-05-28 03:38:33 +02:00
07b0632c95 Tests[ListChannelSubscriptions] 2023-05-28 03:16:29 +02:00
3d1e6cfa17 Tests[ListSubscribedChannelMessages] 2023-05-28 02:50:55 +02:00
3db636d41a Tests[ListChannelMessages] 2023-05-28 02:40:24 +02:00
2053b8f07f Tests[ListMessages, ListMessagesPaginated, ListMessagesPaginatedInvalid] 2023-05-28 02:27:15 +02:00
b1681b53e4 Tests[TokenKeys, TokenKeysInitial, TokenKeysCreate, TokenKeysUpdate, TokenKeysDelete, TokenKeysDeleteSelf, TokenKeysDowngradeSelf, TokenKeysPermissions] 2023-05-28 01:39:55 +02:00
03f60ff316 Tests[RequestLogSimple] 2023-05-27 23:54:14 +02:00
b2df0a5a02 Send channel as prefix for compat clients 2023-05-27 20:02:16 +02:00
8826cb0312 Save used keytoken in messages 2023-05-27 18:51:20 +02:00
a0c72f5b94 Add keytoken explanation to api_more.html 2023-05-27 18:16:32 +02:00
7d9a58ae54 Fix swagger 2023-05-27 17:51:56 +02:00
fd72b512f8 Directly use pygmentize in Makefile (for scn script in Website) 2023-05-27 17:42:06 +02:00
28c2721036 Use gotestsum for make test 2023-05-27 15:39:07 +02:00
a1788bf75a use swaggo 1.8.12 2023-04-25 20:08:30 +02:00
b1bd278f9b Add KeyToken authorization 2023-04-21 21:45:16 +02:00
16f6ab4861 re-implement ack behaviour from version 1.0 for compat 2023-02-03 22:51:03 +01:00
01934e29b1 todos 2023-02-03 20:12:41 +01:00
d1cefb0150 API versioning ( basePath == /api/v2/* ) 2023-01-27 10:04:06 +01:00
27b189d33a BF 2023-01-24 13:52:11 +01:00
e05d88682a Tests[ListClients] 2023-01-18 21:56:37 +01:00
2a5f1f5f7e Tests[CompatUpdate, CompatUpdateFCM] 2023-01-17 23:10:38 +01:00
e7a45d9a05 Tests[UgradeUserToPro, DowngradeUserToNonPro, FailedUgradeUserToPro, FailToCreateProUser, ReuseProToken] 2023-01-17 22:56:04 +01:00
ec9a326002 Tests[CompatUpgrade] 2023-01-17 22:32:13 +01:00
23c7729fcf Tests[CompatRegisterPro] 2023-01-17 22:03:27 +01:00
7fcd324299 Tests[CompatRequery] 2023-01-17 21:47:53 +01:00
1633449638 Tests[CompatExpand] 2023-01-17 21:30:53 +01:00
57231a1406 Tests[CompatAck] 2023-01-17 21:14:42 +01:00
2eb6292733 improve AssertEqual to handle annoying json.Unmarshal type conversions... 2023-01-17 20:41:45 +01:00
ff24493ff3 Tests[CompatRegister, CompatInfo] 2023-01-17 20:27:20 +01:00
3d602af135 Tests[TestSendCompatMessageByQuery, TestSendCompatMessageByFormData] 2023-01-16 20:29:49 +01:00
590665a5e9 create compat-id for messages && TestCreateCompatUser 2023-01-16 18:53:22 +01:00
89fd0dfed7 create migration script for old data 2023-01-15 06:30:30 +01:00
82bc887767 Move to string-ids for all entities (compat translation for existing data) 2023-01-14 00:48:51 +01:00
acd7de0dee cherry-pick caller logprint fix from psycho-backend 2023-01-13 17:51:55 +01:00
e737cd9d5c requests-log db 2023-01-13 17:17:17 +01:00
0ec7a9d274 add methods for (some) missing test cases 2023-01-13 12:54:19 +01:00
e49d9159e4 Added freely editable description_name field to channel 2023-01-13 12:43:20 +01:00
3343285761 goext 55 2023-01-06 02:03:10 +01:00
14bba38324 migrate to multiple sqlite db files ( primary + requests + logs ) 2023-01-06 00:39:21 +01:00
679277d59e catch panic in gin 2022-12-28 00:32:15 +01:00
cebb2ae2b6 small cleanups 2022-12-23 20:27:21 +01:00
56d9f977ae Tests[ListChannelsDefault, ListChannelsOwned, ListChannelsSubscribedAny, ListChannelsAllAny, ListChannelsSubscribed, ListChannelsAll] 2022-12-22 17:29:59 +01:00
984470b47d Fix sql-preprocessor leading to deadlocks in parallel requests 2022-12-22 16:51:04 +01:00
0112d681ac Fix SQL unmarshalling of optional nested structs (LEFT JOIN) 2022-12-22 12:43:40 +01:00
0cb2a977a0 Save internal_name and display_name in channel 2022-12-22 11:22:36 +01:00
f65c231ba0 Properly shutdown database on SIGTERM 2022-12-22 10:21:10 +01:00
dbc014f819 Added a SQL-Preprocessor - this way we can unmarshal recursive structures (LEFT JOIN etc) 2022-12-21 18:14:13 +01:00
bbf7962e29 move server/* to scnserver/* 2022-12-21 12:35:56 +01:00
2b4d77bab4 Cleaner swagger routes 2022-12-21 11:03:31 +01:00
8582674b44 Refactoring 2022-12-20 13:55:09 +01:00
f7675be834 Use multiple DB connections but retry failed requests 2022-12-20 09:52:33 +01:00
00d77e508d Fix TestSendParallel by using only a single DB connection
see https://github.com/mattn/go-sqlite3/issues/274
see https://github.com/mattn/go-sqlite3/issues/209
see https://stackoverflow.com/questions/32479071/sqlite3-error-database-is-locked-in-golang
2022-12-20 09:22:18 +01:00
e90cfe34e9 switch to new registry image-name 2022-12-16 14:57:17 +01:00
54dfd535a4 Move parseConfOverride() to goext 2022-12-16 01:07:48 +01:00
5a02eb6d18 Prefix all config key with SCN_* 2022-12-14 18:46:26 +01:00
97fc9319d1 Fix wrong env keys in config.go 2022-12-14 18:43:32 +01:00
03b4acd13e Tests[CreateProUser] 2022-12-14 18:38:30 +01:00
86f06a3c6a Tests[CreateChannel, CreateChannelNameTooLong, ChannelNameNormalization] 2022-12-14 18:27:22 +01:00
06e8d2a6e2 Tests[SendLongContentPro] 2022-12-14 18:18:02 +01:00
99f248a8ce Tests[SendParallel] (skipped for now) 2022-12-14 18:08:03 +01:00
c7aaa6ad98 Tests[QuotaExeededPro] 2022-12-14 17:56:14 +01:00
cb5ce66c1a Tests[QuotaExeededNoPro] 2022-12-14 17:56:03 +01:00
0750bf1d8a cleanup test local-url 2022-12-14 17:02:18 +01:00
203360e8b5 Tests[SendToTooLongChannel] 2022-12-14 16:57:08 +01:00
ef1844109f Tests[SendToManualChannel] 2022-12-14 16:38:01 +01:00
de6ad35f60 new Endpoint: CreateChannel(*) 2022-12-14 14:29:59 +01:00
fbb289dedf Added error descriptions to swagger 2022-12-14 14:27:41 +01:00
f1e87170f0 Tests[SendToNewChannel] 2022-12-14 14:30:34 +01:00
66ecad27a7 Only soft-delete messages 2022-12-14 12:29:55 +01:00
98b1e8bd80 move ScanAll/ScanSingle in sq package 2022-12-11 03:14:42 +01:00
26cd1533b4 Tests[SearchMessageFTSSimple] 2022-12-11 02:47:23 +01:00
3692b915f3 Messagefilter (+FTS) [WIP] 2022-12-10 03:38:48 +01:00
06788c3e12 TestData-Factory [WIP] 2022-12-09 00:40:50 +01:00
edfcdd1135 TestData-Factory [WIP] 2022-12-09 00:13:10 +01:00
dd2f3baa0c Properly close db cursors after use 2022-12-08 11:31:52 +01:00
7db70e392b Simplify fts table schema 2022-12-07 23:43:52 +01:00
0cae24a612 Move sq + ParseDurShortString() to goext and change conf values by env 2022-12-07 23:32:58 +01:00
8db0fa37db Move to own sql abstraction on top of jmoiron/sqlx 2022-12-07 22:11:44 +01:00
d27e3d9a91 Made sqlite tables strict (type checked) 2022-12-07 22:11:07 +01:00
fa5a4107a6 Added FTS5 table to schema (full-text-search) 2022-12-07 22:10:46 +01:00
234188c4d4 Tests[SendCompat] 2022-12-01 14:45:31 +01:00
9b700581f3 Tests[SendSimpleMessageAlt1] 2022-12-01 14:30:46 +01:00
12db23d076 Improve test performance (better waiting logic until http server is up) 2022-11-30 23:46:28 +01:00
fd182f0abb Fix timeout in ReadSchema/GetMeta etc method (fixes /health call taking 2 seconds) 2022-11-30 22:59:33 +01:00
7eab74e65c Tests[SendWithTimestamp, SendInvalidTimestamp] 2022-11-30 22:29:12 +01:00
e0ecd4d9ff Tests[SendInvalidPriority] 2022-11-30 21:51:48 +01:00
1ca09c16d3 Tests[SendWithPriority] 2022-11-30 21:39:14 +01:00
a7df476e79 Tests[SendIdempotent] 2022-11-30 21:17:29 +01:00
4e5eac6178 Tests[SendLongContent, LongContent, LongTitle] 2022-11-30 20:59:01 +01:00
91a6808ad2 Tests[SendWithSendername] 2022-11-30 20:47:43 +01:00
11a6517156 Tests[SendContentMessage] 2022-11-30 20:39:04 +01:00
7aa7eb234d Tests[SendSimpleMessageQuery, SendSimpleMessageForm, SendSimpleMessageFormAndQuery, SendSimpleMessageJSONAndQuery] 2022-11-30 20:23:31 +01:00
62d7df9710 Tests[TestSendSimpleMessageJSON] 2022-11-30 17:58:04 +01:00
0ff1188c3d Added swagger themes 2022-11-30 16:46:55 +01:00
b6e8d037a0 Add json tags to query structs (otherwise swag does not get the correct names) 2022-11-30 16:46:14 +01:00
7a11b2c76f Tests[UpdateUsername, RecreateKeys, DeleteUser] 2022-11-30 13:57:55 +01:00
7f56dbdbfa Tests[GetClient, CreateClient, DeleteClient, ReuseFCM] 2022-11-30 12:40:03 +01:00
df4eb15df8 Tests[CreateUser] 2022-11-30 10:35:05 +01:00
ac9ae06cc8 Save SenderName || SenderIP per message 2022-11-29 11:07:15 +01:00
464cf3ec7e Better error message on missing envs 2022-11-26 17:03:26 +01:00
bf0ce5c963 dark-mode 2022-11-26 16:30:30 +01:00
3a0c65a849 Added google androidpublisher/v3 api to verify google purchase tokens 2022-11-25 22:42:21 +01:00
6d80638cf8 CreateUser test 2022-11-24 12:53:27 +01:00
37e09d6532 cleanup swagger 2022-11-23 22:12:47 +01:00
8ea3fdcfef tests (boilerplate) 2022-11-23 20:21:49 +01:00
1bc847cdc9 tags/grouping for API 2022-11-23 19:32:23 +01:00
03c35d6446 update HTML with new methods 2023-06-18 04:07:13 +02:00
d5aea1a828 README 2022-11-21 18:46:55 +01:00
f17ddb4ace switch to debian base-image (no more static linking) 2022-11-20 22:18:48 +01:00
0cc6e27267 Use ID types 2022-11-20 22:18:24 +01:00
ca58aa782d Routes to refresh user and channel keys 2022-11-20 21:35:08 +01:00
e8671e8650 Selector param for ListChannels() 2022-11-20 21:15:06 +01:00
d46601be5c CreateMessage() 2022-11-20 20:34:18 +01:00
d30e2cefc0 firebase via REST (less dependencies) 2023-06-18 04:06:52 +02:00
08a93551e7 DeliveryRetryJob 2022-11-20 15:40:22 +01:00
c2899fd727 swagger doku for compat methods 2022-11-20 13:18:09 +01:00
5ec66e1777 cleanup 2022-11-20 12:59:43 +01:00
516809cd02 Dockerfile, CONF_NS and fix sqlite3 under alpine 2022-11-20 03:41:38 +01:00
0d3526221d replace PHP in html with js & bugfixes 2022-11-20 03:18:23 +01:00
728b12107f compat methods 2022-11-20 01:28:32 +01:00
b56c021356 ListChannelMessages() 2022-11-20 00:30:30 +01:00
80f3b982d2 ListMessages() 2022-11-20 00:21:59 +01:00
0d641b727f CreateSubscription(), UpdateSubscription(), GetMessage(), DeleteMessage() 2022-11-19 23:16:54 +01:00
8278c059ad fix context in methods.go 2022-11-19 17:15:46 +01:00
7af0ff5413 TODO's 2022-11-19 17:09:23 +01:00
5c2877bdb8 ListChannels(), GetChannel(), ListUserSubscriptions(), ListChannelSubscriptions(), GetSubscription(), CancelSubscription() 2022-11-19 17:07:30 +01:00
85bfe79115 SendMessage() 2022-11-19 16:29:14 +01:00
fb37f94c0a firebase implementation 2022-11-19 15:11:36 +01:00
e53f40866e DeleteClient() 2022-11-19 12:59:25 +01:00
650ba20e5d AddClient() 2022-11-19 12:56:44 +01:00
6e01c41c22 GetClient() 2022-11-19 12:50:41 +01:00
f555f0f1cf ListClients() 2022-11-19 12:47:23 +01:00
35ef2175bc UpdateUser() works 2022-11-18 23:33:07 +01:00
55f53deadf GetUser() works 2022-11-18 23:12:37 +01:00
5991631bfa POST:/users works 2022-11-18 21:25:40 +01:00
34a27d9ca4 schema 3.0 2022-11-17 21:26:52 +01:00
1671490485 implement a bit of the register.php call (and the DB schema) 2022-11-13 22:31:28 +01:00
0e58a5c5f0 added template for new golang backend 2022-11-13 19:25:44 +01:00
bfb-vserver-wwwdata
bd11d7973c server BF 2022-10-17 21:35:06 +02:00
f3b5b09ed0 A few code fixes 2020-11-04 10:08:06 +01:00
ce641bf7d2 Update dependencies 2020-11-03 14:41:20 +01:00
f1c7314dca better message for ERR::CONTENT_TOO_LONG 2020-08-03 13:25:49 +02:00
019408dc6d use LONGTEXT for content column 2020-04-21 10:45:03 +02:00
jenkins
cdba3540c2 [Jenkins] Increment version 2020-03-05 15:29:40 +00:00
885d997ff3 fix exceptions in register.php 2020-03-05 16:26:58 +01:00
5118ab3cbf Fix notifications not click-able 2020-03-05 16:20:25 +01:00
jenkins
2812377f5c [Jenkins] Increment version 2020-01-05 22:19:08 +00:00
93b40a9c7f skip version code 20 2020-01-05 23:18:20 +01:00
b95ddcc811 backup/export und import 2020-01-05 22:59:57 +01:00
90ba3c1134 fix api urls 2020-01-05 22:33:58 +01:00
05174958b2 Added php example 2019-06-30 21:46:28 +02:00
jenkins
24be9b2013 [Jenkins] Increment version 2018-12-14 22:09:01 +01:00
29ce4b727c More fixes for paid mode 2018-12-14 22:07:43 +01:00
jenkins
f178019ffe [Jenkins] Increment version 2018-12-14 19:32:22 +01:00
0a1b948042 Stupid bug -.- 2018-12-14 19:23:36 +01:00
jenkins
74cbfb235e [Jenkins] Increment version 2018-12-14 18:38:12 +01:00
63c4104a89 Add recieved messages to ComLog 2018-12-14 18:35:51 +01:00
2597287b1e Fixed wrong pro-mode reset 2018-12-14 18:30:03 +01:00
jenkins
316156f0f0 [Jenkins] Increment version 2018-12-11 13:55:38 +01:00
5286e869cc config for preview-line-count 2018-12-11 13:53:47 +01:00
d6dcf28d89 Share+Delete Button 2018-12-11 13:22:39 +01:00
1d983b9ac0 Collapse message on click-again 2018-12-11 12:31:44 +01:00
e525221010 Show Querylog via hidden shit 2018-12-11 12:25:10 +01:00
4cde4703f2 fixed exception in requery.php 2018-12-11 10:40:30 +01:00
4a07e58c16 fix expand + requery 2018-11-25 21:03:05 +01:00
b12356575a Description text correction 2018-11-25 18:11:42 +01:00
77f571de7d Send timestamp with request 2018-11-25 18:02:25 +01:00
jenkins
f5eef7563b [Jenkins] Increment version 2018-11-23 19:40:31 +01:00
741c09b1e4 fixed TabLayout animation 2018-11-23 19:35:22 +01:00
0c0d7d181f enable light in notification channel (android-oreo) + shorter vibrate 2018-11-23 18:48:46 +01:00
9304da9422 fix NPE in SettingsFrame 2018-11-23 18:42:10 +01:00
jenkins
d7afdd00f2 [Jenkins] Increment version 2018-11-19 18:43:36 +01:00
b780ccea1c wrong notification channel name 2018-11-19 18:41:56 +01:00
jenkins
5d8e871871 [Jenkins] Increment version 2018-11-19 12:31:22 +01:00
4655d688c9 README 2018-11-18 17:19:15 +01:00
8263c0ad95 phone image 2018-11-18 17:12:44 +01:00
a701afd09b OWA tracking 2018-11-18 03:34:13 +01:00
jenkins
1e02d8c01f [Jenkins] Increment version 2018-11-18 03:24:46 +01:00
e4651375aa fix qr url 2018-11-18 03:23:22 +01:00
jenkins
afce4c6391 [Jenkins] Increment version 2018-11-18 00:15:21 +01:00
e95d0cb010 added disable warning 2018-11-18 00:13:36 +01:00
f717355519 use "messages" everywhere instead of "notifications" 2018-11-18 00:11:30 +01:00
3368e514ca red quota icon if OOQ 2018-11-18 00:02:00 +01:00
9dca27177f fixed js logic for errors 2018-11-17 23:59:57 +01:00
c63274f7a9 show subtext on android-o 2018-11-17 23:48:39 +01:00
6c2d2d2345 fixed requery only getting first message 2018-11-17 22:51:02 +01:00
a6fbaa192f config example 2018-11-17 19:28:44 +01:00
jenkins
a01f156535 [Jenkins] Increment version 2018-11-17 19:00:13 +01:00
3b021c09dc notification icons 2018-11-17 18:58:18 +01:00
287176c881 screenshots 2018-11-17 18:21:53 +01:00
84eeb5a002 red high-prio icons 2018-11-17 18:21:25 +01:00
b892532023 send big content over FCM 2018-11-17 17:59:43 +01:00
jenkins
56cdc52bb4 [Jenkins] Increment version 2018-11-17 17:19:59 +01:00
9ef6b1dd91 swipe-to-delete setting 2018-11-17 17:18:15 +01:00
6f7585323b fixed notification being overwritten 2018-11-17 17:06:34 +01:00
92135e64a3 css 2018-11-17 17:00:14 +01:00
28efeea30b message delete problems 2018-11-17 16:45:42 +01:00
a8a074907b fixed update when in message-list 2018-11-17 16:02:31 +01:00
6c29ec9820 max-size = 2048 chars (FCM restrictions) 2018-11-17 15:44:44 +01:00
jenkins
8ec23144ca [Jenkins] Increment version 2018-11-17 14:28:04 +01:00
277 changed files with 46210 additions and 480 deletions

22
README.md Normal file
View File

@@ -0,0 +1,22 @@
SimpleCloudNotifier [![Get it in Google Play](data/README/badge_google.png)](https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier)
===================
> SimpleCloudNotifier is an app to display messages that you can send to your phone with simple POST requests.
>
> After you start the app it generates a UserID and a UserSecret.
> Now you can send your message to https://simplecloudnotifier.blackforestbytes.com/send.php and a notification will be pushed to your phone.
> (see https://simplecloudnotifier.blackforestbytes.com/ for an example with curl)
>
>
> Use it to
> - send yourself automated messages from cron jobs
> - notify yourself when long-running scripts finish
> - send server error messages directly to your phone
> - integrate with other online services
>
> The possibilities are endless*
>
> \* Disclaimer: Developer does not actually guarantee endless possibilities
![](store/screenshot_1.png) ![](store/screenshot_2.png) ![](store/screenshot_3.png)

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WizardSettings">
<option name="children">
<map>
<entry key="vectorWizard">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="vectorAssetStep">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="assetSourceType" value="FILE" />
<entry key="outputName" value="ic_garbage" />
<entry key="sourceFile" value="C:\Users\Mike\Downloads\garbage.svg" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@@ -1,29 +1,113 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
<codeStyleSettings language="XML">
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

40
android/.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://dl.bintray.com/gericop/maven" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://maven.google.com" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

View File

@@ -1,7 +1,7 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
compileSdkVersion 30
def versionPropsFile = file('version.properties')
def vNumber
@@ -16,7 +16,7 @@ android {
defaultConfig {
applicationId "com.blackforestbytes.simplecloudnotifier"
minSdkVersion 21
targetSdkVersion 28
targetSdkVersion 30
versionCode vNumber
versionName vName
}
@@ -35,76 +35,82 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.firebase:firebase-core:16.0.4'
implementation 'com.google.firebase:firebase-messaging:17.3.4'
implementation 'com.google.android.gms:play-services-ads:17.1.0'
implementation 'com.android.billingclient:billing:1.2'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.firebase:firebase-core:18.0.0'
implementation 'com.google.firebase:firebase-messaging:21.0.0'
implementation 'com.google.android.gms:play-services-ads:19.5.0'
implementation 'com.android.billingclient:billing:3.0.1'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.github.kenglxn.QRGen:android:2.5.0'
implementation "com.github.DeweyReed:UltimateMusicPicker:2.0.0"
implementation 'com.github.duanhong169:colorpicker:1.1.5'
implementation 'net.danlew:android.joda:2.10.7.1'
}
apply plugin: 'com.google.gms.google-services'
task updateVersion << {
def lastTag = ['git', 'describe', "--abbrev=0", "--tags"].execute().text.trim()
tasks.register("updateVersion") {
group = 'Custom'
def versionPropsFile = file('version.properties')
if (!versionPropsFile.canRead()) throw new FileNotFoundException("Could not read version.properties!")
Properties versionProps = new Properties()
new FileInputStream(versionPropsFile).withCloseable { fis -> versionProps.load(fis) }
doLast {
def lastTag = ['git', 'describe', "--abbrev=0", "--tags"].execute().text.trim()
def matcher = lastTag =~ /^v([0-9]+)\.([0-9]+)\.([0-9]+)$/
def versionPropsFile = file('version.properties')
if (!versionPropsFile.canRead()) throw new FileNotFoundException("Could not read version.properties!")
Properties versionProps = new Properties()
new FileInputStream(versionPropsFile).withCloseable { fis -> versionProps.load(fis) }
if (!matcher.matches()) throw new Exception("Last Tag ('" + lastTag + "') has invalid format :(")
def matcher = lastTag =~ /^v([0-9]+)\.([0-9]+)\.([0-9]+)$/
def vName = (matcher[0][1] as Integer) + "." + (matcher[0][2] as Integer) + "." + (matcher[0][3] as Integer)
def vCode = versionProps['VERSION_CODE'] as Integer
if (!matcher.matches()) throw new Exception("Last Tag ('" + lastTag + "') has invalid format :(")
if (new File(".do_publish_beta_release").exists()) new File(".do_publish_beta_release").delete()
if (new File(".do_publish_prod_release").exists()) new File(".do_publish_prod_release").delete()
def vName = (matcher[0][1] as Integer) + "." + (matcher[0][2] as Integer) + "." + (matcher[0][3] as Integer)
def vCode = versionProps['VERSION_CODE'] as Integer
if (vName == versionProps['VERSION_NAME'].toString()) {
println "This version was already built - skip deployment"
} else if (vName.endsWith(".0")) {
println ""
println "====================================================================="
println "====================================================================="
println "(!) This is a new PRODUCTION release - create deployment trigger file"
println "====================================================================="
println "====================================================================="
println ""
if (new File(".do_publish_beta_release").exists()) new File(".do_publish_beta_release").delete()
if (new File(".do_publish_prod_release").exists()) new File(".do_publish_prod_release").delete()
vCode++
new File(".do_publish_prod_release").createNewFile()
if (vName == versionProps['VERSION_NAME'].toString()) {
println "This version was already built - skip deployment"
} else if (vName.endsWith(".0")) {
println ""
println "====================================================================="
println "====================================================================="
println "(!) This is a new PRODUCTION release - create deployment trigger file"
println "====================================================================="
println "====================================================================="
println ""
versionProps['VERSION_NAME'] = vName.toString()
versionProps['VERSION_CODE'] = vCode.toString()
vCode++
new File(".do_publish_prod_release").createNewFile()
versionPropsFile.newWriter().withCloseable { w -> versionProps.store(w, null) }
} else {
println ""
println "==============================================================="
println "(!) This is a new beta release - create deployment trigger file"
println "==============================================================="
println ""
versionProps['VERSION_NAME'] = vName.toString()
versionProps['VERSION_CODE'] = vCode.toString()
vCode++
new File(".do_publish_beta_release").createNewFile()
versionPropsFile.newWriter().withCloseable { w -> versionProps.store(w, null) }
} else {
println ""
println "==============================================================="
println "(!) This is a new beta release - create deployment trigger file"
println "==============================================================="
println ""
versionProps['VERSION_NAME'] = vName.toString()
versionProps['VERSION_CODE'] = vCode.toString()
vCode++
new File(".do_publish_beta_release").createNewFile()
versionPropsFile.newWriter().withCloseable { w -> versionProps.store(w, null) }
versionProps['VERSION_NAME'] = vName.toString()
versionProps['VERSION_CODE'] = vCode.toString()
versionPropsFile.newWriter().withCloseable { w -> versionProps.store(w, null) }
}
}
}

View File

@@ -1,43 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.blackforestbytes.simplecloudnotifier">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.android.vending.BILLING" />
<application
android:name=".SCNApp"
android:allowBackup="false"
android:name="SCNApp"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".view.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/icon" />
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorAccent" />
<meta-data android:name="com.google.android.gms.ads.AD_MANAGER_APP" android:value="true"/>
<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-3320562328966175~7579972005"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.blackforestbytes.simplecloudnotifier.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
</provider>
<service android:name=".service.FBMService" android:exported="false">
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/icon" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/colorAccent" />
<meta-data
android:name="com.google.android.gms.ads.AD_MANAGER_APP"
android:value="true" />
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3320562328966175~7579972005" />
<service
android:name=".service.FBMService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver android:name=".service.BroadcastReceiverService" android:exported="false" />
<receiver
android:name=".service.BroadcastReceiverService"
android:exported="false" />
<activity
android:name=".view.debug.QueryLogActivity"
android:label="@string/title_activity_query_log"
android:theme="@style/AppTheme" />
<activity android:name=".view.debug.SingleQueryLogActivity" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import android.widget.Toast;
import com.android.billingclient.api.BillingClient;
import com.blackforestbytes.simplecloudnotifier.model.QueryLog;
import com.blackforestbytes.simplecloudnotifier.view.AccountFragment;
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
import com.blackforestbytes.simplecloudnotifier.view.TabAdapter;
@@ -99,14 +100,5 @@ public class SCNApp extends Application implements LifecycleObserver
}
}
/*
==TODO==
[ ] - test notification channels
[ ] - startup time
[ ] - periodically get non-ack (option - even when not in-app)
[ ] - publish (+ HN post ?)
*/
//TODO: Config for collapsed line count
//TODO: Sometimes ads but promode

View File

@@ -24,6 +24,11 @@ public final class CollectionHelper
return output;
}
public static <T> void sort_inplace(List<T> input, Comparator<T> comparator)
{
Collections.sort(input, comparator);
}
public static <T, U extends Comparable<U>> List<T> sort(List<T> input, Func1to1<T, U> mapper)
{
return sort(input, mapper, 1);

View File

@@ -0,0 +1,19 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public class Tuple5<T1, T2, T3, T4, T5>
{
public final T1 Item1;
public final T2 Item2;
public final T3 Item3;
public final T4 Item4;
public final T5 Item5;
public Tuple5(T1 i1, T2 i2, T3 i3, T4 i4, T5 i5)
{
Item1 = i1;
Item2 = i2;
Item3 = i3;
Item4 = i4;
Item5 = i5;
}
}

View File

@@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func5to0<TInput1, TInput2, TInput3, TInput4, TInput5> {
void invoke(TInput1 value1, TInput2 value2, TInput3 value3, TInput4 value4, TInput5 value5);
}

View File

@@ -9,6 +9,8 @@ import java.util.TimeZone;
public class CMessage
{
public boolean IsExpandedInAdapter = false;
public final long SCN_ID;
public final long Timestamp;
public final String Title;

View File

@@ -3,17 +3,21 @@ package com.blackforestbytes.simplecloudnotifier.model;
import android.content.Context;
import android.content.SharedPreferences;
import com.blackforestbytes.simplecloudnotifier.lib.collections.CollectionHelper;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.view.MessageAdapter;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class CMessageList
{
private final Object msg_lock = new Object();
public ArrayList<CMessage> Messages;
public Set<String> AllAcks;
@@ -32,23 +36,31 @@ public class CMessageList
private CMessageList()
{
Messages = new ArrayList<>();
AllAcks = new HashSet<>();
reloadPrefs();
}
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
int count = sharedPref.getInt("message_count", 0);
for (int i=0; i < count; i++)
public void reloadPrefs()
{
synchronized (msg_lock)
{
long time = sharedPref.getLong("message["+i+"].timestamp", 0);
String title = sharedPref.getString("message["+i+"].title", "");
String content = sharedPref.getString("message["+i+"].content", "");
PriorityEnum prio = PriorityEnum.parseAPI(sharedPref.getInt("message["+i+"].priority", 1));
long scnid = sharedPref.getLong("message["+i+"].scnid", 0);
Messages = new ArrayList<>();
AllAcks = new HashSet<>();
Messages.add(new CMessage(scnid, time, title, content, prio));
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
int count = sharedPref.getInt("message_count", 0);
for (int i=0; i < count; i++)
{
long time = sharedPref.getLong("message["+i+"].timestamp", 0);
String title = sharedPref.getString("message["+i+"].title", "");
String content = sharedPref.getString("message["+i+"].content", "");
PriorityEnum prio = PriorityEnum.parseAPI(sharedPref.getInt("message["+i+"].priority", 1));
long scnid = sharedPref.getLong("message["+i+"].scnid", 0);
Messages.add(new CMessage(scnid, time, title, content, prio));
}
AllAcks = sharedPref.getStringSet("acks", new HashSet<>());
}
AllAcks = sharedPref.getStringSet("acks", new HashSet<>());
}
public CMessage add(final long scnid, final long time, final String title, final String content, final PriorityEnum pe)
@@ -60,29 +72,44 @@ public class CMessageList
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
int count = sharedPref.getInt("message_count", 0);
SharedPreferences.Editor e = sharedPref.edit();
synchronized (msg_lock)
{
Messages.add(msg);
AllAcks.add(Long.toHexString(msg.SCN_ID));
Messages.add(msg);
AllAcks.add(Long.toHexString(msg.SCN_ID));
while (Messages.size()>SCNSettings.inst().LocalCacheSize) Messages.remove(0);
}
while (Messages.size()>SCNSettings.inst().LocalCacheSize) Messages.remove(0);
if (Messages.size()>1 && Messages.get(Messages.size()-2).Timestamp < msg.Timestamp)
{
// quick save
e.putInt( "message_count", count+1);
e.putLong( "message["+count+"].timestamp", time);
e.putString("message["+count+"].title", title);
e.putString("message["+count+"].content", content);
e.putInt( "message["+count+"].priority", pe.ID);
e.putLong( "message["+count+"].scnid", scnid);
SharedPreferences.Editor e = sharedPref.edit();
e.putStringSet("acks", AllAcks);
e.putInt( "message_count", count+1);
e.putLong( "message["+count+"].timestamp", time);
e.putString("message["+count+"].title", title);
e.putString("message["+count+"].content", content);
e.putInt( "message["+count+"].priority", pe.ID);
e.putLong( "message["+count+"].scnid", scnid);
e.putStringSet("acks", AllAcks);
e.apply();
}
else
{
// full save
fullSave(); // does sort in here
}
e.apply();
for (WeakReference<MessageAdapter> ref : _listener)
{
MessageAdapter a = ref.get();
if (a == null) continue;
a.customNotifyItemInserted(count);
a.customNotifyDataSetChanged();
a.scrollToTop();
}
CleanUpListener();
@@ -90,9 +117,12 @@ public class CMessageList
if (!run)
{
Messages.add(new CMessage(scnid, time, title, content, pe));
AllAcks.add(Long.toHexString(msg.SCN_ID));
fullSave();
synchronized (msg_lock)
{
Messages.add(new CMessage(scnid, time, title, content, pe));
AllAcks.add(Long.toHexString(msg.SCN_ID));
}
fullSave(); // does sort in here
}
return msg;
@@ -114,31 +144,39 @@ public class CMessageList
public void fullSave()
{
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
SharedPreferences.Editor e = sharedPref.edit();
e.clear();
e.putInt("message_count", Messages.size());
for (int i = 0; i < Messages.size(); i++)
synchronized (msg_lock)
{
e.putLong( "message["+i+"].timestamp", Messages.get(i).Timestamp);
e.putString("message["+i+"].title", Messages.get(i).Title);
e.putString("message["+i+"].content", Messages.get(i).Content);
e.putInt( "message["+i+"].priority", Messages.get(i).Priority.ID);
e.putLong( "message["+i+"].scnid", Messages.get(i).SCN_ID);
CollectionHelper.sort_inplace(Messages, (a,b) -> Long.compare(a.Timestamp, b.Timestamp));
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
SharedPreferences.Editor e = sharedPref.edit();
e.clear();
e.putInt("message_count", Messages.size());
for (int i = 0; i < Messages.size(); i++)
{
e.putLong( "message["+i+"].timestamp", Messages.get(i).Timestamp);
e.putString("message["+i+"].title", Messages.get(i).Title);
e.putString("message["+i+"].content", Messages.get(i).Content);
e.putInt( "message["+i+"].priority", Messages.get(i).Priority.ID);
e.putLong( "message["+i+"].scnid", Messages.get(i).SCN_ID);
}
e.putStringSet("acks", AllAcks);
e.apply();
}
e.putStringSet("acks", AllAcks);
e.apply();
}
public CMessage tryGet(int pos)
{
if (pos < 0 || pos >= Messages.size()) return null;
return Messages.get(pos);
synchronized (msg_lock)
{
if (pos < 0 || pos >= Messages.size()) return null;
return Messages.get(pos);
}
}
public CMessage tryGetFromBack(int pos)
@@ -148,7 +186,10 @@ public class CMessageList
public int size()
{
return Messages.size();
synchronized (msg_lock)
{
return Messages.size();
}
}
public void register(MessageAdapter adp)
@@ -167,18 +208,30 @@ public class CMessageList
public boolean isAck(long id)
{
return AllAcks.contains(Long.toHexString(id));
synchronized (msg_lock)
{
return AllAcks.contains(Long.toHexString(id));
}
}
public void remove(int index)
public CMessage removeFromBack(int pos)
{
Messages.remove(index);
fullSave();
CMessage r;
synchronized (msg_lock)
{
int index = Messages.size() - pos - 1;
r = Messages.remove(index);
}
fullSave(); // does sort in here
return r;
}
public void insert(int index, CMessage item)
{
Messages.add(index, item);
fullSave();
synchronized (msg_lock)
{
Messages.add(index, item);
}
fullSave(); // does sort in here
}
}

View File

@@ -0,0 +1,58 @@
package com.blackforestbytes.simplecloudnotifier.model;
import android.graphics.Color;
public enum LogLevel
{
DEBUG,
INFO,
WARN,
ERROR;
public String toUIString()
{
switch (this)
{
case DEBUG: return "Debug";
case INFO: return "Info";
case WARN: return "Warning";
case ERROR: return "Error";
default: return "???";
}
}
public int getColor()
{
switch (this)
{
case DEBUG: return Color.GRAY;
case WARN: return Color.rgb(171, 145, 68);
case INFO: return Color.BLACK;
case ERROR: return Color.RED;
default: return Color.MAGENTA;
}
}
public int asInt()
{
switch (this)
{
case DEBUG: return 0;
case WARN: return 1;
case INFO: return 2;
case ERROR: return 3;
default: return 999;
}
}
public static LogLevel fromInt(int i)
{
if (i == 0) return LogLevel.DEBUG;
if (i == 1) return LogLevel.WARN;
if (i == 2) return LogLevel.INFO;
if (i == 3) return LogLevel.ERROR;
return LogLevel.ERROR; // ????
}
}

View File

@@ -0,0 +1,69 @@
package com.blackforestbytes.simplecloudnotifier.model;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.collections.CollectionHelper;
import java.util.ArrayList;
import java.util.List;
public class QueryLog
{
private final static int MAX_HISTORY_SIZE = 192;
private static QueryLog _instance;
public static QueryLog inst() { if (_instance == null) synchronized (QueryLog.class) { if (_instance == null) _instance = new QueryLog(); } return _instance; }
private QueryLog(){ reloadPrefs(); }
private final List<SingleQuery> history = new ArrayList<>();
public synchronized void add(SingleQuery r)
{
history.add(r);
while (history.size() > MAX_HISTORY_SIZE) history.remove(0);
save();
}
public synchronized List<SingleQuery> get()
{
List<SingleQuery> r = new ArrayList<>(history);
CollectionHelper.sort_inplace(r, (o1, o2) -> (-1) * o1.Timestamp.compareTo(o2.Timestamp));
return r;
}
public synchronized void save()
{
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("QueryLog", Context.MODE_PRIVATE);
SharedPreferences.Editor e = sharedPref.edit();
e.clear();
e.putInt("history_count", history.size());
for (int i = 0; i < history.size(); i++) history.get(i).save(e, "message["+(i+1000)+"]");
e.apply();
}
public synchronized void reloadPrefs()
{
try
{
Context c = SCNApp.getContext();
SharedPreferences sharedPref = c.getSharedPreferences("QueryLog", Context.MODE_PRIVATE);
int count = sharedPref.getInt("history_count", 0);
for (int i=0; i < count; i++) history.add(SingleQuery.load(sharedPref, "message["+(i+1000)+"]"));
CollectionHelper.sort_inplace(history, (o1, o2) -> (-1) * o1.Timestamp.compareTo(o2.Timestamp));
}
catch (Exception e)
{
Log.e("SC:QL:Load", e.toString());
}
}
}

View File

@@ -6,23 +6,28 @@ import android.content.SharedPreferences;
import android.util.Log;
import android.view.View;
import com.android.billingclient.api.Purchase;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.datatypes.Tuple3;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.installations.FirebaseInstallations;
public class SCNSettings
{
private final static Object _lock = new Object();
private static SCNSettings _inst = null;
private static volatile SCNSettings _inst = null;
public static SCNSettings inst()
{
synchronized (_lock)
SCNSettings local = _inst;
if (local == null)
{
if (_inst != null) return _inst;
return _inst = new SCNSettings();
synchronized (_lock)
{
local = _inst;
if (local == null) _inst = local = new SCNSettings();
}
}
return local;
}
// ------------------------------------------------------------
@@ -47,6 +52,8 @@ public class SCNSettings
public boolean Enabled = true;
public int LocalCacheSize = 500;
public boolean EnableDeleteSwipe = false;
public int PreviewLineCount = 6;
public final NotificationSettings PriorityLow = new NotificationSettings(PriorityEnum.LOW);
public final NotificationSettings PriorityNorm = new NotificationSettings(PriorityEnum.NORMAL);
@@ -55,6 +62,11 @@ public class SCNSettings
// ------------------------------------------------------------
public SCNSettings()
{
reloadPrefs();
}
public void reloadPrefs()
{
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("Config", Context.MODE_PRIVATE);
@@ -70,6 +82,8 @@ public class SCNSettings
Enabled = sharedPref.getBoolean("app_enabled", Enabled);
LocalCacheSize = sharedPref.getInt("local_cache_size", LocalCacheSize);
EnableDeleteSwipe = sharedPref.getBoolean("do_del_swipe", EnableDeleteSwipe);
PreviewLineCount = sharedPref.getInt("preview_line_count", PreviewLineCount);
PriorityLow.EnableLED = sharedPref.getBoolean("priority_low:enabled_led", PriorityLow.EnableLED);
PriorityLow.EnableSound = sharedPref.getBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
@@ -113,9 +127,14 @@ public class SCNSettings
e.putString( "user_key", user_key);
e.putString( "fcm_token_local", fcm_token_local);
e.putString( "fcm_token_server", fcm_token_server);
e.putBoolean("promode_local", promode_local);
e.putBoolean("promode_server", promode_server);
e.putString( "promode_token", promode_token);
e.putBoolean("app_enabled", Enabled);
e.putInt( "local_cache_size", LocalCacheSize);
e.putBoolean("do_del_swipe", EnableDeleteSwipe);
e.putInt( "preview_line_count", PreviewLineCount);
e.putBoolean("priority_low:enabled_led", PriorityLow.EnableLED);
e.putBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
@@ -155,19 +174,21 @@ public class SCNSettings
return user_id>=0 && user_key != null && !user_key.isEmpty();
}
public String createOnlineURL()
public String createOnlineURL(boolean longurl)
{
if (!isConnected()) return ServerCommunication.BASE_URL + "index.php";
return ServerCommunication.BASE_URL + "index.php?preset_user_id="+user_id+"&preset_user_key="+user_key;
String base = longurl ? ServerCommunication.PAGE_URL_LONG : ServerCommunication.PAGE_URL_SHORT;
if (!isConnected()) return base;
return base + "index.php?preset_user_id="+user_id+"&preset_user_key="+user_key;
}
public void setServerToken(String token, View loader)
public void setServerToken(String token, View loader, boolean force)
{
if (isConnected())
{
fcm_token_local = token;
save();
if (!fcm_token_local.equals(fcm_token_server)) ServerCommunication.updateFCMToken(user_id, user_key, fcm_token_local, loader);
if (!fcm_token_local.equals(fcm_token_server) || force) ServerCommunication.updateFCMToken(user_id, user_key, fcm_token_local, loader);
}
else
{
@@ -179,13 +200,12 @@ public class SCNSettings
}
// called at app start
public void work(Activity a)
public void work(Activity a, boolean force)
{
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult ->
FirebaseInstallations.getInstance().getId().addOnSuccessListener(a, newToken ->
{
String newToken = instanceIdResult.getToken();
Log.e("FB::GetInstanceId", newToken);
SCNSettings.inst().setServerToken(newToken, null);
Log.d("FB::GetInstanceId", newToken);
SCNSettings.inst().setServerToken(newToken, null, force);
}).addOnCompleteListener(r ->
{
if (isConnected()) ServerCommunication.info(user_id, user_key, null);
@@ -211,16 +231,15 @@ public class SCNSettings
if (promode_server != promode_local) updateProState(loader);
if (!Str.equals(fcm_token_local, fcm_token_server)) work(a);
if (!Str.equals(fcm_token_local, fcm_token_server)) work(a, false);
}
else
{
// get token then register
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult ->
FirebaseInstallations.getInstance().getId().addOnSuccessListener(a, newToken ->
{
String newToken = instanceIdResult.getToken();
Log.e("FB::GetInstanceId", newToken);
SCNSettings.inst().setServerToken(newToken, loader); // does register in here
Log.d("FB::GetInstanceId", newToken);
SCNSettings.inst().setServerToken(newToken, loader, false); // does register in here
}).addOnCompleteListener(r ->
{
if (isConnected()) ServerCommunication.info(user_id, user_key, null); // info again for safety
@@ -230,14 +249,17 @@ public class SCNSettings
public void updateProState(View loader)
{
Purchase purch = IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE);
boolean promode_real = (purch != null);
Tuple3<Boolean, Boolean, String> state = IABService.inst().getPurchaseCachedExtended(IABService.IAB_PRO_MODE);
if (!state.Item2) return; // not initialized
boolean promode_real = state.Item1;
if (promode_real != promode_local || promode_real != promode_server)
{
promode_local = promode_real;
promode_token = promode_real ? state.Item3 : "";
save();
promode_token = promode_real ? purch.getPurchaseToken() : "";
updateProStateOnServer(loader);
}
}

View File

@@ -4,9 +4,11 @@ import android.util.Log;
import android.view.View;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func5to0;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.service.FBMService;
import org.joda.time.Instant;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -24,7 +26,9 @@ import okhttp3.ResponseBody;
public class ServerCommunication
{
public static final String BASE_URL = /*SCNApp.LOCAL_DEBUG ? "http://localhost:1010/" : */"https://scn.blackforestbytes.com/api/";
public static final String PAGE_URL_LONG = "https://simplecloudnotifier.blackforestbytes.com/";
public static final String PAGE_URL_SHORT = "https://scn.blackforestbytes.com/";
public static final String BASE_URL = "https://scn.blackforestbytes.com/api/";
private static final OkHttpClient client = new OkHttpClient();
@@ -43,27 +47,28 @@ public class ServerCommunication
@Override
public void onFailure(Call call, IOException e)
{
Log.e("SC:register", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("register", call, null, Str.Empty, true, e);
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
}
@Override
public void onResponse(Call call, Response response)
{
String r = Str.Empty;
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
r = responseBody.string();
Log.d("Server::Response", request.url().toString()+"\n"+r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success"))
{
SCNApp.showToast(json_str(json, "message"), 4000);
handleNonSuccess("register", call, response, r);
return;
}
@@ -76,11 +81,12 @@ public class ServerCommunication
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
handleSuccess("register", call, response, r);
}
catch (Exception e)
{
Log.e("SC:register", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("register", call, response, r, false, e);
}
finally
{
@@ -91,8 +97,7 @@ public class ServerCommunication
}
catch (Exception e)
{
Log.e("SC:register", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("register", null, null, Str.Empty, false, e);
}
}
@@ -101,7 +106,7 @@ public class ServerCommunication
try
{
Request request = new Request.Builder()
.url(BASE_URL + "updateFCMToken.php?user_id="+id+"&user_key="+key+"&fcm_token="+token)
.url(BASE_URL + "update.php?user_id="+id+"&user_key="+key+"&fcm_token="+token)
.build();
client.newCall(request).enqueue(new Callback()
@@ -109,27 +114,28 @@ public class ServerCommunication
@Override
public void onFailure(Call call, IOException e)
{
Log.e("SC:update_1", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("update<1>", call, null, Str.Empty, true, e);
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
}
@Override
public void onResponse(Call call, Response response)
{
String r = Str.Empty;
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
r = responseBody.string();
Log.d("Server::Response", request.url().toString()+"\n"+r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success"))
{
SCNApp.showToast(json_str(json, "message"), 4000);
handleNonSuccess("update<1>", call, response, r);
return;
}
@@ -142,10 +148,12 @@ public class ServerCommunication
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
handleSuccess("update<1>", call, response, r);
}
catch (Exception e)
{
Log.e("SC:update_1", e.toString());
handleError("update<1>", call, response, r, false, e);
SCNApp.showToast("Communication with server failed", 4000);
}
finally
@@ -157,8 +165,7 @@ public class ServerCommunication
}
catch (Exception e)
{
Log.e("SC:update_1", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("update<1>", null, null, Str.Empty, false, e);
}
}
@@ -167,30 +174,35 @@ public class ServerCommunication
try
{
Request request = new Request.Builder()
.url(BASE_URL + "updateFCMToken.php?user_id=" + id + "&user_key=" + key)
.url(BASE_URL + "update.php?user_id=" + id + "&user_key=" + key)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("SC:update_2", e.toString());
public void onFailure(Call call, IOException e)
{
handleError("update<1>", call, null, Str.Empty, true, e);
SCNApp.showToast("Communication with server failed", 4000);
}
@Override
public void onResponse(Call call, Response response) {
try (ResponseBody responseBody = response.body()) {
public void onResponse(Call call, Response response)
{
String r = Str.Empty;
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
r = responseBody.string();
Log.d("Server::Response", request.url().toString()+"\n"+r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success")) {
SCNApp.showToast(json_str(json, "message"), 4000);
handleNonSuccess("update<2>", call, response, r);
return;
}
@@ -202,10 +214,16 @@ public class ServerCommunication
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
} catch (Exception e) {
Log.e("SC:update_2", e.toString());
handleSuccess("update<2>", call, response, r);
}
catch (Exception e)
{
handleError("update<2>", call, response, r, false, e);
SCNApp.showToast("Communication with server failed", 4000);
} finally {
}
finally
{
SCNApp.runOnUiThread(() -> {
if (loader != null) loader.setVisibility(View.GONE);
});
@@ -215,8 +233,7 @@ public class ServerCommunication
}
catch (Exception e)
{
Log.e("SC:update_2", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("update<2>", null, null, Str.Empty, false, e);
}
}
@@ -231,28 +248,31 @@ public class ServerCommunication
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("SC:info", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("info", call, null, Str.Empty, true, e);
SCNApp.runOnUiThread(() -> {
if (loader != null) loader.setVisibility(View.GONE);
});
}
@Override
public void onResponse(Call call, Response response) {
try (ResponseBody responseBody = response.body()) {
public void onResponse(Call call, Response response)
{
String r = Str.Empty;
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
r = responseBody.string();
Log.d("Server::Response", request.url().toString()+"\n"+r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success"))
{
SCNApp.showToast(json_str(json, "message"), 4000);
handleNonSuccess("info", call, response, r);
int errid = json.optInt("errid", 0);
@@ -285,21 +305,23 @@ public class ServerCommunication
if (json_int(json, "unack_count")>0) ServerCommunication.requery(id, key, loader);
} catch (Exception e) {
Log.e("SC:info", e.toString());
handleSuccess("info", call, response, r);
}
catch (Exception e)
{
handleError("info", call, response, r, false, e);
SCNApp.showToast("Communication with server failed", 4000);
} finally {
SCNApp.runOnUiThread(() -> {
if (loader != null) loader.setVisibility(View.GONE);
});
}
finally
{
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
}
}
});
}
catch (Exception e)
{
Log.e("SC:info", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("info", null, null, Str.Empty, false, e);
}
}
@@ -314,28 +336,31 @@ public class ServerCommunication
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("SC:requery", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("requery", call, null, Str.Empty, true, e);
SCNApp.runOnUiThread(() -> {
if (loader != null) loader.setVisibility(View.GONE);
});
}
@Override
public void onResponse(Call call, Response response) {
try (ResponseBody responseBody = response.body()) {
public void onResponse(Call call, Response response)
{
String r = Str.Empty;
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
r = responseBody.string();
Log.d("Server::Response", request.url().toString()+"\n"+r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success"))
{
SCNApp.showToast(json_str(json, "message"), 4000);
handleNonSuccess("requery", call, response, r);
return;
}
@@ -343,7 +368,7 @@ public class ServerCommunication
JSONArray arr = json.getJSONArray("data");
for (int i = 0; i < count; i++)
{
JSONObject o = arr.getJSONObject(0);
JSONObject o = arr.getJSONObject(i);
long time = json_lng(o, "timestamp");
String title = json_str(o, "title");
@@ -354,10 +379,15 @@ public class ServerCommunication
FBMService.recieveData(time, title, content, prio, scn_id, true);
}
} catch (Exception e) {
Log.e("SC:info", e.toString());
handleSuccess("requery", call, response, r);
}
catch (Exception e)
{
handleError("requery", call, response, r, false, e);
SCNApp.showToast("Communication with server failed", 4000);
} finally {
}
finally
{
SCNApp.runOnUiThread(() -> {
if (loader != null) loader.setVisibility(View.GONE);
});
@@ -367,8 +397,7 @@ public class ServerCommunication
}
catch (Exception e)
{
Log.e("SC:requery", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleError("requery", null, null, Str.Empty, false, e);
}
}
@@ -382,27 +411,31 @@ public class ServerCommunication
.url(BASE_URL + "upgrade.php?user_id=" + id + "&user_key=" + key + "&pro=" + pro + "&pro_token=" + URLEncoder.encode(pro_token, "utf-8"))
.build();
client.newCall(request).enqueue(new Callback() {
client.newCall(request).enqueue(new Callback()
{
@Override
public void onFailure(Call call, IOException e) {
Log.e("SC:upgrade", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
public void onFailure(Call call, IOException e)
{
handleError("upgrade", call, null, Str.Empty, true, e);
}
@Override
public void onResponse(Call call, Response response) {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
public void onResponse(Call call, Response response)
{
String r = Str.Empty;
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
r = responseBody.string();
Log.d("Server::Response", request.url().toString()+"\n"+r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success")) {
SCNApp.showToast(json_str(json, "message"), 4000);
handleNonSuccess("upgrade", call, response, r);
return;
}
@@ -413,10 +446,15 @@ public class ServerCommunication
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
} catch (Exception e) {
Log.e("SC:upgrade", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
} finally {
handleSuccess("upgrade", call, response, r);
}
catch (Exception e)
{
handleError("upgrade", call, response, r, false, e);
}
finally
{
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
}
}
@@ -424,8 +462,7 @@ public class ServerCommunication
}
catch (Exception e)
{
e.printStackTrace();
SCNApp.showToast("Communication with server failed", 4000);
handleError("upgrade", null, null, Str.Empty, false, e);
}
}
@@ -437,37 +474,113 @@ public class ServerCommunication
.url(BASE_URL + "ack.php?user_id=" + id + "&user_key=" + key + "&scn_msg_id=" + msg_scn_id)
.build();
client.newCall(request).enqueue(new Callback() {
client.newCall(request).enqueue(new Callback()
{
@Override
public void onFailure(Call call, IOException e) {
Log.e("SC:ack", e.toString());
public void onFailure(Call call, IOException e)
{
handleError("ack", call, null, Str.Empty, true, e);
}
@Override
public void onResponse(Call call, Response response)
{
try (ResponseBody responseBody = response.body()) {
String r = Str.Empty;
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
r = responseBody.string();
Log.d("Server::Response", request.url().toString()+"\n"+r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success")) SCNApp.showToast(json_str(json, "message"), 4000);
if (!json_bool(json, "success"))
{
SCNApp.showToast(json_str(json, "message"), 4000);
handleNonSuccess("ack", call, response, r);
}
} catch (Exception e) {
Log.e("SC:ack", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
handleSuccess("ack", call, response, r);
}
catch (Exception e)
{
handleError("ack", call, response, r, false, e);
}
}
});
}
catch (Exception e)
{
Log.e("SC:ack", e.toString());
handleError("ack", null, null, Str.Empty, false, e);
}
}
public static void expand(int id, String key, long scn_msg_id, View loader, Func5to0<String, String, PriorityEnum, Long, Long> okResult)
{
try
{
Request request = new Request.Builder()
.url(BASE_URL + "expand.php?user_id=" + id + "&user_key=" + key + "&scn_msg_id=" + scn_msg_id)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
handleError("expand", call, null, Str.Empty, true, e);
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
}
@Override
public void onResponse(Call call, Response response)
{
String r = Str.Empty;
try (ResponseBody responseBody = response.body())
{
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
r = responseBody.string();
Log.d("Server::Response", request.url().toString()+"\n"+r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success"))
{
SCNApp.showToast(json_str(json, "message"), 4000);
handleNonSuccess("expand", call, response, r);
return;
}
JSONObject o = json.getJSONObject("data");
long time = json_lng(o, "timestamp");
String title = json_str(o, "title");
String content = json_str(o, "body");
PriorityEnum prio = PriorityEnum.parseAPI(json_int(o, "priority"));
long scn_id = json_lng(o, "scn_msg_id");
okResult.invoke(title, content, prio, time, scn_id);
handleSuccess("expand", call, response, r);
}
catch (Exception e)
{
handleError("expand", call, response, r, false, e);
}
finally
{
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
}
}
});
}
catch (Exception e)
{
handleError("expand", null, null, Str.Empty, false, e);
}
}
@@ -495,4 +608,79 @@ public class ServerCommunication
{
return o.getString(key);
}
private static void handleSuccess(String source, Call call, Response resp, String respBody)
{
Log.d("SC:"+source, respBody);
try
{
Instant i = Instant.now();
String s = source;
String u = call.request().url().toString();
int rc = resp.code();
String r = respBody;
LogLevel l = LogLevel.INFO;
SingleQuery q = new SingleQuery(l, i, s, u, r, rc, "SUCCESS");
QueryLog.inst().add(q);
}
catch (Exception e2)
{
Log.e("SC:HandleSuccess", e2.toString());
}
}
private static void handleNonSuccess(String source, Call call, Response resp, String respBody)
{
Log.d("SC:"+source, respBody);
try
{
Instant i = Instant.now();
String s = source;
String u = call.request().url().toString();
int rc = resp.code();
String r = respBody;
LogLevel l = LogLevel.WARN;
SingleQuery q = new SingleQuery(l, i, s, u, r, rc, "NON-SUCCESS");
QueryLog.inst().add(q);
}
catch (Exception e2)
{
Log.e("SC:HandleSuccess", e2.toString());
}
}
private static void handleError(String source, Call call, Response resp, String respBody, boolean isio, Exception e)
{
Log.e("SC:"+source, e.toString());
if (isio)
{
SCNApp.showToast("Can't connect to server", 3000);
}
else
{
SCNApp.showToast("Communication with server failed", 4000);
}
try
{
Instant i = Instant.now();
String s = source;
String u = (call==null)?Str.Empty:call.request().url().toString();
int rc = (resp==null)?-1:resp.code();
String r = respBody;
LogLevel l = isio?LogLevel.WARN:LogLevel.ERROR;
SingleQuery q = new SingleQuery(l, i, s, u, r, rc, e.toString());
QueryLog.inst().add(q);
}
catch (Exception e2)
{
Log.e("SC:HandleError", e2.toString());
}
}
}

View File

@@ -0,0 +1,82 @@
package com.blackforestbytes.simplecloudnotifier.model;
import android.content.SharedPreferences;
import android.os.BaseBundle;
import android.os.Bundle;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import org.joda.time.Instant;
public class SingleQuery
{
public final Instant Timestamp;
public final LogLevel Level;
public final String Name;
public final String URL;
public final String Response;
public final int ResponseCode;
public final String ExceptionString;
public SingleQuery(LogLevel l, Instant i, String n, String u, String r, int rc, String e)
{
Level=l;
Timestamp=i;
Name=n;
URL=u;
Response=r;
ResponseCode=rc;
ExceptionString=e;
}
public void save(SharedPreferences.Editor e, String base)
{
e.putInt(base+".Level", Level.asInt());
e.putLong(base+".Timestamp", Timestamp.getMillis());
e.putString(base+".Name", Name);
e.putString(base+".URL", URL);
e.putString(base+".Response", Response);
e.putInt(base+".ResponseCode", ResponseCode);
e.putString(base+".ExceptionString", ExceptionString);
}
public void save(BaseBundle e, String base)
{
e.putInt(base+".Level", Level.asInt());
e.putLong(base+".Timestamp", Timestamp.getMillis());
e.putString(base+".Name", Name);
e.putString(base+".URL", URL);
e.putString(base+".Response", Response);
e.putInt(base+".ResponseCode", ResponseCode);
e.putString(base+".ExceptionString", ExceptionString);
}
public static SingleQuery load(SharedPreferences e, String base)
{
return new SingleQuery
(
LogLevel.fromInt(e.getInt(base+".Level", 0)),
new Instant(e.getLong(base+".Timestamp", 0)),
e.getString(base+".Name", Str.Empty),
e.getString(base+".URL", Str.Empty),
e.getString(base+".Response", Str.Empty),
e.getInt(base+".ResponseCode", -1),
e.getString(base+".ExceptionString", Str.Empty)
);
}
public static SingleQuery load(BaseBundle e, String base)
{
return new SingleQuery
(
LogLevel.fromInt(e.getInt(base+".Level", 0)),
new Instant(e.getLong(base+".Timestamp", 0)),
e.getString(base+".Name", Str.Empty),
e.getString(base+".URL", Str.Empty),
e.getString(base+".Response", Str.Empty),
e.getInt(base+".ResponseCode", -1),
e.getString(base+".ExceptionString", Str.Empty)
);
}
}

View File

@@ -4,14 +4,21 @@ import android.util.Log;
import android.widget.Toast;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
import com.blackforestbytes.simplecloudnotifier.model.LogLevel;
import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum;
import com.blackforestbytes.simplecloudnotifier.model.QueryLog;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.model.ServerCommunication;
import com.blackforestbytes.simplecloudnotifier.model.SingleQuery;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import org.joda.time.Instant;
import org.json.JSONObject;
public class FBMService extends FirebaseMessagingService
{
@Override
@@ -38,8 +45,20 @@ public class FBMService extends FirebaseMessagingService
String content = remoteMessage.getData().get("body");
PriorityEnum prio = PriorityEnum.parseAPI(remoteMessage.getData().get("priority"));
long scn_id = Long.parseLong(remoteMessage.getData().get("scn_msg_id"));
boolean trimmed = Boolean.parseBoolean(remoteMessage.getData().get("trimmed"));
recieveData(time, title, content, prio, scn_id, false);
SingleQuery q = new SingleQuery(LogLevel.INFO, Instant.now(), "FBM<recieve>", Str.Empty, new JSONObject(remoteMessage.getData()).toString(), 0, "SUCCESS");
QueryLog.inst().add(q);
if (trimmed)
{
ServerCommunication.expand(SCNSettings.inst().user_id, SCNSettings.inst().user_key, scn_id, null, (i1, i2, i3, i4, i5) -> recieveData(i4, i1, i2, i3, i5, false));
}
else
{
recieveData(time, title, content, prio, scn_id, false);
}
}
catch (Exception e)
{

View File

@@ -2,23 +2,37 @@ package com.blackforestbytes.simplecloudnotifier.service;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.widget.Toast;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.datatypes.Tuple2;
import com.blackforestbytes.simplecloudnotifier.lib.datatypes.Tuple3;
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func0to0;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import static androidx.constraintlayout.widget.Constraints.TAG;
@@ -45,18 +59,72 @@ public class IABService implements PurchasesUpdatedListener
}
}
public enum SimplePurchaseState { YES, NO, UNINITIALIZED }
private BillingClient client;
private boolean isServiceConnected;
private final List<Purchase> purchases = new ArrayList<>();
private boolean _isInitialized = false;
private final Map<String, Boolean> _localCache= new HashMap<>();
public IABService(Context c)
{
_isInitialized = false;
loadCache();
client = BillingClient
.newBuilder(c)
.setListener(this)
.build();
startServiceConnection(this::queryPurchases, false);
startServiceConnection(this::querySkuDetails, false);
}
public void reloadPrefs()
{
loadCache();
}
private void loadCache()
{
_localCache.clear();
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("IAB", Context.MODE_PRIVATE);
int count = sharedPref.getInt("c", 0);
for (int i=0; i < count; i++)
{
String k = sharedPref.getString("["+i+"]->key", null);
boolean v = sharedPref.getBoolean("["+i+"]->value", false);
if (k==null)continue;
_localCache.put(k, v);
}
}
private void saveCache()
{
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("IAB", Context.MODE_PRIVATE);
SharedPreferences.Editor editor= sharedPref.edit();
editor.putInt("c", _localCache.size());
int i = 0;
for (Map.Entry<String, Boolean> e : _localCache.entrySet())
{
editor.putString("["+i+"]->key", e.getKey());
editor.putBoolean("["+i+"]->value", e.getValue());
i++;
}
editor.apply();
}
@SuppressWarnings("ConstantConditions")
private synchronized void updateCache(String k, boolean v)
{
if (_localCache.containsKey(k) && _localCache.get(k)==v) return;
_localCache.put(k, v);
saveCache();
}
public void queryPurchases()
@@ -67,14 +135,16 @@ public class IABService implements PurchasesUpdatedListener
Purchase.PurchasesResult purchasesResult = client.queryPurchases(BillingClient.SkuType.INAPP);
Log.i(TAG, "Querying purchases elapsed time: " + (System.currentTimeMillis() - time) + "ms");
if (purchasesResult.getResponseCode() == BillingClient.BillingResponse.OK)
if (purchasesResult.getResponseCode() == BillingClient.BillingResponseCode.OK)
{
for (Purchase p : purchasesResult.getPurchasesList())
for (Purchase p : Objects.requireNonNull(purchasesResult.getPurchasesList()))
{
handlePurchase(p);
handlePurchase(p, false);
}
boolean newProMode = getPurchaseCached(IAB_PRO_MODE) != null;
_isInitialized = true;
boolean newProMode = getPurchaseCachedSimple(IAB_PRO_MODE);
if (newProMode != SCNSettings.inst().promode_local)
{
refreshProModeListener();
@@ -89,20 +159,39 @@ public class IABService implements PurchasesUpdatedListener
executeServiceRequest(queryToExecute, false);
}
public void purchase(Activity a, String id)
{
executeServiceRequest(() ->
{
BillingFlowParams flowParams = BillingFlowParams
.newBuilder()
.setSku(id)
.setType(BillingClient.SkuType.INAPP) // SkuType.SUB for subscription
.build();
client.launchBillingFlow(a, flowParams);
}, true);
public void querySkuDetails() {
}
private void executeServiceRequest(Func0to0 runnable, final boolean userRequest) {
public void purchase(Activity a, String id)
{
Func0to0 queryRequest = () -> {
// Query the purchase async
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(Collections.singletonList(id)).setType(BillingClient.SkuType.INAPP);
client.querySkuDetailsAsync(params.build(), (billingResult, skuDetailsList) ->
{
if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK || skuDetailsList == null || skuDetailsList.size() != 1)
{
SCNApp.showToast("Could not find product", Toast.LENGTH_SHORT);
return;
}
executeServiceRequest(() ->
{
BillingFlowParams flowParams = BillingFlowParams
.newBuilder()
.setSkuDetails(skuDetailsList.get(0))
.build();
client.launchBillingFlow(a, flowParams);
}, true);
});
};
executeServiceRequest(queryRequest, false);
}
private void executeServiceRequest(Func0to0 runnable, final boolean userRequest)
{
if (isServiceConnected)
{
runnable.invoke();
@@ -124,34 +213,37 @@ public class IABService implements PurchasesUpdatedListener
}
@Override
public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases)
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases)
{
if (responseCode == BillingClient.BillingResponse.OK && purchases != null)
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null)
{
for (Purchase purchase : purchases)
{
handlePurchase(purchase);
handlePurchase(purchase, true);
}
}
else if (responseCode == BillingClient.BillingResponse.ITEM_ALREADY_OWNED && purchases != null)
else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED && purchases != null)
{
for (Purchase purchase : purchases)
{
handlePurchase(purchase);
handlePurchase(purchase, true);
}
}
}
private void handlePurchase(Purchase purchase)
private void handlePurchase(Purchase purchase, boolean triggerUpdate)
{
Log.d(TAG, "Got a verified purchase: " + purchase);
purchases.add(purchase);
refreshProModeListener();
if (triggerUpdate) refreshProModeListener();
updateCache(purchase.getSku(), true);
}
private void refreshProModeListener() {
private void refreshProModeListener()
{
MainActivity ma = SCNApp.getMainActivity();
if (ma != null) ma.adpTabs.tab3.updateProState();
if (ma != null) ma.adpTabs.tab1.updateProState();
@@ -163,9 +255,9 @@ public class IABService implements PurchasesUpdatedListener
client.startConnection(new BillingClientStateListener()
{
@Override
public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponseCode)
public void onBillingSetupFinished(@NonNull BillingResult billingResult)
{
if (billingResponseCode == BillingClient.BillingResponse.OK)
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK)
{
isServiceConnected = true;
if (executeOnSuccess != null) executeOnSuccess.invoke();
@@ -183,13 +275,31 @@ public class IABService implements PurchasesUpdatedListener
});
}
public Purchase getPurchaseCached(String id)
public boolean getPurchaseCachedSimple(String id)
{
for (Purchase p : purchases)
return getPurchaseCachedExtended(id).Item1;
}
@SuppressWarnings("ConstantConditions")
public Tuple3<Boolean, Boolean, String> getPurchaseCachedExtended(String id)
{
// <state, initialized, token>
if (!_isInitialized)
{
if (Str.equals(p.getSku(), id)) return p;
if (_localCache.containsKey(id) && _localCache.get(id)) return new Tuple3<>(true, false, Str.Empty);
}
return null;
for (Purchase p : purchases)
{
if (Str.equals(p.getSku(), id))
{
updateCache(id, true);
return new Tuple3<>(true, true, p.getPurchaseToken());
}
}
updateCache(id, false);
return new Tuple3<>(false, true, Str.Empty);
}
}

View File

@@ -6,6 +6,7 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.AudioManager;
import android.net.Uri;
@@ -64,7 +65,8 @@ public class NotificationService
channel0.setDescription("Push notifications from the server with low priority.\nGo to the in-app settings to configure ringtone, volume and vibrations");
channel0.setSound(null, null);
channel0.setVibrationPattern(null);
channel0.setLightColor(Color.BLUE);
channel0.setLightColor(Color.CYAN);
channel0.enableLights(true);
notifman.createNotificationChannel(channel0);
}
}
@@ -76,7 +78,8 @@ public class NotificationService
channel1.setDescription("Push notifications from the server with low priority.\nGo to the in-app settings to configure ringtone, volume and vibrations");
channel1.setSound(null, null);
channel1.setVibrationPattern(null);
channel1.setLightColor(Color.BLUE);
channel1.setLightColor(Color.CYAN);
channel1.enableLights(true);
notifman.createNotificationChannel(channel1);
}
}
@@ -84,11 +87,13 @@ public class NotificationService
NotificationChannel channel2 = notifman.getNotificationChannel(CHANNEL_P2_ID);
if (channel2 == null)
{
channel2 = new NotificationChannel(CHANNEL_P1_ID, "Push notifications (high priority)", NotificationManager.IMPORTANCE_DEFAULT);
channel2 = new NotificationChannel(CHANNEL_P2_ID, "Push notifications (high priority)", NotificationManager.IMPORTANCE_DEFAULT);
channel2.setDescription("Push notifications from the server with low priority.\nGo to the in-app settings to configure ringtone, volume and vibrations");
channel2.setSound(null, null);
channel2.setVibrationPattern(null);
channel2.setLightColor(Color.BLUE);
channel2.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
channel2.setLightColor(Color.CYAN);
channel2.enableLights(true);
notifman.createNotificationChannel(channel2);
}
}
@@ -114,9 +119,9 @@ public class NotificationService
{
Vibrator v = (Vibrator) SCNApp.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
v.vibrate(VibrationEffect.createOneShot(1500, VibrationEffect.DEFAULT_AMPLITUDE));
v.vibrate(VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
v.vibrate(1500);
v.vibrate(500);
}
}
}
@@ -165,12 +170,15 @@ public class NotificationService
private void showBackground_old(CMessage msg, Context ctxt, NotificationSettings ns, PriorityEnum prio)
{
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, getChannel(prio));
mBuilder.setSmallIcon(R.drawable.ic_bfb);
mBuilder.setSmallIcon(R.drawable.ic_notification_white);
mBuilder.setLargeIcon(BitmapFactory.decodeResource(ctxt.getResources(), R.mipmap.ic_notification_full));
mBuilder.setContentTitle(msg.Title);
mBuilder.setContentText(msg.Content);
mBuilder.setShowWhen(true);
mBuilder.setWhen(msg.Timestamp * 1000);
mBuilder.setAutoCancel(true);
mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
mBuilder.setGroup("com.blackforestbytes.simplecloudnotifier.notifications.group."+prio.toString());
if (msg.Priority == PriorityEnum.LOW) mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
@@ -198,19 +206,22 @@ public class NotificationService
Notification n = mBuilder.build();
if (mNotificationManager != null) mNotificationManager.notify(0, n);
if (mNotificationManager != null) mNotificationManager.notify((int)msg.SCN_ID, n);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private void showBackground_new(CMessage msg, Context ctxt, NotificationSettings ns, PriorityEnum prio)
{
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, getChannel(prio));
mBuilder.setSmallIcon(R.drawable.ic_bfb);
mBuilder.setSmallIcon(R.drawable.ic_notification_white);
mBuilder.setLargeIcon(BitmapFactory.decodeResource(ctxt.getResources(), R.mipmap.ic_notification_full));
mBuilder.setContentTitle(msg.Title);
mBuilder.setContentText(msg.Content);
mBuilder.setShowWhen(true);
mBuilder.setWhen(msg.Timestamp * 1000);
mBuilder.setAutoCancel(true);
mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
mBuilder.setGroup("com.blackforestbytes.simplecloudnotifier.notifications.group."+prio.toString());
if (ns.EnableLED) mBuilder.setLights(ns.LEDColor, 500, 500);
@@ -218,10 +229,10 @@ public class NotificationService
if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
Intent intnt_click = new Intent(SCNApp.getContext(), BroadcastReceiverService.class);
intnt_click.putExtra(BroadcastReceiverService.ID_KEY, BroadcastReceiverService.NOTIF_SHOW_MAIN);
PendingIntent pi = PendingIntent.getBroadcast(ctxt, 0, intnt_click, 0);
Intent intent = new Intent(ctxt, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0);
mBuilder.setContentIntent(pi);
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
if (mNotificationManager == null) return;
@@ -242,12 +253,12 @@ public class NotificationService
Notification n = mBuilder.build();
n.flags |= Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(0, n);
mNotificationManager.notify((int)msg.SCN_ID, n);
if (ns.EnableVibration)
{
Vibrator v = (Vibrator) SCNApp.getContext().getSystemService(Context.VIBRATOR_SERVICE);
v.vibrate(VibrationEffect.createOneShot(1500, VibrationEffect.DEFAULT_AMPLITUDE));
v.vibrate(VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE));
}
//if (ns.EnableLED) { } // no LED in Android-O -- configure via Channel

View File

@@ -0,0 +1,56 @@
package com.blackforestbytes.simplecloudnotifier.util;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.ScrollView;
import com.blackforestbytes.simplecloudnotifier.R;
public class MaxHeightScrollView extends ScrollView
{
public int maxHeight = Integer.MAX_VALUE;//dp
public MaxHeightScrollView(Context context)
{
super(context);
}
public MaxHeightScrollView(Context context, AttributeSet attrs)
{
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView, 0, 0);
try {
maxHeight = a.getInteger(R.styleable.MaxHeightScrollView_maxHeightOverride, Integer.MAX_VALUE);
} finally {
a.recycle();
}
}
public MaxHeightScrollView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView, 0, 0);
try {
maxHeight = a.getInteger(R.styleable.MaxHeightScrollView_maxHeightOverride, Integer.MAX_VALUE);
} finally {
a.recycle();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
heightMeasureSpec = MeasureSpec.makeMeasureSpec(dpToPx(getResources(), maxHeight), MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private int dpToPx(Resources res, int dp)
{
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, res.getDisplayMetrics());
}
}

View File

@@ -3,6 +3,7 @@ package com.blackforestbytes.simplecloudnotifier.util;
import android.graphics.Canvas;
import android.view.View;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.view.MessageAdapter;
import androidx.annotation.NonNull;
@@ -13,10 +14,21 @@ public class MessageAdapterTouchHelper extends ItemTouchHelper.SimpleCallback
{
private MessageAdapterTouchHelperListener listener;
private int dir = 0;
public MessageAdapterTouchHelper(int dragDirs, int swipeDirs, MessageAdapterTouchHelperListener listener)
{
super(dragDirs, swipeDirs);
this.dir = swipeDirs;
this.listener = listener;
updateEnabled();
}
public void updateEnabled()
{
int sdir = SCNSettings.inst().EnableDeleteSwipe ? ItemTouchHelper.LEFT : 0;
if (dir == sdir) return;
setDefaultSwipeDirs(dir = sdir);
}
@Override

View File

@@ -0,0 +1,25 @@
package com.blackforestbytes.simplecloudnotifier.util;
import android.text.Editable;
import android.text.TextWatcher;
public abstract class TextChangedListener<T> implements TextWatcher {
private T target;
public TextChangedListener(T target) {
this.target = target;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
this.onTextChanged(target, s);
}
public abstract void onTextChanged(T target, Editable s);
}

View File

@@ -6,12 +6,14 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.blackforestbytes.simplecloudnotifier.R;
@@ -91,7 +93,7 @@ public class AccountFragment extends Fragment
builder.setPositiveButton("YES", (dialog, which) -> {
CMessageList.inst().clear();
SCNApp.showToast("Notifications cleared", 1000);
SCNApp.showToast("Messages cleared", 1000);
dialog.dismiss();
});
@@ -103,7 +105,7 @@ public class AccountFragment extends Fragment
v.findViewById(R.id.btnQR).setOnClickListener(cv ->
{
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(SCNSettings.inst().createOnlineURL()));
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(SCNSettings.inst().createOnlineURL(true)));
startActivity(browserIntent);
});
@@ -126,10 +128,11 @@ public class AccountFragment extends Fragment
public void updateUI(View v)
{
if (v == null) return;
TextView tvUserID = v.findViewById(R.id.tvUserID);
TextView tvUserKey = v.findViewById(R.id.tvUserKey);
TextView tvQuota = v.findViewById(R.id.tvQuota);
ImageButton btnQR = v.findViewById(R.id.btnQR);
TextView tvUserID = v.findViewById(R.id.tvUserID);
TextView tvUserKey = v.findViewById(R.id.tvUserKey);
TextView tvQuota = v.findViewById(R.id.tvQuota);
ImageView ivQuota = v.findViewById(R.id.ic_img_quota);
ImageButton btnQR = v.findViewById(R.id.btnQR);
SCNSettings s = SCNSettings.inst();
@@ -138,7 +141,8 @@ public class AccountFragment extends Fragment
tvUserID.setText(String.valueOf(s.user_id));
tvUserKey.setText(s.user_key);
tvQuota.setText(String.format("%d / %d", s.quota_curr, s.quota_max));
btnQR.setImageBitmap(QRCode.from(s.createOnlineURL()).to(ImageType.PNG).withSize(512, 512).bitmap());
btnQR.setImageBitmap(QRCode.from(s.createOnlineURL(false)).to(ImageType.PNG).withSize(512, 512).bitmap());
ivQuota.setColorFilter(s.quota_curr>=s.quota_max ? Color.rgb(200, 0, 0) : Color.rgb(128, 128, 128));
}
else
{
@@ -146,6 +150,7 @@ public class AccountFragment extends Fragment
tvUserKey.setText(R.string.str_not_connected);
tvQuota.setText(R.string.str_not_connected);
btnQR.setImageResource(R.drawable.qr_default);
ivQuota.setColorFilter(0x80_80_80);
}
}
}

View File

@@ -1,14 +1,23 @@
package com.blackforestbytes.simplecloudnotifier.view;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
import com.blackforestbytes.simplecloudnotifier.model.QueryLog;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
import com.blackforestbytes.simplecloudnotifier.view.debug.QueryLogActivity;
import com.google.android.material.tabs.TabLayout;
import androidx.appcompat.app.AppCompatActivity;
@@ -16,6 +25,12 @@ import androidx.appcompat.widget.Toolbar;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import java.io.File;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.Map;
import java.util.Set;
public class MainActivity extends AppCompatActivity
{
public TabAdapter adpTabs;
@@ -24,6 +39,8 @@ public class MainActivity extends AppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState)
{
QueryLog.inst();
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
@@ -33,6 +50,7 @@ public class MainActivity extends AppCompatActivity
layoutRoot = findViewById(R.id.layoutRoot);
Toolbar toolbar = findViewById(R.id.toolbar);
toolbar.setOnClickListener(this::onToolbackClicked);
setSupportActionBar(toolbar);
ViewPager viewPager = findViewById(R.id.pager);
@@ -61,7 +79,7 @@ public class MainActivity extends AppCompatActivity
SCNApp.register(this);
IABService.startup(this);
SCNSettings.inst().work(this);
SCNSettings.inst().work(this, true);
}
@Override
@@ -81,4 +99,126 @@ public class MainActivity extends AppCompatActivity
CMessageList.inst().fullSave();
IABService.inst().destroy();
}
private int clickCount = 0;
private long lastClick = 0;
private void onToolbackClicked(View v)
{
long now = System.currentTimeMillis();
if (now - lastClick > 200) clickCount=0;
clickCount++;
lastClick = now;
if (clickCount == 4) startActivity(new Intent(this, QueryLogActivity.class));
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 1991 && resultCode == RESULT_OK)
{
Uri uri = data.getData(); //The uri with the location of the file
Context ctxt = this;
try
{
ObjectInputStream stream = new ObjectInputStream(getContentResolver().openInputStream(uri));
Map<String, ?> d1 = (Map<String, ?>)stream.readObject();
Map<String, ?> d2 = (Map<String, ?>)stream.readObject();
Map<String, ?> d3 = (Map<String, ?>)stream.readObject();
Map<String, ?> d4 = (Map<String, ?>)stream.readObject();
stream.close();
runOnUiThread(() ->
{
SharedPreferences.Editor e1 = ctxt.getSharedPreferences("Config", Context.MODE_PRIVATE).edit();
SharedPreferences.Editor e2 = ctxt.getSharedPreferences("IAB", Context.MODE_PRIVATE).edit();
SharedPreferences.Editor e3 = ctxt.getSharedPreferences("CMessageList", Context.MODE_PRIVATE).edit();
SharedPreferences.Editor e4 = ctxt.getSharedPreferences("QueryLog", Context.MODE_PRIVATE).edit();
e1.clear();
for (Map.Entry<String, ?> entry : d1.entrySet())
{
if (entry.getValue() instanceof String) e1.putString(entry.getKey(), (String)entry.getValue());
if (entry.getValue() instanceof Boolean) e1.putBoolean(entry.getKey(), (Boolean)entry.getValue());
if (entry.getValue() instanceof Float) e1.putFloat(entry.getKey(), (Float)entry.getValue());
if (entry.getValue() instanceof Integer) e1.putInt(entry.getKey(), (Integer)entry.getValue());
if (entry.getValue() instanceof Long) e1.putLong(entry.getKey(), (Long)entry.getValue());
if (entry.getValue() instanceof Set<?>) e1.putStringSet(entry.getKey(), (Set<String>)entry.getValue());
}
e2.clear();
for (Map.Entry<String, ?> entry : d2.entrySet())
{
if (entry.getValue() instanceof String) e2.putString(entry.getKey(), (String)entry.getValue());
if (entry.getValue() instanceof Boolean) e2.putBoolean(entry.getKey(), (Boolean)entry.getValue());
if (entry.getValue() instanceof Float) e2.putFloat(entry.getKey(), (Float)entry.getValue());
if (entry.getValue() instanceof Integer) e2.putInt(entry.getKey(), (Integer)entry.getValue());
if (entry.getValue() instanceof Long) e2.putLong(entry.getKey(), (Long)entry.getValue());
if (entry.getValue() instanceof Set<?>) e2.putStringSet(entry.getKey(), (Set<String>)entry.getValue());
}
e2.clear();
for (Map.Entry<String, ?> entry : d3.entrySet())
{
if (entry.getValue() instanceof String) e3.putString(entry.getKey(), (String)entry.getValue());
if (entry.getValue() instanceof Boolean) e3.putBoolean(entry.getKey(), (Boolean)entry.getValue());
if (entry.getValue() instanceof Float) e3.putFloat(entry.getKey(), (Float)entry.getValue());
if (entry.getValue() instanceof Integer) e3.putInt(entry.getKey(), (Integer)entry.getValue());
if (entry.getValue() instanceof Long) e3.putLong(entry.getKey(), (Long)entry.getValue());
if (entry.getValue() instanceof Set<?>) e3.putStringSet(entry.getKey(), (Set<String>)entry.getValue());
}
e4.clear();
for (Map.Entry<String, ?> entry : d4.entrySet())
{
if (entry.getValue() instanceof String) e4.putString(entry.getKey(), (String)entry.getValue());
if (entry.getValue() instanceof Boolean) e4.putBoolean(entry.getKey(), (Boolean)entry.getValue());
if (entry.getValue() instanceof Float) e4.putFloat(entry.getKey(), (Float)entry.getValue());
if (entry.getValue() instanceof Integer) e4.putInt(entry.getKey(), (Integer)entry.getValue());
if (entry.getValue() instanceof Long) e4.putLong(entry.getKey(), (Long)entry.getValue());
if (entry.getValue() instanceof Set<?>) e4.putStringSet(entry.getKey(), (Set<String>)entry.getValue());
}
e1.apply();
e2.apply();
e3.apply();
e4.apply();
SCNSettings.inst().reloadPrefs();
IABService.inst().reloadPrefs();
CMessageList.inst().reloadPrefs();
QueryLog.inst().reloadPrefs();
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ViewPager viewPager = findViewById(R.id.pager);
PagerAdapter adapter = adpTabs = new TabAdapter(getSupportFragmentManager());
viewPager.setAdapter(adapter);
TabLayout tabLayout = findViewById(R.id.tab_layout);
tabLayout.setupWithViewPager(viewPager);
SCNSettings.inst().work(this, true);
SCNApp.showToast("Backup imported", Toast.LENGTH_LONG);
finish();
});
}
catch (Exception e)
{
Log.e("Import:Err", e.toString());
SCNApp.showToast("Import failed", Toast.LENGTH_LONG);
}
}
}
}

View File

@@ -1,5 +1,7 @@
package com.blackforestbytes.simplecloudnotifier.view;
import android.content.Intent;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -8,8 +10,11 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.google.android.material.button.MaterialButton;
import java.lang.ref.WeakReference;
import java.util.Collections;
@@ -52,12 +57,11 @@ public class MessageAdapter extends RecyclerView.Adapter
{
CMessage msg = CMessageList.inst().tryGetFromBack(position);
MessagePresenter view = (MessagePresenter) holder;
view.setMessage(msg);
view.setMessage(msg, position);
viewHolders.put(view, true);
}
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder)
{
@@ -87,16 +91,17 @@ public class MessageAdapter extends RecyclerView.Adapter
manLayout.smoothScrollToPosition(viewRecycler, null, 0);
}
public void removeItem(int position)
public CMessage removeItem(int position)
{
CMessageList.inst().remove(position);
notifyItemRemoved(position);
CMessage i = CMessageList.inst().removeFromBack(position);
notifyDataSetChanged();
return i;
}
public void restoreItem(CMessage item, int position)
{
CMessageList.inst().insert(position, item);
notifyItemInserted(position);
notifyDataSetChanged();
}
public class MessagePresenter extends RecyclerView.ViewHolder implements View.OnClickListener
@@ -109,7 +114,11 @@ public class MessageAdapter extends RecyclerView.Adapter
public RelativeLayout viewForeground;
public RelativeLayout viewBackground;
public MaterialButton btnShare;
public MaterialButton btnDelete;
private CMessage data;
private int datapos;
MessagePresenter(View itemView)
{
@@ -120,6 +129,8 @@ public class MessageAdapter extends RecyclerView.Adapter
ivPriority = itemView.findViewById(R.id.ivPriority);
viewForeground = itemView.findViewById(R.id.layoutFront);
viewBackground = itemView.findViewById(R.id.layoutBack);
btnShare = itemView.findViewById(R.id.btnShare);
btnDelete = itemView.findViewById(R.id.btnDelete);
itemView.setOnClickListener(this);
tvTimestamp.setOnClickListener(this);
@@ -127,9 +138,22 @@ public class MessageAdapter extends RecyclerView.Adapter
tvMessage.setOnClickListener(this);
ivPriority.setOnClickListener(this);
viewForeground.setOnClickListener(this);
btnShare.setOnClickListener(v ->
{
if (data == null) return;
Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
sharingIntent.setType("text/plain");
sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, data.Title);
sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, data.Content);
SCNApp.getMainActivity().startActivity(Intent.createChooser(sharingIntent, "Share message"));
});
btnDelete.setOnClickListener(v -> { if (data != null) SCNApp.getMainActivity().adpTabs.tab1.deleteMessage(datapos); });
}
void setMessage(CMessage msg)
void setMessage(CMessage msg, int pos)
{
tvTimestamp.setText(msg.formatTimestamp());
tvTitle.setText(msg.Title);
@@ -140,32 +164,63 @@ public class MessageAdapter extends RecyclerView.Adapter
case LOW:
ivPriority.setVisibility(View.VISIBLE);
ivPriority.setImageResource(R.drawable.priority_low);
ivPriority.setColorFilter(Color.rgb(176, 176, 176));
break;
case NORMAL:
ivPriority.setVisibility(View.GONE);
ivPriority.setColorFilter(Color.rgb(176, 176, 176));
break;
case HIGH:
ivPriority.setVisibility(View.VISIBLE);
ivPriority.setImageResource(R.drawable.priority_high);
ivPriority.setColorFilter(Color.rgb(200, 0, 0));
break;
}
data = msg;
datapos = pos;
if (msg.IsExpandedInAdapter) expand(true); else collapse(true);
}
private void expand(boolean force)
{
if (data != null && data.IsExpandedInAdapter && !force) return;
if (data != null) data.IsExpandedInAdapter = true;
if (tvMessage != null) tvMessage.setMaxLines(9999);
if (btnDelete != null) btnDelete.setVisibility(View.VISIBLE);
if (btnShare != null) btnShare.setVisibility(View.VISIBLE);
}
private int norm(int i) { return (i<=0)?0:((i>9999)?9999:i); }
private void collapse(boolean force)
{
if (data != null && !data.IsExpandedInAdapter && !force) return;
if (data != null) data.IsExpandedInAdapter = false;
if (tvMessage != null) tvMessage.setMaxLines(norm(SCNSettings.inst().PreviewLineCount));
if (btnDelete != null) btnDelete.setVisibility(View.GONE);
if (btnShare != null) btnShare.setVisibility(View.GONE);
}
@Override
public void onClick(View v)
{
if (data.IsExpandedInAdapter)
{
collapse(false);
return;
}
for (MessagePresenter holder : MessageAdapter.this.viewHolders.keySet())
{
if (holder == null) continue;
if (holder == this) continue;
if (holder.tvMessage == null) continue;
if (holder.tvMessage.getMaxLines() == 6) continue;
holder.tvMessage.setMaxLines(6);
holder.collapse(false);
}
tvMessage.setMaxLines(9999);
expand(false);
}
}
}

View File

@@ -9,7 +9,6 @@ import android.view.ViewGroup;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.blackforestbytes.simplecloudnotifier.util.MessageAdapterTouchHelper;
@@ -28,6 +27,8 @@ public class NotificationsFragment extends Fragment implements MessageAdapterTou
private PublisherAdView adView;
private MessageAdapter adpMessages;
public MessageAdapterTouchHelper touchHelper;
public NotificationsFragment()
{
// Required empty public constructor
@@ -43,7 +44,7 @@ public class NotificationsFragment extends Fragment implements MessageAdapterTou
rvMessages.setLayoutManager(lman);
rvMessages.setAdapter(adpMessages = new MessageAdapter(v.findViewById(R.id.tvNoElements), lman, rvMessages));
ItemTouchHelper.SimpleCallback itemTouchHelperCallback = new MessageAdapterTouchHelper(0, ItemTouchHelper.LEFT, this);
ItemTouchHelper.SimpleCallback itemTouchHelperCallback = touchHelper = new MessageAdapterTouchHelper(0, ItemTouchHelper.LEFT, this);
new ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(rvMessages);
adView = v.findViewById(R.id.adBanner);
@@ -57,7 +58,7 @@ public class NotificationsFragment extends Fragment implements MessageAdapterTou
public void updateProState()
{
if (adView != null) adView.setVisibility(IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE) != null ? View.GONE : View.VISIBLE);
if (adView != null) adView.setVisibility(IABService.inst().getPurchaseCachedSimple(IABService.IAB_PRO_MODE) ? View.GONE : View.VISIBLE);
}
@Override
@@ -65,17 +66,25 @@ public class NotificationsFragment extends Fragment implements MessageAdapterTou
{
if (viewHolder instanceof MessageAdapter.MessagePresenter)
{
final CMessage deletedItem = CMessageList.inst().tryGet(viewHolder.getAdapterPosition());
final int deletedIndex = viewHolder.getAdapterPosition();
String name = deletedItem.Title;
adpMessages.removeItem(viewHolder.getAdapterPosition());
Snackbar snackbar = Snackbar.make(SCNApp.getMainActivity().layoutRoot, name + " removed", Snackbar.LENGTH_LONG);
snackbar.setAction("UNDO", view -> adpMessages.restoreItem(deletedItem, deletedIndex));
snackbar.setActionTextColor(Color.YELLOW);
snackbar.show();
deleteMessage(viewHolder.getAdapterPosition());
}
}
public void deleteMessage(int pos)
{
final int deletedIndex = pos;
final CMessage deletedItem = adpMessages.removeItem(pos);
String name = deletedItem.Title;
Snackbar snackbar = Snackbar.make(SCNApp.getMainActivity().layoutRoot, name + " removed", Snackbar.LENGTH_LONG);
snackbar.setAction("UNDO", view -> adpMessages.restoreItem(deletedItem, deletedIndex));
snackbar.setActionTextColor(Color.YELLOW);
snackbar.show();
}
public void updateDeleteSwipeEnabled()
{
if (touchHelper != null) touchHelper.updateEnabled();
}
}

View File

@@ -1,15 +1,15 @@
package com.blackforestbytes.simplecloudnotifier.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -17,27 +17,34 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import com.android.billingclient.api.Purchase;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.android.ThreadUtils;
import com.blackforestbytes.simplecloudnotifier.lib.lambda.FI;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.blackforestbytes.simplecloudnotifier.util.TextChangedListener;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import top.defaults.colorpicker.ColorPickerPopup;
import xyz.aprildown.ultimatemusicpicker.MusicPickerListener;
import xyz.aprildown.ultimatemusicpicker.UltimateMusicPicker;
@@ -49,6 +56,8 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
private Button prefUpgradeAccount;
private TextView prefUpgradeAccount_msg;
private TextView prefUpgradeAccount_info;
private Switch prefEnableDeleteSwipe;
private EditText prefPreviewLineCount;
private Switch prefMsgLowEnableSound;
private TextView prefMsgLowRingtone_value;
@@ -86,6 +95,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
private SeekBar prefMsgHighVolume;
private ImageView prefMsgHighVolumeTest;
private Button prefBtnImport;
private Button prefBtnExport;
private int musicPickerSwitch = -1;
private MediaPlayer[] mPlayers = new MediaPlayer[3];
@@ -114,6 +126,8 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefUpgradeAccount = v.findViewById(R.id.prefUpgradeAccount);
prefUpgradeAccount_msg = v.findViewById(R.id.prefUpgradeAccount2);
prefUpgradeAccount_info = v.findViewById(R.id.prefUpgradeAccount_info);
prefEnableDeleteSwipe = v.findViewById(R.id.prefEnableDeleteSwipe);
prefPreviewLineCount = v.findViewById(R.id.prefPreviewLineCount);
prefMsgLowEnableSound = v.findViewById(R.id.prefMsgLowEnableSound);
prefMsgLowRingtone_value = v.findViewById(R.id.prefMsgLowRingtone_value);
@@ -147,11 +161,19 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefMsgHighLedColor_value = v.findViewById(R.id.prefMsgHighLedColor_value);
prefMsgHighLedColor_container = v.findViewById(R.id.prefMsgHighLedColor_container);
prefMsgHighEnableVibrations = v.findViewById(R.id.prefMsgHighEnableVibrations);
prefMsgHighForceVolume = v.findViewById(R.id.prefMsgHighForceVolume);
prefMsgHighVolume = v.findViewById(R.id.prefMsgHighVolume);
prefMsgHighVolumeTest = v.findViewById(R.id.btnHighVolumeTest);
prefMsgHighForceVolume = v.findViewById(R.id.prefMsgHighForceVolume);
prefMsgHighVolume = v.findViewById(R.id.prefMsgHighVolume);
prefMsgHighVolumeTest = v.findViewById(R.id.btnHighVolumeTest);
prefBtnExport = v.findViewById(R.id.prefExport);
prefBtnImport = v.findViewById(R.id.prefImport);
ArrayAdapter<Integer> plcsa = new ArrayAdapter<>(v.getContext(), android.R.layout.simple_spinner_item, SCNSettings.CHOOSABLE_CACHE_SIZES);
plcsa.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
prefLocalCacheSize.setAdapter(plcsa);
}
@SuppressLint("SetTextI18n")
private void updateUI()
{
SCNSettings s = SCNSettings.inst();
@@ -159,15 +181,14 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
if (c == null) return;
if (prefAppEnabled.isChecked() != s.Enabled) prefAppEnabled.setChecked(s.Enabled);
if (prefEnableDeleteSwipe.isChecked() != s.EnableDeleteSwipe) prefEnableDeleteSwipe.setChecked(s.EnableDeleteSwipe);
if (!prefPreviewLineCount.getText().toString().equals(Integer.toString(s.PreviewLineCount))) prefPreviewLineCount.setText(Integer.toString(s.PreviewLineCount));
prefUpgradeAccount.setVisibility( SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
prefUpgradeAccount_info.setVisibility(SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
prefUpgradeAccount_msg.setVisibility( SCNSettings.inst().promode_local ? View.VISIBLE : View.GONE );
ArrayAdapter<Integer> plcsa = new ArrayAdapter<>(c, android.R.layout.simple_spinner_item, SCNSettings.CHOOSABLE_CACHE_SIZES);
plcsa.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
prefLocalCacheSize.setAdapter(plcsa);
prefLocalCacheSize.setSelection(getCacheSizeIndex(s.LocalCacheSize));
if (prefLocalCacheSize.getSelectedItemPosition() != getCacheSizeIndex(s.LocalCacheSize)) prefLocalCacheSize.setSelection(getCacheSizeIndex(s.LocalCacheSize));
if (prefMsgLowEnableSound.isChecked() != s.PriorityLow.EnableSound) prefMsgLowEnableSound.setChecked(s.PriorityLow.EnableSound);
if (!prefMsgLowRingtone_value.getText().equals(s.PriorityLow.SoundName)) prefMsgLowRingtone_value.setText(s.PriorityLow.SoundName);
@@ -213,7 +234,14 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
{
SCNSettings s = SCNSettings.inst();
prefAppEnabled.setOnCheckedChangeListener((a,b) -> { s.Enabled=b; saveAndUpdate(); });
prefAppEnabled.setOnCheckedChangeListener((a,b) -> { boolean prev=s.Enabled; s.Enabled=b; saveAndUpdate(); updateEnabled(prev, b); });
prefEnableDeleteSwipe.setOnCheckedChangeListener((a,b) -> { s.EnableDeleteSwipe=b; saveAndUpdate(); });
prefPreviewLineCount.addTextChangedListener(new TextChangedListener<EditText>(prefPreviewLineCount) {
@Override
public void onTextChanged(EditText target, Editable ed) {
if (!ed.toString().isEmpty()) try { s.PreviewLineCount=Integer.parseInt(ed.toString()); saveAndUpdate(); } catch (Exception e) { /* */ }
}
});
prefLocalCacheSize.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
{
@@ -226,6 +254,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefUpgradeAccount.setOnClickListener(a -> onUpgradeAccount());
prefBtnExport.setOnClickListener(a -> onExport());
prefBtnImport.setOnClickListener(a -> onImport());
prefMsgLowEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableSound=b; saveAndUpdate(); });
prefMsgLowRingtone_container.setOnClickListener(a -> chooseRingtoneLow());
prefMsgLowRepeatSound.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.RepeatSound=b; saveAndUpdate(); });
@@ -257,6 +288,67 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefMsgHighVolumeTest.setOnClickListener((v) -> { if (s.PriorityHigh.ForceVolume) playTestSound(2, prefMsgHighVolumeTest, s.PriorityHigh.SoundSource, s.PriorityHigh.ForceVolumeValue); });
}
private void onExport()
{
Context ctxt = getContext();
if (ctxt == null) return;
try
{
File outputDir = ctxt.getCacheDir(); // context being the Activity pointer
File outputFile = File.createTempFile("scn_export_", ".dat", outputDir);
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(outputFile));
Map<String, ?> d1 = ctxt.getSharedPreferences("Config", Context.MODE_PRIVATE).getAll();
Map<String, ?> d2 = ctxt.getSharedPreferences("IAB", Context.MODE_PRIVATE).getAll();
Map<String, ?> d3 = ctxt.getSharedPreferences("CMessageList", Context.MODE_PRIVATE).getAll();
Map<String, ?> d4 = ctxt.getSharedPreferences("QueryLog", Context.MODE_PRIVATE).getAll();
output.writeObject(d1);
output.writeObject(d2);
output.writeObject(d3);
output.writeObject(d4);
Intent intent = new Intent(Intent.ACTION_SEND);
Uri uri = FileProvider.getUriForFile(ctxt, "com.blackforestbytes.simplecloudnotifier.fileprovider", outputFile);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setType("*/*");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "Export"));
}
catch (IOException e)
{
Log.e("Export:Err", e.toString());
SCNApp.showToast("Export failed", Toast.LENGTH_LONG);
}
}
private void onImport()
{
SCNApp.getMainActivity().setContentView(R.layout.activity_main);
Intent intent = new Intent()
.setType("*/*")
.setAction(Intent.ACTION_GET_CONTENT);
((MainActivity)getActivity()).startActivityForResult(Intent.createChooser(intent, "Select a file"), 1991);
}
private void updateEnabled(boolean prev, boolean now)
{
if (!prev && now)
{
SCNApp.showToast("SimpleCloudNotifier is now enabled", Toast.LENGTH_SHORT);
}
else if (prev && !now)
{
SCNApp.showToast("SimpleCloudNotifier is now disabled\nYou won't recieve new messages.", Toast.LENGTH_LONG);
}
}
private void updateVolume(int idx, int volume)
{
if (mPlayers[idx] != null && mPlayers[idx].isPlaying())
@@ -324,6 +416,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
{
SCNSettings.inst().save();
updateUI();
SCNApp.getMainActivity().adpTabs.tab1.updateDeleteSwipeEnabled();
}
private void onUpgradeAccount()
@@ -333,11 +426,11 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
public void updateProState()
{
Purchase p = IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE);
boolean pmode = IABService.inst().getPurchaseCachedSimple(IABService.IAB_PRO_MODE);
if (prefUpgradeAccount != null) prefUpgradeAccount.setVisibility( p != null ? View.GONE : View.VISIBLE);
if (prefUpgradeAccount_info != null) prefUpgradeAccount_info.setVisibility(p != null ? View.GONE : View.VISIBLE);
if (prefUpgradeAccount_msg != null) prefUpgradeAccount_msg.setVisibility( p != null ? View.VISIBLE : View.GONE );
if (prefUpgradeAccount != null) prefUpgradeAccount.setVisibility( pmode ? View.GONE : View.VISIBLE);
if (prefUpgradeAccount_info != null) prefUpgradeAccount_info.setVisibility(pmode ? View.GONE : View.VISIBLE);
if (prefUpgradeAccount_msg != null) prefUpgradeAccount_msg.setVisibility( pmode ? View.VISIBLE : View.GONE );
}
private int getCacheSizeIndex(int value)

View File

@@ -35,7 +35,7 @@ public class TabAdapter extends FragmentStatePagerAdapter {
{
switch (position)
{
case 0: return "Notifications";
case 0: return "Messages";
case 1: return "Account";
case 2: return "Settings";
default: return null;

View File

@@ -0,0 +1,44 @@
package com.blackforestbytes.simplecloudnotifier.view.debug;
import android.content.Intent;
import android.os.Bundle;
import com.blackforestbytes.simplecloudnotifier.model.QueryLog;
import com.blackforestbytes.simplecloudnotifier.model.SingleQuery;
import androidx.appcompat.app.AppCompatActivity;
import android.widget.ListView;
import com.blackforestbytes.simplecloudnotifier.R;
public class QueryLogActivity extends AppCompatActivity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_querylog);
ListView lvMain = findViewById(R.id.lvQueryList);
SingleQuery[] arr = QueryLog.inst().get().toArray(new SingleQuery[0]);
QueryLogAdapter a = new QueryLogAdapter(this, arr);
lvMain.setAdapter(a);
lvMain.setOnItemClickListener((parent, view, position, id) ->
{
if (position >= 0 && position < arr.length)
{
Intent i = new Intent(QueryLogActivity.this, SingleQueryLogActivity.class);
Bundle b = new Bundle();
arr[position].save(b, "data");
i.putExtra("query", b);
startActivity(i);
}
});
}
}

View File

@@ -0,0 +1,66 @@
package com.blackforestbytes.simplecloudnotifier.view.debug;
import android.content.Context;
import android.graphics.Color;
import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.model.SingleQuery;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
public class QueryLogAdapter extends ArrayAdapter<SingleQuery>
{
public static DateTimeFormatter UI_FULLTIME_FORMATTER = DateTimeFormat.forPattern("HH:mm:ss");
public QueryLogAdapter(@NonNull Context context, @NonNull SingleQuery[] objects)
{
super(context, R.layout.adapter_querylog, objects);
}
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent)
{
View v = convertView;
if (v == null) {
LayoutInflater vi;
vi = LayoutInflater.from(getContext());
v = vi.inflate(R.layout.adapter_querylog, parent, false);
}
SingleQuery p = getItem(position);
if (p != null)
{
TextView tt1 = v.findViewById(R.id.list_item_debuglogrow_time);
if (tt1 != null) tt1.setText(p.Timestamp.toString(UI_FULLTIME_FORMATTER));
if (tt1 != null) tt1.setTextColor(Color.BLACK);
TextView tt2 = v.findViewById(R.id.list_item_debuglogrow_level);
if (tt2 != null) tt2.setText(p.Level.toUIString());
if (tt2 != null) tt2.setTextColor(Color.BLACK);
TextView tt3 = v.findViewById(R.id.list_item_debuglogrow_info);
if (tt3 != null) tt3.setText("");
if (tt3 != null) tt3.setTextColor(Color.BLUE);
TextView tt4 = v.findViewById(R.id.list_item_debuglogrow_id);
if (tt4 != null) tt4.setText(p.Name);
if (tt4 != null) tt4.setTextColor(p.Level.getColor());
TextView tt5 = v.findViewById(R.id.list_item_debuglogrow_message);
if (tt5 != null) tt5.setText(p.ExceptionString.length()> 40 ? p.ExceptionString.substring(0, 40-3)+"..." : p.ExceptionString);
if (tt5 != null) tt5.setTextColor(p.Level.getColor());
}
return v;
}
}

View File

@@ -0,0 +1,38 @@
package com.blackforestbytes.simplecloudnotifier.view.debug;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.widget.TextView;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.lib.string.CompactJsonFormatter;
import com.blackforestbytes.simplecloudnotifier.model.SingleQuery;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.Objects;
public class SingleQueryLogActivity extends AppCompatActivity
{
@Override
@SuppressLint("SetTextI18n")
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_singlequerylog);
SingleQuery q = SingleQuery.load(getIntent().getBundleExtra("query"), "data");
this.<TextView>findViewById(R.id.tvQL_Timestamp).setText(q.Timestamp.toString(DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss")));
this.<TextView>findViewById(R.id.tvQL_Level).setText(q.Level.toUIString());
this.<TextView>findViewById(R.id.tvQL_Level).setTextColor(q.Level.getColor());
this.<TextView>findViewById(R.id.tvQL_Name).setText(q.Name);
this.<TextView>findViewById(R.id.tvQL_URL).setText(q.URL.replace("?", "\r\n?").replace("&", "\r\n&"));
this.<TextView>findViewById(R.id.tvQL_Response).setText(CompactJsonFormatter.formatJSON(q.Response, 999));
this.<TextView>findViewById(R.id.tvQL_ResponseCode).setText(Integer.toString(q.ResponseCode));
this.<TextView>findViewById(R.id.tvQL_ExceptionString).setText(q.ExceptionString);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@android:color/white" />
<stroke android:width="1dip" android:color="#888888"/>
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,15 +0,0 @@
<vector android:height="24dp" android:viewportHeight="1000"
android:viewportWidth="1000" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000"
android:pathData="M500,500m-500,0a500,500 0,1 1,1000 0a500,500 0,1 1,-1000 0"
android:strokeColor="#000000" android:strokeWidth="1"/>
<path android:fillColor="#ffffff" android:pathData="M300,694L700,694L500,136L300,694"/>
<path android:fillColor="#ffffff" android:pathData="M473,559L527,559L527,774L473,774"/>
<path android:fillColor="#000000" android:pathData="M376,640L624,640L500,295L376,640"/>
<path android:fillColor="#ffffff" android:pathData="M100,730L500,730L300,172L100,730"/>
<path android:fillColor="#000000" android:pathData="M176,676L424,676L300,331L176,676"/>
<path android:fillColor="#ffffff" android:pathData="M273,595L327,595L327,810L273,810"/>
<path android:fillColor="#ffffff" android:pathData="M500,730L900,730L700,172L500,730"/>
<path android:fillColor="#000000" android:pathData="M576,676L824,676L700,331L576,676"/>
<path android:fillColor="#ffffff" android:pathData="M673,595L727,595L727,810L673,810"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12sp"
android:height="12sp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@@ -0,0 +1,16 @@
<vector
android:height="12sp"
android:width="12sp"
android:viewportHeight="53"
android:viewportWidth="53"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FFFFFF"
android:pathData="M42.943,6H33.5V3c0,-1.654 -1.346,-3 -3,-3h-8c-1.654,0 -3,1.346 -3,3v3h-9.443C8.096,6 6.5,7.596 6.5,9.557V14h2h36h2V9.557C46.5,7.596 44.904,6 42.943,6zM31.5,6h-10V3c0,-0.552 0.449,-1 1,-1h8c0.551,0 1,0.448 1,1V6z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M8.5,49.271C8.5,51.327 10.173,53 12.229,53h28.541c2.057,0 3.729,-1.673 3.729,-3.729V16h-36V49.271z"/>
</vector>

View File

@@ -1,31 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:id="@+id/layoutRoot"
<RelativeLayout android:id="@+id/layoutRoot"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:showIn="@layout/activity_main">
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
app:titleTextColor="@color/colorOnPrimary"
android:background="?attr/colorPrimary"
android:elevation="6dp"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
android:minHeight="?attr/actionBarSize" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/toolbar"
app:titleTextColor="@color/colorOnPrimary"
app:tabTextColor="@color/colorOnPrimary"
app:tabSelectedTextColor="@color/colorSecondary"
android:background="?attr/colorPrimary"
android:elevation="6dp"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>
android:minHeight="?attr/actionBarSize" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/pager"

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:id="@+id/layoutRoot"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/lvQueryList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View File

@@ -0,0 +1,240 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:padding="4sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAlignment="center"
android:textStyle="bold"
android:layout_margin="2dip"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:text="Server Query" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_margin="2dip"
android:layout_height="wrap_content"
android:layout_width="100dp"
android:text="Timestamp" />
<com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView
app:maxHeightOverride="100"
android:background="@drawable/simple_black_border"
android:layout_margin="2dip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<TextView
android:id="@+id/tvQL_Timestamp"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="1dip"
android:textIsSelectable="true"
android:text="" />
</com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_margin="2dip"
android:layout_height="wrap_content"
android:layout_width="100dp"
android:text="Level" />
<com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView
app:maxHeightOverride="100"
android:background="@drawable/simple_black_border"
android:layout_margin="2dip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<TextView
android:id="@+id/tvQL_Level"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="1dip"
android:textIsSelectable="true"
android:text="" />
</com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_margin="2dip"
android:layout_height="wrap_content"
android:layout_width="100dp"
android:text="Name" />
<com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView
app:maxHeightOverride="100"
android:layout_margin="2dip"
android:background="@drawable/simple_black_border"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<TextView
android:id="@+id/tvQL_Name"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="1dip"
android:textIsSelectable="true"
android:text="" />
</com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_margin="2dip"
android:layout_height="wrap_content"
android:layout_width="100dp"
android:text="URL" />
<com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView
app:maxHeightOverride="100"
android:layout_margin="2dip"
android:background="@drawable/simple_black_border"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<TextView
android:id="@+id/tvQL_URL"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="1dip"
android:textIsSelectable="true"
android:text=""/>
</com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_margin="2dip"
android:layout_height="wrap_content"
android:layout_width="100dp"
android:text="ResponeCode" />
<com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView
app:maxHeightOverride="64"
android:layout_margin="2dip"
android:background="@drawable/simple_black_border"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<TextView
android:id="@+id/tvQL_ResponseCode"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="1dip"
android:textIsSelectable="true"
android:text="" />
</com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_margin="2dip"
android:layout_height="wrap_content"
android:layout_width="100dp"
android:text="Response" />
<com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView
app:maxHeightOverride="100"
android:layout_margin="2dip"
android:background="@drawable/simple_black_border"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<TextView
android:id="@+id/tvQL_Response"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="1dip"
android:textIsSelectable="true"
android:text="" />
</com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_margin="2dip"
android:layout_height="wrap_content"
android:layout_width="100dp"
android:text="Exception" />
<com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView
app:maxHeightOverride="100"
android:layout_margin="2dip"
android:background="@drawable/simple_black_border"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<TextView
android:id="@+id/tvQL_ExceptionString"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="1dip"
android:textIsSelectable="true"
android:text="" />
</com.blackforestbytes.simplecloudnotifier.util.MaxHeightScrollView>
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list_item_imagerow"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<LinearLayout
android:orientation="vertical"
android:layout_width="96dp"
android:layout_height="match_parent">
<TextView
android:id="@+id/list_item_debuglogrow_time"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/list_item_debuglogrow_level"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/list_item_debuglogrow_info"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/list_item_debuglogrow_id"
android:textStyle="bold"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/list_item_debuglogrow_message"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>

View File

@@ -198,19 +198,27 @@
app:layout_constraintBottom_toTopOf="@+id/btnAccountReset"
app:layout_constraintTop_toBottomOf="@+id/ic_img_quota" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/btnAccountReset"
app:cornerRadius="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:backgroundTint="#fa315b"
android:text="@string/str_reset_account"
app:layout_constraintBottom_toTopOf="@+id/btnClearLocalStorage" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearLocalStorage"
app:cornerRadius="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Clear Messages"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:backgroundTint="#607D8B"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -79,6 +79,62 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:id="@+id/prefEnableDeleteSwipe"
android:text="@string/str_deleteswipe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp">
<TextView
android:id="@+id/tvPreviewLineCount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/str_previewlinecount"
android:textColor="#000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/prefPreviewLineCount"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:minWidth="64dp"
android:id="@+id/prefPreviewLineCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="number"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:importantForAutofill="no"
tools:ignore="LabelFor,UnusedAttribute" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
@@ -95,8 +151,10 @@
android:gravity="center|center"
android:minHeight="48dp">
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/prefUpgradeAccount"
app:cornerRadius="0dp"
android:backgroundTint="#4CAF50"
android:text="@string/str_upgrade_account"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
@@ -747,6 +805,24 @@
</androidx.cardview.widget.CardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/prefExport"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:cornerRadius="0dp"
android:backgroundTint="#444444"
android:text="@string/export_settings" />
<com.google.android.material.button.MaterialButton
android:id="@+id/prefImport"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
app:cornerRadius="0dp"
android:backgroundTint="#666666"
android:text="@string/import_settings" />
</LinearLayout>
</ScrollView>

View File

@@ -108,6 +108,43 @@
android:paddingTop="3dp"
android:contentDescription="@string/desc_priority_icon" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnShare"
style="@style/Widget.MaterialComponents.Button.Icon"
app:cornerRadius="0dp"
app:icon="@drawable/ic_share_small"
android:layout_width="wrap_content"
android:layout_height="25dp"
android:layout_marginEnd="8sp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:insetLeft="0dp"
android:insetRight="0dp"
android:textSize="12sp"
card_view:layout_constraintTop_toBottomOf="@id/tvMessage"
app:layout_constraintRight_toLeftOf="@id/btnDelete"
app:layout_constraintBottom_toBottomOf="parent"
android:backgroundTint="#03A9F4"
android:text="Share" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDelete"
style="@style/Widget.MaterialComponents.Button.Icon"
app:cornerRadius="0dp"
app:icon="@drawable/ic_trash_small"
android:layout_width="wrap_content"
android:layout_height="25dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:insetLeft="0dp"
android:insetRight="0dp"
android:textSize="12sp"
card_view:layout_constraintTop_toBottomOf="@id/tvMessage"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:backgroundTint="#F44336"
android:text="Delete"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MaxHeightScrollView">
<attr name="maxHeightOverride" format="integer" />
</declare-styleable>
</resources>

View File

@@ -6,6 +6,9 @@
<color name="colorHeader">#3F51B5</color>
<color name="colorHeaderForeground">#FFFFFF</color>
<color name="colorOnPrimary">#ecf0f1</color>
<color name="colorSecondary">#FF5722</color>
<color name="colorBlack">#000</color>
<color name="bg_row_background">#fa315b</color>

View File

@@ -6,4 +6,5 @@
<dimen name="padd_10">10dp</dimen>
<dimen name="ic_delete">30dp</dimen>
<dimen name="thumbnail">90dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@@ -11,13 +11,13 @@
<string name="ic_img_fuel_desc">Icon Fuel</string>
<string name="str_qr_code">QR Code</string>
<string name="str_reset_account">Reset Account</string>
<string name="no_notifications">No notifications</string>
<string name="no_notifications">No messages</string>
<string name="str_not_connected">not connected</string>
<string name="str_reload">reload</string>
<string name="desc_priority_icon">Priority icon</string>
<string name="str_common_settings">Common Settings</string>
<string name="str_enabled">Enabled</string>
<string name="str_localcachesize">Remember the last x notifications locally</string>
<string name="str_localcachesize">Remember the last x messages locally</string>
<string name="str_header_prio0">Notifications (priority 0 - Low)</string>
<string name="str_header_prio1">Notifications (priority 1 - Normal)</string>
<string name="str_header_prio2">Notifications (priority 2 - High)</string>
@@ -29,9 +29,14 @@
<string name="str_ledcolor">Notification light color</string>
<string name="str_enable_vibration">Enable notification vibration</string>
<string name="str_upgrade_account">Upgrade account</string>
<string name="str_deleteswipe">Delete messages by swiping left</string>
<string name="str_previewlinecount">Number of visibile lines in collapsed messages</string>
<string name="str_promode">Thank you for supporting the app and using the pro mode</string>
<string name="str_promode_info">Increase your daily quota, remove the ad banner and support the developer (that\'s me)</string>
<string name="volume_icon">Volume icon</string>
<string name="play_test_sound">Play test sound</string>
<string name="delete">DELETE</string>
<string name="title_activity_query_log">QueryLogActivity</string>
<string name="import_settings">Import settings</string>
<string name="export_settings">Export settings</string>
</resources>

View File

@@ -1,13 +1,20 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- your app branding color for the app bar -->
<item name="colorPrimary">#3F51B5</item>
<!-- darker variant for the status bar and contextual app bars -->
<item name="colorPrimaryDark">#303F9F</item>
<!-- theme UI controls like checkboxes and text fields -->
<item name="colorAccent">#FF4081</item>
<item name="colorAccent">#FF5722</item>
<item name="colorSecondary">#FF5722</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light" />
</resources>

View File

@@ -0,0 +1,7 @@
<paths>
<files-path path="/" name="files" />
<cache-path path="/" name="cache" />
<external-path path="/" name="external" />
<external-files-path path="/" name="external-files" />
<external-cache-path path="/" name="external-cache" />
</paths>

View File

@@ -1,3 +1,3 @@
#Sat Nov 17 03:25:30 CET 2018
VERSION_NAME=0.0.8
VERSION_CODE=8
#Thu Mar 05 15:29:10 UTC 2020
VERSION_NAME=1.8.0
VERSION_CODE=23

View File

@@ -7,8 +7,8 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.google.gms:google-services:4.3.4'
}
}

View File

@@ -1,6 +1,6 @@
#Wed Sep 26 22:10:14 CEST 2018
#Tue Nov 03 14:10:19 CET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

58
androidExportReader/.gitignore vendored Normal file
View File

@@ -0,0 +1,58 @@
# Created by https://www.toptal.com/developers/gitignore/api/java,gradle
# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle
### Java ###
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### Gradle ###
.gradle
**/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### Gradle Patch ###
# Java heap dump
*.hprof
# End of https://www.toptal.com/developers/gitignore/api/java,gradle

8
androidExportReader/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

6
androidExportReader/.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="18" />
</component>
</project>

17
androidExportReader/.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -0,0 +1,11 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JavadocReference" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="WARNING_ATTRIBUTES" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
</component>
</project>

10
androidExportReader/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_18" default="true" project-jdk-name="openjdk-18" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
androidExportReader/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,47 @@
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2'
}
}
plugins {
id 'java'
id("com.github.johnrengelman.shadow") version "7.1.2"
id 'application'
}
group 'com.blackforestbytes'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
maven { url "https://jitpack.io" }
}
application {
mainClass = 'com.blackforestbytes.Main'
}
jar {
manifest {
attributes 'Main-Class': application.mainClass
}
}
tasks.jar {
manifest.attributes["Main-Class"] = application.mainClass
}
dependencies {
implementation 'com.github.RalleYTN:SimpleJSON:2.1.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

240
androidExportReader/gradlew vendored Executable file
View File

@@ -0,0 +1,240 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

91
androidExportReader/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,91 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,2 @@
rootProject.name = 'androidExportReader'

View File

@@ -0,0 +1,104 @@
package com.blackforestbytes;
import de.ralleytn.simple.json.JSONArray;
import de.ralleytn.simple.json.JSONFormatter;
import de.ralleytn.simple.json.JSONObject;
import java.io.ObjectInputStream;
import java.net.URI;
import java.nio.file.FileSystems;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class Main {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("call with ./androidExportConvert scn_export.dat");
return;
}
try {
var path = FileSystems.getDefault().getPath(args[0]).normalize().toAbsolutePath().toUri().toURL();
ObjectInputStream stream = new ObjectInputStream(path.openStream());
Map<String, ?> d1 = new HashMap<>((Map<String, ?>)stream.readObject());
Map<String, ?> d2 = new HashMap<>((Map<String, ?>)stream.readObject());
Map<String, ?> d3 = new HashMap<>((Map<String, ?>)stream.readObject());
Map<String, ?> d4 = new HashMap<>((Map<String, ?>)stream.readObject());
stream.close();
JSONObject root = new JSONObject();
var subConfig = new JSONObject();
var subIAB = new JSONArray();
var subCMessageList = new JSONArray();
var subAcks = new JSONArray();
var subQueryLog = new JSONArray();
for (Map.Entry<String, ?> entry : d1.entrySet())
{
if (entry.getValue() instanceof String) subConfig.put(entry.getKey(), (String)entry.getValue());
if (entry.getValue() instanceof Boolean) subConfig.put(entry.getKey(), (Boolean)entry.getValue());
if (entry.getValue() instanceof Float) subConfig.put(entry.getKey(), (Float)entry.getValue());
if (entry.getValue() instanceof Integer) subConfig.put(entry.getKey(), (Integer)entry.getValue());
if (entry.getValue() instanceof Long) subConfig.put(entry.getKey(), (Long)entry.getValue());
if (entry.getValue() instanceof Set<?>) subConfig.put(entry.getKey(), ((Set<String>)entry.getValue()).toArray());
}
for (int i = 0; i < (Integer)d2.get("c"); i++) {
var obj = new JSONObject();
obj.put("key", d2.get("["+i+"]->key"));
obj.put("value", d2.get("["+i+"]->value"));
subIAB.add(obj);
}
for (int i = 0; i < (Integer)d3.get("message_count"); i++) {
if (d3.get("message["+i+"].scnid") == null)
throw new Exception("ONF");
var obj = new JSONObject();
obj.put("timestamp", d3.get("message["+i+"].timestamp"));
obj.put("title", d3.get("message["+i+"].title"));
obj.put("content", d3.get("message["+i+"].content"));
obj.put("priority", d3.get("message["+i+"].priority"));
obj.put("scnid", d3.get("message["+i+"].scnid"));
subCMessageList.add(obj);
}
subAcks.addAll(((Set<String>)d3.get("acks")).stream().map(p -> Long.decode("0x"+p)).toList());
for (int i = 0; i < (Integer)d4.get("history_count"); i++) {
if (d4.get("message["+(i+1000)+"].Name") == null)
throw new Exception("ONF");
var obj = new JSONObject();
obj.put("Level", d4.get("message["+(i+1000)+"].Level"));
obj.put("Timestamp", d4.get("message["+(i+1000)+"].Timestamp"));
obj.put("Name", d4.get("message["+(i+1000)+"].Name"));
obj.put("URL", d4.get("message["+(i+1000)+"].URL"));
obj.put("Response", d4.get("message["+(i+1000)+"].Response"));
obj.put("ResponseCode", d4.get("message["+(i+1000)+"].ResponseCode"));
obj.put("ExceptionString", d4.get("message["+(i+1000)+"].ExceptionString"));
subQueryLog.add(obj);
}
root.put("config", subConfig);
root.put("iab", subIAB);
root.put("cmessagelist", subCMessageList);
root.put("acks", subAcks);
root.put("querylog", subQueryLog);
System.out.println(new JSONFormatter().format(root.toString()));
} catch (Exception e) {
e.printStackTrace();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
data/README/badge_apple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
data/icon_512_nobox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
data/phone.pdn Normal file

Binary file not shown.

BIN
data/phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

35
examples/scn_send.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
/**
* @param string $title
* @param string $content
* @param int $priority
* @return bool
*/
function sendSCN($title, $content, $priority) {
global $config;
$data =
[
'user_id' => '', //TODO set your userid
'user_key' => '', //TODO set your userkey
'title' => $title,
'content' => $content,
'priority' => $priority,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://simplecloudnotifier.blackforestbytes.com/send.php");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
if ($result === false) return false;
$json = json_decode($result, true);
return $json['success'];
}

View File

@@ -21,6 +21,7 @@ user_key="????????????????????????????????????????????????????????????????"
title=$1
content=""
sendtime=$(date +%s)
if [ "$#" -gt 1 ]; then
content=$2
@@ -37,7 +38,7 @@ usr_msg_id=$(uuidgen)
while true ; do
curlresp=$(curl -s -o /dev/null -w "%{http_code}" \
-d "user_id=$user_id" -d "user_key=$user_key" -d "title=$title" \
-d "user_id=$user_id" -d "user_key=$user_key" -d "title=$title" -d "timestamp=$sendtime" \
-d "content=$content" -d "priority=$priority" -d "msg_id=$usr_msg_id" \
https://scn.blackforestbytes.com/send.php)

96
scnserver/.gitignore vendored Normal file
View File

@@ -0,0 +1,96 @@
_build
.run-data
DOCKER_GIT_INFO
scn_export.dat
scn_export.json
identifier.sqlite
.idea/dataSources.xml
##############
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/aws.xml
.idea/**/contentModel.xml
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
.idea/**/mongoSettings.xml
.idea/**/sonarlint/
.idea/**/sonarIssues.xml
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
.idea/**/azureSettings.xml
.idea/replstate.xml
.idea/sonarlint/
.idea/httpRequests
.idea/caches/build_file_checksums.ser
.idea/$CACHE_FILE$
.idea/codestream.xml
.idea_modules/
cmake-build-*/
*.iws
out/
atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
*.icloud

40
scnserver/.golangci.yml Normal file
View File

@@ -0,0 +1,40 @@
# https://golangci-lint.run/usage/configuration/
run:
go: '1.20'
linters:
enable-all: true
disable:
- golint # deprecated
- exhaustivestruct # deprecated
- deadcode # deprecated
- scopelint # deprecated
- structcheck # deprecated
- varcheck # deprecated
- nosnakecase # deprecated
- maligned # deprecated
- interfacer # deprecated
- ifshort # deprecated
- dupl # (i disagree)
- ireturn # (i disagree)
- wrapcheck # (waiting for bferr)
- goerr113 # (waiting for bferr)
- varnamelen # (too many false-positives)
- gomnd # (i disagree)
- depguard # (not configured)
- gofumpt # (we do not use gofumpt)
- gci # (we do no use gci)
- lll # (i disagree)
- gochecknoglobals # (i disagree)
issues:
exclude-rules:
- path: api/handler/.*.go
linters:
- funlen
linters-settings:
tagalign:
align: true
sort: false

8
scnserver/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="SqlRedundantOrderingDirectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

8
scnserver/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/server.iml" filepath="$PROJECT_DIR$/.idea/server.iml" />
</modules>
</component>
</project>

21
scnserver/.idea/server.iml generated Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true">
<buildTags>
<option name="customFlags">
<array>
<option value="timetzdata" />
<option value="sqlite_fts5" />
<option value="sqlite_foreign_keys" />
</array>
</option>
</buildTags>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/_pygments" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

11
scnserver/.idea/sqldialects.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/db/schema/primary_3.ddl" dialect="SQLite" />
<file url="PROJECT" dialect="SQLite" />
</component>
<component name="SqlResolveMappings">
<file url="file://$PROJECT_DIR$" scope="{&quot;node&quot;:{ &quot;@negative&quot;:&quot;1&quot;, &quot;group&quot;:{ &quot;@kind&quot;:&quot;root&quot;, &quot;node&quot;:{ &quot;name&quot;:{ &quot;@qname&quot;:&quot;b3228d61-4c36-41ce-803f-63bd80e198b3&quot; }, &quot;group&quot;:{ &quot;@kind&quot;:&quot;schema&quot;, &quot;node&quot;:{ &quot;name&quot;:{ &quot;@qname&quot;:&quot;schema_3.0.ddl&quot; } } } } } }}" />
<file url="PROJECT" scope="" />
</component>
</project>

6
scnserver/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

34
scnserver/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
FROM golang:1-bullseye AS builder
RUN apt-get update && \
apt-get install -y ca-certificates openssl make git tar coreutils && \
rm -rf /var/lib/apt/lists/*
COPY . /buildsrc
RUN cd /buildsrc && make build
FROM debian:bookworm
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates && \
apt-get install -y --no-install-recommends tzdata && \
rm -rf /var/cache/apt/archives && \
rm -rf /var/lib/apt/lists
COPY --from=builder /buildsrc/_build/scn_backend /app/server
RUN mkdir /data
WORKDIR /app
EXPOSE 80
CMD ["/app/server"]

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