Compare commits
231 Commits
v1.2.0
...
reactnativ
Author | SHA1 | Date | |
---|---|---|---|
5b381eb3b9
|
|||
9f3e183d72
|
|||
51f5f1005a
|
|||
0a380f861e
|
|||
b712ad3488
|
|||
9f656bdefe
|
|||
a4a651229c
|
|||
4773800f23
|
|||
bef0b8189e
|
|||
674714f0f3
|
|||
ee9e858584
|
|||
165c6d8614
|
|||
8a6719fc19
|
|||
308361a834
|
|||
44df964f6f
|
|||
56bf266919
|
|||
f3658d6636
|
|||
1bb37eec30
|
|||
59511b2345
|
|||
5b7bc02c61
|
|||
b329f537e7
|
|||
5879e81759
|
|||
f4e88bef77
|
|||
b3ec45309c
|
|||
2fbc892898
|
|||
c46190c3fc
|
|||
860e540de1
|
|||
8cde286cac
|
|||
90830fe384
|
|||
686f89f75d
|
|||
4210af5680
|
|||
aefc368cfd
|
|||
67218d8045
|
|||
c05deb3a41
|
|||
43d0107fb5
|
|||
ece7612f9d
|
|||
a9809d90cb
|
|||
bbc9a79996
|
|||
b71f1885ec
|
|||
885aad2047
|
|||
7121afab08
|
|||
d9a4c4ffd6
|
|||
fb826919a6
|
|||
22720169a2
|
|||
7fefd251db
|
|||
5de4f67344
|
|||
d396a12d68
|
|||
3888c91a6b
|
|||
562bac6987
|
|||
e825b4dd85
|
|||
08587b7a7a
|
|||
0daca2cf8f
|
|||
3a9b15c2be
|
|||
e9b4db0f1c
|
|||
312a31ce9e
|
|||
d4a8a2e720
|
|||
dcb4f253d8
|
|||
d0a04bae84
|
|||
34ac96edd7
|
|||
b42ce84c3e
|
|||
2db779b44f
|
|||
397bfe78aa
|
|||
efaad3f97c
|
|||
624c613bd1
|
|||
07b0632c95
|
|||
3d1e6cfa17
|
|||
3db636d41a
|
|||
2053b8f07f
|
|||
b1681b53e4
|
|||
03f60ff316
|
|||
b2df0a5a02
|
|||
8826cb0312
|
|||
a0c72f5b94
|
|||
7d9a58ae54
|
|||
fd72b512f8
|
|||
28c2721036
|
|||
a1788bf75a
|
|||
b1bd278f9b
|
|||
16f6ab4861
|
|||
01934e29b1
|
|||
d1cefb0150
|
|||
27b189d33a
|
|||
e05d88682a
|
|||
2a5f1f5f7e
|
|||
e7a45d9a05
|
|||
ec9a326002
|
|||
23c7729fcf
|
|||
7fcd324299
|
|||
1633449638
|
|||
57231a1406
|
|||
2eb6292733
|
|||
ff24493ff3
|
|||
3d602af135
|
|||
590665a5e9
|
|||
89fd0dfed7
|
|||
82bc887767
|
|||
acd7de0dee
|
|||
e737cd9d5c
|
|||
0ec7a9d274
|
|||
e49d9159e4
|
|||
3343285761
|
|||
14bba38324
|
|||
679277d59e
|
|||
cebb2ae2b6
|
|||
56d9f977ae
|
|||
984470b47d
|
|||
0112d681ac
|
|||
0cb2a977a0
|
|||
f65c231ba0
|
|||
dbc014f819
|
|||
bbf7962e29
|
|||
2b4d77bab4
|
|||
8582674b44
|
|||
f7675be834
|
|||
00d77e508d
|
|||
e90cfe34e9
|
|||
54dfd535a4
|
|||
5a02eb6d18
|
|||
97fc9319d1
|
|||
03b4acd13e
|
|||
86f06a3c6a
|
|||
06e8d2a6e2
|
|||
99f248a8ce
|
|||
c7aaa6ad98
|
|||
cb5ce66c1a
|
|||
0750bf1d8a
|
|||
203360e8b5
|
|||
ef1844109f
|
|||
de6ad35f60
|
|||
fbb289dedf
|
|||
f1e87170f0
|
|||
66ecad27a7
|
|||
98b1e8bd80
|
|||
26cd1533b4
|
|||
3692b915f3
|
|||
06788c3e12
|
|||
edfcdd1135
|
|||
dd2f3baa0c
|
|||
7db70e392b
|
|||
0cae24a612
|
|||
8db0fa37db
|
|||
d27e3d9a91
|
|||
fa5a4107a6
|
|||
234188c4d4
|
|||
9b700581f3
|
|||
12db23d076
|
|||
fd182f0abb
|
|||
7eab74e65c
|
|||
e0ecd4d9ff
|
|||
1ca09c16d3
|
|||
a7df476e79
|
|||
4e5eac6178
|
|||
91a6808ad2
|
|||
11a6517156
|
|||
7aa7eb234d
|
|||
62d7df9710
|
|||
0ff1188c3d
|
|||
b6e8d037a0
|
|||
7a11b2c76f
|
|||
7f56dbdbfa
|
|||
df4eb15df8
|
|||
ac9ae06cc8
|
|||
464cf3ec7e
|
|||
bf0ce5c963
|
|||
3a0c65a849
|
|||
6d80638cf8
|
|||
37e09d6532
|
|||
8ea3fdcfef
|
|||
1bc847cdc9
|
|||
03c35d6446
|
|||
d5aea1a828
|
|||
f17ddb4ace
|
|||
0cc6e27267
|
|||
ca58aa782d
|
|||
e8671e8650
|
|||
d46601be5c
|
|||
d30e2cefc0
|
|||
08a93551e7
|
|||
c2899fd727
|
|||
5ec66e1777
|
|||
516809cd02
|
|||
0d3526221d
|
|||
728b12107f
|
|||
b56c021356
|
|||
80f3b982d2
|
|||
0d641b727f
|
|||
8278c059ad
|
|||
7af0ff5413
|
|||
5c2877bdb8
|
|||
85bfe79115
|
|||
fb37f94c0a
|
|||
e53f40866e
|
|||
650ba20e5d
|
|||
6e01c41c22
|
|||
f555f0f1cf
|
|||
35ef2175bc
|
|||
55f53deadf
|
|||
5991631bfa
|
|||
34a27d9ca4
|
|||
1671490485
|
|||
0e58a5c5f0
|
|||
![]() |
bd11d7973c | ||
f3b5b09ed0
|
|||
ce641bf7d2
|
|||
f1c7314dca
|
|||
019408dc6d
|
|||
![]() |
cdba3540c2 | ||
885d997ff3
|
|||
5118ab3cbf
|
|||
![]() |
2812377f5c | ||
93b40a9c7f
|
|||
b95ddcc811
|
|||
90ba3c1134
|
|||
05174958b2
|
|||
![]() |
24be9b2013 | ||
29ce4b727c
|
|||
![]() |
f178019ffe | ||
0a1b948042
|
|||
![]() |
74cbfb235e | ||
63c4104a89
|
|||
2597287b1e
|
|||
![]() |
316156f0f0 | ||
5286e869cc
|
|||
d6dcf28d89
|
|||
1d983b9ac0
|
|||
e525221010
|
|||
4cde4703f2
|
|||
4a07e58c16
|
|||
b12356575a
|
|||
77f571de7d
|
|||
![]() |
f5eef7563b |
43
.gitea/workflows/build_and_deploy.yml
Normal file
43
.gitea/workflows/build_and_deploy.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
# https://docs.gitea.com/next/usage/actions/quickstart
|
||||||
|
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
|
||||||
|
# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||||
|
|
||||||
|
name: Build Docker and Deploy
|
||||||
|
run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['master']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_job:
|
||||||
|
name: Build Docker Container
|
||||||
|
runs-on: bfb-cicd-latest
|
||||||
|
steps:
|
||||||
|
- run: echo -n "${{ secrets.DOCKER_REG_PASS }}" | docker login registry.blackforestbytes.com -u docker --password-stdin
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- run: cd "${{ gitea.workspace }}/scnserver" && make clean
|
||||||
|
- run: cd "${{ gitea.workspace }}/scnserver" && make docker
|
||||||
|
- run: cd "${{ gitea.workspace }}/scnserver" && make push-docker
|
||||||
|
|
||||||
|
deploy_job:
|
||||||
|
name: Deploy to Server
|
||||||
|
needs: [build_job]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Execute deploy on remote (via ssh)
|
||||||
|
uses: appleboy/ssh-action@v1.0.0
|
||||||
|
with:
|
||||||
|
host: simplecloudnotifier.de
|
||||||
|
username: bfb-deploy-bot
|
||||||
|
port: 4477
|
||||||
|
key: "${{ secrets.SSH_KEY_BFBDEPLOYBOT }}"
|
||||||
|
script: cd /var/docker/deploy-scripts/simplecloudnotifier && ./deploy.sh master "${{ gitea.sha }}" || exit 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
85
android/.idea/assetWizardSettings.xml
generated
85
android/.idea/assetWizardSettings.xml
generated
@@ -1,85 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="WizardSettings">
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="imageWizard">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="imageAssetPanel">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="launcherLegacy">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="assetType" value="IMAGE" />
|
|
||||||
<entry key="cropped" value="true" />
|
|
||||||
<entry key="iconShape" value="NONE" />
|
|
||||||
<entry key="imageAsset" value="F:\Eigene Dateien\Dropbox\Programming\Java\AndroidStudioProjects\SimpleCloudNotifier\data\icon_512_nobox.png" />
|
|
||||||
<entry key="outputName" value="ic_notification_full" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="notification">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="assetType" value="IMAGE" />
|
|
||||||
<entry key="imageAsset" value="F:\Eigene Dateien\Dropbox\Programming\Java\AndroidStudioProjects\SimpleCloudNotifier\data\icon_512_transparent.png" />
|
|
||||||
<entry key="outputName" value="ic_notification_white" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="outputIconType" value="LAUNCHER_LEGACY" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<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>
|
|
134
android/.idea/codeStyles/Project.xml
generated
134
android/.idea/codeStyles/Project.xml
generated
@@ -1,29 +1,113 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<Objective-C-extensions>
|
<codeStyleSettings language="XML">
|
||||||
<file>
|
<arrangement>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
|
<rules>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
|
<section>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
|
<rule>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
|
<match>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
|
<AND>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
|
<NAME>xmlns:android</NAME>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
|
<XML_ATTRIBUTE />
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
|
</AND>
|
||||||
</file>
|
</match>
|
||||||
<class>
|
</rule>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
|
</section>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
|
<section>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
|
<rule>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
|
<match>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
|
<AND>
|
||||||
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
|
<NAME>xmlns:.*</NAME>
|
||||||
</class>
|
<XML_ATTRIBUTE />
|
||||||
<extensions>
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
<pair source="cpp" header="h" fileNamingConvention="NONE" />
|
</AND>
|
||||||
<pair source="c" header="h" fileNamingConvention="NONE" />
|
</match>
|
||||||
</extensions>
|
<order>BY_NAME</order>
|
||||||
</Objective-C-extensions>
|
</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>
|
</code_scheme>
|
||||||
</component>
|
</component>
|
40
android/.idea/jarRepositories.xml
generated
Normal file
40
android/.idea/jarRepositories.xml
generated
Normal 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>
|
@@ -1,7 +1,7 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 30
|
||||||
|
|
||||||
def versionPropsFile = file('version.properties')
|
def versionPropsFile = file('version.properties')
|
||||||
def vNumber
|
def vNumber
|
||||||
@@ -16,7 +16,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.blackforestbytes.simplecloudnotifier"
|
applicationId "com.blackforestbytes.simplecloudnotifier"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 28
|
targetSdkVersion 30
|
||||||
versionCode vNumber
|
versionCode vNumber
|
||||||
versionName vName
|
versionName vName
|
||||||
}
|
}
|
||||||
@@ -35,76 +35,82 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
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.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.2.1'
|
||||||
implementation 'com.google.firebase:firebase-core:16.0.4'
|
implementation 'com.google.firebase:firebase-core:18.0.0'
|
||||||
implementation 'com.google.firebase:firebase-messaging:17.3.4'
|
implementation 'com.google.firebase:firebase-messaging:21.0.0'
|
||||||
implementation 'com.google.android.gms:play-services-ads:17.1.0'
|
implementation 'com.google.android.gms:play-services-ads:19.5.0'
|
||||||
implementation 'com.android.billingclient:billing:1.2'
|
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.kenglxn.QRGen:android:2.5.0'
|
||||||
implementation "com.github.DeweyReed:UltimateMusicPicker:2.0.0"
|
implementation "com.github.DeweyReed:UltimateMusicPicker:2.0.0"
|
||||||
implementation 'com.github.duanhong169:colorpicker:1.1.5'
|
implementation 'com.github.duanhong169:colorpicker:1.1.5'
|
||||||
|
|
||||||
|
implementation 'net.danlew:android.joda:2.10.7.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
|
||||||
task updateVersion << {
|
tasks.register("updateVersion") {
|
||||||
def lastTag = ['git', 'describe', "--abbrev=0", "--tags"].execute().text.trim()
|
group = 'Custom'
|
||||||
|
|
||||||
def versionPropsFile = file('version.properties')
|
doLast {
|
||||||
if (!versionPropsFile.canRead()) throw new FileNotFoundException("Could not read version.properties!")
|
def lastTag = ['git', 'describe', "--abbrev=0", "--tags"].execute().text.trim()
|
||||||
Properties versionProps = new Properties()
|
|
||||||
new FileInputStream(versionPropsFile).withCloseable { fis -> versionProps.load(fis) }
|
|
||||||
|
|
||||||
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)
|
if (!matcher.matches()) throw new Exception("Last Tag ('" + lastTag + "') has invalid format :(")
|
||||||
def vCode = versionProps['VERSION_CODE'] as Integer
|
|
||||||
|
|
||||||
if (new File(".do_publish_beta_release").exists()) new File(".do_publish_beta_release").delete()
|
def vName = (matcher[0][1] as Integer) + "." + (matcher[0][2] as Integer) + "." + (matcher[0][3] as Integer)
|
||||||
if (new File(".do_publish_prod_release").exists()) new File(".do_publish_prod_release").delete()
|
def vCode = versionProps['VERSION_CODE'] as Integer
|
||||||
|
|
||||||
if (vName == versionProps['VERSION_NAME'].toString()) {
|
if (new File(".do_publish_beta_release").exists()) new File(".do_publish_beta_release").delete()
|
||||||
println "This version was already built - skip deployment"
|
if (new File(".do_publish_prod_release").exists()) new File(".do_publish_prod_release").delete()
|
||||||
} else if (vName.endsWith(".0")) {
|
|
||||||
println ""
|
|
||||||
println "====================================================================="
|
|
||||||
println "====================================================================="
|
|
||||||
println "(!) This is a new PRODUCTION release - create deployment trigger file"
|
|
||||||
println "====================================================================="
|
|
||||||
println "====================================================================="
|
|
||||||
println ""
|
|
||||||
|
|
||||||
vCode++
|
if (vName == versionProps['VERSION_NAME'].toString()) {
|
||||||
new File(".do_publish_prod_release").createNewFile()
|
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()
|
vCode++
|
||||||
versionProps['VERSION_CODE'] = vCode.toString()
|
new File(".do_publish_prod_release").createNewFile()
|
||||||
|
|
||||||
versionPropsFile.newWriter().withCloseable { w -> versionProps.store(w, null) }
|
versionProps['VERSION_NAME'] = vName.toString()
|
||||||
} else {
|
versionProps['VERSION_CODE'] = vCode.toString()
|
||||||
println ""
|
|
||||||
println "==============================================================="
|
|
||||||
println "(!) This is a new beta release - create deployment trigger file"
|
|
||||||
println "==============================================================="
|
|
||||||
println ""
|
|
||||||
|
|
||||||
vCode++
|
versionPropsFile.newWriter().withCloseable { w -> versionProps.store(w, null) }
|
||||||
new File(".do_publish_beta_release").createNewFile()
|
} else {
|
||||||
|
println ""
|
||||||
|
println "==============================================================="
|
||||||
|
println "(!) This is a new beta release - create deployment trigger file"
|
||||||
|
println "==============================================================="
|
||||||
|
println ""
|
||||||
|
|
||||||
versionProps['VERSION_NAME'] = vName.toString()
|
vCode++
|
||||||
versionProps['VERSION_CODE'] = vCode.toString()
|
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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,43 +1,68 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.blackforestbytes.simplecloudnotifier">
|
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.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="com.android.vending.BILLING" />
|
<uses-permission android:name="com.android.vending.BILLING" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".SCNApp"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:name="SCNApp"
|
|
||||||
android:icon="@drawable/icon"
|
android:icon="@drawable/icon"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
|
||||||
<activity android:name=".view.MainActivity">
|
<activity android:name=".view.MainActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/icon" />
|
<provider
|
||||||
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorAccent" />
|
android:name="androidx.core.content.FileProvider"
|
||||||
<meta-data android:name="com.google.android.gms.ads.AD_MANAGER_APP" android:value="true"/>
|
android:authorities="com.blackforestbytes.simplecloudnotifier.fileprovider"
|
||||||
<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-3320562328966175~7579972005"/>
|
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>
|
<intent-filter>
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
@@ -5,6 +5,7 @@ import android.content.Context;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.android.billingclient.api.BillingClient;
|
import com.android.billingclient.api.BillingClient;
|
||||||
|
import com.blackforestbytes.simplecloudnotifier.model.QueryLog;
|
||||||
import com.blackforestbytes.simplecloudnotifier.view.AccountFragment;
|
import com.blackforestbytes.simplecloudnotifier.view.AccountFragment;
|
||||||
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
|
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
|
||||||
import com.blackforestbytes.simplecloudnotifier.view.TabAdapter;
|
import com.blackforestbytes.simplecloudnotifier.view.TabAdapter;
|
||||||
@@ -98,3 +99,6 @@ public class SCNApp extends Application implements LifecycleObserver
|
|||||||
isBackground = false;
|
isBackground = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Config for collapsed line count
|
||||||
|
//TODO: Sometimes ads but promode
|
||||||
|
@@ -9,6 +9,8 @@ import java.util.TimeZone;
|
|||||||
|
|
||||||
public class CMessage
|
public class CMessage
|
||||||
{
|
{
|
||||||
|
public boolean IsExpandedInAdapter = false;
|
||||||
|
|
||||||
public final long SCN_ID;
|
public final long SCN_ID;
|
||||||
public final long Timestamp;
|
public final long Timestamp;
|
||||||
public final String Title;
|
public final String Title;
|
||||||
|
@@ -35,6 +35,11 @@ public class CMessageList
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CMessageList()
|
private CMessageList()
|
||||||
|
{
|
||||||
|
reloadPrefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadPrefs()
|
||||||
{
|
{
|
||||||
synchronized (msg_lock)
|
synchronized (msg_lock)
|
||||||
{
|
{
|
||||||
|
@@ -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; // ????
|
||||||
|
}
|
||||||
|
}
|
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -6,11 +6,11 @@ import android.content.SharedPreferences;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.android.billingclient.api.Purchase;
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||||
|
import com.blackforestbytes.simplecloudnotifier.lib.datatypes.Tuple3;
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||||
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
||||||
import com.google.firebase.iid.FirebaseInstanceId;
|
import com.google.firebase.installations.FirebaseInstallations;
|
||||||
|
|
||||||
public class SCNSettings
|
public class SCNSettings
|
||||||
{
|
{
|
||||||
@@ -52,7 +52,8 @@ public class SCNSettings
|
|||||||
|
|
||||||
public boolean Enabled = true;
|
public boolean Enabled = true;
|
||||||
public int LocalCacheSize = 500;
|
public int LocalCacheSize = 500;
|
||||||
public boolean EnableDeleteSwipe = true;
|
public boolean EnableDeleteSwipe = false;
|
||||||
|
public int PreviewLineCount = 6;
|
||||||
|
|
||||||
public final NotificationSettings PriorityLow = new NotificationSettings(PriorityEnum.LOW);
|
public final NotificationSettings PriorityLow = new NotificationSettings(PriorityEnum.LOW);
|
||||||
public final NotificationSettings PriorityNorm = new NotificationSettings(PriorityEnum.NORMAL);
|
public final NotificationSettings PriorityNorm = new NotificationSettings(PriorityEnum.NORMAL);
|
||||||
@@ -61,6 +62,11 @@ public class SCNSettings
|
|||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
public SCNSettings()
|
public SCNSettings()
|
||||||
|
{
|
||||||
|
reloadPrefs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadPrefs()
|
||||||
{
|
{
|
||||||
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("Config", Context.MODE_PRIVATE);
|
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("Config", Context.MODE_PRIVATE);
|
||||||
|
|
||||||
@@ -77,6 +83,7 @@ public class SCNSettings
|
|||||||
Enabled = sharedPref.getBoolean("app_enabled", Enabled);
|
Enabled = sharedPref.getBoolean("app_enabled", Enabled);
|
||||||
LocalCacheSize = sharedPref.getInt("local_cache_size", LocalCacheSize);
|
LocalCacheSize = sharedPref.getInt("local_cache_size", LocalCacheSize);
|
||||||
EnableDeleteSwipe = sharedPref.getBoolean("do_del_swipe", EnableDeleteSwipe);
|
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.EnableLED = sharedPref.getBoolean("priority_low:enabled_led", PriorityLow.EnableLED);
|
||||||
PriorityLow.EnableSound = sharedPref.getBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
|
PriorityLow.EnableSound = sharedPref.getBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
|
||||||
@@ -120,10 +127,14 @@ public class SCNSettings
|
|||||||
e.putString( "user_key", user_key);
|
e.putString( "user_key", user_key);
|
||||||
e.putString( "fcm_token_local", fcm_token_local);
|
e.putString( "fcm_token_local", fcm_token_local);
|
||||||
e.putString( "fcm_token_server", fcm_token_server);
|
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.putBoolean("app_enabled", Enabled);
|
||||||
e.putInt( "local_cache_size", LocalCacheSize);
|
e.putInt( "local_cache_size", LocalCacheSize);
|
||||||
e.putBoolean("do_del_swipe", EnableDeleteSwipe);
|
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_led", PriorityLow.EnableLED);
|
||||||
e.putBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
|
e.putBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
|
||||||
@@ -171,13 +182,13 @@ public class SCNSettings
|
|||||||
return base + "index.php?preset_user_id="+user_id+"&preset_user_key="+user_key;
|
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())
|
if (isConnected())
|
||||||
{
|
{
|
||||||
fcm_token_local = token;
|
fcm_token_local = token;
|
||||||
save();
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -189,13 +200,12 @@ public class SCNSettings
|
|||||||
}
|
}
|
||||||
|
|
||||||
// called at app start
|
// 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.d("FB::GetInstanceId", newToken);
|
Log.d("FB::GetInstanceId", newToken);
|
||||||
SCNSettings.inst().setServerToken(newToken, null);
|
SCNSettings.inst().setServerToken(newToken, null, force);
|
||||||
}).addOnCompleteListener(r ->
|
}).addOnCompleteListener(r ->
|
||||||
{
|
{
|
||||||
if (isConnected()) ServerCommunication.info(user_id, user_key, null);
|
if (isConnected()) ServerCommunication.info(user_id, user_key, null);
|
||||||
@@ -221,16 +231,15 @@ public class SCNSettings
|
|||||||
|
|
||||||
if (promode_server != promode_local) updateProState(loader);
|
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
|
else
|
||||||
{
|
{
|
||||||
// get token then register
|
// get token then register
|
||||||
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult ->
|
FirebaseInstallations.getInstance().getId().addOnSuccessListener(a, newToken ->
|
||||||
{
|
{
|
||||||
String newToken = instanceIdResult.getToken();
|
|
||||||
Log.d("FB::GetInstanceId", newToken);
|
Log.d("FB::GetInstanceId", newToken);
|
||||||
SCNSettings.inst().setServerToken(newToken, loader); // does register in here
|
SCNSettings.inst().setServerToken(newToken, loader, false); // does register in here
|
||||||
}).addOnCompleteListener(r ->
|
}).addOnCompleteListener(r ->
|
||||||
{
|
{
|
||||||
if (isConnected()) ServerCommunication.info(user_id, user_key, null); // info again for safety
|
if (isConnected()) ServerCommunication.info(user_id, user_key, null); // info again for safety
|
||||||
@@ -240,14 +249,17 @@ public class SCNSettings
|
|||||||
|
|
||||||
public void updateProState(View loader)
|
public void updateProState(View loader)
|
||||||
{
|
{
|
||||||
Purchase purch = IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE);
|
Tuple3<Boolean, Boolean, String> state = IABService.inst().getPurchaseCachedExtended(IABService.IAB_PRO_MODE);
|
||||||
boolean promode_real = (purch != null);
|
if (!state.Item2) return; // not initialized
|
||||||
|
|
||||||
|
boolean promode_real = state.Item1;
|
||||||
|
|
||||||
if (promode_real != promode_local || promode_real != promode_server)
|
if (promode_real != promode_local || promode_real != promode_server)
|
||||||
{
|
{
|
||||||
promode_local = promode_real;
|
promode_local = promode_real;
|
||||||
|
promode_token = promode_real ? state.Item3 : "";
|
||||||
|
save();
|
||||||
|
|
||||||
promode_token = promode_real ? purch.getPurchaseToken() : "";
|
|
||||||
updateProStateOnServer(loader);
|
updateProStateOnServer(loader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,12 +4,11 @@ import android.util.Log;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.datatypes.Tuple5;
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func1to0;
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func5to0;
|
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func5to0;
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||||
import com.blackforestbytes.simplecloudnotifier.service.FBMService;
|
import com.blackforestbytes.simplecloudnotifier.service.FBMService;
|
||||||
|
|
||||||
|
import org.joda.time.Instant;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
@@ -48,20 +47,20 @@ public class ServerCommunication
|
|||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e)
|
public void onFailure(Call call, IOException e)
|
||||||
{
|
{
|
||||||
Log.e("SC:register", e.toString());
|
handleError("register", call, null, Str.Empty, true, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
|
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call call, Response response)
|
public void onResponse(Call call, Response response)
|
||||||
{
|
{
|
||||||
|
String r = Str.Empty;
|
||||||
try (ResponseBody responseBody = response.body())
|
try (ResponseBody responseBody = response.body())
|
||||||
{
|
{
|
||||||
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
|
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
|
||||||
if (responseBody == null) throw new IOException("No response");
|
if (responseBody == null) throw new IOException("No response");
|
||||||
|
|
||||||
String r = responseBody.string();
|
r = responseBody.string();
|
||||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||||
|
|
||||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||||
@@ -69,6 +68,7 @@ public class ServerCommunication
|
|||||||
if (!json_bool(json, "success"))
|
if (!json_bool(json, "success"))
|
||||||
{
|
{
|
||||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||||
|
handleNonSuccess("register", call, response, r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,11 +81,12 @@ public class ServerCommunication
|
|||||||
SCNSettings.inst().save();
|
SCNSettings.inst().save();
|
||||||
|
|
||||||
SCNApp.refreshAccountTab();
|
SCNApp.refreshAccountTab();
|
||||||
|
|
||||||
|
handleSuccess("register", call, response, r);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.e("SC:register", e.toString());
|
handleError("register", call, response, r, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -96,8 +97,7 @@ public class ServerCommunication
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.e("SC:register", e.toString());
|
handleError("register", null, null, Str.Empty, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ public class ServerCommunication
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Request request = new Request.Builder()
|
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();
|
.build();
|
||||||
|
|
||||||
client.newCall(request).enqueue(new Callback()
|
client.newCall(request).enqueue(new Callback()
|
||||||
@@ -114,20 +114,20 @@ public class ServerCommunication
|
|||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e)
|
public void onFailure(Call call, IOException e)
|
||||||
{
|
{
|
||||||
Log.e("SC:update_1", e.toString());
|
handleError("update<1>", call, null, Str.Empty, true, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
|
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call call, Response response)
|
public void onResponse(Call call, Response response)
|
||||||
{
|
{
|
||||||
|
String r = Str.Empty;
|
||||||
try (ResponseBody responseBody = response.body())
|
try (ResponseBody responseBody = response.body())
|
||||||
{
|
{
|
||||||
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
|
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
|
||||||
if (responseBody == null) throw new IOException("No response");
|
if (responseBody == null) throw new IOException("No response");
|
||||||
|
|
||||||
String r = responseBody.string();
|
r = responseBody.string();
|
||||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||||
|
|
||||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||||
@@ -135,6 +135,7 @@ public class ServerCommunication
|
|||||||
if (!json_bool(json, "success"))
|
if (!json_bool(json, "success"))
|
||||||
{
|
{
|
||||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||||
|
handleNonSuccess("update<1>", call, response, r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,10 +148,12 @@ public class ServerCommunication
|
|||||||
SCNSettings.inst().save();
|
SCNSettings.inst().save();
|
||||||
|
|
||||||
SCNApp.refreshAccountTab();
|
SCNApp.refreshAccountTab();
|
||||||
|
|
||||||
|
handleSuccess("update<1>", call, response, r);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
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);
|
SCNApp.showToast("Communication with server failed", 4000);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -162,8 +165,7 @@ public class ServerCommunication
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.e("SC:update_1", e.toString());
|
handleError("update<1>", null, null, Str.Empty, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,30 +174,35 @@ public class ServerCommunication
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Request request = new Request.Builder()
|
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();
|
.build();
|
||||||
|
|
||||||
client.newCall(request).enqueue(new Callback() {
|
client.newCall(request).enqueue(new Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e) {
|
public void onFailure(Call call, IOException e)
|
||||||
Log.e("SC:update_2", e.toString());
|
{
|
||||||
|
handleError("update<1>", call, null, Str.Empty, true, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
SCNApp.showToast("Communication with server failed", 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call call, Response response) {
|
public void onResponse(Call call, Response response)
|
||||||
try (ResponseBody responseBody = response.body()) {
|
{
|
||||||
|
String r = Str.Empty;
|
||||||
|
try (ResponseBody responseBody = response.body())
|
||||||
|
{
|
||||||
if (!response.isSuccessful())
|
if (!response.isSuccessful())
|
||||||
throw new IOException("Unexpected code " + response);
|
throw new IOException("Unexpected code " + response);
|
||||||
if (responseBody == null) throw new IOException("No response");
|
if (responseBody == null) throw new IOException("No response");
|
||||||
|
|
||||||
String r = responseBody.string();
|
r = responseBody.string();
|
||||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||||
|
|
||||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||||
|
|
||||||
if (!json_bool(json, "success")) {
|
if (!json_bool(json, "success")) {
|
||||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||||
|
handleNonSuccess("update<2>", call, response, r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,10 +214,16 @@ public class ServerCommunication
|
|||||||
SCNSettings.inst().save();
|
SCNSettings.inst().save();
|
||||||
|
|
||||||
SCNApp.refreshAccountTab();
|
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);
|
SCNApp.showToast("Communication with server failed", 4000);
|
||||||
} finally {
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
SCNApp.runOnUiThread(() -> {
|
SCNApp.runOnUiThread(() -> {
|
||||||
if (loader != null) loader.setVisibility(View.GONE);
|
if (loader != null) loader.setVisibility(View.GONE);
|
||||||
});
|
});
|
||||||
@@ -220,8 +233,7 @@ public class ServerCommunication
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.e("SC:update_2", e.toString());
|
handleError("update<2>", null, null, Str.Empty, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,21 +248,23 @@ public class ServerCommunication
|
|||||||
client.newCall(request).enqueue(new Callback() {
|
client.newCall(request).enqueue(new Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e) {
|
public void onFailure(Call call, IOException e) {
|
||||||
Log.e("SC:info", e.toString());
|
handleError("info", call, null, Str.Empty, true, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
SCNApp.runOnUiThread(() -> {
|
SCNApp.runOnUiThread(() -> {
|
||||||
if (loader != null) loader.setVisibility(View.GONE);
|
if (loader != null) loader.setVisibility(View.GONE);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call call, Response response) {
|
public void onResponse(Call call, Response response)
|
||||||
try (ResponseBody responseBody = response.body()) {
|
{
|
||||||
|
String r = Str.Empty;
|
||||||
|
try (ResponseBody responseBody = response.body())
|
||||||
|
{
|
||||||
if (!response.isSuccessful())
|
if (!response.isSuccessful())
|
||||||
throw new IOException("Unexpected code " + response);
|
throw new IOException("Unexpected code " + response);
|
||||||
if (responseBody == null) throw new IOException("No response");
|
if (responseBody == null) throw new IOException("No response");
|
||||||
|
|
||||||
String r = responseBody.string();
|
r = responseBody.string();
|
||||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||||
|
|
||||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||||
@@ -258,6 +272,7 @@ public class ServerCommunication
|
|||||||
if (!json_bool(json, "success"))
|
if (!json_bool(json, "success"))
|
||||||
{
|
{
|
||||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||||
|
handleNonSuccess("info", call, response, r);
|
||||||
|
|
||||||
int errid = json.optInt("errid", 0);
|
int errid = json.optInt("errid", 0);
|
||||||
|
|
||||||
@@ -290,21 +305,23 @@ public class ServerCommunication
|
|||||||
|
|
||||||
if (json_int(json, "unack_count")>0) ServerCommunication.requery(id, key, loader);
|
if (json_int(json, "unack_count")>0) ServerCommunication.requery(id, key, loader);
|
||||||
|
|
||||||
} catch (Exception e) {
|
handleSuccess("info", call, response, r);
|
||||||
Log.e("SC:info", e.toString());
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
handleError("info", call, response, r, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
SCNApp.showToast("Communication with server failed", 4000);
|
||||||
} finally {
|
}
|
||||||
SCNApp.runOnUiThread(() -> {
|
finally
|
||||||
if (loader != null) loader.setVisibility(View.GONE);
|
{
|
||||||
});
|
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.e("SC:info", e.toString());
|
handleError("info", null, null, Str.Empty, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,21 +336,23 @@ public class ServerCommunication
|
|||||||
client.newCall(request).enqueue(new Callback() {
|
client.newCall(request).enqueue(new Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e) {
|
public void onFailure(Call call, IOException e) {
|
||||||
Log.e("SC:requery", e.toString());
|
handleError("requery", call, null, Str.Empty, true, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
SCNApp.runOnUiThread(() -> {
|
SCNApp.runOnUiThread(() -> {
|
||||||
if (loader != null) loader.setVisibility(View.GONE);
|
if (loader != null) loader.setVisibility(View.GONE);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call call, Response response) {
|
public void onResponse(Call call, Response response)
|
||||||
try (ResponseBody responseBody = response.body()) {
|
{
|
||||||
|
String r = Str.Empty;
|
||||||
|
try (ResponseBody responseBody = response.body())
|
||||||
|
{
|
||||||
if (!response.isSuccessful())
|
if (!response.isSuccessful())
|
||||||
throw new IOException("Unexpected code " + response);
|
throw new IOException("Unexpected code " + response);
|
||||||
if (responseBody == null) throw new IOException("No response");
|
if (responseBody == null) throw new IOException("No response");
|
||||||
|
|
||||||
String r = responseBody.string();
|
r = responseBody.string();
|
||||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||||
|
|
||||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||||
@@ -341,6 +360,7 @@ public class ServerCommunication
|
|||||||
if (!json_bool(json, "success"))
|
if (!json_bool(json, "success"))
|
||||||
{
|
{
|
||||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||||
|
handleNonSuccess("requery", call, response, r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,10 +379,15 @@ public class ServerCommunication
|
|||||||
FBMService.recieveData(time, title, content, prio, scn_id, true);
|
FBMService.recieveData(time, title, content, prio, scn_id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
handleSuccess("requery", call, response, r);
|
||||||
Log.e("SC:info", e.toString());
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
handleError("requery", call, response, r, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
SCNApp.showToast("Communication with server failed", 4000);
|
||||||
} finally {
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
SCNApp.runOnUiThread(() -> {
|
SCNApp.runOnUiThread(() -> {
|
||||||
if (loader != null) loader.setVisibility(View.GONE);
|
if (loader != null) loader.setVisibility(View.GONE);
|
||||||
});
|
});
|
||||||
@@ -372,8 +397,7 @@ public class ServerCommunication
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.e("SC:requery", e.toString());
|
handleError("requery", null, null, Str.Empty, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,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"))
|
.url(BASE_URL + "upgrade.php?user_id=" + id + "&user_key=" + key + "&pro=" + pro + "&pro_token=" + URLEncoder.encode(pro_token, "utf-8"))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
client.newCall(request).enqueue(new Callback() {
|
client.newCall(request).enqueue(new Callback()
|
||||||
|
{
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e) {
|
public void onFailure(Call call, IOException e)
|
||||||
Log.e("SC:upgrade", e.toString());
|
{
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
handleError("upgrade", call, null, Str.Empty, true, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call call, Response response) {
|
public void onResponse(Call call, Response response)
|
||||||
try (ResponseBody responseBody = response.body()) {
|
{
|
||||||
if (!response.isSuccessful())
|
String r = Str.Empty;
|
||||||
throw new IOException("Unexpected code " + response);
|
try (ResponseBody responseBody = response.body())
|
||||||
|
{
|
||||||
|
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
|
||||||
if (responseBody == null) throw new IOException("No response");
|
if (responseBody == null) throw new IOException("No response");
|
||||||
|
|
||||||
String r = responseBody.string();
|
r = responseBody.string();
|
||||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||||
|
|
||||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||||
|
|
||||||
if (!json_bool(json, "success")) {
|
if (!json_bool(json, "success")) {
|
||||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||||
|
handleNonSuccess("upgrade", call, response, r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,10 +446,15 @@ public class ServerCommunication
|
|||||||
SCNSettings.inst().save();
|
SCNSettings.inst().save();
|
||||||
|
|
||||||
SCNApp.refreshAccountTab();
|
SCNApp.refreshAccountTab();
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("SC:upgrade", e.toString());
|
handleSuccess("upgrade", call, response, r);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
}
|
||||||
} finally {
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
handleError("upgrade", call, response, r, false, e);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
|
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,8 +462,7 @@ public class ServerCommunication
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
e.printStackTrace();
|
handleError("upgrade", null, null, Str.Empty, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,37 +474,47 @@ public class ServerCommunication
|
|||||||
.url(BASE_URL + "ack.php?user_id=" + id + "&user_key=" + key + "&scn_msg_id=" + msg_scn_id)
|
.url(BASE_URL + "ack.php?user_id=" + id + "&user_key=" + key + "&scn_msg_id=" + msg_scn_id)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
client.newCall(request).enqueue(new Callback() {
|
client.newCall(request).enqueue(new Callback()
|
||||||
|
{
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e) {
|
public void onFailure(Call call, IOException e)
|
||||||
Log.e("SC:ack", e.toString());
|
{
|
||||||
|
handleError("ack", call, null, Str.Empty, true, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call call, Response response)
|
public void onResponse(Call call, Response response)
|
||||||
{
|
{
|
||||||
try (ResponseBody responseBody = response.body()) {
|
String r = Str.Empty;
|
||||||
|
try (ResponseBody responseBody = response.body())
|
||||||
|
{
|
||||||
if (!response.isSuccessful())
|
if (!response.isSuccessful())
|
||||||
throw new IOException("Unexpected code " + response);
|
throw new IOException("Unexpected code " + response);
|
||||||
if (responseBody == null) throw new IOException("No response");
|
if (responseBody == null) throw new IOException("No response");
|
||||||
|
|
||||||
String r = responseBody.string();
|
r = responseBody.string();
|
||||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||||
|
|
||||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
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) {
|
handleSuccess("ack", call, response, r);
|
||||||
Log.e("SC:ack", e.toString());
|
}
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
handleError("ack", call, response, r, false, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.e("SC:ack", e.toString());
|
handleError("ack", null, null, Str.Empty, false, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,21 +529,21 @@ public class ServerCommunication
|
|||||||
client.newCall(request).enqueue(new Callback() {
|
client.newCall(request).enqueue(new Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e) {
|
public void onFailure(Call call, IOException e) {
|
||||||
Log.e("SC:expand", e.toString());
|
handleError("expand", call, null, Str.Empty, true, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
|
||||||
SCNApp.runOnUiThread(() -> {
|
|
||||||
if (loader != null) loader.setVisibility(View.GONE);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call call, Response response) {
|
public void onResponse(Call call, Response response)
|
||||||
try (ResponseBody responseBody = response.body()) {
|
{
|
||||||
|
String r = Str.Empty;
|
||||||
|
try (ResponseBody responseBody = response.body())
|
||||||
|
{
|
||||||
if (!response.isSuccessful())
|
if (!response.isSuccessful())
|
||||||
throw new IOException("Unexpected code " + response);
|
throw new IOException("Unexpected code " + response);
|
||||||
if (responseBody == null) throw new IOException("No response");
|
if (responseBody == null) throw new IOException("No response");
|
||||||
|
|
||||||
String r = responseBody.string();
|
r = responseBody.string();
|
||||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||||
|
|
||||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||||
@@ -509,6 +551,7 @@ public class ServerCommunication
|
|||||||
if (!json_bool(json, "success"))
|
if (!json_bool(json, "success"))
|
||||||
{
|
{
|
||||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||||
|
handleNonSuccess("expand", call, response, r);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,21 +565,22 @@ public class ServerCommunication
|
|||||||
|
|
||||||
okResult.invoke(title, content, prio, time, scn_id);
|
okResult.invoke(title, content, prio, time, scn_id);
|
||||||
|
|
||||||
} catch (Exception e) {
|
handleSuccess("expand", call, response, r);
|
||||||
Log.e("SC:expand", e.toString());
|
}
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
catch (Exception e)
|
||||||
} finally {
|
{
|
||||||
SCNApp.runOnUiThread(() -> {
|
handleError("expand", call, response, r, false, e);
|
||||||
if (loader != null) loader.setVisibility(View.GONE);
|
}
|
||||||
});
|
finally
|
||||||
|
{
|
||||||
|
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.e("SC:expand", e.toString());
|
handleError("expand", null, null, Str.Empty, false, e);
|
||||||
SCNApp.showToast("Communication with server failed", 4000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,4 +608,79 @@ public class ServerCommunication
|
|||||||
{
|
{
|
||||||
return o.getString(key);
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -4,16 +4,21 @@ import android.util.Log;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.datatypes.Tuple4;
|
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.datatypes.Tuple5;
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
|
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
|
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
|
||||||
|
import com.blackforestbytes.simplecloudnotifier.model.LogLevel;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum;
|
import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum;
|
||||||
|
import com.blackforestbytes.simplecloudnotifier.model.QueryLog;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.ServerCommunication;
|
import com.blackforestbytes.simplecloudnotifier.model.ServerCommunication;
|
||||||
|
import com.blackforestbytes.simplecloudnotifier.model.SingleQuery;
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||||
import com.google.firebase.messaging.RemoteMessage;
|
import com.google.firebase.messaging.RemoteMessage;
|
||||||
|
|
||||||
|
import org.joda.time.Instant;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class FBMService extends FirebaseMessagingService
|
public class FBMService extends FirebaseMessagingService
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
@@ -42,6 +47,10 @@ public class FBMService extends FirebaseMessagingService
|
|||||||
long scn_id = Long.parseLong(remoteMessage.getData().get("scn_msg_id"));
|
long scn_id = Long.parseLong(remoteMessage.getData().get("scn_msg_id"));
|
||||||
boolean trimmed = Boolean.parseBoolean(remoteMessage.getData().get("trimmed"));
|
boolean trimmed = Boolean.parseBoolean(remoteMessage.getData().get("trimmed"));
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
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));
|
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));
|
||||||
|
@@ -2,23 +2,37 @@ package com.blackforestbytes.simplecloudnotifier.service;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.android.billingclient.api.BillingClient;
|
import com.android.billingclient.api.BillingClient;
|
||||||
import com.android.billingclient.api.BillingClientStateListener;
|
import com.android.billingclient.api.BillingClientStateListener;
|
||||||
import com.android.billingclient.api.BillingFlowParams;
|
import com.android.billingclient.api.BillingFlowParams;
|
||||||
|
import com.android.billingclient.api.BillingResult;
|
||||||
import com.android.billingclient.api.Purchase;
|
import com.android.billingclient.api.Purchase;
|
||||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
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.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.lambda.Func0to0;
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||||
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
|
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
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.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import static androidx.constraintlayout.widget.Constraints.TAG;
|
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 BillingClient client;
|
||||||
private boolean isServiceConnected;
|
private boolean isServiceConnected;
|
||||||
private final List<Purchase> purchases = new ArrayList<>();
|
private final List<Purchase> purchases = new ArrayList<>();
|
||||||
|
private boolean _isInitialized = false;
|
||||||
|
|
||||||
|
private final Map<String, Boolean> _localCache= new HashMap<>();
|
||||||
|
|
||||||
public IABService(Context c)
|
public IABService(Context c)
|
||||||
{
|
{
|
||||||
|
_isInitialized = false;
|
||||||
|
|
||||||
|
loadCache();
|
||||||
|
|
||||||
client = BillingClient
|
client = BillingClient
|
||||||
.newBuilder(c)
|
.newBuilder(c)
|
||||||
.setListener(this)
|
.setListener(this)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
startServiceConnection(this::queryPurchases, false);
|
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()
|
public void queryPurchases()
|
||||||
@@ -67,14 +135,16 @@ public class IABService implements PurchasesUpdatedListener
|
|||||||
Purchase.PurchasesResult purchasesResult = client.queryPurchases(BillingClient.SkuType.INAPP);
|
Purchase.PurchasesResult purchasesResult = client.queryPurchases(BillingClient.SkuType.INAPP);
|
||||||
Log.i(TAG, "Querying purchases elapsed time: " + (System.currentTimeMillis() - time) + "ms");
|
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)
|
if (newProMode != SCNSettings.inst().promode_local)
|
||||||
{
|
{
|
||||||
refreshProModeListener();
|
refreshProModeListener();
|
||||||
@@ -89,20 +159,39 @@ public class IABService implements PurchasesUpdatedListener
|
|||||||
executeServiceRequest(queryToExecute, false);
|
executeServiceRequest(queryToExecute, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void purchase(Activity a, String id)
|
public void querySkuDetails() {
|
||||||
{
|
|
||||||
executeServiceRequest(() ->
|
|
||||||
{
|
|
||||||
BillingFlowParams flowParams = BillingFlowParams
|
|
||||||
.newBuilder()
|
|
||||||
.setSku(id)
|
|
||||||
.setType(BillingClient.SkuType.INAPP) // SkuType.SUB for subscription
|
|
||||||
.build();
|
|
||||||
client.launchBillingFlow(a, flowParams);
|
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
if (isServiceConnected)
|
||||||
{
|
{
|
||||||
runnable.invoke();
|
runnable.invoke();
|
||||||
@@ -124,34 +213,37 @@ public class IABService implements PurchasesUpdatedListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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)
|
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)
|
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);
|
Log.d(TAG, "Got a verified purchase: " + purchase);
|
||||||
|
|
||||||
purchases.add(purchase);
|
purchases.add(purchase);
|
||||||
|
|
||||||
refreshProModeListener();
|
if (triggerUpdate) refreshProModeListener();
|
||||||
|
|
||||||
|
updateCache(purchase.getSku(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshProModeListener() {
|
private void refreshProModeListener()
|
||||||
|
{
|
||||||
MainActivity ma = SCNApp.getMainActivity();
|
MainActivity ma = SCNApp.getMainActivity();
|
||||||
if (ma != null) ma.adpTabs.tab3.updateProState();
|
if (ma != null) ma.adpTabs.tab3.updateProState();
|
||||||
if (ma != null) ma.adpTabs.tab1.updateProState();
|
if (ma != null) ma.adpTabs.tab1.updateProState();
|
||||||
@@ -163,9 +255,9 @@ public class IABService implements PurchasesUpdatedListener
|
|||||||
client.startConnection(new BillingClientStateListener()
|
client.startConnection(new BillingClientStateListener()
|
||||||
{
|
{
|
||||||
@Override
|
@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;
|
isServiceConnected = true;
|
||||||
if (executeOnSuccess != null) executeOnSuccess.invoke();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -229,10 +229,10 @@ public class NotificationService
|
|||||||
if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
||||||
if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
|
if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
|
||||||
|
|
||||||
Intent intnt_click = new Intent(SCNApp.getContext(), BroadcastReceiverService.class);
|
Intent intent = new Intent(ctxt, MainActivity.class);
|
||||||
intnt_click.putExtra(BroadcastReceiverService.ID_KEY, BroadcastReceiverService.NOTIF_SHOW_MAIN);
|
PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0);
|
||||||
PendingIntent pi = PendingIntent.getBroadcast(ctxt, 0, intnt_click, 0);
|
|
||||||
mBuilder.setContentIntent(pi);
|
mBuilder.setContentIntent(pi);
|
||||||
|
|
||||||
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
if (mNotificationManager == null) return;
|
if (mNotificationManager == null) return;
|
||||||
|
|
||||||
|
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
@@ -1,14 +1,23 @@
|
|||||||
package com.blackforestbytes.simplecloudnotifier.view;
|
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.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.R;
|
import com.blackforestbytes.simplecloudnotifier.R;
|
||||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
|
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
|
||||||
|
import com.blackforestbytes.simplecloudnotifier.model.QueryLog;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||||
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
||||||
import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
|
import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
|
||||||
|
import com.blackforestbytes.simplecloudnotifier.view.debug.QueryLogActivity;
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@@ -16,6 +25,12 @@ import androidx.appcompat.widget.Toolbar;
|
|||||||
import androidx.viewpager.widget.PagerAdapter;
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
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 class MainActivity extends AppCompatActivity
|
||||||
{
|
{
|
||||||
public TabAdapter adpTabs;
|
public TabAdapter adpTabs;
|
||||||
@@ -24,6 +39,8 @@ public class MainActivity extends AppCompatActivity
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState)
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
{
|
{
|
||||||
|
QueryLog.inst();
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
@@ -33,6 +50,7 @@ public class MainActivity extends AppCompatActivity
|
|||||||
layoutRoot = findViewById(R.id.layoutRoot);
|
layoutRoot = findViewById(R.id.layoutRoot);
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
toolbar.setOnClickListener(this::onToolbackClicked);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
ViewPager viewPager = findViewById(R.id.pager);
|
ViewPager viewPager = findViewById(R.id.pager);
|
||||||
@@ -61,7 +79,7 @@ public class MainActivity extends AppCompatActivity
|
|||||||
|
|
||||||
SCNApp.register(this);
|
SCNApp.register(this);
|
||||||
IABService.startup(this);
|
IABService.startup(this);
|
||||||
SCNSettings.inst().work(this);
|
SCNSettings.inst().work(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -81,4 +99,126 @@ public class MainActivity extends AppCompatActivity
|
|||||||
CMessageList.inst().fullSave();
|
CMessageList.inst().fullSave();
|
||||||
IABService.inst().destroy();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.blackforestbytes.simplecloudnotifier.view;
|
package com.blackforestbytes.simplecloudnotifier.view;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -9,8 +10,11 @@ import android.widget.RelativeLayout;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.R;
|
import com.blackforestbytes.simplecloudnotifier.R;
|
||||||
|
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
|
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
|
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.lang.ref.WeakReference;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -53,7 +57,7 @@ public class MessageAdapter extends RecyclerView.Adapter
|
|||||||
{
|
{
|
||||||
CMessage msg = CMessageList.inst().tryGetFromBack(position);
|
CMessage msg = CMessageList.inst().tryGetFromBack(position);
|
||||||
MessagePresenter view = (MessagePresenter) holder;
|
MessagePresenter view = (MessagePresenter) holder;
|
||||||
view.setMessage(msg);
|
view.setMessage(msg, position);
|
||||||
|
|
||||||
viewHolders.put(view, true);
|
viewHolders.put(view, true);
|
||||||
}
|
}
|
||||||
@@ -110,7 +114,11 @@ public class MessageAdapter extends RecyclerView.Adapter
|
|||||||
public RelativeLayout viewForeground;
|
public RelativeLayout viewForeground;
|
||||||
public RelativeLayout viewBackground;
|
public RelativeLayout viewBackground;
|
||||||
|
|
||||||
|
public MaterialButton btnShare;
|
||||||
|
public MaterialButton btnDelete;
|
||||||
|
|
||||||
private CMessage data;
|
private CMessage data;
|
||||||
|
private int datapos;
|
||||||
|
|
||||||
MessagePresenter(View itemView)
|
MessagePresenter(View itemView)
|
||||||
{
|
{
|
||||||
@@ -121,6 +129,8 @@ public class MessageAdapter extends RecyclerView.Adapter
|
|||||||
ivPriority = itemView.findViewById(R.id.ivPriority);
|
ivPriority = itemView.findViewById(R.id.ivPriority);
|
||||||
viewForeground = itemView.findViewById(R.id.layoutFront);
|
viewForeground = itemView.findViewById(R.id.layoutFront);
|
||||||
viewBackground = itemView.findViewById(R.id.layoutBack);
|
viewBackground = itemView.findViewById(R.id.layoutBack);
|
||||||
|
btnShare = itemView.findViewById(R.id.btnShare);
|
||||||
|
btnDelete = itemView.findViewById(R.id.btnDelete);
|
||||||
|
|
||||||
itemView.setOnClickListener(this);
|
itemView.setOnClickListener(this);
|
||||||
tvTimestamp.setOnClickListener(this);
|
tvTimestamp.setOnClickListener(this);
|
||||||
@@ -128,9 +138,22 @@ public class MessageAdapter extends RecyclerView.Adapter
|
|||||||
tvMessage.setOnClickListener(this);
|
tvMessage.setOnClickListener(this);
|
||||||
ivPriority.setOnClickListener(this);
|
ivPriority.setOnClickListener(this);
|
||||||
viewForeground.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());
|
tvTimestamp.setText(msg.formatTimestamp());
|
||||||
tvTitle.setText(msg.Title);
|
tvTitle.setText(msg.Title);
|
||||||
@@ -155,21 +178,49 @@ public class MessageAdapter extends RecyclerView.Adapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
data = msg;
|
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
|
@Override
|
||||||
public void onClick(View v)
|
public void onClick(View v)
|
||||||
{
|
{
|
||||||
|
if (data.IsExpandedInAdapter)
|
||||||
|
{
|
||||||
|
collapse(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (MessagePresenter holder : MessageAdapter.this.viewHolders.keySet())
|
for (MessagePresenter holder : MessageAdapter.this.viewHolders.keySet())
|
||||||
{
|
{
|
||||||
if (holder == null) continue;
|
if (holder == null) continue;
|
||||||
if (holder == this) continue;
|
if (holder == this) continue;
|
||||||
if (holder.tvMessage == null) continue;
|
holder.collapse(false);
|
||||||
if (holder.tvMessage.getMaxLines() == 6) continue;
|
|
||||||
holder.tvMessage.setMaxLines(6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tvMessage.setMaxLines(9999);
|
expand(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -58,7 +58,7 @@ public class NotificationsFragment extends Fragment implements MessageAdapterTou
|
|||||||
|
|
||||||
public void updateProState()
|
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
|
@Override
|
||||||
@@ -66,18 +66,23 @@ public class NotificationsFragment extends Fragment implements MessageAdapterTou
|
|||||||
{
|
{
|
||||||
if (viewHolder instanceof MessageAdapter.MessagePresenter)
|
if (viewHolder instanceof MessageAdapter.MessagePresenter)
|
||||||
{
|
{
|
||||||
final int deletedIndex = viewHolder.getAdapterPosition();
|
deleteMessage(viewHolder.getAdapterPosition());
|
||||||
|
|
||||||
final CMessage deletedItem = adpMessages.removeItem(viewHolder.getAdapterPosition());
|
|
||||||
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 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()
|
public void updateDeleteSwipeEnabled()
|
||||||
{
|
{
|
||||||
if (touchHelper != null) touchHelper.updateEnabled();
|
if (touchHelper != null) touchHelper.updateEnabled();
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
package com.blackforestbytes.simplecloudnotifier.view;
|
package com.blackforestbytes.simplecloudnotifier.view;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.media.AudioAttributes;
|
import android.media.AudioAttributes;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.MediaPlayer;
|
import android.media.MediaPlayer;
|
||||||
import android.media.Ringtone;
|
|
||||||
import android.media.RingtoneManager;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -17,6 +17,7 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
@@ -24,21 +25,26 @@ import android.widget.Switch;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.android.billingclient.api.Purchase;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.R;
|
import com.blackforestbytes.simplecloudnotifier.R;
|
||||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.android.ThreadUtils;
|
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.lambda.FI;
|
import com.blackforestbytes.simplecloudnotifier.lib.lambda.FI;
|
||||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||||
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
||||||
|
import com.blackforestbytes.simplecloudnotifier.util.TextChangedListener;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
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 top.defaults.colorpicker.ColorPickerPopup;
|
||||||
import xyz.aprildown.ultimatemusicpicker.MusicPickerListener;
|
import xyz.aprildown.ultimatemusicpicker.MusicPickerListener;
|
||||||
import xyz.aprildown.ultimatemusicpicker.UltimateMusicPicker;
|
import xyz.aprildown.ultimatemusicpicker.UltimateMusicPicker;
|
||||||
@@ -51,6 +57,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
|||||||
private TextView prefUpgradeAccount_msg;
|
private TextView prefUpgradeAccount_msg;
|
||||||
private TextView prefUpgradeAccount_info;
|
private TextView prefUpgradeAccount_info;
|
||||||
private Switch prefEnableDeleteSwipe;
|
private Switch prefEnableDeleteSwipe;
|
||||||
|
private EditText prefPreviewLineCount;
|
||||||
|
|
||||||
private Switch prefMsgLowEnableSound;
|
private Switch prefMsgLowEnableSound;
|
||||||
private TextView prefMsgLowRingtone_value;
|
private TextView prefMsgLowRingtone_value;
|
||||||
@@ -88,6 +95,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
|||||||
private SeekBar prefMsgHighVolume;
|
private SeekBar prefMsgHighVolume;
|
||||||
private ImageView prefMsgHighVolumeTest;
|
private ImageView prefMsgHighVolumeTest;
|
||||||
|
|
||||||
|
private Button prefBtnImport;
|
||||||
|
private Button prefBtnExport;
|
||||||
|
|
||||||
private int musicPickerSwitch = -1;
|
private int musicPickerSwitch = -1;
|
||||||
|
|
||||||
private MediaPlayer[] mPlayers = new MediaPlayer[3];
|
private MediaPlayer[] mPlayers = new MediaPlayer[3];
|
||||||
@@ -117,6 +127,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
|||||||
prefUpgradeAccount_msg = v.findViewById(R.id.prefUpgradeAccount2);
|
prefUpgradeAccount_msg = v.findViewById(R.id.prefUpgradeAccount2);
|
||||||
prefUpgradeAccount_info = v.findViewById(R.id.prefUpgradeAccount_info);
|
prefUpgradeAccount_info = v.findViewById(R.id.prefUpgradeAccount_info);
|
||||||
prefEnableDeleteSwipe = v.findViewById(R.id.prefEnableDeleteSwipe);
|
prefEnableDeleteSwipe = v.findViewById(R.id.prefEnableDeleteSwipe);
|
||||||
|
prefPreviewLineCount = v.findViewById(R.id.prefPreviewLineCount);
|
||||||
|
|
||||||
prefMsgLowEnableSound = v.findViewById(R.id.prefMsgLowEnableSound);
|
prefMsgLowEnableSound = v.findViewById(R.id.prefMsgLowEnableSound);
|
||||||
prefMsgLowRingtone_value = v.findViewById(R.id.prefMsgLowRingtone_value);
|
prefMsgLowRingtone_value = v.findViewById(R.id.prefMsgLowRingtone_value);
|
||||||
@@ -154,11 +165,15 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
|||||||
prefMsgHighVolume = v.findViewById(R.id.prefMsgHighVolume);
|
prefMsgHighVolume = v.findViewById(R.id.prefMsgHighVolume);
|
||||||
prefMsgHighVolumeTest = v.findViewById(R.id.btnHighVolumeTest);
|
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);
|
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);
|
plcsa.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||||
prefLocalCacheSize.setAdapter(plcsa);
|
prefLocalCacheSize.setAdapter(plcsa);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
private void updateUI()
|
private void updateUI()
|
||||||
{
|
{
|
||||||
SCNSettings s = SCNSettings.inst();
|
SCNSettings s = SCNSettings.inst();
|
||||||
@@ -167,6 +182,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
|||||||
|
|
||||||
if (prefAppEnabled.isChecked() != s.Enabled) prefAppEnabled.setChecked(s.Enabled);
|
if (prefAppEnabled.isChecked() != s.Enabled) prefAppEnabled.setChecked(s.Enabled);
|
||||||
if (prefEnableDeleteSwipe.isChecked() != s.EnableDeleteSwipe) prefEnableDeleteSwipe.setChecked(s.EnableDeleteSwipe);
|
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.setVisibility( SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
|
||||||
prefUpgradeAccount_info.setVisibility(SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
|
prefUpgradeAccount_info.setVisibility(SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
|
||||||
@@ -220,6 +236,12 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
|||||||
|
|
||||||
prefAppEnabled.setOnCheckedChangeListener((a,b) -> { boolean prev=s.Enabled; s.Enabled=b; saveAndUpdate(); updateEnabled(prev, b); });
|
prefAppEnabled.setOnCheckedChangeListener((a,b) -> { boolean prev=s.Enabled; s.Enabled=b; saveAndUpdate(); updateEnabled(prev, b); });
|
||||||
prefEnableDeleteSwipe.setOnCheckedChangeListener((a,b) -> { s.EnableDeleteSwipe=b; saveAndUpdate(); });
|
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()
|
prefLocalCacheSize.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
|
||||||
{
|
{
|
||||||
@@ -232,6 +254,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
|||||||
|
|
||||||
prefUpgradeAccount.setOnClickListener(a -> onUpgradeAccount());
|
prefUpgradeAccount.setOnClickListener(a -> onUpgradeAccount());
|
||||||
|
|
||||||
|
prefBtnExport.setOnClickListener(a -> onExport());
|
||||||
|
prefBtnImport.setOnClickListener(a -> onImport());
|
||||||
|
|
||||||
prefMsgLowEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableSound=b; saveAndUpdate(); });
|
prefMsgLowEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableSound=b; saveAndUpdate(); });
|
||||||
prefMsgLowRingtone_container.setOnClickListener(a -> chooseRingtoneLow());
|
prefMsgLowRingtone_container.setOnClickListener(a -> chooseRingtoneLow());
|
||||||
prefMsgLowRepeatSound.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.RepeatSound=b; saveAndUpdate(); });
|
prefMsgLowRepeatSound.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.RepeatSound=b; saveAndUpdate(); });
|
||||||
@@ -263,6 +288,55 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
|||||||
prefMsgHighVolumeTest.setOnClickListener((v) -> { if (s.PriorityHigh.ForceVolume) playTestSound(2, prefMsgHighVolumeTest, s.PriorityHigh.SoundSource, s.PriorityHigh.ForceVolumeValue); });
|
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)
|
private void updateEnabled(boolean prev, boolean now)
|
||||||
{
|
{
|
||||||
if (!prev && now)
|
if (!prev && now)
|
||||||
@@ -352,11 +426,11 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
|||||||
|
|
||||||
public void updateProState()
|
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 != null) prefUpgradeAccount.setVisibility( pmode ? View.GONE : View.VISIBLE);
|
||||||
if (prefUpgradeAccount_info != null) prefUpgradeAccount_info.setVisibility(p != null ? View.GONE : View.VISIBLE);
|
if (prefUpgradeAccount_info != null) prefUpgradeAccount_info.setVisibility(pmode ? View.GONE : View.VISIBLE);
|
||||||
if (prefUpgradeAccount_msg != null) prefUpgradeAccount_msg.setVisibility( p != null ? View.VISIBLE : View.GONE );
|
if (prefUpgradeAccount_msg != null) prefUpgradeAccount_msg.setVisibility( pmode ? View.VISIBLE : View.GONE );
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getCacheSizeIndex(int value)
|
private int getCacheSizeIndex(int value)
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
10
android/app/src/main/res/drawable/ic_share_small.xml
Normal file
10
android/app/src/main/res/drawable/ic_share_small.xml
Normal 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>
|
16
android/app/src/main/res/drawable/ic_trash_small.xml
Normal file
16
android/app/src/main/res/drawable/ic_trash_small.xml
Normal 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>
|
@@ -1,31 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout
|
<RelativeLayout android:id="@+id/layoutRoot"
|
||||||
android:id="@+id/layoutRoot"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="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
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
|
app:titleTextColor="@color/colorOnPrimary"
|
||||||
android:background="?attr/colorPrimary"
|
android:background="?attr/colorPrimary"
|
||||||
android:elevation="6dp"
|
android:elevation="6dp"
|
||||||
android:minHeight="?attr/actionBarSize"
|
android:minHeight="?attr/actionBarSize" />
|
||||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/tab_layout"
|
android:id="@+id/tab_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/toolbar"
|
android:layout_below="@+id/toolbar"
|
||||||
|
app:titleTextColor="@color/colorOnPrimary"
|
||||||
|
app:tabTextColor="@color/colorOnPrimary"
|
||||||
|
app:tabSelectedTextColor="@color/colorSecondary"
|
||||||
android:background="?attr/colorPrimary"
|
android:background="?attr/colorPrimary"
|
||||||
android:elevation="6dp"
|
android:elevation="6dp"
|
||||||
android:minHeight="?attr/actionBarSize"
|
android:minHeight="?attr/actionBarSize" />
|
||||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>
|
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
<androidx.viewpager.widget.ViewPager
|
||||||
android:id="@+id/pager"
|
android:id="@+id/pager"
|
||||||
|
13
android/app/src/main/res/layout/activity_querylog.xml
Normal file
13
android/app/src/main/res/layout/activity_querylog.xml
Normal 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>
|
240
android/app/src/main/res/layout/activity_singlequerylog.xml
Normal file
240
android/app/src/main/res/layout/activity_singlequerylog.xml
Normal 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>
|
50
android/app/src/main/res/layout/adapter_querylog.xml
Normal file
50
android/app/src/main/res/layout/adapter_querylog.xml
Normal 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>
|
@@ -198,19 +198,27 @@
|
|||||||
app:layout_constraintBottom_toTopOf="@+id/btnAccountReset"
|
app:layout_constraintBottom_toTopOf="@+id/btnAccountReset"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/ic_img_quota" />
|
app:layout_constraintTop_toBottomOf="@+id/ic_img_quota" />
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnAccountReset"
|
android:id="@+id/btnAccountReset"
|
||||||
|
app:cornerRadius="0dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:backgroundTint="#fa315b"
|
||||||
android:text="@string/str_reset_account"
|
android:text="@string/str_reset_account"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/btnClearLocalStorage" />
|
app:layout_constraintBottom_toTopOf="@+id/btnClearLocalStorage" />
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnClearLocalStorage"
|
android:id="@+id/btnClearLocalStorage"
|
||||||
|
app:cornerRadius="0dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Clear Messages"
|
android:text="Clear Messages"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:backgroundTint="#607D8B"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
@@ -95,6 +95,46 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minHeight="48dp" />
|
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
|
<View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
@@ -111,8 +151,10 @@
|
|||||||
android:gravity="center|center"
|
android:gravity="center|center"
|
||||||
android:minHeight="48dp">
|
android:minHeight="48dp">
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/prefUpgradeAccount"
|
android:id="@+id/prefUpgradeAccount"
|
||||||
|
app:cornerRadius="0dp"
|
||||||
|
android:backgroundTint="#4CAF50"
|
||||||
android:text="@string/str_upgrade_account"
|
android:text="@string/str_upgrade_account"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
@@ -763,6 +805,24 @@
|
|||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
</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>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
@@ -108,6 +108,43 @@
|
|||||||
android:paddingTop="3dp"
|
android:paddingTop="3dp"
|
||||||
android:contentDescription="@string/desc_priority_icon" />
|
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.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
8
android/app/src/main/res/values/attrs.xml
Normal file
8
android/app/src/main/res/values/attrs.xml
Normal 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>
|
@@ -6,6 +6,9 @@
|
|||||||
<color name="colorHeader">#3F51B5</color>
|
<color name="colorHeader">#3F51B5</color>
|
||||||
<color name="colorHeaderForeground">#FFFFFF</color>
|
<color name="colorHeaderForeground">#FFFFFF</color>
|
||||||
|
|
||||||
|
<color name="colorOnPrimary">#ecf0f1</color>
|
||||||
|
<color name="colorSecondary">#FF5722</color>
|
||||||
|
|
||||||
<color name="colorBlack">#000</color>
|
<color name="colorBlack">#000</color>
|
||||||
|
|
||||||
<color name="bg_row_background">#fa315b</color>
|
<color name="bg_row_background">#fa315b</color>
|
||||||
|
@@ -6,4 +6,5 @@
|
|||||||
<dimen name="padd_10">10dp</dimen>
|
<dimen name="padd_10">10dp</dimen>
|
||||||
<dimen name="ic_delete">30dp</dimen>
|
<dimen name="ic_delete">30dp</dimen>
|
||||||
<dimen name="thumbnail">90dp</dimen>
|
<dimen name="thumbnail">90dp</dimen>
|
||||||
|
<dimen name="fab_margin">16dp</dimen>
|
||||||
</resources>
|
</resources>
|
@@ -30,9 +30,13 @@
|
|||||||
<string name="str_enable_vibration">Enable notification vibration</string>
|
<string name="str_enable_vibration">Enable notification vibration</string>
|
||||||
<string name="str_upgrade_account">Upgrade account</string>
|
<string name="str_upgrade_account">Upgrade account</string>
|
||||||
<string name="str_deleteswipe">Delete messages by swiping left</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">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="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="volume_icon">Volume icon</string>
|
||||||
<string name="play_test_sound">Play test sound</string>
|
<string name="play_test_sound">Play test sound</string>
|
||||||
<string name="delete">DELETE</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>
|
</resources>
|
||||||
|
@@ -1,13 +1,20 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
<!-- 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 -->
|
<!-- your app branding color for the app bar -->
|
||||||
<item name="colorPrimary">#3F51B5</item>
|
<item name="colorPrimary">#3F51B5</item>
|
||||||
|
|
||||||
<!-- darker variant for the status bar and contextual app bars -->
|
<!-- darker variant for the status bar and contextual app bars -->
|
||||||
<item name="colorPrimaryDark">#303F9F</item>
|
<item name="colorPrimaryDark">#303F9F</item>
|
||||||
|
|
||||||
<!-- theme UI controls like checkboxes and text fields -->
|
<!-- 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>
|
||||||
|
|
||||||
|
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
|
||||||
|
|
||||||
|
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
7
android/app/src/main/res/xml/filepaths.xml
Normal file
7
android/app/src/main/res/xml/filepaths.xml
Normal 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>
|
@@ -1,3 +1,3 @@
|
|||||||
#Mon Nov 19 18:43:09 CET 2018
|
#Thu Mar 05 15:29:10 UTC 2020
|
||||||
VERSION_NAME=1.1.0
|
VERSION_NAME=1.8.0
|
||||||
VERSION_CODE=15
|
VERSION_CODE=23
|
||||||
|
@@ -7,8 +7,8 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||||
classpath 'com.google.gms:google-services:4.2.0'
|
classpath 'com.google.gms:google-services:4.3.4'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
#Wed Sep 26 22:10:14 CEST 2018
|
#Tue Nov 03 14:10:19 CET 2020
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
58
androidExportReader/.gitignore
vendored
Normal 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
8
androidExportReader/.idea/.gitignore
generated
vendored
Normal 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
6
androidExportReader/.idea/compiler.xml
generated
Normal 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
17
androidExportReader/.idea/gradle.xml
generated
Normal 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>
|
11
androidExportReader/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
11
androidExportReader/.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
25
androidExportReader/.idea/jarRepositories.xml
generated
Normal file
25
androidExportReader/.idea/jarRepositories.xml
generated
Normal 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
10
androidExportReader/.idea/misc.xml
generated
Normal 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
6
androidExportReader/.idea/vcs.xml
generated
Normal 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>
|
47
androidExportReader/build.gradle
Normal file
47
androidExportReader/build.gradle
Normal 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()
|
||||||
|
}
|
BIN
androidExportReader/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
androidExportReader/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
androidExportReader/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
androidExportReader/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
240
androidExportReader/gradlew
vendored
Executable 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
91
androidExportReader/gradlew.bat
vendored
Normal 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
|
2
androidExportReader/settings.gradle
Normal file
2
androidExportReader/settings.gradle
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
rootProject.name = 'androidExportReader'
|
||||||
|
|
104
androidExportReader/src/main/java/com/blackforestbytes/Main.java
Normal file
104
androidExportReader/src/main/java/com/blackforestbytes/Main.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
35
examples/scn_send.php
Normal file
35
examples/scn_send.php
Normal 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'];
|
||||||
|
}
|
@@ -21,6 +21,7 @@ user_key="????????????????????????????????????????????????????????????????"
|
|||||||
|
|
||||||
title=$1
|
title=$1
|
||||||
content=""
|
content=""
|
||||||
|
sendtime=$(date +%s)
|
||||||
|
|
||||||
if [ "$#" -gt 1 ]; then
|
if [ "$#" -gt 1 ]; then
|
||||||
content=$2
|
content=$2
|
||||||
@@ -37,7 +38,7 @@ usr_msg_id=$(uuidgen)
|
|||||||
while true ; do
|
while true ; do
|
||||||
|
|
||||||
curlresp=$(curl -s -o /dev/null -w "%{http_code}" \
|
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" \
|
-d "content=$content" -d "priority=$priority" -d "msg_id=$usr_msg_id" \
|
||||||
https://scn.blackforestbytes.com/send.php)
|
https://scn.blackforestbytes.com/send.php)
|
||||||
|
|
||||||
|
35
reactnative/.gitignore
vendored
Normal file
35
reactnative/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
web-build/
|
||||||
|
|
||||||
|
# Native
|
||||||
|
*.orig.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
|
||||||
|
# Metro
|
||||||
|
.metro-health-check*
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.*
|
||||||
|
yarn-debug.*
|
||||||
|
yarn-error.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
83
reactnative/App.tsx
Normal file
83
reactnative/App.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { FlashList } from '@shopify/flash-list';
|
||||||
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { LayoutAnimation, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const generateArray = (size: number) => {
|
||||||
|
const arr = new Array(size);
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
arr[i] = i;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [data, setData] = useState(generateArray(100));
|
||||||
|
|
||||||
|
const list = useRef<FlashList<number> | null>(null);
|
||||||
|
|
||||||
|
const removeItem = (item: number) => {
|
||||||
|
setData(
|
||||||
|
data.filter((dataItem) => {
|
||||||
|
return dataItem !== item;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
list.current?.prepareForLayoutAnimationRender();
|
||||||
|
// after removing the item, we start animation
|
||||||
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: number }) => {
|
||||||
|
const backgroundColor = item % 2 === 0 ? "#00a1f1" : "#ffbb00";
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
removeItem(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
...styles.container,
|
||||||
|
backgroundColor: item > 97 ? "red" : backgroundColor,
|
||||||
|
height: item % 2 === 0 ? 100 : 200,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>Cell Id: {item}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlashList
|
||||||
|
ref={list}
|
||||||
|
style={{flexGrow: 1, width: '100%', height: 50, backgroundColor: '#88F'}}
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={() => {
|
||||||
|
setRefreshing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setRefreshing(false);
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
keyExtractor={(item: number) => {
|
||||||
|
return item.toString();
|
||||||
|
}}
|
||||||
|
getItemType={(item: number) => {
|
||||||
|
return item > 97 ? "even" : "odd";
|
||||||
|
}}
|
||||||
|
renderItem={renderItem}
|
||||||
|
estimatedItemSize={100}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f8f',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
});
|
4
reactnative/Makefile
Normal file
4
reactnative/Makefile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
run:
|
||||||
|
npx expo start
|
30
reactnative/app.json
Normal file
30
reactnative/app.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "simplecloudnotifier",
|
||||||
|
"slug": "simplecloudnotifier",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"userInterfaceStyle": "light",
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/splash.png",
|
||||||
|
"resizeMode": "contain",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"assetBundlePatterns": [
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
reactnative/assets/adaptive-icon.png
Normal file
BIN
reactnative/assets/adaptive-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
reactnative/assets/favicon.png
Normal file
BIN
reactnative/assets/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
reactnative/assets/icon.png
Normal file
BIN
reactnative/assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
reactnative/assets/splash.png
Normal file
BIN
reactnative/assets/splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
6
reactnative/babel.config.js
Normal file
6
reactnative/babel.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = function(api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: ['babel-preset-expo'],
|
||||||
|
};
|
||||||
|
};
|
13710
reactnative/package-lock.json
generated
Normal file
13710
reactnative/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
reactnative/package.json
Normal file
24
reactnative/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "simplecloudnotifier",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "node_modules/expo/AppEntry.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "expo start",
|
||||||
|
"android": "expo start --android",
|
||||||
|
"ios": "expo start --ios",
|
||||||
|
"web": "expo start --web"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"expo": "~49.0.10",
|
||||||
|
"expo-status-bar": "~1.6.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-native": "0.72.4",
|
||||||
|
"@shopify/flash-list": "1.4.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.20.0",
|
||||||
|
"@types/react": "~18.2.14",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
6
reactnative/tsconfig.json
Normal file
6
reactnative/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "expo/tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
184
scn_send.sh
Normal file
184
scn_send.sh
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
#
|
||||||
|
# Wrapper around SCN ( https://simplecloudnotifier.de/ )
|
||||||
|
# ======================================================
|
||||||
|
#
|
||||||
|
# ./scn_send [@channel] title [content] [priority]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Call with scn_send "${title}"
|
||||||
|
# or scn_send "${title}" ${content}"
|
||||||
|
# or scn_send "${title}" ${content}" "${priority:0|1|2}"
|
||||||
|
# or scn_send "@${channel} "${title}"
|
||||||
|
# or scn_send "@${channel} "${title}" ${content}"
|
||||||
|
# or scn_send "@${channel} "${title}" ${content}" "${priority:0|1|2}"
|
||||||
|
#
|
||||||
|
# content can be of format "--scnsend-read-body-from-file={path}" to read body from file
|
||||||
|
# (this circumvents max commandline length)
|
||||||
|
#
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: "
|
||||||
|
echo " scn_send [@channel] title [content] [priority]"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function cfgcol { [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; }
|
||||||
|
|
||||||
|
function rederr() { if cfgcol; then >&2 echo -e "\x1B[31m$1\x1B[0m"; else >&2 echo "$1"; fi; }
|
||||||
|
function green() { if cfgcol; then echo -e "\x1B[32m$1\x1B[0m"; else echo "$1"; fi; }
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
#
|
||||||
|
# Get env 'SCN_UID' and 'SCN_KEY' from conf file
|
||||||
|
#
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. "/etc/scn.conf"
|
||||||
|
SCN_UID=${SCN_UID:-}
|
||||||
|
SCN_KEY=${SCN_KEY:-}
|
||||||
|
|
||||||
|
[ -z "${SCN_UID}" ] && { rederr "Missing config value 'SCN_UID' in /etc/scn.conf"; exit 1; }
|
||||||
|
[ -z "${SCN_KEY}" ] && { rederr "Missing config value 'SCN_KEY' in /etc/scn.conf"; exit 1; }
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
args=( "$@" )
|
||||||
|
|
||||||
|
title=""
|
||||||
|
content=""
|
||||||
|
channel=""
|
||||||
|
priority=""
|
||||||
|
usr_msg_id="$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)"
|
||||||
|
sendtime="$(date +%s)"
|
||||||
|
sender="$(hostname)"
|
||||||
|
|
||||||
|
if command -v srvname &> /dev/null; then
|
||||||
|
sender="$( srvname )"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${args[0]}" = "--" ]]; then
|
||||||
|
# only positional args form here on (currently not handled)
|
||||||
|
args=("${args[@]:1}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#args[@]} -lt 1 ]; then
|
||||||
|
rederr "[ERROR]: no title supplied via parameter"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${args[0]}" =~ ^@.* ]]; then
|
||||||
|
channel="${args[0]}"
|
||||||
|
args=("${args[@]:1}")
|
||||||
|
channel="${channel:1}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#args[@]} -lt 1 ]; then
|
||||||
|
rederr "[ERROR]: no title supplied via parameter"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
title="${args[0]}"
|
||||||
|
args=("${args[@]:1}")
|
||||||
|
|
||||||
|
content=""
|
||||||
|
|
||||||
|
if [ ${#args[@]} -gt 0 ]; then
|
||||||
|
content="${args[0]}"
|
||||||
|
args=("${args[@]:1}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#args[@]} -gt 0 ]; then
|
||||||
|
priority="${args[0]}"
|
||||||
|
args=("${args[@]:1}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#args[@]} -gt 0 ]; then
|
||||||
|
rederr "Too many arguments to scn_send"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$content" == --scnsend-read-body-from-file=* ]]; then
|
||||||
|
path="$( awk '{ print substr($0, 31) }' <<< "$content" )"
|
||||||
|
content="$( cat "$path" )"
|
||||||
|
fi
|
||||||
|
|
||||||
|
curlparams=()
|
||||||
|
|
||||||
|
curlparams+=( "--data-urlencode" "user_id=${SCN_UID}" )
|
||||||
|
curlparams+=( "--data-urlencode" "key=${SCN_KEY}" )
|
||||||
|
curlparams+=( "--data-urlencode" "title=$title" )
|
||||||
|
curlparams+=( "--data-urlencode" "timestamp=$sendtime" )
|
||||||
|
curlparams+=( "--data-urlencode" "msg_id=$usr_msg_id" )
|
||||||
|
|
||||||
|
if [[ -n "$content" ]]; then
|
||||||
|
curlparams+=("--data-urlencode" "content=$content")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$priority" ]]; then
|
||||||
|
curlparams+=("--data-urlencode" "priority=$priority")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$channel" ]]; then
|
||||||
|
curlparams+=("--data-urlencode" "channel=$channel")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$sender" ]]; then
|
||||||
|
curlparams+=("--data-urlencode" "sender_name=$sender")
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true ; do
|
||||||
|
|
||||||
|
outf="$(mktemp)"
|
||||||
|
|
||||||
|
curlresp=$(curl --silent \
|
||||||
|
--output "${outf}" \
|
||||||
|
--write-out "%{http_code}" \
|
||||||
|
"${curlparams[@]}" \
|
||||||
|
"https://simplecloudnotifier.de/" )
|
||||||
|
|
||||||
|
curlout="$(cat "$outf")"
|
||||||
|
rm "$outf"
|
||||||
|
|
||||||
|
if [ "$curlresp" == 200 ] ; then
|
||||||
|
green "Successfully send"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$curlresp" == 400 ] ; then
|
||||||
|
rederr "Bad request - something went wrong"
|
||||||
|
echo "$curlout"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$curlresp" == 401 ] ; then
|
||||||
|
rederr "Unauthorized - wrong userid/userkey"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$curlresp" == 403 ] ; then
|
||||||
|
rederr "Quota exceeded - wait 5 min before re-try"
|
||||||
|
sleep 300
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$curlresp" == 412 ] ; then
|
||||||
|
rederr "Precondition Failed - No device linked"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$curlresp" == 500 ] ; then
|
||||||
|
rederr "Internal server error - waiting for better times"
|
||||||
|
sleep 60
|
||||||
|
fi
|
||||||
|
|
||||||
|
# if none of the above matched we probably have no network ...
|
||||||
|
rederr "Send failed (response code $curlresp) ... try again in 5s"
|
||||||
|
sleep 5
|
||||||
|
done
|
106
scnserver/.gitignore
vendored
Normal file
106
scnserver/.gitignore
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
|
||||||
|
|
||||||
|
_build
|
||||||
|
.run-data
|
||||||
|
|
||||||
|
DOCKER_GIT_INFO
|
||||||
|
|
||||||
|
scn_export.dat
|
||||||
|
scn_export.json
|
||||||
|
|
||||||
|
scn_export_*.dat
|
||||||
|
scn_export_*.json
|
||||||
|
|
||||||
|
simple_cloud_notifier-202306172202.sql
|
||||||
|
simple_cloud_notifier-*.sql
|
||||||
|
|
||||||
|
identifier.sqlite
|
||||||
|
|
||||||
|
.idea/dataSources.xml
|
||||||
|
|
||||||
|
.swaggobin
|
||||||
|
|
||||||
|
scn_send.sh
|
||||||
|
|
||||||
|
##############
|
||||||
|
|
||||||
|
|
||||||
|
.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
40
scnserver/.golangci.yml
Normal 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
8
scnserver/.idea/.gitignore
generated
vendored
Normal 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
scnserver/.idea/golinter.xml
generated
Normal file
6
scnserver/.idea/golinter.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GoLinterSettings">
|
||||||
|
<option name="checkGoLinterExe" value="false" />
|
||||||
|
</component>
|
||||||
|
</project>
|
12
scnserver/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
12
scnserver/.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
8
scnserver/.idea/modules.xml
generated
Normal 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
21
scnserver/.idea/server.iml
generated
Normal 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
11
scnserver/.idea/sqldialects.xml
generated
Normal 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="{"node":{ "@negative":"1", "group":{ "@kind":"root", "node":{ "name":{ "@qname":"b3228d61-4c36-41ce-803f-63bd80e198b3" }, "group":{ "@kind":"schema", "node":{ "name":{ "@qname":"schema_3.0.ddl" } } } } } }}" />
|
||||||
|
<file url="PROJECT" scope="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
6
scnserver/.idea/vcs.xml
generated
Normal file
6
scnserver/.idea/vcs.xml
generated
Normal 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>
|
36
scnserver/Dockerfile
Normal file
36
scnserver/Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
|
||||||
|
FROM golang:1-bullseye AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y ca-certificates openssl make git tar coreutils && \
|
||||||
|
apt-get install -y python3 python3-pip && \
|
||||||
|
pip install virtualenv && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY . /buildsrc
|
||||||
|
|
||||||
|
RUN cd /buildsrc && cp "scn_send.sh" "../scn_send.sh" && 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"]
|
112
scnserver/Makefile
Normal file
112
scnserver/Makefile
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
DOCKER_REPO=registry.blackforestbytes.com
|
||||||
|
DOCKER_NAME=mikescher/simplecloudnotifier
|
||||||
|
PORT=9090
|
||||||
|
|
||||||
|
NAMESPACE=$(shell git rev-parse --abbrev-ref HEAD)
|
||||||
|
HASH=$(shell git rev-parse HEAD)
|
||||||
|
|
||||||
|
.PHONY: test swagger pygmentize docker migrate dgi pygmentize lint docker
|
||||||
|
|
||||||
|
SWAGGO_VERSION=v1.8.12
|
||||||
|
SWAGGO=github.com/swaggo/swag/cmd/swag@$(SWAGGO_VERSION)
|
||||||
|
|
||||||
|
build: ids enums swagger pygmentize fmt
|
||||||
|
mkdir -p _build
|
||||||
|
rm -f ./_build/scn_backend
|
||||||
|
CGO_ENABLED=1 go build -v -o _build/scn_backend -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" ./cmd/scnserver
|
||||||
|
|
||||||
|
enums:
|
||||||
|
go generate models/enums.go
|
||||||
|
|
||||||
|
ids:
|
||||||
|
go generate models/ids.go
|
||||||
|
|
||||||
|
run: build
|
||||||
|
mkdir -p .run-data
|
||||||
|
_build/scn_backend
|
||||||
|
|
||||||
|
gow:
|
||||||
|
which gow || go install github.com/mitranim/gow@latest
|
||||||
|
gow -e "go,mod,html,css,json,yaml,js" run -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" blackforestbytes.com/simplecloudnotifier/cmd/scnserver
|
||||||
|
|
||||||
|
dgi:
|
||||||
|
[ ! -f "DOCKER_GIT_INFO" ] || rm DOCKER_GIT_INFO
|
||||||
|
echo -n "VCSTYPE=" >> DOCKER_GIT_INFO ; echo "git" >> DOCKER_GIT_INFO
|
||||||
|
echo -n "BRANCH=" >> DOCKER_GIT_INFO ; git rev-parse --abbrev-ref HEAD >> DOCKER_GIT_INFO
|
||||||
|
echo -n "HASH=" >> DOCKER_GIT_INFO ; git rev-parse HEAD >> DOCKER_GIT_INFO
|
||||||
|
echo -n "COMMITTIME=" >> DOCKER_GIT_INFO ; git log -1 --format=%cd --date=iso >> DOCKER_GIT_INFO
|
||||||
|
echo -n "REMOTE=" >> DOCKER_GIT_INFO ; git config --get remote.origin.url >> DOCKER_GIT_INFO
|
||||||
|
|
||||||
|
docker: dgi
|
||||||
|
cp ../scn_send.sh .
|
||||||
|
docker build \
|
||||||
|
-t "$(DOCKER_NAME):$(HASH)" \
|
||||||
|
-t "$(DOCKER_NAME):$(NAMESPACE)-latest" \
|
||||||
|
-t "$(DOCKER_NAME):latest" \
|
||||||
|
-t "$(DOCKER_REPO)/$(DOCKER_NAME):$(HASH)" \
|
||||||
|
-t "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest" \
|
||||||
|
-t "$(DOCKER_REPO)/$(DOCKER_NAME):latest" \
|
||||||
|
.
|
||||||
|
[ -f "scn_send.sh" ] && rm scn_send.sh
|
||||||
|
|
||||||
|
swagger-setup:
|
||||||
|
mkdir -p ".swaggobin"
|
||||||
|
[ -f ".swaggobin/swag_$(SWAGGO_VERSION)" ] || { GOBIN=/tmp/_swaggo go install $(SWAGGO); cp "/tmp/_swaggo/swag" ".swaggobin/swag_$(SWAGGO_VERSION)"; rm -rf "/tmp/_swaggo"; }
|
||||||
|
|
||||||
|
swagger: swagger-setup
|
||||||
|
".swaggobin/swag_$(SWAGGO_VERSION)" init -generalInfo ./api/router.go --propertyStrategy camelcase --output ./swagger/ --outputTypes "json,yaml"
|
||||||
|
|
||||||
|
pygmentize: website/scn_send.html
|
||||||
|
|
||||||
|
website/scn_send.html: ../scn_send.sh
|
||||||
|
_pygments/pygmentizew -l bash -f html "$(shell pwd)/../scn_send.sh" > "$(shell pwd)/website/scn_send.html"
|
||||||
|
_pygments/pygmentizew -S monokai -f html > "$(shell pwd)/website/css/pygmnetize-dark.css"
|
||||||
|
_pygments/pygmentizew -S borland -f html > "$(shell pwd)/website/css/pygmnetize-light.css"
|
||||||
|
|
||||||
|
run-docker-local: docker
|
||||||
|
mkdir -p .run-data
|
||||||
|
docker run --rm \
|
||||||
|
--init \
|
||||||
|
--env "CONF_NS=local-docker" \
|
||||||
|
--volume "$(shell pwd)/.run-data/docker-local:/data" \
|
||||||
|
--publish "8080:80" \
|
||||||
|
$(DOCKER_NAME):latest
|
||||||
|
|
||||||
|
inspect-docker: docker
|
||||||
|
mkdir -p .run-data
|
||||||
|
docker run -ti \
|
||||||
|
--rm \
|
||||||
|
--volume "$(shell pwd)/.run-data/docker-inspect:/data" \
|
||||||
|
$(DOCKER_NAME):latest \
|
||||||
|
bash
|
||||||
|
|
||||||
|
push-docker:
|
||||||
|
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(HASH)"
|
||||||
|
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):$(NAMESPACE)-latest"
|
||||||
|
docker image push "$(DOCKER_REPO)/$(DOCKER_NAME):latest"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf _build/*
|
||||||
|
rm -rf .run-data/*
|
||||||
|
rm -rf _pygments/env
|
||||||
|
git clean -fdx
|
||||||
|
! which go 2>&1 >> /dev/null || go clean
|
||||||
|
! which go 2>&1 >> /dev/null || go clean -testcache
|
||||||
|
|
||||||
|
fmt: swagger-setup
|
||||||
|
go fmt ./...
|
||||||
|
".swaggobin/swag_$(SWAGGO_VERSION)" fmt
|
||||||
|
|
||||||
|
test:
|
||||||
|
which gotestsum || go install gotest.tools/gotestsum@latest
|
||||||
|
gotestsum --format "testname" -- -tags="timetzdata sqlite_fts5 sqlite_foreign_keys" "./test"
|
||||||
|
|
||||||
|
migrate:
|
||||||
|
CGO_ENABLED=1 go build -v -o _build/scn_migrate -tags "timetzdata sqlite_fts5 sqlite_foreign_keys" ./cmd/migrate
|
||||||
|
./_build/scn_migrate
|
||||||
|
|
||||||
|
lint:
|
||||||
|
# curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.2
|
||||||
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
|
73
scnserver/TODO.md
Normal file
73
scnserver/TODO.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
|
||||||
|
TODO
|
||||||
|
========
|
||||||
|
|
||||||
|
|
||||||
|
#### DO DO DO
|
||||||
|
|
||||||
|
- app-store link in HTML
|
||||||
|
|
||||||
|
- ios purchase verification
|
||||||
|
|
||||||
|
- (!) use goext.ginWrapper
|
||||||
|
|
||||||
|
- (!) use goext.exerr
|
||||||
|
|
||||||
|
- use bfcodegen (enums+id)
|
||||||
|
|
||||||
|
#### UNSURE
|
||||||
|
|
||||||
|
- (?) default-priority for channels
|
||||||
|
|
||||||
|
- (?) "login" on website and list/search/filter messages
|
||||||
|
|
||||||
|
- (?) make channels deleteable (soft-delete) (what do with messages in channel?)
|
||||||
|
|
||||||
|
- (?) desktop client for notifications
|
||||||
|
|
||||||
|
- (?) add querylog (similar to requestlog/errorlog) - only for main-db
|
||||||
|
|
||||||
|
- (?) specify 'type' of message (debug, info, warn, error, fatal) -> distinct from priority
|
||||||
|
|
||||||
|
#### LATER
|
||||||
|
|
||||||
|
- do i need bool2db()? it seems to work for keytokens without them?
|
||||||
|
|
||||||
|
- We no longer have a route to reshuffle all keys (previously in updateUser), add a /user/:uid/keys/reset ?
|
||||||
|
Would delete all existing keys and create 3 new ones?
|
||||||
|
|
||||||
|
- error logging as goroutine, gets all errors via channel,
|
||||||
|
(channel buffered - nonblocking send, second channel that gets a message when sender failed )
|
||||||
|
(then all errors end up in _second_ sqlite table)
|
||||||
|
due to message channel etc everything is non blocking and cant fail in main
|
||||||
|
|
||||||
|
- => implement proper error logging in goext, kinda combines zerolog and wrapped-errors
|
||||||
|
copy basic code from bringman, but remove all bm specific stuff and make it abstract
|
||||||
|
Register(ErrType) methods, errtypes then as structs
|
||||||
|
log.xxx package with same interface as zerolog
|
||||||
|
|
||||||
|
- jobs to clear error-db to only keep X entries... (requests-db already exists)
|
||||||
|
|
||||||
|
- route to re-check all pro-token (for me)
|
||||||
|
|
||||||
|
- endpoint to list all servernames of user (distinct select)
|
||||||
|
|
||||||
|
- weblogin, webapp, ...
|
||||||
|
|
||||||
|
- Pagination for ListChannels / ListSubscriptions / ListClients / ListChannelSubscriptions / ListUserSubscriptions
|
||||||
|
|
||||||
|
- Use only single struct for DB|Model|JSON
|
||||||
|
* needs sq.Converter implementation
|
||||||
|
* needs to handle joined data
|
||||||
|
* rfctime.Time...
|
||||||
|
|
||||||
|
- use job superclass (copy from isi/bnet/?), reduce duplicate code
|
||||||
|
|
||||||
|
- admin panel (especially errors and requests)
|
||||||
|
|
||||||
|
- cli app (?)
|
||||||
|
|
||||||
|
#### FUTURE
|
||||||
|
|
||||||
|
- Remove compat, especially do not create compat id for every new message...
|
21
scnserver/_gen/enum-generate.go
Normal file
21
scnserver/_gen/enum-generate.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/bfcodegen"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dest := os.Args[2]
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bfcodegen.GenerateEnumSpecs(wd, dest)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
20
scnserver/_gen/id-generate.go
Normal file
20
scnserver/_gen/id-generate.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/bfcodegen"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dest := os.Args[2]
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bfcodegen.GenerateCharsetIDSpecs(wd, dest)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
3
scnserver/_pygments/.gitignore
vendored
Normal file
3
scnserver/_pygments/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
env
|
16
scnserver/_pygments/pygmentizew
Executable file
16
scnserver/_pygments/pygmentizew
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -o nounset # disallow usage of unset vars ( set -u )
|
||||||
|
set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
|
||||||
|
set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
|
||||||
|
set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
|
||||||
|
IFS=$'\n\t' # Set $IFS to only newline and tab.
|
||||||
|
|
||||||
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
|
||||||
|
1>&2 virtualenv env
|
||||||
|
1>&2 source env/bin/activate
|
||||||
|
|
||||||
|
1>&2 pip install Pygments
|
||||||
|
|
||||||
|
pygmentize "$@"
|
63
scnserver/api/apierr/enums.go
Normal file
63
scnserver/api/apierr/enums.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package apierr
|
||||||
|
|
||||||
|
type APIError int //@enum:type
|
||||||
|
|
||||||
|
//goland:noinspection GoSnakeCaseUsage
|
||||||
|
const (
|
||||||
|
UNDEFINED APIError = -1
|
||||||
|
|
||||||
|
NO_ERROR APIError = 0000
|
||||||
|
|
||||||
|
MISSING_UID APIError = 1101
|
||||||
|
MISSING_TOK APIError = 1102
|
||||||
|
MISSING_TITLE APIError = 1103
|
||||||
|
INVALID_PRIO APIError = 1104
|
||||||
|
REQ_METHOD APIError = 1105
|
||||||
|
INVALID_CLIENTTYPE APIError = 1106
|
||||||
|
PAGETOKEN_ERROR APIError = 1121
|
||||||
|
BINDFAIL_QUERY_PARAM APIError = 1151
|
||||||
|
BINDFAIL_BODY_PARAM APIError = 1152
|
||||||
|
BINDFAIL_URI_PARAM APIError = 1153
|
||||||
|
INVALID_BODY_PARAM APIError = 1161
|
||||||
|
INVALID_ENUM_VALUE APIError = 1171
|
||||||
|
|
||||||
|
NO_TITLE APIError = 1201
|
||||||
|
TITLE_TOO_LONG APIError = 1202
|
||||||
|
CONTENT_TOO_LONG APIError = 1203
|
||||||
|
USR_MSG_ID_TOO_LONG APIError = 1204
|
||||||
|
TIMESTAMP_OUT_OF_RANGE APIError = 1205
|
||||||
|
SENDERNAME_TOO_LONG APIError = 1206
|
||||||
|
CHANNEL_TOO_LONG APIError = 1207
|
||||||
|
CHANNEL_DESCRIPTION_TOO_LONG APIError = 1208
|
||||||
|
CHANNEL_NAME_EMPTY APIError = 1209
|
||||||
|
|
||||||
|
USER_NOT_FOUND APIError = 1301
|
||||||
|
CLIENT_NOT_FOUND APIError = 1302
|
||||||
|
CHANNEL_NOT_FOUND APIError = 1303
|
||||||
|
SUBSCRIPTION_NOT_FOUND APIError = 1304
|
||||||
|
MESSAGE_NOT_FOUND APIError = 1305
|
||||||
|
SUBSCRIPTION_USER_MISMATCH APIError = 1306
|
||||||
|
KEY_NOT_FOUND APIError = 1307
|
||||||
|
USER_AUTH_FAILED APIError = 1311
|
||||||
|
|
||||||
|
NO_DEVICE_LINKED APIError = 1401
|
||||||
|
|
||||||
|
CHANNEL_ALREADY_EXISTS APIError = 1501
|
||||||
|
CANNOT_SELFDELETE_KEY APIError = 1511
|
||||||
|
CANNOT_SELFUPDATE_KEY APIError = 1512
|
||||||
|
|
||||||
|
QUOTA_REACHED APIError = 2101
|
||||||
|
|
||||||
|
FAILED_VERIFY_PRO_TOKEN APIError = 3001
|
||||||
|
INVALID_PRO_TOKEN APIError = 3002
|
||||||
|
|
||||||
|
COMMIT_FAILED = 9001
|
||||||
|
DATABASE_ERROR = 9002
|
||||||
|
PERM_QUERY_FAIL = 9003
|
||||||
|
|
||||||
|
FIREBASE_COM_FAILED APIError = 9901
|
||||||
|
FIREBASE_COM_ERRORED APIError = 9902
|
||||||
|
INTERNAL_EXCEPTION APIError = 9903
|
||||||
|
PANIC APIError = 9904
|
||||||
|
NOT_IMPLEMENTED APIError = 9905
|
||||||
|
)
|
16
scnserver/api/apihighlight/highlights.go
Normal file
16
scnserver/api/apihighlight/highlights.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package apihighlight
|
||||||
|
|
||||||
|
type ErrHighlight int //@enum:type
|
||||||
|
|
||||||
|
//goland:noinspection GoSnakeCaseUsage
|
||||||
|
const (
|
||||||
|
NONE ErrHighlight = -1
|
||||||
|
USER_ID ErrHighlight = 101
|
||||||
|
USER_KEY ErrHighlight = 102
|
||||||
|
TITLE ErrHighlight = 103
|
||||||
|
CONTENT ErrHighlight = 104
|
||||||
|
PRIORITY ErrHighlight = 105
|
||||||
|
CHANNEL ErrHighlight = 106
|
||||||
|
SENDER_NAME ErrHighlight = 107
|
||||||
|
USER_MESSAGE_ID ErrHighlight = 108
|
||||||
|
)
|
21
scnserver/api/ginext/cors.go
Normal file
21
scnserver/api/ginext/cors.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package ginext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CorsMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE")
|
||||||
|
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(http.StatusOK)
|
||||||
|
} else {
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
scnserver/api/ginext/gin.go
Normal file
31
scnserver/api/ginext/gin.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package ginext
|
||||||
|
|
||||||
|
import (
|
||||||
|
scn "blackforestbytes.com/simplecloudnotifier"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SuppressGinLogs = false
|
||||||
|
|
||||||
|
func NewEngine(cfg scn.Config) *gin.Engine {
|
||||||
|
engine := gin.New()
|
||||||
|
|
||||||
|
engine.RedirectFixedPath = false
|
||||||
|
engine.RedirectTrailingSlash = false
|
||||||
|
|
||||||
|
if cfg.Cors {
|
||||||
|
engine.Use(CorsMiddleware())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.GinDebug {
|
||||||
|
ginlogger := gin.Logger()
|
||||||
|
engine.Use(func(context *gin.Context) {
|
||||||
|
if SuppressGinLogs {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ginlogger(context)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine
|
||||||
|
}
|
24
scnserver/api/ginext/handler.go
Normal file
24
scnserver/api/ginext/handler.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package ginext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RedirectFound(newuri string) gin.HandlerFunc {
|
||||||
|
return func(g *gin.Context) {
|
||||||
|
g.Redirect(http.StatusFound, newuri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RedirectTemporary(newuri string) gin.HandlerFunc {
|
||||||
|
return func(g *gin.Context) {
|
||||||
|
g.Redirect(http.StatusTemporaryRedirect, newuri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RedirectPermanent(newuri string) gin.HandlerFunc {
|
||||||
|
return func(g *gin.Context) {
|
||||||
|
g.Redirect(http.StatusPermanentRedirect, newuri)
|
||||||
|
}
|
||||||
|
}
|
23
scnserver/api/ginresp/apiError.go
Normal file
23
scnserver/api/ginresp/apiError.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package ginresp
|
||||||
|
|
||||||
|
type apiError struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error int `json:"error"`
|
||||||
|
ErrorHighlight int `json:"errhighlight"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type extendedAPIError struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error int `json:"error"`
|
||||||
|
ErrorHighlight int `json:"errhighlight"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
RawError *string `json:"__error"`
|
||||||
|
Trace []string `json:"__trace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type compatAPIError struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ErrorID int `json:"errid,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
212
scnserver/api/ginresp/resp.go
Normal file
212
scnserver/api/ginresp/resp.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package ginresp
|
||||||
|
|
||||||
|
import (
|
||||||
|
scn "blackforestbytes.com/simplecloudnotifier"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/api/apihighlight"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
json "gogs.mikescher.com/BlackForestBytes/goext/gojson"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPResponse interface {
|
||||||
|
Write(g *gin.Context)
|
||||||
|
Statuscode() int
|
||||||
|
BodyString() *string
|
||||||
|
ContentType() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonHTTPResponse struct {
|
||||||
|
statusCode int
|
||||||
|
data any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonHTTPResponse) Write(g *gin.Context) {
|
||||||
|
g.Render(j.statusCode, json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonHTTPResponse) Statuscode() int {
|
||||||
|
return j.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonHTTPResponse) BodyString() *string {
|
||||||
|
v, err := json.Marshal(j.data)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return langext.Ptr(string(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonHTTPResponse) ContentType() string {
|
||||||
|
return "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
type emptyHTTPResponse struct {
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j emptyHTTPResponse) Write(g *gin.Context) {
|
||||||
|
g.Status(j.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j emptyHTTPResponse) Statuscode() int {
|
||||||
|
return j.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j emptyHTTPResponse) BodyString() *string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j emptyHTTPResponse) ContentType() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type textHTTPResponse struct {
|
||||||
|
statusCode int
|
||||||
|
data string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j textHTTPResponse) Write(g *gin.Context) {
|
||||||
|
g.String(j.statusCode, "%s", j.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j textHTTPResponse) Statuscode() int {
|
||||||
|
return j.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j textHTTPResponse) BodyString() *string {
|
||||||
|
return langext.Ptr(j.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j textHTTPResponse) ContentType() string {
|
||||||
|
return "text/plain"
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataHTTPResponse struct {
|
||||||
|
statusCode int
|
||||||
|
data []byte
|
||||||
|
contentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j dataHTTPResponse) Write(g *gin.Context) {
|
||||||
|
g.Data(j.statusCode, j.contentType, j.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j dataHTTPResponse) Statuscode() int {
|
||||||
|
return j.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j dataHTTPResponse) BodyString() *string {
|
||||||
|
return langext.Ptr(string(j.data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j dataHTTPResponse) ContentType() string {
|
||||||
|
return j.contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorHTTPResponse struct {
|
||||||
|
statusCode int
|
||||||
|
data any
|
||||||
|
error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j errorHTTPResponse) Write(g *gin.Context) {
|
||||||
|
g.JSON(j.statusCode, j.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j errorHTTPResponse) Statuscode() int {
|
||||||
|
return j.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j errorHTTPResponse) BodyString() *string {
|
||||||
|
v, err := json.Marshal(j.data)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return langext.Ptr(string(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j errorHTTPResponse) ContentType() string {
|
||||||
|
return "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
func Status(sc int) HTTPResponse {
|
||||||
|
return &emptyHTTPResponse{statusCode: sc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func JSON(sc int, data any) HTTPResponse {
|
||||||
|
return &jsonHTTPResponse{statusCode: sc, data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Data(sc int, contentType string, data []byte) HTTPResponse {
|
||||||
|
return &dataHTTPResponse{statusCode: sc, contentType: contentType, data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Text(sc int, data string) HTTPResponse {
|
||||||
|
return &textHTTPResponse{statusCode: sc, data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func InternalError(e error) HTTPResponse {
|
||||||
|
return createApiError(nil, "InternalError", 500, apierr.INTERNAL_EXCEPTION, 0, e.Error(), e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func APIError(g *gin.Context, status int, errorid apierr.APIError, msg string, e error) HTTPResponse {
|
||||||
|
return createApiError(g, "APIError", status, errorid, 0, msg, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendAPIError(g *gin.Context, status int, errorid apierr.APIError, highlight apihighlight.ErrHighlight, msg string, e error) HTTPResponse {
|
||||||
|
return createApiError(g, "SendAPIError", status, errorid, highlight, msg, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotImplemented(g *gin.Context) HTTPResponse {
|
||||||
|
return createApiError(g, "NotImplemented", 500, apierr.NOT_IMPLEMENTED, 0, "Not Implemented", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createApiError(g *gin.Context, ident string, status int, errorid apierr.APIError, highlight apihighlight.ErrHighlight, msg string, e error) HTTPResponse {
|
||||||
|
reqUri := ""
|
||||||
|
if g != nil && g.Request != nil {
|
||||||
|
reqUri = g.Request.Method + " :: " + g.Request.RequestURI
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().
|
||||||
|
Int("errorid", int(errorid)).
|
||||||
|
Int("highlight", int(highlight)).
|
||||||
|
Str("uri", reqUri).
|
||||||
|
AnErr("err", e).
|
||||||
|
Stack().
|
||||||
|
Msg(fmt.Sprintf("[%s] %s", ident, msg))
|
||||||
|
|
||||||
|
if scn.Conf.ReturnRawErrors {
|
||||||
|
return &errorHTTPResponse{
|
||||||
|
statusCode: status,
|
||||||
|
data: extendedAPIError{
|
||||||
|
Success: false,
|
||||||
|
Error: int(errorid),
|
||||||
|
ErrorHighlight: int(highlight),
|
||||||
|
Message: msg,
|
||||||
|
RawError: langext.Ptr(langext.Conditional(e == nil, "", fmt.Sprintf("%+v", e))),
|
||||||
|
Trace: strings.Split(string(debug.Stack()), "\n"),
|
||||||
|
},
|
||||||
|
error: e,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &errorHTTPResponse{
|
||||||
|
statusCode: status,
|
||||||
|
data: apiError{
|
||||||
|
Success: false,
|
||||||
|
Error: int(errorid),
|
||||||
|
ErrorHighlight: int(highlight),
|
||||||
|
Message: msg,
|
||||||
|
},
|
||||||
|
error: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompatAPIError(errid int, msg string) HTTPResponse {
|
||||||
|
return &jsonHTTPResponse{statusCode: 200, data: compatAPIError{Success: false, ErrorID: errid, Message: msg}}
|
||||||
|
}
|
184
scnserver/api/ginresp/wrapper.go
Normal file
184
scnserver/api/ginresp/wrapper.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package ginresp
|
||||||
|
|
||||||
|
import (
|
||||||
|
scn "blackforestbytes.com/simplecloudnotifier"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WHandlerFunc func(*gin.Context) HTTPResponse
|
||||||
|
|
||||||
|
type RequestLogAcceptor interface {
|
||||||
|
InsertRequestLog(data models.RequestLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Wrap(rlacc RequestLogAcceptor, fn WHandlerFunc) gin.HandlerFunc {
|
||||||
|
|
||||||
|
maxRetry := scn.Conf.RequestMaxRetry
|
||||||
|
retrySleep := scn.Conf.RequestRetrySleep
|
||||||
|
|
||||||
|
return func(g *gin.Context) {
|
||||||
|
|
||||||
|
reqctx := g.Request.Context()
|
||||||
|
|
||||||
|
if g.Request.Body != nil {
|
||||||
|
g.Request.Body = dataext.NewBufferedReadCloser(g.Request.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
t0 := time.Now()
|
||||||
|
|
||||||
|
for ctr := 1; ; ctr++ {
|
||||||
|
|
||||||
|
wrap, stackTrace, panicObj := callPanicSafe(fn, g)
|
||||||
|
if panicObj != nil {
|
||||||
|
log.Error().Interface("panicObj", panicObj).Msg("Panic occured (in gin handler)")
|
||||||
|
log.Error().Msg(stackTrace)
|
||||||
|
wrap = APIError(g, 500, apierr.PANIC, "A panic occured in the HTTP handler", errors.New(fmt.Sprintf("%+v\n\n@:\n%s", panicObj, stackTrace)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Writer.Written() {
|
||||||
|
if scn.Conf.ReqLogEnabled {
|
||||||
|
rlacc.InsertRequestLog(createRequestLog(g, t0, ctr, nil, langext.Ptr("Writing in WrapperFunc is not supported")))
|
||||||
|
}
|
||||||
|
panic("Writing in WrapperFunc is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctr < maxRetry && isSqlite3Busy(wrap) {
|
||||||
|
log.Warn().Int("counter", ctr).Str("url", g.Request.URL.String()).Msg("Retry request (ErrBusy)")
|
||||||
|
|
||||||
|
err := resetBody(g)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(retrySleep)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqctx.Err() == nil {
|
||||||
|
if scn.Conf.ReqLogEnabled {
|
||||||
|
rlacc.InsertRequestLog(createRequestLog(g, t0, ctr, wrap, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
statuscode := wrap.Statuscode()
|
||||||
|
if statuscode/100 != 2 {
|
||||||
|
log.Warn().Str("url", g.Request.Method+"::"+g.Request.URL.String()).Msg(fmt.Sprintf("Request failed with statuscode %d", statuscode))
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap.Write(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRequestLog(g *gin.Context, t0 time.Time, ctr int, resp HTTPResponse, panicstr *string) models.RequestLog {
|
||||||
|
|
||||||
|
t1 := time.Now()
|
||||||
|
|
||||||
|
ua := g.Request.UserAgent()
|
||||||
|
auth := g.Request.Header.Get("Authorization")
|
||||||
|
ct := g.Request.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
var reqbody []byte = nil
|
||||||
|
if g.Request.Body != nil {
|
||||||
|
brcbody, err := g.Request.Body.(dataext.BufferedReadCloser).BufferedAll()
|
||||||
|
if err == nil {
|
||||||
|
reqbody = brcbody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var strreqbody *string = nil
|
||||||
|
if len(reqbody) < scn.Conf.ReqLogMaxBodySize {
|
||||||
|
strreqbody = langext.Ptr(string(reqbody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var respbody *string = nil
|
||||||
|
|
||||||
|
var strrespbody *string = nil
|
||||||
|
if resp != nil {
|
||||||
|
respbody = resp.BodyString()
|
||||||
|
if respbody != nil && len(*respbody) < scn.Conf.ReqLogMaxBodySize {
|
||||||
|
strrespbody = respbody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
permObj, hasPerm := g.Get("perm")
|
||||||
|
|
||||||
|
hasTok := false
|
||||||
|
if hasPerm {
|
||||||
|
hasTok = permObj.(models.PermissionSet).Token != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.RequestLog{
|
||||||
|
Method: g.Request.Method,
|
||||||
|
URI: g.Request.URL.String(),
|
||||||
|
UserAgent: langext.Conditional(ua == "", nil, &ua),
|
||||||
|
Authentication: langext.Conditional(auth == "", nil, &auth),
|
||||||
|
RequestBody: strreqbody,
|
||||||
|
RequestBodySize: int64(len(reqbody)),
|
||||||
|
RequestContentType: ct,
|
||||||
|
RemoteIP: g.RemoteIP(),
|
||||||
|
KeyID: langext.ConditionalFn10(hasTok, func() *models.KeyTokenID { return langext.Ptr(permObj.(models.PermissionSet).Token.KeyTokenID) }, nil),
|
||||||
|
UserID: langext.ConditionalFn10(hasTok, func() *models.UserID { return langext.Ptr(permObj.(models.PermissionSet).Token.OwnerUserID) }, nil),
|
||||||
|
Permissions: langext.ConditionalFn10(hasTok, func() *string { return langext.Ptr(permObj.(models.PermissionSet).Token.Permissions.String()) }, nil),
|
||||||
|
ResponseStatuscode: langext.ConditionalFn10(resp != nil, func() *int64 { return langext.Ptr(int64(resp.Statuscode())) }, nil),
|
||||||
|
ResponseBodySize: langext.ConditionalFn10(strrespbody != nil, func() *int64 { return langext.Ptr(int64(len(*respbody))) }, nil),
|
||||||
|
ResponseBody: strrespbody,
|
||||||
|
ResponseContentType: langext.ConditionalFn10(resp != nil, func() string { return resp.ContentType() }, ""),
|
||||||
|
RetryCount: int64(ctr),
|
||||||
|
Panicked: panicstr != nil,
|
||||||
|
PanicStr: panicstr,
|
||||||
|
ProcessingTime: t1.Sub(t0),
|
||||||
|
TimestampStart: t0,
|
||||||
|
TimestampFinish: t1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callPanicSafe(fn WHandlerFunc, g *gin.Context) (res HTTPResponse, stackTrace string, panicObj any) {
|
||||||
|
defer func() {
|
||||||
|
if rec := recover(); rec != nil {
|
||||||
|
res = nil
|
||||||
|
stackTrace = string(debug.Stack())
|
||||||
|
panicObj = rec
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
res = fn(g)
|
||||||
|
return res, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetBody(g *gin.Context) error {
|
||||||
|
if g.Request.Body == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := g.Request.Body.(dataext.BufferedReadCloser).Reset()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSqlite3Busy(r HTTPResponse) bool {
|
||||||
|
if errwrap, ok := r.(*errorHTTPResponse); ok && errwrap != nil {
|
||||||
|
if s3err, ok := (errwrap.error).(sqlite3.Error); ok {
|
||||||
|
if s3err.Code == sqlite3.ErrBusy {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
18
scnserver/api/handler/api.go
Normal file
18
scnserver/api/handler/api.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
primarydb "blackforestbytes.com/simplecloudnotifier/db/impl/primary"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIHandler struct {
|
||||||
|
app *logic.Application
|
||||||
|
database *primarydb.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIHandler(app *logic.Application) APIHandler {
|
||||||
|
return APIHandler{
|
||||||
|
app: app,
|
||||||
|
database: app.Database.Primary,
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user