Compare commits
64 Commits
Author | SHA1 | Date | |
---|---|---|---|
e95d0cb010
|
|||
f717355519
|
|||
3368e514ca
|
|||
9dca27177f
|
|||
c63274f7a9
|
|||
6c2d2d2345
|
|||
a6fbaa192f
|
|||
![]() |
a01f156535 | ||
3b021c09dc
|
|||
287176c881
|
|||
84eeb5a002
|
|||
b892532023
|
|||
![]() |
56cdc52bb4 | ||
9ef6b1dd91
|
|||
6f7585323b
|
|||
92135e64a3
|
|||
28efeea30b
|
|||
a8a074907b
|
|||
6c29ec9820
|
|||
![]() |
8ec23144ca | ||
b703853b28
|
|||
6f7529fc9b
|
|||
27b45098f0
|
|||
21f66d99cd
|
|||
1745051e6e
|
|||
![]() |
b395e054e6 | ||
90f4270b74
|
|||
f68d75c226
|
|||
9cf5133469
|
|||
75929aad48
|
|||
dca149ec4e
|
|||
75a7b97d24
|
|||
eb62873fb6
|
|||
f8effe7d1e
|
|||
87a3d34315
|
|||
0f38ad6e5c
|
|||
0ec6ab71b2
|
|||
b42204c87d
|
|||
53cb259ec1
|
|||
0150cc9e8e
|
|||
20214473f1
|
|||
![]() |
586ac07ef3 | ||
66f82f26d5
|
|||
6cb2aa00fb
|
|||
96a236803e
|
|||
9d24044eb3
|
|||
90248bcb54
|
|||
a3b2a1b14f
|
|||
b21b159b95
|
|||
93ec261dc0
|
|||
ae246e9219
|
|||
3f18fdd35a
|
|||
faf5207478
|
|||
71f003dd66
|
|||
3f85ab514e
|
|||
9eb5a6b1b9
|
|||
8e26cd6078
|
|||
36b9263730
|
|||
3d29fecaec
|
|||
![]() |
92ac05f1e3 | ||
cae6ff6271
|
|||
2163ae4dbf
|
|||
083945852b
|
|||
![]() |
e69b1232d7 |
57
android/.idea/assetWizardSettings.xml
generated
@@ -3,6 +3,59 @@
|
||||
<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>
|
||||
@@ -14,8 +67,8 @@
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="assetSourceType" value="FILE" />
|
||||
<entry key="outputName" value="priority_low" />
|
||||
<entry key="sourceFile" value="C:\Users\Mike\Downloads\Low Priority-595b40b75ba036ed117d9842.svg" />
|
||||
<entry key="outputName" value="ic_garbage" />
|
||||
<entry key="sourceFile" value="C:\Users\Mike\Downloads\garbage.svg" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
|
@@ -35,7 +35,7 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
@@ -45,7 +45,8 @@ dependencies {
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.google.firebase:firebase-core:16.0.4'
|
||||
implementation 'com.google.firebase:firebase-messaging:17.3.4'
|
||||
implementation 'com.google.android.gms:play-services-ads:17.0.0'
|
||||
implementation 'com.google.android.gms:play-services-ads:17.1.0'
|
||||
implementation 'com.android.billingclient:billing:1.2'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
|
||||
implementation 'com.github.kenglxn.QRGen:android:2.5.0'
|
||||
|
@@ -6,6 +6,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
|
||||
<application
|
||||
@@ -35,6 +36,8 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".service.BroadcastReceiverService" android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
BIN
android/app/src/main/ic_bfb_raster-web.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
android/app/src/main/ic_notification_full-web.png
Normal file
After Width: | Height: | Size: 67 KiB |
@@ -4,6 +4,7 @@ import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.billingclient.api.BillingClient;
|
||||
import com.blackforestbytes.simplecloudnotifier.view.AccountFragment;
|
||||
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
|
||||
import com.blackforestbytes.simplecloudnotifier.view.TabAdapter;
|
||||
@@ -18,7 +19,7 @@ import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
public class SCNApp extends Application implements LifecycleObserver
|
||||
{
|
||||
private static SCNApp instance;
|
||||
private static WeakReference<MainActivity> mainActivity;
|
||||
private static WeakReference<MainActivity> mainActivity = new WeakReference<>(null);
|
||||
|
||||
public static final boolean LOCAL_DEBUG = BuildConfig.DEBUG;
|
||||
public static final boolean DEBUG = BuildConfig.DEBUG || !BuildConfig.VERSION_NAME.endsWith(".0");
|
||||
@@ -98,32 +99,4 @@ public class SCNApp extends Application implements LifecycleObserver
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
==TODO==
|
||||
|
||||
- Pro mode
|
||||
- no ads
|
||||
- more quota
|
||||
- restore pro mode
|
||||
- send pro state to server
|
||||
|
||||
- prevent duplicate-send
|
||||
- send custom msg-id in API
|
||||
- prevent second ack on same msg-id
|
||||
|
||||
- more in-depth API doc on website (?)
|
||||
|
||||
- perhaps response codes in api (?)
|
||||
|
||||
- test notification channels
|
||||
|
||||
- publish (+ HN post ?)
|
||||
|
||||
- Use for mscom server errrors
|
||||
- Use for bfb server errors
|
||||
- Use for transmission state
|
||||
- Message on connnection lost (seperate process - resend until succ)
|
||||
- Message on connnection regained
|
||||
- Message on seed-count changed
|
||||
|
||||
*/
|
||||
//TODO TabLayout indicator does not corretly animate when directly clicking on tabs
|
@@ -0,0 +1,23 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.android;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public final class ThreadUtils
|
||||
{
|
||||
public static void safeSleep(int millisMin, int millisMax)
|
||||
{
|
||||
safeSleep(millisMin + (int)(Math.random()*(millisMax-millisMin)));
|
||||
}
|
||||
|
||||
public static void safeSleep(int millis)
|
||||
{
|
||||
try
|
||||
{
|
||||
Thread.sleep(millis);
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
Log.d("ThreadUtils", e.toString());
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.collections;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func1to1;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public final class CollectionHelper
|
||||
{
|
||||
public static <T, C> List<T> unique(List<T> input, Func1to1<T, C> mapping)
|
||||
{
|
||||
List<T> output = new ArrayList<>(input.size());
|
||||
|
||||
HashSet<C> seen = new HashSet<>();
|
||||
|
||||
for (T v : input) if (seen.add(mapping.invoke(v))) output.add(v);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public static <T> List<T> sort(List<T> input, Comparator<T> comparator)
|
||||
{
|
||||
List<T> output = new ArrayList<>(input);
|
||||
Collections.sort(output, comparator);
|
||||
return output;
|
||||
}
|
||||
|
||||
public static <T> void sort_inplace(List<T> input, Comparator<T> comparator)
|
||||
{
|
||||
Collections.sort(input, comparator);
|
||||
}
|
||||
|
||||
public static <T, U extends Comparable<U>> List<T> sort(List<T> input, Func1to1<T, U> mapper)
|
||||
{
|
||||
return sort(input, mapper, 1);
|
||||
}
|
||||
|
||||
public static <T, U extends Comparable<U>> List<T> sort(List<T> input, Func1to1<T, U> mapper, int sortMod)
|
||||
{
|
||||
List<T> output = new ArrayList<>(input);
|
||||
Collections.sort(output, (o1, o2) -> sortMod * mapper.invoke(o1).compareTo(mapper.invoke(o2)));
|
||||
return output;
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
|
||||
|
||||
public class IntRange
|
||||
{
|
||||
private int Start;
|
||||
public int Start() { return Start; }
|
||||
|
||||
private int End;
|
||||
public int End() { return End; }
|
||||
|
||||
public IntRange(int s, int e) { Start = s; End = e; }
|
||||
|
||||
private IntRange() { }
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
|
||||
|
||||
public class NInt
|
||||
{
|
||||
public int Value;
|
||||
|
||||
public NInt(int v) { Value = v; }
|
||||
|
||||
private NInt() { }
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
|
||||
|
||||
public final class Nothing
|
||||
{
|
||||
public final static Nothing Inst = new Nothing();
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
|
||||
|
||||
public class Tuple1<T1>
|
||||
{
|
||||
public final T1 Item1;
|
||||
|
||||
public Tuple1(T1 i1)
|
||||
{
|
||||
Item1 = i1;
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
|
||||
|
||||
public class Tuple2<T1, T2>
|
||||
{
|
||||
public final T1 Item1;
|
||||
public final T2 Item2;
|
||||
|
||||
public Tuple2(T1 i1, T2 i2)
|
||||
{
|
||||
Item1 = i1;
|
||||
Item2 = i2;
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
|
||||
|
||||
public class Tuple3<T1, T2, T3>
|
||||
{
|
||||
public final T1 Item1;
|
||||
public final T2 Item2;
|
||||
public final T3 Item3;
|
||||
|
||||
public Tuple3(T1 i1, T2 i2, T3 i3)
|
||||
{
|
||||
Item1 = i1;
|
||||
Item2 = i2;
|
||||
Item3 = i3;
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
|
||||
|
||||
public class Tuple4<T1, T2, T3, T4>
|
||||
{
|
||||
public final T1 Item1;
|
||||
public final T2 Item2;
|
||||
public final T3 Item3;
|
||||
public final T4 Item4;
|
||||
|
||||
public Tuple4(T1 i1, T2 i2, T3 i3, T4 i4)
|
||||
{
|
||||
Item1 = i1;
|
||||
Item2 = i2;
|
||||
Item3 = i3;
|
||||
Item4 = i4;
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
|
||||
|
||||
public class Tuple5<T1, T2, T3, T4, T5>
|
||||
{
|
||||
public final T1 Item1;
|
||||
public final T2 Item2;
|
||||
public final T3 Item3;
|
||||
public final T4 Item4;
|
||||
public final T5 Item5;
|
||||
|
||||
public Tuple5(T1 i1, T2 i2, T3 i3, T4 i4, T5 i5)
|
||||
{
|
||||
Item1 = i1;
|
||||
Item2 = i2;
|
||||
Item3 = i3;
|
||||
Item4 = i4;
|
||||
Item5 = i5;
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
import android.widget.SeekBar;
|
||||
|
||||
public final class FI
|
||||
{
|
||||
private FI() throws InstantiationException { throw new InstantiationException(); }
|
||||
|
||||
public static SeekBar.OnSeekBarChangeListener SeekBarChanged(Func3to0<SeekBar, Integer, Boolean> action)
|
||||
{
|
||||
return new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
action.invoke(seekBar, progress, fromUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func0to0 {
|
||||
|
||||
Func0to0 EMPTY = ()->{};
|
||||
|
||||
void invoke();
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func0to1<TResult> {
|
||||
TResult invoke();
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func0to1WithIOException<TResult> {
|
||||
TResult invoke() throws IOException;
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func1to0<TInput1> {
|
||||
void invoke(TInput1 value);
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func1to1<TInput1, TResult> {
|
||||
TResult invoke(TInput1 value);
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func2to0<TInput1, TInput2> {
|
||||
void invoke(TInput1 value1, TInput2 value2);
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func2to1<TInput1, TInput2, TResult> {
|
||||
TResult invoke(TInput1 value1, TInput2 value2);
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func3to0<TInput1, TInput2, TInput3> {
|
||||
void invoke(TInput1 value1, TInput2 value2, TInput3 value3);
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func4to0<TInput1, TInput2, TInput3, TInput4> {
|
||||
void invoke(TInput1 value1, TInput2 value2, TInput3 value3, TInput4 value4);
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Func5to0<TInput1, TInput2, TInput3, TInput4, TInput5> {
|
||||
void invoke(TInput1 value1, TInput2 value2, TInput3 value3, TInput4 value4, TInput5 value5);
|
||||
}
|
@@ -0,0 +1,231 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.string;
|
||||
|
||||
// from MonoSAMFramework.Portable.DebugTools.CompactJsonFormatter
|
||||
public class CompactJsonFormatter
|
||||
{
|
||||
private static final String INDENT_STRING = " ";
|
||||
|
||||
public static String formatJSON(String str, int maxIndent)
|
||||
{
|
||||
int indent = 0;
|
||||
boolean quoted = false;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char last = ' ';
|
||||
for (int i = 0; i < str.length(); i++)
|
||||
{
|
||||
char ch = str.charAt(i);
|
||||
switch (ch)
|
||||
{
|
||||
case '\r':
|
||||
case '\n':
|
||||
break;
|
||||
case '{':
|
||||
case '[':
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
if (!quoted)
|
||||
{
|
||||
indent++;
|
||||
if (indent >= maxIndent) break;
|
||||
sb.append("\n");
|
||||
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
|
||||
last = ' ';
|
||||
}
|
||||
break;
|
||||
case '}':
|
||||
case ']':
|
||||
if (!quoted)
|
||||
{
|
||||
indent--;
|
||||
if (indent + 1 >= maxIndent) { sb.append(ch); break; }
|
||||
sb.append("\n");
|
||||
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
|
||||
}
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
break;
|
||||
case '"':
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
boolean escaped = false;
|
||||
int index = i;
|
||||
while (index > 0 && str.charAt(--index) == '\\')
|
||||
escaped = !escaped;
|
||||
if (!escaped)
|
||||
quoted = !quoted;
|
||||
break;
|
||||
case ',':
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
if (!quoted)
|
||||
{
|
||||
if (indent >= maxIndent) { sb.append(' '); last = ' '; break; }
|
||||
sb.append("\n");
|
||||
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
|
||||
}
|
||||
break;
|
||||
case ':':
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
if (!quoted) { sb.append(" "); last = ' '; }
|
||||
break;
|
||||
case ' ':
|
||||
case '\t':
|
||||
if (quoted)
|
||||
{
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
}
|
||||
else if (last != ' ')
|
||||
{
|
||||
sb.append(' ');
|
||||
last = ' ';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String compressJson(String str, int compressionLevel)
|
||||
{
|
||||
int indent = 0;
|
||||
boolean quoted = false;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char last = ' ';
|
||||
int compress = 0;
|
||||
for (int i = 0; i < str.length(); i++)
|
||||
{
|
||||
char ch = str.charAt(i);
|
||||
switch (ch)
|
||||
{
|
||||
case '\r':
|
||||
case '\n':
|
||||
break;
|
||||
case '{':
|
||||
case '[':
|
||||
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
if (!quoted)
|
||||
{
|
||||
if (compress == 0 && getJsonDepth(str, i) <= compressionLevel)
|
||||
compress = 1;
|
||||
else if (compress > 0)
|
||||
compress++;
|
||||
|
||||
indent++;
|
||||
if (compress > 0) break;
|
||||
sb.append("\n");
|
||||
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
|
||||
last = ' ';
|
||||
}
|
||||
break;
|
||||
case '}':
|
||||
case ']':
|
||||
if (!quoted)
|
||||
{
|
||||
indent--;
|
||||
if (compress > 0) { compress--; sb.append(ch); break; }
|
||||
compress--;
|
||||
sb.append("\n");
|
||||
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
|
||||
}
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
break;
|
||||
case '"':
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
boolean escaped = false;
|
||||
int index = i;
|
||||
while (index > 0 && str.charAt(--index) == '\\')
|
||||
escaped = !escaped;
|
||||
if (!escaped)
|
||||
quoted = !quoted;
|
||||
break;
|
||||
case ',':
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
if (!quoted)
|
||||
{
|
||||
if (compress > 0) { sb.append(' '); last = ' '; break; }
|
||||
sb.append("\n");
|
||||
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
|
||||
}
|
||||
break;
|
||||
case ':':
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
if (!quoted) { sb.append(" "); last = ' '; }
|
||||
break;
|
||||
case ' ':
|
||||
case '\t':
|
||||
if (quoted)
|
||||
{
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
}
|
||||
else if (last != ' ')
|
||||
{
|
||||
sb.append(' ');
|
||||
last = ' ';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sb.append(ch);
|
||||
last = ch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static int getJsonDepth(String str, int i)
|
||||
{
|
||||
int maxindent = 0;
|
||||
int indent = 0;
|
||||
boolean quoted = false;
|
||||
for (; i < str.length(); i++)
|
||||
{
|
||||
char ch = str.charAt(i);
|
||||
switch (ch)
|
||||
{
|
||||
case '{':
|
||||
case '[':
|
||||
if (!quoted)
|
||||
{
|
||||
indent++;
|
||||
maxindent = Math.max(indent, maxindent);
|
||||
}
|
||||
break;
|
||||
|
||||
case '}':
|
||||
case ']':
|
||||
if (!quoted)
|
||||
{
|
||||
indent--;
|
||||
if (indent <= 0) return maxindent;
|
||||
}
|
||||
break;
|
||||
|
||||
case '"':
|
||||
boolean escaped = false;
|
||||
int index = i;
|
||||
while (index > 0 && str.charAt(--index) == '\\')
|
||||
escaped = !escaped;
|
||||
if (!escaped)
|
||||
quoted = !quoted;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return maxindent;
|
||||
}
|
||||
}
|
@@ -0,0 +1,98 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.lib.string;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func1to1;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.List;
|
||||
|
||||
public class Str
|
||||
{
|
||||
public final static String Empty = "";
|
||||
|
||||
public static String format(String fmt, Object... data)
|
||||
{
|
||||
return MessageFormat.format(fmt, data);
|
||||
}
|
||||
|
||||
public static String rformat(int fmtResId, Object... data)
|
||||
{
|
||||
Context inst = SCNApp.getContext();
|
||||
if (inst == null)
|
||||
{
|
||||
Log.e("StringFormat", "rformat::NoInstance --> inst==null for" + fmtResId);
|
||||
return "?ERR?";
|
||||
}
|
||||
|
||||
return MessageFormat.format(inst.getResources().getString(fmtResId), data);
|
||||
}
|
||||
|
||||
public static String firstLine(String content)
|
||||
{
|
||||
int idx = content.indexOf('\n');
|
||||
if (idx == -1) return content;
|
||||
|
||||
if (idx == 0) return Str.Empty;
|
||||
|
||||
if (content.charAt(idx-1) == '\r') return content.substring(0, idx-1);
|
||||
|
||||
return content.substring(0, idx);
|
||||
}
|
||||
|
||||
public static boolean isNullOrWhitespace(String str)
|
||||
{
|
||||
return str == null || str.length() == 0 || str.trim().length() == 0;
|
||||
}
|
||||
|
||||
public static boolean isNullOrEmpty(String str)
|
||||
{
|
||||
return str == null || str.length() == 0;
|
||||
}
|
||||
|
||||
public static boolean equals(String a, String b)
|
||||
{
|
||||
if (a == null) return (b == null);
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
public static String join(String sep, List<String> list)
|
||||
{
|
||||
StringBuilder b = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (String v : list)
|
||||
{
|
||||
if (!first) b.append(sep);
|
||||
b.append(v);
|
||||
first = false;
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
public static <T> String join(String sep, List<T> list, Func1to1<T, String> map)
|
||||
{
|
||||
StringBuilder b = new StringBuilder();
|
||||
boolean first = true;
|
||||
for (T v : list)
|
||||
{
|
||||
if (!first) b.append(sep);
|
||||
b.append(map.invoke(v));
|
||||
first = false;
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
public static Integer tryParseToInt(String s)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(s);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@ import java.util.TimeZone;
|
||||
|
||||
public class CMessage
|
||||
{
|
||||
public final long SCN_ID;
|
||||
public final long Timestamp;
|
||||
public final String Title;
|
||||
public final String Content;
|
||||
@@ -21,8 +22,9 @@ public class CMessage
|
||||
_format.setTimeZone(TimeZone.getDefault());
|
||||
}
|
||||
|
||||
public CMessage(long t, String mt, String mc, PriorityEnum p)
|
||||
public CMessage(long id, long t, String mt, String mc, PriorityEnum p)
|
||||
{
|
||||
SCN_ID = id;
|
||||
Timestamp = t;
|
||||
Title = mt;
|
||||
Content = mc;
|
||||
|
@@ -3,15 +3,23 @@ package com.blackforestbytes.simplecloudnotifier.model;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.collections.CollectionHelper;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||
import com.blackforestbytes.simplecloudnotifier.view.MessageAdapter;
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class CMessageList
|
||||
{
|
||||
private final Object msg_lock = new Object();
|
||||
|
||||
public ArrayList<CMessage> Messages;
|
||||
public Set<String> AllAcks;
|
||||
|
||||
private ArrayList<WeakReference<MessageAdapter>> _listener = new ArrayList<>();
|
||||
|
||||
@@ -27,8 +35,11 @@ public class CMessageList
|
||||
}
|
||||
|
||||
private CMessageList()
|
||||
{
|
||||
synchronized (msg_lock)
|
||||
{
|
||||
Messages = new ArrayList<>();
|
||||
AllAcks = new HashSet<>();
|
||||
|
||||
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
|
||||
int count = sharedPref.getInt("message_count", 0);
|
||||
@@ -38,47 +49,75 @@ public class CMessageList
|
||||
String title = sharedPref.getString("message["+i+"].title", "");
|
||||
String content = sharedPref.getString("message["+i+"].content", "");
|
||||
PriorityEnum prio = PriorityEnum.parseAPI(sharedPref.getInt("message["+i+"].priority", 1));
|
||||
long scnid = sharedPref.getLong("message["+i+"].scnid", 0);
|
||||
|
||||
Messages.add(new CMessage(time, title, content, prio));
|
||||
Messages.add(new CMessage(scnid, time, title, content, prio));
|
||||
}
|
||||
|
||||
AllAcks = sharedPref.getStringSet("acks", new HashSet<>());
|
||||
}
|
||||
}
|
||||
|
||||
public CMessage add(final long time, final String title, final String content, final PriorityEnum pe)
|
||||
public CMessage add(final long scnid, final long time, final String title, final String content, final PriorityEnum pe)
|
||||
{
|
||||
CMessage msg = new CMessage(time, title, content, pe);
|
||||
CMessage msg = new CMessage(scnid, time, title, content, pe);
|
||||
|
||||
boolean run = SCNApp.runOnUiThread(() ->
|
||||
{
|
||||
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
|
||||
int count = sharedPref.getInt("message_count", 0);
|
||||
|
||||
SharedPreferences.Editor e = sharedPref.edit();
|
||||
|
||||
synchronized (msg_lock)
|
||||
{
|
||||
Messages.add(msg);
|
||||
AllAcks.add(Long.toHexString(msg.SCN_ID));
|
||||
|
||||
while (Messages.size()>SCNSettings.inst().LocalCacheSize) Messages.remove(0);
|
||||
}
|
||||
|
||||
if (Messages.size()>1 && Messages.get(Messages.size()-2).Timestamp < msg.Timestamp)
|
||||
{
|
||||
// quick save
|
||||
|
||||
SharedPreferences.Editor e = sharedPref.edit();
|
||||
|
||||
e.putInt( "message_count", count+1);
|
||||
e.putLong( "message["+count+"].timestamp", time);
|
||||
e.putString("message["+count+"].title", title);
|
||||
e.putString("message["+count+"].content", content);
|
||||
e.putInt( "message["+count+"].priority", pe.ID);
|
||||
e.putLong( "message["+count+"].scnid", scnid);
|
||||
|
||||
e.putStringSet("acks", AllAcks);
|
||||
|
||||
e.apply();
|
||||
}
|
||||
else
|
||||
{
|
||||
// full save
|
||||
|
||||
fullSave(); // does sort in here
|
||||
}
|
||||
|
||||
|
||||
for (WeakReference<MessageAdapter> ref : _listener)
|
||||
{
|
||||
MessageAdapter a = ref.get();
|
||||
if (a == null) continue;
|
||||
a.customNotifyItemInserted(count);
|
||||
a.customNotifyDataSetChanged();
|
||||
a.scrollToTop();
|
||||
}
|
||||
CleanUpListener();
|
||||
});
|
||||
|
||||
if (!run)
|
||||
{
|
||||
Messages.add(new CMessage(time, title, content, pe));
|
||||
fullSave();
|
||||
synchronized (msg_lock)
|
||||
{
|
||||
Messages.add(new CMessage(scnid, time, title, content, pe));
|
||||
AllAcks.add(Long.toHexString(msg.SCN_ID));
|
||||
}
|
||||
fullSave(); // does sort in here
|
||||
}
|
||||
|
||||
return msg;
|
||||
@@ -100,6 +139,10 @@ public class CMessageList
|
||||
|
||||
public void fullSave()
|
||||
{
|
||||
synchronized (msg_lock)
|
||||
{
|
||||
CollectionHelper.sort_inplace(Messages, (a,b) -> Long.compare(a.Timestamp, b.Timestamp));
|
||||
|
||||
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor e = sharedPref.edit();
|
||||
|
||||
@@ -113,21 +156,36 @@ public class CMessageList
|
||||
e.putString("message["+i+"].title", Messages.get(i).Title);
|
||||
e.putString("message["+i+"].content", Messages.get(i).Content);
|
||||
e.putInt( "message["+i+"].priority", Messages.get(i).Priority.ID);
|
||||
e.putLong( "message["+i+"].scnid", Messages.get(i).SCN_ID);
|
||||
}
|
||||
|
||||
e.putStringSet("acks", AllAcks);
|
||||
|
||||
e.apply();
|
||||
}
|
||||
}
|
||||
|
||||
public CMessage tryGet(int pos)
|
||||
{
|
||||
synchronized (msg_lock)
|
||||
{
|
||||
if (pos < 0 || pos >= Messages.size()) return null;
|
||||
return Messages.get(pos);
|
||||
}
|
||||
}
|
||||
|
||||
public CMessage tryGetFromBack(int pos)
|
||||
{
|
||||
return tryGet(Messages.size() - pos - 1);
|
||||
}
|
||||
|
||||
public int size()
|
||||
{
|
||||
synchronized (msg_lock)
|
||||
{
|
||||
return Messages.size();
|
||||
}
|
||||
}
|
||||
|
||||
public void register(MessageAdapter adp)
|
||||
{
|
||||
@@ -142,4 +200,33 @@ public class CMessageList
|
||||
if (_listener.get(i).get() == null) _listener.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAck(long id)
|
||||
{
|
||||
synchronized (msg_lock)
|
||||
{
|
||||
return AllAcks.contains(Long.toHexString(id));
|
||||
}
|
||||
}
|
||||
|
||||
public CMessage removeFromBack(int pos)
|
||||
{
|
||||
CMessage r;
|
||||
synchronized (msg_lock)
|
||||
{
|
||||
int index = Messages.size() - pos - 1;
|
||||
r = Messages.remove(index);
|
||||
}
|
||||
fullSave(); // does sort in here
|
||||
return r;
|
||||
}
|
||||
|
||||
public void insert(int index, CMessage item)
|
||||
{
|
||||
synchronized (msg_lock)
|
||||
{
|
||||
Messages.add(index, item);
|
||||
}
|
||||
fullSave(); // does sort in here
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,34 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.model;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
|
||||
public class NotificationSettings
|
||||
{
|
||||
public boolean EnableSound = false;
|
||||
public String SoundName = "";
|
||||
public String SoundSource = Uri.EMPTY.toString();
|
||||
public boolean RepeatSound = false;
|
||||
public boolean EnableSound;
|
||||
public String SoundName;
|
||||
public String SoundSource;
|
||||
public boolean RepeatSound;
|
||||
|
||||
public boolean EnableLED = false;
|
||||
public int LEDColor = Color.BLUE;
|
||||
public boolean ForceVolume;
|
||||
public int ForceVolumeValue;
|
||||
|
||||
public boolean EnableVibration = false;
|
||||
public boolean EnableLED;
|
||||
public int LEDColor;
|
||||
|
||||
public boolean EnableVibration;
|
||||
|
||||
public NotificationSettings(PriorityEnum p)
|
||||
{
|
||||
EnableSound = (p == PriorityEnum.HIGH);
|
||||
SoundName = "Default";
|
||||
SoundSource = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION).toString();
|
||||
RepeatSound = false;
|
||||
EnableLED = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL);
|
||||
LEDColor = Color.BLUE;
|
||||
EnableVibration = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL);
|
||||
ForceVolume = false;
|
||||
ForceVolumeValue = 50;
|
||||
}
|
||||
}
|
||||
|
@@ -6,21 +6,29 @@ import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
|
||||
public class SCNSettings
|
||||
{
|
||||
private final static Object _lock = new Object();
|
||||
private static SCNSettings _inst = null;
|
||||
private static volatile SCNSettings _inst = null;
|
||||
public static SCNSettings inst()
|
||||
{
|
||||
SCNSettings local = _inst;
|
||||
if (local == null)
|
||||
{
|
||||
synchronized (_lock)
|
||||
{
|
||||
if (_inst != null) return _inst;
|
||||
return _inst = new SCNSettings();
|
||||
local = _inst;
|
||||
if (local == null) _inst = local = new SCNSettings();
|
||||
}
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -36,14 +44,19 @@ public class SCNSettings
|
||||
public String fcm_token_local;
|
||||
public String fcm_token_server;
|
||||
|
||||
public String promode_token;
|
||||
public boolean promode_local;
|
||||
public boolean promode_server;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
public boolean Enabled = true;
|
||||
public int LocalCacheSize = 500;
|
||||
public boolean EnableDeleteSwipe = true;
|
||||
|
||||
public final NotificationSettings PriorityLow = new NotificationSettings();
|
||||
public final NotificationSettings PriorityNorm = new NotificationSettings();
|
||||
public final NotificationSettings PriorityHigh = new NotificationSettings();
|
||||
public final NotificationSettings PriorityLow = new NotificationSettings(PriorityEnum.LOW);
|
||||
public final NotificationSettings PriorityNorm = new NotificationSettings(PriorityEnum.NORMAL);
|
||||
public final NotificationSettings PriorityHigh = new NotificationSettings(PriorityEnum.HIGH);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -57,9 +70,13 @@ public class SCNSettings
|
||||
user_key = sharedPref.getString("user_key", "");
|
||||
fcm_token_local = sharedPref.getString("fcm_token_local", "");
|
||||
fcm_token_server = sharedPref.getString("fcm_token_server", "");
|
||||
promode_local = sharedPref.getBoolean("promode_local", false);
|
||||
promode_server = sharedPref.getBoolean("promode_server", false);
|
||||
promode_token = sharedPref.getString("promode_token", "");
|
||||
|
||||
Enabled = sharedPref.getBoolean("app_enabled", Enabled);
|
||||
LocalCacheSize = sharedPref.getInt("local_cache_size", LocalCacheSize);
|
||||
EnableDeleteSwipe = sharedPref.getBoolean("do_del_swipe", EnableDeleteSwipe);
|
||||
|
||||
PriorityLow.EnableLED = sharedPref.getBoolean("priority_low:enabled_led", PriorityLow.EnableLED);
|
||||
PriorityLow.EnableSound = sharedPref.getBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
|
||||
@@ -68,6 +85,8 @@ public class SCNSettings
|
||||
PriorityLow.SoundName = sharedPref.getString( "priority_low:sound_name", PriorityLow.SoundName);
|
||||
PriorityLow.SoundSource = sharedPref.getString( "priority_low:sound_source", PriorityLow.SoundSource);
|
||||
PriorityLow.LEDColor = sharedPref.getInt( "priority_low:led_color", PriorityLow.LEDColor);
|
||||
PriorityLow.ForceVolume = sharedPref.getBoolean("priority_low:force_volume", PriorityLow.ForceVolume);
|
||||
PriorityLow.ForceVolumeValue = sharedPref.getInt( "priority_low:force_volume_value", PriorityLow.ForceVolumeValue);
|
||||
|
||||
PriorityNorm.EnableLED = sharedPref.getBoolean("priority_norm:enabled_led", PriorityNorm.EnableLED);
|
||||
PriorityNorm.EnableSound = sharedPref.getBoolean("priority_norm:enabled_sound", PriorityNorm.EnableSound);
|
||||
@@ -76,6 +95,8 @@ public class SCNSettings
|
||||
PriorityNorm.SoundName = sharedPref.getString( "priority_norm:sound_name", PriorityNorm.SoundName);
|
||||
PriorityNorm.SoundSource = sharedPref.getString( "priority_norm:sound_source", PriorityNorm.SoundSource);
|
||||
PriorityNorm.LEDColor = sharedPref.getInt( "priority_norm:led_color", PriorityNorm.LEDColor);
|
||||
PriorityNorm.ForceVolume = sharedPref.getBoolean("priority_norm:force_volume", PriorityNorm.ForceVolume);
|
||||
PriorityNorm.ForceVolumeValue = sharedPref.getInt( "priority_norm:force_volume_value", PriorityNorm.ForceVolumeValue);
|
||||
|
||||
PriorityHigh.EnableLED = sharedPref.getBoolean("priority_high:enabled_led", PriorityHigh.EnableLED);
|
||||
PriorityHigh.EnableSound = sharedPref.getBoolean("priority_high:enabled_sound", PriorityHigh.EnableSound);
|
||||
@@ -84,6 +105,8 @@ public class SCNSettings
|
||||
PriorityHigh.SoundName = sharedPref.getString( "priority_high:sound_name", PriorityHigh.SoundName);
|
||||
PriorityHigh.SoundSource = sharedPref.getString( "priority_high:sound_source", PriorityHigh.SoundSource);
|
||||
PriorityHigh.LEDColor = sharedPref.getInt( "priority_high:led_color", PriorityHigh.LEDColor);
|
||||
PriorityHigh.ForceVolume = sharedPref.getBoolean("priority_high:force_volume", PriorityHigh.ForceVolume);
|
||||
PriorityHigh.ForceVolumeValue = sharedPref.getInt( "priority_high:force_volume_value", PriorityHigh.ForceVolumeValue);
|
||||
}
|
||||
|
||||
public void save()
|
||||
@@ -100,6 +123,7 @@ public class SCNSettings
|
||||
|
||||
e.putBoolean("app_enabled", Enabled);
|
||||
e.putInt( "local_cache_size", LocalCacheSize);
|
||||
e.putBoolean("do_del_swipe", EnableDeleteSwipe);
|
||||
|
||||
e.putBoolean("priority_low:enabled_led", PriorityLow.EnableLED);
|
||||
e.putBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
|
||||
@@ -108,6 +132,8 @@ public class SCNSettings
|
||||
e.putString( "priority_low:sound_name", PriorityLow.SoundName);
|
||||
e.putString( "priority_low:sound_source", PriorityLow.SoundSource);
|
||||
e.putInt( "priority_low:led_color", PriorityLow.LEDColor);
|
||||
e.putBoolean("priority_low:force_volume", PriorityLow.ForceVolume);
|
||||
e.putInt( "priority_low:force_volume_value", PriorityLow.ForceVolumeValue);
|
||||
|
||||
e.putBoolean("priority_norm:enabled_led", PriorityNorm.EnableLED);
|
||||
e.putBoolean("priority_norm:enabled_sound", PriorityNorm.EnableSound);
|
||||
@@ -116,6 +142,8 @@ public class SCNSettings
|
||||
e.putString( "priority_norm:sound_name", PriorityNorm.SoundName);
|
||||
e.putString( "priority_norm:sound_source", PriorityNorm.SoundSource);
|
||||
e.putInt( "priority_norm:led_color", PriorityNorm.LEDColor);
|
||||
e.putBoolean("priority_norm:force_volume", PriorityNorm.ForceVolume);
|
||||
e.putInt( "priority_norm:force_volume_value", PriorityNorm.ForceVolumeValue);
|
||||
|
||||
e.putBoolean("priority_high:enabled_led", PriorityHigh.EnableLED);
|
||||
e.putBoolean("priority_high:enabled_sound", PriorityHigh.EnableSound);
|
||||
@@ -124,6 +152,8 @@ public class SCNSettings
|
||||
e.putString( "priority_high:sound_name", PriorityHigh.SoundName);
|
||||
e.putString( "priority_high:sound_source", PriorityHigh.SoundSource);
|
||||
e.putInt( "priority_high:led_color", PriorityHigh.LEDColor);
|
||||
e.putBoolean("priority_high:force_volume", PriorityHigh.ForceVolume);
|
||||
e.putInt( "priority_high:force_volume_value", PriorityHigh.ForceVolumeValue);
|
||||
|
||||
e.apply();
|
||||
}
|
||||
@@ -145,53 +175,85 @@ public class SCNSettings
|
||||
{
|
||||
fcm_token_local = token;
|
||||
save();
|
||||
if (!fcm_token_local.equals(fcm_token_server)) ServerCommunication.update(user_id, user_key, fcm_token_local, loader);
|
||||
if (!fcm_token_local.equals(fcm_token_server)) ServerCommunication.updateFCMToken(user_id, user_key, fcm_token_local, loader);
|
||||
}
|
||||
else
|
||||
{
|
||||
fcm_token_local = token;
|
||||
save();
|
||||
ServerCommunication.register(fcm_token_local, loader);
|
||||
ServerCommunication.register(fcm_token_local, loader, promode_local, promode_token);
|
||||
updateProState(loader);
|
||||
}
|
||||
}
|
||||
|
||||
// called at app start
|
||||
public void work(Activity a)
|
||||
{
|
||||
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult ->
|
||||
{
|
||||
String newToken = instanceIdResult.getToken();
|
||||
Log.e("FB::GetInstanceId", newToken);
|
||||
Log.d("FB::GetInstanceId", newToken);
|
||||
SCNSettings.inst().setServerToken(newToken, null);
|
||||
}).addOnCompleteListener(r ->
|
||||
{
|
||||
if (isConnected()) ServerCommunication.info(user_id, user_key, null);
|
||||
});
|
||||
|
||||
updateProState(null);
|
||||
}
|
||||
|
||||
// reset account key
|
||||
public void reset(View loader)
|
||||
{
|
||||
if (!isConnected()) return;
|
||||
|
||||
ServerCommunication.update(user_id, user_key, loader);
|
||||
ServerCommunication.resetSecret(user_id, user_key, loader);
|
||||
}
|
||||
|
||||
// refresh account data
|
||||
public void refresh(View loader, Activity a)
|
||||
{
|
||||
if (isConnected())
|
||||
{
|
||||
ServerCommunication.info(user_id, user_key, loader);
|
||||
|
||||
if (promode_server != promode_local) updateProState(loader);
|
||||
|
||||
if (!Str.equals(fcm_token_local, fcm_token_server)) work(a);
|
||||
}
|
||||
else
|
||||
{
|
||||
// get token then register
|
||||
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult ->
|
||||
{
|
||||
String newToken = instanceIdResult.getToken();
|
||||
Log.e("FB::GetInstanceId", newToken);
|
||||
SCNSettings.inst().setServerToken(newToken, loader);
|
||||
Log.d("FB::GetInstanceId", newToken);
|
||||
SCNSettings.inst().setServerToken(newToken, loader); // does register in here
|
||||
}).addOnCompleteListener(r ->
|
||||
{
|
||||
if (isConnected()) ServerCommunication.info(user_id, user_key, null);
|
||||
if (isConnected()) ServerCommunication.info(user_id, user_key, null); // info again for safety
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void updateProState(View loader)
|
||||
{
|
||||
Purchase purch = IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE);
|
||||
boolean promode_real = (purch != null);
|
||||
|
||||
if (promode_real != promode_local || promode_real != promode_server)
|
||||
{
|
||||
promode_local = promode_real;
|
||||
|
||||
promode_token = promode_real ? purch.getPurchaseToken() : "";
|
||||
updateProStateOnServer(loader);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateProStateOnServer(View loader)
|
||||
{
|
||||
if (!isConnected()) return;
|
||||
|
||||
ServerCommunication.upgrade(user_id, user_key, loader, promode_local, promode_token);
|
||||
}
|
||||
}
|
||||
|
@@ -4,11 +4,19 @@ import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
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.string.Str;
|
||||
import com.blackforestbytes.simplecloudnotifier.service.FBMService;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONTokener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
@@ -19,18 +27,18 @@ import okhttp3.ResponseBody;
|
||||
|
||||
public class ServerCommunication
|
||||
{
|
||||
public static final String BASE_URL = /*SCNApp.LOCAL_DEBUG ? "http://localhost:1010/" : */"https://scn.blackforestbytes.com/";
|
||||
public static final String BASE_URL = /*SCNApp.LOCAL_DEBUG ? "http://localhost:1010/" : */"https://scn.blackforestbytes.com/api/";
|
||||
|
||||
private static final OkHttpClient client = new OkHttpClient();
|
||||
|
||||
private ServerCommunication(){ throw new Error("no."); }
|
||||
|
||||
public static void register(String token, View loader)
|
||||
public static void register(String token, View loader, boolean pro, String pro_token)
|
||||
{
|
||||
try
|
||||
{
|
||||
Request request = new Request.Builder()
|
||||
.url(BASE_URL + "register.php?fcm_token="+token)
|
||||
.url(BASE_URL + "register.php?fcm_token=" + token + "&pro=" + pro + "&pro_token=" + URLEncoder.encode(pro_token, "utf-8"))
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new Callback()
|
||||
@@ -38,7 +46,7 @@ public class ServerCommunication
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
Log.e("SC:register", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
|
||||
}
|
||||
@@ -52,28 +60,29 @@ public class ServerCommunication
|
||||
if (responseBody == null) throw new IOException("No response");
|
||||
|
||||
String r = responseBody.string();
|
||||
Log.d("Server::Response", r);
|
||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||
|
||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||
|
||||
if (!json.getBoolean("success"))
|
||||
if (!json_bool(json, "success"))
|
||||
{
|
||||
SCNApp.showToast(json.getString("message"), 4000);
|
||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||
return;
|
||||
}
|
||||
|
||||
SCNSettings.inst().user_id = json.getInt("user_id");
|
||||
SCNSettings.inst().user_key = json.getString("user_key");
|
||||
SCNSettings.inst().user_id = json_int(json, "user_id");
|
||||
SCNSettings.inst().user_key = json_str(json, "user_key");
|
||||
SCNSettings.inst().fcm_token_server = token;
|
||||
SCNSettings.inst().quota_curr = json.getInt("quota");
|
||||
SCNSettings.inst().quota_max = json.getInt("quota_max");
|
||||
SCNSettings.inst().quota_curr = json_int(json, "quota");
|
||||
SCNSettings.inst().quota_max = json_int(json, "quota_max");
|
||||
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
|
||||
SCNSettings.inst().save();
|
||||
|
||||
SCNApp.refreshAccountTab();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
Log.e("SC:register", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
finally
|
||||
@@ -85,17 +94,17 @@ public class ServerCommunication
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
Log.e("SC:register", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
public static void update(int id, String key, String token, View loader)
|
||||
public static void updateFCMToken(int id, String key, String token, View loader)
|
||||
{
|
||||
try
|
||||
{
|
||||
Request request = new Request.Builder()
|
||||
.url(BASE_URL + "update.php?user_id="+id+"&user_key="+key+"&fcm_token="+token)
|
||||
.url(BASE_URL + "updateFCMToken.php?user_id="+id+"&user_key="+key+"&fcm_token="+token)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new Callback()
|
||||
@@ -103,7 +112,7 @@ public class ServerCommunication
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
Log.e("SC:update_1", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
|
||||
}
|
||||
@@ -117,28 +126,29 @@ public class ServerCommunication
|
||||
if (responseBody == null) throw new IOException("No response");
|
||||
|
||||
String r = responseBody.string();
|
||||
Log.d("Server::Response", r);
|
||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||
|
||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||
|
||||
if (!json.getBoolean("success"))
|
||||
if (!json_bool(json, "success"))
|
||||
{
|
||||
SCNApp.showToast(json.getString("message"), 4000);
|
||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||
return;
|
||||
}
|
||||
|
||||
SCNSettings.inst().user_id = json.getInt("user_id");
|
||||
SCNSettings.inst().user_key = json.getString("user_key");
|
||||
SCNSettings.inst().user_id = json_int(json, "user_id");
|
||||
SCNSettings.inst().user_key = json_str(json, "user_key");
|
||||
SCNSettings.inst().fcm_token_server = token;
|
||||
SCNSettings.inst().quota_curr = json.getInt("quota");
|
||||
SCNSettings.inst().quota_max = json.getInt("quota_max");
|
||||
SCNSettings.inst().quota_curr = json_int(json, "quota");
|
||||
SCNSettings.inst().quota_max = json_int(json, "quota_max");
|
||||
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
|
||||
SCNSettings.inst().save();
|
||||
|
||||
SCNApp.refreshAccountTab();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
Log.e("SC:update_1", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
finally
|
||||
@@ -150,23 +160,23 @@ public class ServerCommunication
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
Log.e("SC:update_1", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
public static void update(int id, String key, View loader)
|
||||
public static void resetSecret(int id, String key, View loader)
|
||||
{
|
||||
try
|
||||
{
|
||||
Request request = new Request.Builder()
|
||||
.url(BASE_URL + "update.php?user_id=" + id + "&user_key=" + key)
|
||||
.url(BASE_URL + "updateFCMToken.php?user_id=" + id + "&user_key=" + key)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e("SC:update_2", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
|
||||
@@ -178,24 +188,25 @@ public class ServerCommunication
|
||||
if (responseBody == null) throw new IOException("No response");
|
||||
|
||||
String r = responseBody.string();
|
||||
Log.d("Server::Response", r);
|
||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||
|
||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||
|
||||
if (!json.getBoolean("success")) {
|
||||
SCNApp.showToast(json.getString("message"), 4000);
|
||||
if (!json_bool(json, "success")) {
|
||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||
return;
|
||||
}
|
||||
|
||||
SCNSettings.inst().user_id = json.getInt("user_id");
|
||||
SCNSettings.inst().user_key = json.getString("user_key");
|
||||
SCNSettings.inst().quota_curr = json.getInt("quota");
|
||||
SCNSettings.inst().quota_max = json.getInt("quota_max");
|
||||
SCNSettings.inst().user_id = json_int(json, "user_id");
|
||||
SCNSettings.inst().user_key = json_str(json, "user_key");
|
||||
SCNSettings.inst().quota_curr = json_int(json, "quota");
|
||||
SCNSettings.inst().quota_max = json_int(json, "quota_max");
|
||||
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
|
||||
SCNSettings.inst().save();
|
||||
|
||||
SCNApp.refreshAccountTab();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e("SC:update_2", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
} finally {
|
||||
SCNApp.runOnUiThread(() -> {
|
||||
@@ -207,7 +218,7 @@ public class ServerCommunication
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
Log.e("SC:update_2", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
}
|
||||
@@ -223,7 +234,7 @@ public class ServerCommunication
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.e("SC:info", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
SCNApp.runOnUiThread(() -> {
|
||||
if (loader != null) loader.setVisibility(View.GONE);
|
||||
@@ -238,23 +249,47 @@ public class ServerCommunication
|
||||
if (responseBody == null) throw new IOException("No response");
|
||||
|
||||
String r = responseBody.string();
|
||||
Log.d("Server::Response", r);
|
||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||
|
||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||
|
||||
if (!json.getBoolean("success")) {
|
||||
SCNApp.showToast(json.getString("message"), 4000);
|
||||
return;
|
||||
}
|
||||
if (!json_bool(json, "success"))
|
||||
{
|
||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||
|
||||
SCNSettings.inst().user_id = json.getInt("user_id");
|
||||
SCNSettings.inst().quota_curr = json.getInt("quota");
|
||||
SCNSettings.inst().quota_max = json.getInt("quota_max");
|
||||
int errid = json.optInt("errid", 0);
|
||||
|
||||
if (errid == 201 || errid == 202 || errid == 203 || errid == 204)
|
||||
{
|
||||
// user not found or auth failed
|
||||
|
||||
SCNSettings.inst().user_id = -1;
|
||||
SCNSettings.inst().user_key = "";
|
||||
SCNSettings.inst().quota_curr = 0;
|
||||
SCNSettings.inst().quota_max = 0;
|
||||
SCNSettings.inst().promode_server = false;
|
||||
SCNSettings.inst().fcm_token_server = "";
|
||||
SCNSettings.inst().save();
|
||||
|
||||
SCNApp.refreshAccountTab();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SCNSettings.inst().user_id = json_int(json, "user_id");
|
||||
SCNSettings.inst().quota_curr = json_int(json, "quota");
|
||||
SCNSettings.inst().quota_max = json_int(json, "quota_max");
|
||||
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
|
||||
if (!json_bool(json, "fcm_token_set")) SCNSettings.inst().fcm_token_server = "";
|
||||
SCNSettings.inst().save();
|
||||
|
||||
SCNApp.refreshAccountTab();
|
||||
|
||||
if (json_int(json, "unack_count")>0) ServerCommunication.requery(id, key, loader);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e("SC:info", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
} finally {
|
||||
SCNApp.runOnUiThread(() -> {
|
||||
@@ -265,9 +300,266 @@ public class ServerCommunication
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.e("SC:info", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
public static void requery(int id, String key, View loader)
|
||||
{
|
||||
try
|
||||
{
|
||||
Request request = new Request.Builder()
|
||||
.url(BASE_URL + "requery.php?user_id=" + id + "&user_key=" + key)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e("SC:requery", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
SCNApp.runOnUiThread(() -> {
|
||||
if (loader != null) loader.setVisibility(View.GONE);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) {
|
||||
try (ResponseBody responseBody = response.body()) {
|
||||
if (!response.isSuccessful())
|
||||
throw new IOException("Unexpected code " + response);
|
||||
if (responseBody == null) throw new IOException("No response");
|
||||
|
||||
String r = responseBody.string();
|
||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||
|
||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||
|
||||
if (!json_bool(json, "success"))
|
||||
{
|
||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||
return;
|
||||
}
|
||||
|
||||
int count = json_int(json, "count");
|
||||
JSONArray arr = json.getJSONArray("data");
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
JSONObject o = arr.getJSONObject(i);
|
||||
|
||||
long time = json_lng(o, "timestamp");
|
||||
String title = json_str(o, "title");
|
||||
String content = json_str(o, "body");
|
||||
PriorityEnum prio = PriorityEnum.parseAPI(json_int(o, "priority"));
|
||||
long scn_id = json_lng(o, "scn_msg_id");
|
||||
|
||||
FBMService.recieveData(time, title, content, prio, scn_id, true);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e("SC:info", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
} finally {
|
||||
SCNApp.runOnUiThread(() -> {
|
||||
if (loader != null) loader.setVisibility(View.GONE);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.e("SC:requery", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
public static void upgrade(int id, String key, View loader, boolean pro, String pro_token)
|
||||
{
|
||||
try
|
||||
{
|
||||
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(BASE_URL + "upgrade.php?user_id=" + id + "&user_key=" + key + "&pro=" + pro + "&pro_token=" + URLEncoder.encode(pro_token, "utf-8"))
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e("SC:upgrade", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) {
|
||||
try (ResponseBody responseBody = response.body()) {
|
||||
if (!response.isSuccessful())
|
||||
throw new IOException("Unexpected code " + response);
|
||||
if (responseBody == null) throw new IOException("No response");
|
||||
|
||||
String r = responseBody.string();
|
||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||
|
||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||
|
||||
if (!json_bool(json, "success")) {
|
||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||
return;
|
||||
}
|
||||
|
||||
SCNSettings.inst().user_id = json_int(json, "user_id");
|
||||
SCNSettings.inst().quota_curr = json_int(json, "quota");
|
||||
SCNSettings.inst().quota_max = json_int(json, "quota_max");
|
||||
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
|
||||
SCNSettings.inst().save();
|
||||
|
||||
SCNApp.refreshAccountTab();
|
||||
} catch (Exception e) {
|
||||
Log.e("SC:upgrade", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
} finally {
|
||||
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ack(int id, String key, long msg_scn_id)
|
||||
{
|
||||
try
|
||||
{
|
||||
Request request = new Request.Builder()
|
||||
.url(BASE_URL + "ack.php?user_id=" + id + "&user_key=" + key + "&scn_msg_id=" + msg_scn_id)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e("SC:ack", e.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response)
|
||||
{
|
||||
try (ResponseBody responseBody = response.body()) {
|
||||
if (!response.isSuccessful())
|
||||
throw new IOException("Unexpected code " + response);
|
||||
if (responseBody == null) throw new IOException("No response");
|
||||
|
||||
String r = responseBody.string();
|
||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||
|
||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||
|
||||
if (!json_bool(json, "success")) SCNApp.showToast(json_str(json, "message"), 4000);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e("SC:ack", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.e("SC:ack", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void expand(int id, String key, long scn_msg_id, View loader, Func5to0<String, String, PriorityEnum, Long, Long> okResult)
|
||||
{
|
||||
try
|
||||
{
|
||||
Request request = new Request.Builder()
|
||||
.url(BASE_URL + "expand.php?user_id=" + id + "&user_key=" + key + "&scn_msg_id=" + scn_msg_id)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.e("SC:expand", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
SCNApp.runOnUiThread(() -> {
|
||||
if (loader != null) loader.setVisibility(View.GONE);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) {
|
||||
try (ResponseBody responseBody = response.body()) {
|
||||
if (!response.isSuccessful())
|
||||
throw new IOException("Unexpected code " + response);
|
||||
if (responseBody == null) throw new IOException("No response");
|
||||
|
||||
String r = responseBody.string();
|
||||
Log.d("Server::Response", request.url().toString()+"\n"+r);
|
||||
|
||||
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
|
||||
|
||||
if (!json_bool(json, "success"))
|
||||
{
|
||||
SCNApp.showToast(json_str(json, "message"), 4000);
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject o = json.getJSONObject("data");
|
||||
|
||||
long time = json_lng(o, "timestamp");
|
||||
String title = json_str(o, "title");
|
||||
String content = json_str(o, "body");
|
||||
PriorityEnum prio = PriorityEnum.parseAPI(json_int(o, "priority"));
|
||||
long scn_id = json_lng(o, "scn_msg_id");
|
||||
|
||||
okResult.invoke(title, content, prio, time, scn_id);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e("SC:expand", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
} finally {
|
||||
SCNApp.runOnUiThread(() -> {
|
||||
if (loader != null) loader.setVisibility(View.GONE);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.e("SC:expand", e.toString());
|
||||
SCNApp.showToast("Communication with server failed", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean json_bool(JSONObject o, String key) throws JSONException
|
||||
{
|
||||
Object v = o.get(key);
|
||||
if (v instanceof Integer) return ((int)v) != 0;
|
||||
if (v instanceof Boolean) return ((boolean)v);
|
||||
if (v instanceof String) return !Str.equals(((String)v), "0") && !Str.equals(((String)v), "false");
|
||||
|
||||
return o.getBoolean(key);
|
||||
}
|
||||
|
||||
private static int json_int(JSONObject o, String key) throws JSONException
|
||||
{
|
||||
return o.getInt(key);
|
||||
}
|
||||
|
||||
private static long json_lng(JSONObject o, String key) throws JSONException
|
||||
{
|
||||
return o.getLong(key);
|
||||
}
|
||||
|
||||
private static String json_str(JSONObject o, String key) throws JSONException
|
||||
{
|
||||
return o.getString(key);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,42 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
|
||||
|
||||
public class BroadcastReceiverService extends BroadcastReceiver
|
||||
{
|
||||
public static final int NOTIF_SHOW_MAIN = 10021;
|
||||
public static final int NOTIF_STOP_SOUND = 10022;
|
||||
public static final String ID_KEY = "com.blackforestbytes.simplecloudnotifier.BroadcastID";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent)
|
||||
{
|
||||
if (intent == null) return;
|
||||
Bundle extras = intent.getExtras();
|
||||
if (extras == null) return;
|
||||
int notificationId = extras.getInt(ID_KEY, 0);
|
||||
|
||||
if (notificationId == 0) return;
|
||||
else if (notificationId == NOTIF_SHOW_MAIN) showMain(context);
|
||||
else if (notificationId == NOTIF_STOP_SOUND) stopNotificationSound();
|
||||
else return;
|
||||
}
|
||||
|
||||
private void stopNotificationSound()
|
||||
{
|
||||
SoundService.stop();
|
||||
}
|
||||
|
||||
private void showMain(Context ctxt)
|
||||
{
|
||||
SoundService.stop();
|
||||
|
||||
Intent intent = new Intent(ctxt, MainActivity.class);
|
||||
ctxt.startActivity(intent);
|
||||
}
|
||||
}
|
@@ -4,10 +4,13 @@ import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.datatypes.Tuple4;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.datatypes.Tuple5;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.ServerCommunication;
|
||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||
import com.google.firebase.messaging.RemoteMessage;
|
||||
|
||||
@@ -36,9 +39,35 @@ public class FBMService extends FirebaseMessagingService
|
||||
String title = remoteMessage.getData().get("title");
|
||||
String content = remoteMessage.getData().get("body");
|
||||
PriorityEnum prio = PriorityEnum.parseAPI(remoteMessage.getData().get("priority"));
|
||||
long scn_id = Long.parseLong(remoteMessage.getData().get("scn_msg_id"));
|
||||
boolean trimmed = Boolean.parseBoolean(remoteMessage.getData().get("trimmed"));
|
||||
|
||||
CMessage msg = CMessageList.inst().add(time, title, content, prio);
|
||||
if (trimmed)
|
||||
{
|
||||
ServerCommunication.expand(SCNSettings.inst().user_id, SCNSettings.inst().user_key, scn_id, null, (i1, i2, i3, i4, i5) -> recieveData(i4, i1, i2, i3, i5, false));
|
||||
}
|
||||
else
|
||||
{
|
||||
recieveData(time, title, content, prio, scn_id, false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.e("FB:Err", e.toString());
|
||||
SCNApp.showToast("Recieved invalid message from server", Toast.LENGTH_LONG);
|
||||
}
|
||||
}
|
||||
|
||||
public static void recieveData(long time, String title, String content, PriorityEnum prio, long scn_id, boolean alwaysAck)
|
||||
{
|
||||
if (CMessageList.inst().isAck(scn_id))
|
||||
{
|
||||
Log.w("FB::MessageReceived", "Recieved ack-ed message: " + scn_id);
|
||||
if (alwaysAck) ServerCommunication.ack(SCNSettings.inst().user_id, SCNSettings.inst().user_key, scn_id);
|
||||
return;
|
||||
}
|
||||
|
||||
CMessage msg = CMessageList.inst().add(scn_id, time, title, content, prio);
|
||||
|
||||
if (SCNApp.isBackground())
|
||||
{
|
||||
@@ -48,11 +77,7 @@ public class FBMService extends FirebaseMessagingService
|
||||
{
|
||||
NotificationService.inst().showForeground(msg);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.e("FB:Err", e.toString());
|
||||
SCNApp.showToast("Recieved invalid message from server", Toast.LENGTH_LONG);
|
||||
}
|
||||
|
||||
ServerCommunication.ack(SCNSettings.inst().user_id, SCNSettings.inst().user_key, scn_id);
|
||||
}
|
||||
}
|
@@ -0,0 +1,195 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.service;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.billingclient.api.BillingClient;
|
||||
import com.android.billingclient.api.BillingClientStateListener;
|
||||
import com.android.billingclient.api.BillingFlowParams;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func0to0;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static androidx.constraintlayout.widget.Constraints.TAG;
|
||||
|
||||
public class IABService implements PurchasesUpdatedListener
|
||||
{
|
||||
public static final String IAB_PRO_MODE = "scn.pro.tier1";
|
||||
|
||||
private final static Object _lock = new Object();
|
||||
private static IABService _inst = null;
|
||||
public static IABService inst()
|
||||
{
|
||||
synchronized (_lock)
|
||||
{
|
||||
if (_inst != null) return _inst;
|
||||
throw new Error("IABService == null");
|
||||
}
|
||||
}
|
||||
public static void startup(MainActivity a)
|
||||
{
|
||||
synchronized (_lock)
|
||||
{
|
||||
_inst = new IABService(a);
|
||||
}
|
||||
}
|
||||
|
||||
private BillingClient client;
|
||||
private boolean isServiceConnected;
|
||||
private final List<Purchase> purchases = new ArrayList<>();
|
||||
|
||||
public IABService(Context c)
|
||||
{
|
||||
client = BillingClient
|
||||
.newBuilder(c)
|
||||
.setListener(this)
|
||||
.build();
|
||||
|
||||
startServiceConnection(this::queryPurchases, false);
|
||||
}
|
||||
|
||||
public void queryPurchases()
|
||||
{
|
||||
Func0to0 queryToExecute = () ->
|
||||
{
|
||||
long time = System.currentTimeMillis();
|
||||
Purchase.PurchasesResult purchasesResult = client.queryPurchases(BillingClient.SkuType.INAPP);
|
||||
Log.i(TAG, "Querying purchases elapsed time: " + (System.currentTimeMillis() - time) + "ms");
|
||||
|
||||
if (purchasesResult.getResponseCode() == BillingClient.BillingResponse.OK)
|
||||
{
|
||||
for (Purchase p : purchasesResult.getPurchasesList())
|
||||
{
|
||||
handlePurchase(p);
|
||||
}
|
||||
|
||||
boolean newProMode = getPurchaseCached(IAB_PRO_MODE) != null;
|
||||
if (newProMode != SCNSettings.inst().promode_local)
|
||||
{
|
||||
refreshProModeListener();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.w(TAG, "queryPurchases() got an error response code: " + purchasesResult.getResponseCode());
|
||||
}
|
||||
};
|
||||
|
||||
executeServiceRequest(queryToExecute, false);
|
||||
}
|
||||
|
||||
public void purchase(Activity a, String id)
|
||||
{
|
||||
executeServiceRequest(() ->
|
||||
{
|
||||
BillingFlowParams flowParams = BillingFlowParams
|
||||
.newBuilder()
|
||||
.setSku(id)
|
||||
.setType(BillingClient.SkuType.INAPP) // SkuType.SUB for subscription
|
||||
.build();
|
||||
client.launchBillingFlow(a, flowParams);
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void executeServiceRequest(Func0to0 runnable, final boolean userRequest) {
|
||||
if (isServiceConnected)
|
||||
{
|
||||
runnable.invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
// If billing service was disconnected, we try to reconnect 1 time.
|
||||
// (feel free to introduce your retry policy here).
|
||||
startServiceConnection(runnable, userRequest);
|
||||
}
|
||||
}
|
||||
public void destroy()
|
||||
{
|
||||
if (client != null && client.isReady()) {
|
||||
client.endConnection();
|
||||
client = null;
|
||||
isServiceConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases)
|
||||
{
|
||||
if (responseCode == BillingClient.BillingResponse.OK && purchases != null)
|
||||
{
|
||||
for (Purchase purchase : purchases)
|
||||
{
|
||||
handlePurchase(purchase);
|
||||
}
|
||||
}
|
||||
else if (responseCode == BillingClient.BillingResponse.ITEM_ALREADY_OWNED && purchases != null)
|
||||
{
|
||||
for (Purchase purchase : purchases)
|
||||
{
|
||||
handlePurchase(purchase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePurchase(Purchase purchase)
|
||||
{
|
||||
Log.d(TAG, "Got a verified purchase: " + purchase);
|
||||
|
||||
purchases.add(purchase);
|
||||
|
||||
refreshProModeListener();
|
||||
}
|
||||
|
||||
private void refreshProModeListener() {
|
||||
MainActivity ma = SCNApp.getMainActivity();
|
||||
if (ma != null) ma.adpTabs.tab3.updateProState();
|
||||
if (ma != null) ma.adpTabs.tab1.updateProState();
|
||||
SCNSettings.inst().updateProState(null);
|
||||
}
|
||||
|
||||
public void startServiceConnection(final Func0to0 executeOnSuccess, final boolean userRequest)
|
||||
{
|
||||
client.startConnection(new BillingClientStateListener()
|
||||
{
|
||||
@Override
|
||||
public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponseCode)
|
||||
{
|
||||
if (billingResponseCode == BillingClient.BillingResponse.OK)
|
||||
{
|
||||
isServiceConnected = true;
|
||||
if (executeOnSuccess != null) executeOnSuccess.invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (userRequest) SCNApp.showToast("Could not connect to google services", Toast.LENGTH_SHORT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
isServiceConnected = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Purchase getPurchaseCached(String id)
|
||||
{
|
||||
for (Purchase p : purchases)
|
||||
{
|
||||
if (Str.equals(p.getSku(), id)) return p;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -6,28 +6,32 @@ import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.R;
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.NotificationSettings;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
public class NotificationService
|
||||
{
|
||||
private final static String CHANNEL_ID_LOW = "CHAN_BFB_SCN_MESSAGES_LOW";
|
||||
private final static String CHANNEL_ID_NORM = "CHAN_BFB_SCN_MESSAGES_NORM";
|
||||
private final static String CHANNEL_ID_HIGH = "CHAN_BFB_SCN_MESSAGES_HIGH";
|
||||
private final static String CHANNEL_P0_ID = "CHAN_BFB_SCN_MESSAGES_P0";
|
||||
private final static String CHANNEL_P1_ID = "CHAN_BFB_SCN_MESSAGES_P1";
|
||||
private final static String CHANNEL_P2_ID = "CHAN_BFB_SCN_MESSAGES_P2";
|
||||
|
||||
private final static Object _lock = new Object();
|
||||
private static NotificationService _inst = null;
|
||||
@@ -42,10 +46,10 @@ public class NotificationService
|
||||
|
||||
private NotificationService()
|
||||
{
|
||||
updateChannels();
|
||||
createChannels();
|
||||
}
|
||||
|
||||
public void updateChannels()
|
||||
private void createChannels()
|
||||
{
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||
|
||||
@@ -53,81 +57,207 @@ public class NotificationService
|
||||
NotificationManager notifman = ctxt.getSystemService(NotificationManager.class);
|
||||
if (notifman == null) return;
|
||||
|
||||
NotificationChannel channelLow = notifman.getNotificationChannel(CHANNEL_ID_LOW);
|
||||
if (channelLow == null) notifman.createNotificationChannel(channelLow = new NotificationChannel(CHANNEL_ID_LOW, "Push notifications (low priority)", NotificationManager.IMPORTANCE_LOW));
|
||||
NotificationChannel channelNorm = notifman.getNotificationChannel(CHANNEL_ID_NORM);
|
||||
if (channelNorm == null) notifman.createNotificationChannel(channelNorm = new NotificationChannel(CHANNEL_ID_NORM, "Push notifications (normal priority)", NotificationManager.IMPORTANCE_DEFAULT));
|
||||
NotificationChannel channelHigh = notifman.getNotificationChannel(CHANNEL_ID_HIGH);
|
||||
if (channelHigh == null) notifman.createNotificationChannel(channelHigh = new NotificationChannel(CHANNEL_ID_HIGH, "Push notifications (high priority)", NotificationManager.IMPORTANCE_HIGH));
|
||||
|
||||
channelLow.setDescription("Messages from the API with priority set to low");
|
||||
channelLow.enableLights(SCNSettings.inst().PriorityLow.EnableLED);
|
||||
channelLow.setLightColor(SCNSettings.inst().PriorityLow.LEDColor);
|
||||
channelLow.enableVibration(SCNSettings.inst().PriorityLow.EnableVibration);
|
||||
channelLow.setVibrationPattern(new long[]{200});
|
||||
|
||||
channelNorm.setDescription("Messages from the API with priority set to normal");
|
||||
channelNorm.enableLights(SCNSettings.inst().PriorityNorm.EnableLED);
|
||||
channelNorm.setLightColor(SCNSettings.inst().PriorityNorm.LEDColor);
|
||||
channelNorm.enableVibration(SCNSettings.inst().PriorityNorm.EnableVibration);
|
||||
channelNorm.setVibrationPattern(new long[]{200});
|
||||
|
||||
channelHigh.setDescription("Messages from the API with priority set to high");
|
||||
channelHigh.enableLights(SCNSettings.inst().PriorityHigh.EnableLED);
|
||||
channelHigh.setLightColor(SCNSettings.inst().PriorityHigh.LEDColor);
|
||||
channelHigh.enableVibration(SCNSettings.inst().PriorityHigh.EnableVibration);
|
||||
channelHigh.setVibrationPattern(new long[]{200});
|
||||
channelLow.setBypassDnd(true);
|
||||
channelLow.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
{
|
||||
NotificationChannel channel0 = notifman.getNotificationChannel(CHANNEL_P0_ID);
|
||||
if (channel0 == null)
|
||||
{
|
||||
channel0 = new NotificationChannel(CHANNEL_P0_ID, "Push notifications (low priority)", NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel0.setDescription("Push notifications from the server with low priority.\nGo to the in-app settings to configure ringtone, volume and vibrations");
|
||||
channel0.setSound(null, null);
|
||||
channel0.setVibrationPattern(null);
|
||||
channel0.setLightColor(Color.BLUE);
|
||||
notifman.createNotificationChannel(channel0);
|
||||
}
|
||||
}
|
||||
{
|
||||
NotificationChannel channel1 = notifman.getNotificationChannel(CHANNEL_P1_ID);
|
||||
if (channel1 == null)
|
||||
{
|
||||
channel1 = new NotificationChannel(CHANNEL_P1_ID, "Push notifications (normal priority)", NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel1.setDescription("Push notifications from the server with low priority.\nGo to the in-app settings to configure ringtone, volume and vibrations");
|
||||
channel1.setSound(null, null);
|
||||
channel1.setVibrationPattern(null);
|
||||
channel1.setLightColor(Color.BLUE);
|
||||
notifman.createNotificationChannel(channel1);
|
||||
}
|
||||
}
|
||||
{
|
||||
NotificationChannel channel2 = notifman.getNotificationChannel(CHANNEL_P2_ID);
|
||||
if (channel2 == null)
|
||||
{
|
||||
channel2 = new NotificationChannel(CHANNEL_P1_ID, "Push notifications (high priority)", NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel2.setDescription("Push notifications from the server with low priority.\nGo to the in-app settings to configure ringtone, volume and vibrations");
|
||||
channel2.setSound(null, null);
|
||||
channel2.setVibrationPattern(null);
|
||||
channel2.setLightColor(Color.BLUE);
|
||||
notifman.createNotificationChannel(channel2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showForeground(CMessage msg)
|
||||
{
|
||||
SCNApp.showToast("Message recieved: " + msg.Title, Toast.LENGTH_LONG);
|
||||
|
||||
try
|
||||
{
|
||||
NotificationSettings ns = SCNSettings.inst().PriorityNorm;
|
||||
switch (msg.Priority)
|
||||
{
|
||||
case LOW: ns = SCNSettings.inst().PriorityLow; break;
|
||||
case NORMAL: ns = SCNSettings.inst().PriorityNorm; break;
|
||||
case HIGH: ns = SCNSettings.inst().PriorityHigh; break;
|
||||
}
|
||||
|
||||
SoundService.play(ns.EnableSound, ns.SoundSource, ns.ForceVolume, ns.ForceVolumeValue, false);
|
||||
|
||||
if (ns.EnableVibration)
|
||||
{
|
||||
Vibrator v = (Vibrator) SCNApp.getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
v.vibrate(VibrationEffect.createOneShot(1500, VibrationEffect.DEFAULT_AMPLITUDE));
|
||||
} else {
|
||||
v.vibrate(1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void showBackground(CMessage msg)
|
||||
{
|
||||
Context ctxt = SCNApp.getContext();
|
||||
|
||||
String channel = CHANNEL_ID_NORM;
|
||||
NotificationSettings ns = SCNSettings.inst().PriorityNorm;
|
||||
switch (msg.Priority)
|
||||
{
|
||||
case LOW: ns = SCNSettings.inst().PriorityLow; channel = CHANNEL_ID_LOW; break;
|
||||
case NORMAL: ns = SCNSettings.inst().PriorityNorm; channel = CHANNEL_ID_NORM; break;
|
||||
case HIGH: ns = SCNSettings.inst().PriorityHigh; channel = CHANNEL_ID_HIGH; break;
|
||||
case LOW: ns = SCNSettings.inst().PriorityLow; break;
|
||||
case NORMAL: ns = SCNSettings.inst().PriorityNorm; break;
|
||||
case HIGH: ns = SCNSettings.inst().PriorityHigh; break;
|
||||
}
|
||||
|
||||
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, channel);
|
||||
mBuilder.setSmallIcon(R.drawable.ic_bfb);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
{
|
||||
// old
|
||||
showBackground_old(msg, ctxt, ns, msg.Priority);
|
||||
}
|
||||
else
|
||||
{
|
||||
// new
|
||||
showBackground_new(msg, ctxt, ns, msg.Priority);
|
||||
}
|
||||
}
|
||||
|
||||
private String getChannel(PriorityEnum p)
|
||||
{
|
||||
switch (p)
|
||||
{
|
||||
case LOW: return CHANNEL_P0_ID;
|
||||
case NORMAL: return CHANNEL_P1_ID;
|
||||
case HIGH: return CHANNEL_P2_ID;
|
||||
|
||||
default: return CHANNEL_P0_ID;
|
||||
}
|
||||
}
|
||||
|
||||
private void showBackground_old(CMessage msg, Context ctxt, NotificationSettings ns, PriorityEnum prio)
|
||||
{
|
||||
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, getChannel(prio));
|
||||
mBuilder.setSmallIcon(R.drawable.ic_notification_white);
|
||||
mBuilder.setLargeIcon(BitmapFactory.decodeResource(ctxt.getResources(), R.mipmap.ic_notification_full));
|
||||
mBuilder.setContentTitle(msg.Title);
|
||||
mBuilder.setContentText(msg.Content);
|
||||
mBuilder.setShowWhen(true);
|
||||
mBuilder.setWhen(msg.Timestamp);
|
||||
mBuilder.setWhen(msg.Timestamp * 1000);
|
||||
mBuilder.setAutoCancel(true);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
{
|
||||
mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
|
||||
mBuilder.setGroup("com.blackforestbytes.simplecloudnotifier.notifications.group."+prio.toString());
|
||||
|
||||
if (msg.Priority == PriorityEnum.LOW) mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||
if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
||||
if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
|
||||
if (ns.EnableVibration) mBuilder.setVibrate(new long[]{200});
|
||||
if (ns.EnableVibration) mBuilder.setVibrate(new long[]{500});
|
||||
if (ns.EnableLED) mBuilder.setLights(ns.LEDColor, 500, 500);
|
||||
}
|
||||
|
||||
if (ns.EnableSound && !ns.SoundSource.isEmpty())
|
||||
{
|
||||
mBuilder.setSound(Uri.parse(ns.SoundSource), AudioManager.STREAM_ALARM);
|
||||
}
|
||||
if (ns.EnableSound && !ns.SoundSource.isEmpty() && !ns.RepeatSound) mBuilder.setSound(Uri.parse(ns.SoundSource), AudioManager.STREAM_NOTIFICATION);
|
||||
|
||||
Intent intent = new Intent(ctxt, MainActivity.class);
|
||||
PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0);
|
||||
mBuilder.setContentIntent(pi);
|
||||
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
Notification n = mBuilder.build();
|
||||
if (ns.EnableSound && !ns.SoundSource.isEmpty() && ns.RepeatSound) n.flags |= Notification.FLAG_INSISTENT;
|
||||
if (ns.EnableSound && !ns.SoundSource.isEmpty() && ns.RepeatSound)
|
||||
{
|
||||
Intent intnt_stop = new Intent(SCNApp.getContext(), BroadcastReceiverService.class);
|
||||
intnt_stop.putExtra(BroadcastReceiverService.ID_KEY, BroadcastReceiverService.NOTIF_STOP_SOUND);
|
||||
PendingIntent pi_stop = PendingIntent.getBroadcast(SCNApp.getContext().getApplicationContext(), BroadcastReceiverService.NOTIF_STOP_SOUND, intnt_stop, 0);
|
||||
mBuilder.addAction(new NotificationCompat.Action(-1, "Stop", pi_stop));
|
||||
mBuilder.setDeleteIntent(pi_stop);
|
||||
|
||||
if (mNotificationManager != null) mNotificationManager.notify(0, n);
|
||||
SoundService.play(ns.EnableSound, ns.SoundSource, ns.ForceVolume, ns.ForceVolumeValue, ns.RepeatSound);
|
||||
}
|
||||
|
||||
Notification n = mBuilder.build();
|
||||
|
||||
if (mNotificationManager != null) mNotificationManager.notify((int)msg.SCN_ID, n);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private void showBackground_new(CMessage msg, Context ctxt, NotificationSettings ns, PriorityEnum prio)
|
||||
{
|
||||
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, getChannel(prio));
|
||||
mBuilder.setSmallIcon(R.drawable.ic_notification_white);
|
||||
mBuilder.setLargeIcon(BitmapFactory.decodeResource(ctxt.getResources(), R.mipmap.ic_notification_full));
|
||||
mBuilder.setContentTitle(msg.Title);
|
||||
mBuilder.setContentText(msg.Content);
|
||||
mBuilder.setShowWhen(true);
|
||||
mBuilder.setWhen(msg.Timestamp * 1000);
|
||||
mBuilder.setAutoCancel(true);
|
||||
mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
|
||||
mBuilder.setGroup("com.blackforestbytes.simplecloudnotifier.notifications.group."+prio.toString());
|
||||
|
||||
if (ns.EnableLED) mBuilder.setLights(ns.LEDColor, 500, 500);
|
||||
|
||||
if (msg.Priority == PriorityEnum.LOW) mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||
if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
||||
if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
|
||||
|
||||
Intent intnt_click = new Intent(SCNApp.getContext(), BroadcastReceiverService.class);
|
||||
intnt_click.putExtra(BroadcastReceiverService.ID_KEY, BroadcastReceiverService.NOTIF_SHOW_MAIN);
|
||||
PendingIntent pi = PendingIntent.getBroadcast(ctxt, 0, intnt_click, 0);
|
||||
mBuilder.setContentIntent(pi);
|
||||
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (mNotificationManager == null) return;
|
||||
|
||||
if (ns.EnableSound && !Str.isNullOrWhitespace(ns.SoundSource))
|
||||
{
|
||||
if (ns.RepeatSound)
|
||||
{
|
||||
Intent intnt_stop = new Intent(SCNApp.getContext(), BroadcastReceiverService.class);
|
||||
intnt_stop.putExtra(BroadcastReceiverService.ID_KEY, BroadcastReceiverService.NOTIF_STOP_SOUND);
|
||||
PendingIntent pi_stop = PendingIntent.getBroadcast(ctxt, BroadcastReceiverService.NOTIF_STOP_SOUND, intnt_stop, 0);
|
||||
mBuilder.addAction(new NotificationCompat.Action(-1, "Stop", pi_stop));
|
||||
mBuilder.setDeleteIntent(pi_stop);
|
||||
}
|
||||
|
||||
SoundService.play(ns.EnableSound, ns.SoundSource, ns.ForceVolume, ns.ForceVolumeValue, ns.RepeatSound);
|
||||
}
|
||||
|
||||
Notification n = mBuilder.build();
|
||||
n.flags |= Notification.FLAG_AUTO_CANCEL;
|
||||
|
||||
mNotificationManager.notify((int)msg.SCN_ID, n);
|
||||
|
||||
if (ns.EnableVibration)
|
||||
{
|
||||
Vibrator v = (Vibrator) SCNApp.getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||
v.vibrate(VibrationEffect.createOneShot(1500, VibrationEffect.DEFAULT_AMPLITUDE));
|
||||
}
|
||||
|
||||
//if (ns.EnableLED) { } // no LED in Android-O -- configure via Channel
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,56 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SoundService
|
||||
{
|
||||
private static MediaPlayer mpLast = null;
|
||||
|
||||
public static void play(boolean enableSound, String soundSource, boolean forceVolume, int forceVolumeValue, boolean loop)
|
||||
{
|
||||
if (!enableSound) return;
|
||||
if (Str.isNullOrWhitespace(soundSource)) return;
|
||||
|
||||
stop();
|
||||
|
||||
if (forceVolume)
|
||||
{
|
||||
AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE);
|
||||
int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
|
||||
aman.setStreamVolume(AudioManager.STREAM_NOTIFICATION, (int)(maxVolume * (forceVolumeValue / 100.0)), 0);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
MediaPlayer player = new MediaPlayer();
|
||||
player.setAudioAttributes(new AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_NOTIFICATION).build());
|
||||
player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
|
||||
player.setDataSource(SCNApp.getContext(), Uri.parse(soundSource));
|
||||
player.setLooping(loop);
|
||||
player.setOnCompletionListener( mp -> { mp.stop(); mp.release(); });
|
||||
player.setOnSeekCompleteListener(mp -> { mp.stop(); mp.release(); });
|
||||
player.prepare();
|
||||
player.start();
|
||||
mpLast = player;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.e("Sound::play", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void stop()
|
||||
{
|
||||
if (mpLast != null && mpLast.isPlaying()) { mpLast.stop(); mpLast.release(); mpLast = null; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.util;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.view.View;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||
import com.blackforestbytes.simplecloudnotifier.view.MessageAdapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class MessageAdapterTouchHelper extends ItemTouchHelper.SimpleCallback
|
||||
{
|
||||
private MessageAdapterTouchHelperListener listener;
|
||||
|
||||
private int dir = 0;
|
||||
|
||||
public MessageAdapterTouchHelper(int dragDirs, int swipeDirs, MessageAdapterTouchHelperListener listener)
|
||||
{
|
||||
super(dragDirs, swipeDirs);
|
||||
this.dir = swipeDirs;
|
||||
this.listener = listener;
|
||||
updateEnabled();
|
||||
}
|
||||
|
||||
public void updateEnabled()
|
||||
{
|
||||
int sdir = SCNSettings.inst().EnableDeleteSwipe ? ItemTouchHelper.LEFT : 0;
|
||||
if (dir == sdir) return;
|
||||
setDefaultSwipeDirs(dir = sdir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)
|
||||
{
|
||||
if (viewHolder != null)
|
||||
{
|
||||
final View foregroundView = ((MessageAdapter.MessagePresenter) viewHolder).viewForeground;
|
||||
|
||||
getDefaultUIUtil().onSelected(foregroundView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)
|
||||
{
|
||||
final View foregroundView = ((MessageAdapter.MessagePresenter) viewHolder).viewForeground;
|
||||
getDefaultUIUtil().onDrawOver(c, recyclerView, foregroundView, dX, dY, actionState, isCurrentlyActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder)
|
||||
{
|
||||
final View foregroundView = ((MessageAdapter.MessagePresenter) viewHolder).viewForeground;
|
||||
getDefaultUIUtil().clearView(foregroundView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)
|
||||
{
|
||||
final View foregroundView = ((MessageAdapter.MessagePresenter) viewHolder).viewForeground;
|
||||
|
||||
getDefaultUIUtil().onDraw(c, recyclerView, foregroundView, dX, dY, actionState, isCurrentlyActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction)
|
||||
{
|
||||
listener.onSwiped(viewHolder, direction, viewHolder.getAdapterPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int convertToAbsoluteDirection(int flags, int layoutDirection)
|
||||
{
|
||||
return super.convertToAbsoluteDirection(flags, layoutDirection);
|
||||
}
|
||||
|
||||
public interface MessageAdapterTouchHelperListener
|
||||
{
|
||||
void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position);
|
||||
}
|
||||
}
|
@@ -1,15 +1,19 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.view;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.R;
|
||||
@@ -21,6 +25,7 @@ import net.glxn.qrgen.android.QRCode;
|
||||
import net.glxn.qrgen.core.image.ImageType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import static android.content.Context.CLIPBOARD_SERVICE;
|
||||
@@ -55,15 +60,47 @@ public class AccountFragment extends Fragment
|
||||
|
||||
v.findViewById(R.id.btnAccountReset).setOnClickListener(cv ->
|
||||
{
|
||||
Activity a = getActivity();
|
||||
if (a == null) return;
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(a);
|
||||
|
||||
builder.setTitle("Confirm");
|
||||
builder.setMessage("Reset account key?");
|
||||
|
||||
builder.setPositiveButton("YES", (dialog, which) -> {
|
||||
View lpnl = v.findViewById(R.id.loadingPanel);
|
||||
lpnl.setVisibility(View.VISIBLE);
|
||||
SCNSettings.inst().reset(lpnl);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
builder.setNegativeButton("NO", (dialog, which) -> dialog.dismiss());
|
||||
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
});
|
||||
|
||||
v.findViewById(R.id.btnClearLocalStorage).setOnClickListener(cv ->
|
||||
{
|
||||
Activity a = getActivity();
|
||||
if (a == null) return;
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(a);
|
||||
|
||||
builder.setTitle("Confirm");
|
||||
builder.setMessage("Clear local messages?");
|
||||
|
||||
builder.setPositiveButton("YES", (dialog, which) -> {
|
||||
CMessageList.inst().clear();
|
||||
SCNApp.showToast("Notifications cleared", 1000);
|
||||
SCNApp.showToast("Messages cleared", 1000);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
builder.setNegativeButton("NO", (dialog, which) -> dialog.dismiss());
|
||||
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
});
|
||||
|
||||
v.findViewById(R.id.btnQR).setOnClickListener(cv ->
|
||||
@@ -94,6 +131,7 @@ public class AccountFragment extends Fragment
|
||||
TextView tvUserID = v.findViewById(R.id.tvUserID);
|
||||
TextView tvUserKey = v.findViewById(R.id.tvUserKey);
|
||||
TextView tvQuota = v.findViewById(R.id.tvQuota);
|
||||
ImageView ivQuota = v.findViewById(R.id.ic_img_quota);
|
||||
ImageButton btnQR = v.findViewById(R.id.btnQR);
|
||||
|
||||
SCNSettings s = SCNSettings.inst();
|
||||
@@ -104,6 +142,7 @@ public class AccountFragment extends Fragment
|
||||
tvUserKey.setText(s.user_key);
|
||||
tvQuota.setText(String.format("%d / %d", s.quota_curr, s.quota_max));
|
||||
btnQR.setImageBitmap(QRCode.from(s.createOnlineURL()).to(ImageType.PNG).withSize(512, 512).bitmap());
|
||||
ivQuota.setColorFilter(s.quota_curr>=s.quota_max ? Color.rgb(200, 0, 0) : Color.rgb(128, 128, 128));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -111,6 +150,7 @@ public class AccountFragment extends Fragment
|
||||
tvUserKey.setText(R.string.str_not_connected);
|
||||
tvQuota.setText(R.string.str_not_connected);
|
||||
btnQR.setImageResource(R.drawable.qr_default);
|
||||
ivQuota.setColorFilter(0x80_80_80);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,16 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.view;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.R;
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
||||
import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
@@ -20,6 +19,7 @@ import androidx.viewpager.widget.ViewPager;
|
||||
public class MainActivity extends AppCompatActivity
|
||||
{
|
||||
public TabAdapter adpTabs;
|
||||
public RelativeLayout layoutRoot;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
@@ -30,6 +30,8 @@ public class MainActivity extends AppCompatActivity
|
||||
NotificationService.inst();
|
||||
CMessageList.inst();
|
||||
|
||||
layoutRoot = findViewById(R.id.layoutRoot);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
@@ -41,8 +43,24 @@ public class MainActivity extends AppCompatActivity
|
||||
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
|
||||
SCNApp.register(this);
|
||||
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { /* */ }
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position)
|
||||
{
|
||||
if (position != 2) adpTabs.tab3.onViewpagerHide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
SCNApp.register(this);
|
||||
IABService.startup(this);
|
||||
SCNSettings.inst().work(this);
|
||||
}
|
||||
|
||||
@@ -51,6 +69,16 @@ public class MainActivity extends AppCompatActivity
|
||||
{
|
||||
super.onStop();
|
||||
|
||||
SCNSettings.inst().save();
|
||||
CMessageList.inst().fullSave();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy()
|
||||
{
|
||||
super.onDestroy();
|
||||
|
||||
CMessageList.inst().fullSave();
|
||||
IABService.inst().destroy();
|
||||
}
|
||||
}
|
||||
|
@@ -1,25 +1,40 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.view;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.R;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class MessageAdapter extends RecyclerView.Adapter
|
||||
{
|
||||
private final View vNoElements;
|
||||
private final LinearLayoutManager manLayout;
|
||||
private final RecyclerView viewRecycler;
|
||||
|
||||
public MessageAdapter(View noElementsView)
|
||||
private WeakHashMap<MessagePresenter, Boolean> viewHolders = new WeakHashMap<>();
|
||||
|
||||
public MessageAdapter(View noElementsView, LinearLayoutManager layout, RecyclerView recycler)
|
||||
{
|
||||
vNoElements = noElementsView;
|
||||
manLayout = layout;
|
||||
viewRecycler = recycler;
|
||||
CMessageList.inst().register(this);
|
||||
|
||||
vNoElements.setVisibility(getItemCount()>0 ? View.GONE : View.VISIBLE);
|
||||
@@ -36,9 +51,17 @@ public class MessageAdapter extends RecyclerView.Adapter
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position)
|
||||
{
|
||||
CMessage msg = CMessageList.inst().tryGet(position);
|
||||
CMessage msg = CMessageList.inst().tryGetFromBack(position);
|
||||
MessagePresenter view = (MessagePresenter) holder;
|
||||
view.setMessage(msg);
|
||||
|
||||
viewHolders.put(view, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder)
|
||||
{
|
||||
if (holder instanceof MessagePresenter) viewHolders.remove(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,13 +82,34 @@ public class MessageAdapter extends RecyclerView.Adapter
|
||||
vNoElements.setVisibility(getItemCount()>0 ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private class MessagePresenter extends RecyclerView.ViewHolder implements View.OnClickListener
|
||||
public void scrollToTop()
|
||||
{
|
||||
manLayout.smoothScrollToPosition(viewRecycler, null, 0);
|
||||
}
|
||||
|
||||
public CMessage removeItem(int position)
|
||||
{
|
||||
CMessage i = CMessageList.inst().removeFromBack(position);
|
||||
notifyDataSetChanged();
|
||||
return i;
|
||||
}
|
||||
|
||||
public void restoreItem(CMessage item, int position)
|
||||
{
|
||||
CMessageList.inst().insert(position, item);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public class MessagePresenter extends RecyclerView.ViewHolder implements View.OnClickListener
|
||||
{
|
||||
private TextView tvTimestamp;
|
||||
private TextView tvTitle;
|
||||
private TextView tvMessage;
|
||||
private ImageView ivPriority;
|
||||
|
||||
public RelativeLayout viewForeground;
|
||||
public RelativeLayout viewBackground;
|
||||
|
||||
private CMessage data;
|
||||
|
||||
MessagePresenter(View itemView)
|
||||
@@ -75,7 +119,15 @@ public class MessageAdapter extends RecyclerView.Adapter
|
||||
tvTitle = itemView.findViewById(R.id.tvTitle);
|
||||
tvMessage = itemView.findViewById(R.id.tvMessage);
|
||||
ivPriority = itemView.findViewById(R.id.ivPriority);
|
||||
viewForeground = itemView.findViewById(R.id.layoutFront);
|
||||
viewBackground = itemView.findViewById(R.id.layoutBack);
|
||||
|
||||
itemView.setOnClickListener(this);
|
||||
tvTimestamp.setOnClickListener(this);
|
||||
tvTitle.setOnClickListener(this);
|
||||
tvMessage.setOnClickListener(this);
|
||||
ivPriority.setOnClickListener(this);
|
||||
viewForeground.setOnClickListener(this);
|
||||
}
|
||||
|
||||
void setMessage(CMessage msg)
|
||||
@@ -89,13 +141,16 @@ public class MessageAdapter extends RecyclerView.Adapter
|
||||
case LOW:
|
||||
ivPriority.setVisibility(View.VISIBLE);
|
||||
ivPriority.setImageResource(R.drawable.priority_low);
|
||||
ivPriority.setColorFilter(Color.rgb(176, 176, 176));
|
||||
break;
|
||||
case NORMAL:
|
||||
ivPriority.setVisibility(View.GONE);
|
||||
ivPriority.setColorFilter(Color.rgb(176, 176, 176));
|
||||
break;
|
||||
case HIGH:
|
||||
ivPriority.setVisibility(View.VISIBLE);
|
||||
ivPriority.setImageResource(R.drawable.priority_high);
|
||||
ivPriority.setColorFilter(Color.rgb(200, 0, 0));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -105,7 +160,16 @@ public class MessageAdapter extends RecyclerView.Adapter
|
||||
@Override
|
||||
public void onClick(View v)
|
||||
{
|
||||
//SCNApp.showToast(data.Title, Toast.LENGTH_LONG);
|
||||
for (MessagePresenter holder : MessageAdapter.this.viewHolders.keySet())
|
||||
{
|
||||
if (holder == null) continue;
|
||||
if (holder == this) continue;
|
||||
if (holder.tvMessage == null) continue;
|
||||
if (holder.tvMessage.getMaxLines() == 6) continue;
|
||||
holder.tvMessage.setMaxLines(6);
|
||||
}
|
||||
|
||||
tvMessage.setMaxLines(9999);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,21 +1,34 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.view;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.blackforestbytes.simplecloudnotifier.R;
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
||||
import com.blackforestbytes.simplecloudnotifier.util.MessageAdapterTouchHelper;
|
||||
import com.google.android.gms.ads.doubleclick.PublisherAdRequest;
|
||||
import com.google.android.gms.ads.doubleclick.PublisherAdView;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class NotificationsFragment extends Fragment
|
||||
public class NotificationsFragment extends Fragment implements MessageAdapterTouchHelper.MessageAdapterTouchHelperListener
|
||||
{
|
||||
private PublisherAdView adView;
|
||||
private MessageAdapter adpMessages;
|
||||
|
||||
public MessageAdapterTouchHelper touchHelper;
|
||||
|
||||
public NotificationsFragment()
|
||||
{
|
||||
// Required empty public constructor
|
||||
@@ -27,14 +40,41 @@ public class NotificationsFragment extends Fragment
|
||||
View v = inflater.inflate(R.layout.fragment_notifications, container, false);
|
||||
|
||||
RecyclerView rvMessages = v.findViewById(R.id.rvMessages);
|
||||
rvMessages.setLayoutManager(new LinearLayoutManager(this.getContext(), RecyclerView.VERTICAL, true));
|
||||
rvMessages.setAdapter(new MessageAdapter(v.findViewById(R.id.tvNoElements)));
|
||||
LinearLayoutManager lman = new LinearLayoutManager(this.getContext(), RecyclerView.VERTICAL, false);
|
||||
rvMessages.setLayoutManager(lman);
|
||||
rvMessages.setAdapter(adpMessages = new MessageAdapter(v.findViewById(R.id.tvNoElements), lman, rvMessages));
|
||||
|
||||
PublisherAdView mPublisherAdView = v.findViewById(R.id.adBanner);
|
||||
ItemTouchHelper.SimpleCallback itemTouchHelperCallback = touchHelper = new MessageAdapterTouchHelper(0, ItemTouchHelper.LEFT, this);
|
||||
new ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(rvMessages);
|
||||
|
||||
adView = v.findViewById(R.id.adBanner);
|
||||
PublisherAdRequest adRequest = new PublisherAdRequest.Builder().build();
|
||||
mPublisherAdView.loadAd(adRequest);
|
||||
adView.loadAd(adRequest);
|
||||
|
||||
adView.setVisibility(SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public void updateProState()
|
||||
{
|
||||
if (adView != null) adView.setVisibility(IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE) != null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position)
|
||||
{
|
||||
if (viewHolder instanceof MessageAdapter.MessagePresenter)
|
||||
{
|
||||
final int deletedIndex = 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,16 @@
|
||||
package com.blackforestbytes.simplecloudnotifier.view;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -12,16 +18,25 @@ import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.blackforestbytes.simplecloudnotifier.R;
|
||||
import com.blackforestbytes.simplecloudnotifier.SCNApp;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.android.ThreadUtils;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.lambda.FI;
|
||||
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
|
||||
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
|
||||
import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
|
||||
import com.blackforestbytes.simplecloudnotifier.service.IABService;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import top.defaults.colorpicker.ColorPickerPopup;
|
||||
@@ -33,6 +48,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
private Switch prefAppEnabled;
|
||||
private Spinner prefLocalCacheSize;
|
||||
private Button prefUpgradeAccount;
|
||||
private TextView prefUpgradeAccount_msg;
|
||||
private TextView prefUpgradeAccount_info;
|
||||
private Switch prefEnableDeleteSwipe;
|
||||
|
||||
private Switch prefMsgLowEnableSound;
|
||||
private TextView prefMsgLowRingtone_value;
|
||||
@@ -42,6 +60,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
private View prefMsgLowLedColor_container;
|
||||
private ImageView prefMsgLowLedColor_value;
|
||||
private Switch prefMsgLowEnableVibrations;
|
||||
private Switch prefMsgLowForceVolume;
|
||||
private SeekBar prefMsgLowVolume;
|
||||
private ImageView prefMsgLowVolumeTest;
|
||||
|
||||
private Switch prefMsgNormEnableSound;
|
||||
private TextView prefMsgNormRingtone_value;
|
||||
@@ -51,6 +72,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
private View prefMsgNormLedColor_container;
|
||||
private ImageView prefMsgNormLedColor_value;
|
||||
private Switch prefMsgNormEnableVibrations;
|
||||
private Switch prefMsgNormForceVolume;
|
||||
private SeekBar prefMsgNormVolume;
|
||||
private ImageView prefMsgNormVolumeTest;
|
||||
|
||||
private Switch prefMsgHighEnableSound;
|
||||
private TextView prefMsgHighRingtone_value;
|
||||
@@ -60,9 +84,14 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
private View prefMsgHighLedColor_container;
|
||||
private ImageView prefMsgHighLedColor_value;
|
||||
private Switch prefMsgHighEnableVibrations;
|
||||
private Switch prefMsgHighForceVolume;
|
||||
private SeekBar prefMsgHighVolume;
|
||||
private ImageView prefMsgHighVolumeTest;
|
||||
|
||||
private int musicPickerSwitch = -1;
|
||||
|
||||
private MediaPlayer[] mPlayers = new MediaPlayer[3];
|
||||
|
||||
public SettingsFragment()
|
||||
{
|
||||
// Required empty public constructor
|
||||
@@ -85,6 +114,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
prefAppEnabled = v.findViewById(R.id.prefAppEnabled);
|
||||
prefLocalCacheSize = v.findViewById(R.id.prefLocalCacheSize);
|
||||
prefUpgradeAccount = v.findViewById(R.id.prefUpgradeAccount);
|
||||
prefUpgradeAccount_msg = v.findViewById(R.id.prefUpgradeAccount2);
|
||||
prefUpgradeAccount_info = v.findViewById(R.id.prefUpgradeAccount_info);
|
||||
prefEnableDeleteSwipe = v.findViewById(R.id.prefEnableDeleteSwipe);
|
||||
|
||||
prefMsgLowEnableSound = v.findViewById(R.id.prefMsgLowEnableSound);
|
||||
prefMsgLowRingtone_value = v.findViewById(R.id.prefMsgLowRingtone_value);
|
||||
@@ -94,6 +126,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
prefMsgLowLedColor_value = v.findViewById(R.id.prefMsgLowLedColor_value);
|
||||
prefMsgLowLedColor_container = v.findViewById(R.id.prefMsgLowLedColor_container);
|
||||
prefMsgLowEnableVibrations = v.findViewById(R.id.prefMsgLowEnableVibrations);
|
||||
prefMsgLowForceVolume = v.findViewById(R.id.prefMsgLowForceVolume);
|
||||
prefMsgLowVolume = v.findViewById(R.id.prefMsgLowVolume);
|
||||
prefMsgLowVolumeTest = v.findViewById(R.id.btnLowVolumeTest);
|
||||
|
||||
prefMsgNormEnableSound = v.findViewById(R.id.prefMsgNormEnableSound);
|
||||
prefMsgNormRingtone_value = v.findViewById(R.id.prefMsgNormRingtone_value);
|
||||
@@ -103,6 +138,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
prefMsgNormLedColor_value = v.findViewById(R.id.prefMsgNormLedColor_value);
|
||||
prefMsgNormLedColor_container = v.findViewById(R.id.prefMsgNormLedColor_container);
|
||||
prefMsgNormEnableVibrations = v.findViewById(R.id.prefMsgNormEnableVibrations);
|
||||
prefMsgNormForceVolume = v.findViewById(R.id.prefMsgNormForceVolume);
|
||||
prefMsgNormVolume = v.findViewById(R.id.prefMsgNormVolume);
|
||||
prefMsgNormVolumeTest = v.findViewById(R.id.btnNormVolumeTest);
|
||||
|
||||
prefMsgHighEnableSound = v.findViewById(R.id.prefMsgHighEnableSound);
|
||||
prefMsgHighRingtone_value = v.findViewById(R.id.prefMsgHighRingtone_value);
|
||||
@@ -112,6 +150,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
prefMsgHighLedColor_value = v.findViewById(R.id.prefMsgHighLedColor_value);
|
||||
prefMsgHighLedColor_container = v.findViewById(R.id.prefMsgHighLedColor_container);
|
||||
prefMsgHighEnableVibrations = v.findViewById(R.id.prefMsgHighEnableVibrations);
|
||||
prefMsgHighForceVolume = v.findViewById(R.id.prefMsgHighForceVolume);
|
||||
prefMsgHighVolume = v.findViewById(R.id.prefMsgHighVolume);
|
||||
prefMsgHighVolumeTest = v.findViewById(R.id.btnHighVolumeTest);
|
||||
}
|
||||
|
||||
private void updateUI()
|
||||
@@ -121,6 +162,11 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
if (c == null) return;
|
||||
|
||||
if (prefAppEnabled.isChecked() != s.Enabled) prefAppEnabled.setChecked(s.Enabled);
|
||||
if (prefEnableDeleteSwipe.isChecked() != s.EnableDeleteSwipe) prefEnableDeleteSwipe.setChecked(s.EnableDeleteSwipe);
|
||||
|
||||
prefUpgradeAccount.setVisibility( SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
|
||||
prefUpgradeAccount_info.setVisibility(SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
|
||||
prefUpgradeAccount_msg.setVisibility( SCNSettings.inst().promode_local ? View.VISIBLE : View.GONE );
|
||||
|
||||
ArrayAdapter<Integer> plcsa = new ArrayAdapter<>(c, android.R.layout.simple_spinner_item, SCNSettings.CHOOSABLE_CACHE_SIZES);
|
||||
plcsa.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
@@ -133,6 +179,12 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
if (prefMsgLowEnableLED.isChecked() != s.PriorityLow.EnableLED) prefMsgLowEnableLED.setChecked(s.PriorityLow.EnableLED);
|
||||
prefMsgLowLedColor_value.setColorFilter(s.PriorityLow.LEDColor);
|
||||
if (prefMsgLowEnableVibrations.isChecked() != s.PriorityLow.EnableVibration) prefMsgLowEnableVibrations.setChecked(s.PriorityLow.EnableVibration);
|
||||
if (prefMsgLowForceVolume.isChecked() != s.PriorityLow.ForceVolume) prefMsgLowForceVolume.setChecked(s.PriorityLow.ForceVolume);
|
||||
if (prefMsgLowVolume.getMax() != 100) prefMsgLowVolume.setMax(100);
|
||||
if (prefMsgLowVolume.getProgress() != s.PriorityLow.ForceVolumeValue) prefMsgLowVolume.setProgress(s.PriorityLow.ForceVolumeValue);
|
||||
if (prefMsgLowVolume.isEnabled() != s.PriorityLow.ForceVolume) prefMsgLowVolume.setEnabled(s.PriorityLow.ForceVolume);
|
||||
if (prefMsgLowVolumeTest.isEnabled() != s.PriorityLow.ForceVolume) prefMsgLowVolumeTest.setEnabled(s.PriorityLow.ForceVolume);
|
||||
if (s.PriorityLow.ForceVolume) prefMsgLowVolumeTest.setColorFilter(null); else prefMsgLowVolumeTest.setColorFilter(Color.argb(150,200,200,200));
|
||||
|
||||
if (prefMsgNormEnableSound.isChecked() != s.PriorityNorm.EnableSound) prefMsgNormEnableSound.setChecked(s.PriorityNorm.EnableSound);
|
||||
if (!prefMsgNormRingtone_value.getText().equals(s.PriorityNorm.SoundName)) prefMsgNormRingtone_value.setText(s.PriorityNorm.SoundName);
|
||||
@@ -140,6 +192,12 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
if (prefMsgNormEnableLED.isChecked() != s.PriorityNorm.EnableLED) prefMsgNormEnableLED.setChecked(s.PriorityNorm.EnableLED);
|
||||
prefMsgNormLedColor_value.setColorFilter(s.PriorityNorm.LEDColor);
|
||||
if (prefMsgNormEnableVibrations.isChecked() != s.PriorityNorm.EnableVibration) prefMsgNormEnableVibrations.setChecked(s.PriorityNorm.EnableVibration);
|
||||
if (prefMsgNormForceVolume.isChecked() != s.PriorityNorm.ForceVolume) prefMsgNormForceVolume.setChecked(s.PriorityNorm.ForceVolume);
|
||||
if (prefMsgNormVolume.getMax() != 100) prefMsgNormVolume.setMax(100);
|
||||
if (prefMsgNormVolume.getProgress() != s.PriorityNorm.ForceVolumeValue) prefMsgNormVolume.setProgress(s.PriorityNorm.ForceVolumeValue);
|
||||
if (prefMsgNormVolume.isEnabled() != s.PriorityNorm.ForceVolume) prefMsgNormVolume.setEnabled(s.PriorityNorm.ForceVolume);
|
||||
if (prefMsgNormVolumeTest.isEnabled() != s.PriorityNorm.ForceVolume) prefMsgNormVolumeTest.setEnabled(s.PriorityNorm.ForceVolume);
|
||||
if (s.PriorityNorm.ForceVolume) prefMsgNormVolumeTest.setColorFilter(null); else prefMsgNormVolumeTest.setColorFilter(Color.argb(150,200,200,200));
|
||||
|
||||
if (prefMsgHighEnableSound.isChecked() != s.PriorityHigh.EnableSound) prefMsgHighEnableSound.setChecked(s.PriorityHigh.EnableSound);
|
||||
if (!prefMsgHighRingtone_value.getText().equals(s.PriorityHigh.SoundName)) prefMsgHighRingtone_value.setText(s.PriorityHigh.SoundName);
|
||||
@@ -147,13 +205,20 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
if (prefMsgHighEnableLED.isChecked() != s.PriorityHigh.EnableLED) prefMsgHighEnableLED.setChecked(s.PriorityHigh.EnableLED);
|
||||
prefMsgHighLedColor_value.setColorFilter(s.PriorityHigh.LEDColor);
|
||||
if (prefMsgHighEnableVibrations.isChecked() != s.PriorityHigh.EnableVibration) prefMsgHighEnableVibrations.setChecked(s.PriorityHigh.EnableVibration);
|
||||
if (prefMsgHighForceVolume.isChecked() != s.PriorityHigh.ForceVolume) prefMsgHighForceVolume.setChecked(s.PriorityHigh.ForceVolume);
|
||||
if (prefMsgHighVolume.getMax() != 100) prefMsgHighVolume.setMax(100);
|
||||
if (prefMsgHighVolume.getProgress() != s.PriorityHigh.ForceVolumeValue) prefMsgHighVolume.setProgress(s.PriorityHigh.ForceVolumeValue);
|
||||
if (prefMsgHighVolume.isEnabled() != s.PriorityHigh.ForceVolume) prefMsgHighVolume.setEnabled(s.PriorityHigh.ForceVolume);
|
||||
if (prefMsgHighVolumeTest.isEnabled() != s.PriorityHigh.ForceVolume) prefMsgHighVolumeTest.setEnabled(s.PriorityHigh.ForceVolume);
|
||||
if (s.PriorityHigh.ForceVolume) prefMsgHighVolumeTest.setColorFilter(null); else prefMsgHighVolumeTest.setColorFilter(Color.argb(150,200,200,200));
|
||||
}
|
||||
|
||||
private void initListener()
|
||||
{
|
||||
SCNSettings s = SCNSettings.inst();
|
||||
|
||||
prefAppEnabled.setOnCheckedChangeListener((a,b) -> { s.Enabled=b; saveAndUpdate(); });
|
||||
prefAppEnabled.setOnCheckedChangeListener((a,b) -> { boolean prev=s.Enabled; s.Enabled=b; saveAndUpdate(); updateEnabled(prev, b); });
|
||||
prefEnableDeleteSwipe.setOnCheckedChangeListener((a,b) -> { s.EnableDeleteSwipe=b; saveAndUpdate(); });
|
||||
|
||||
prefLocalCacheSize.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
|
||||
{
|
||||
@@ -172,6 +237,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
prefMsgLowEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableLED=b; saveAndUpdate(); });
|
||||
prefMsgLowLedColor_container.setOnClickListener(a -> chooseLEDColorLow());
|
||||
prefMsgLowEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableVibration=b; saveAndUpdate(); });
|
||||
prefMsgLowForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.ForceVolume=b; saveAndUpdate(); });
|
||||
prefMsgLowVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityLow.ForceVolumeValue=b; saveAndUpdate(); updateVolume(0, b); } }));
|
||||
prefMsgLowVolumeTest.setOnClickListener((v) -> { if (s.PriorityLow.ForceVolume) playTestSound(0, prefMsgLowVolumeTest, s.PriorityLow.SoundSource, s.PriorityLow.ForceVolumeValue); });
|
||||
|
||||
prefMsgNormEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableSound=b; saveAndUpdate(); });
|
||||
prefMsgNormRingtone_container.setOnClickListener(a -> chooseRingtoneNorm());
|
||||
@@ -179,6 +247,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
prefMsgNormEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableLED=b; saveAndUpdate(); });
|
||||
prefMsgNormLedColor_container.setOnClickListener(a -> chooseLEDColorNorm());
|
||||
prefMsgNormEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableVibration=b; saveAndUpdate(); });
|
||||
prefMsgNormForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.ForceVolume=b; saveAndUpdate(); });
|
||||
prefMsgNormVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityNorm.ForceVolumeValue=b; saveAndUpdate(); updateVolume(1, b); } }));
|
||||
prefMsgNormVolumeTest.setOnClickListener((v) -> { if (s.PriorityNorm.ForceVolume) playTestSound(1, prefMsgNormVolumeTest, s.PriorityNorm.SoundSource, s.PriorityNorm.ForceVolumeValue); });
|
||||
|
||||
prefMsgHighEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableSound=b; saveAndUpdate(); });
|
||||
prefMsgHighRingtone_container.setOnClickListener(a -> chooseRingtoneHigh());
|
||||
@@ -186,18 +257,105 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
prefMsgHighEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableLED=b; saveAndUpdate(); });
|
||||
prefMsgHighLedColor_container.setOnClickListener(a -> chooseLEDColorHigh());
|
||||
prefMsgHighEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableVibration=b; saveAndUpdate(); });
|
||||
prefMsgHighForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.ForceVolume=b; saveAndUpdate(); });
|
||||
prefMsgHighVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityHigh.ForceVolumeValue=b; saveAndUpdate(); updateVolume(2, b); } }));
|
||||
prefMsgHighVolumeTest.setOnClickListener((v) -> { if (s.PriorityHigh.ForceVolume) playTestSound(2, prefMsgHighVolumeTest, s.PriorityHigh.SoundSource, s.PriorityHigh.ForceVolumeValue); });
|
||||
}
|
||||
|
||||
private void updateEnabled(boolean prev, boolean now)
|
||||
{
|
||||
if (!prev && now)
|
||||
{
|
||||
SCNApp.showToast("SimpleCloudNotifier is now enabled", Toast.LENGTH_SHORT);
|
||||
}
|
||||
else if (prev && !now)
|
||||
{
|
||||
SCNApp.showToast("SimpleCloudNotifier is now disabled\nYou won't recieve new messages.", Toast.LENGTH_LONG);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateVolume(int idx, int volume)
|
||||
{
|
||||
if (mPlayers[idx] != null && mPlayers[idx].isPlaying())
|
||||
{
|
||||
AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE);
|
||||
int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
|
||||
aman.setStreamVolume(AudioManager.STREAM_NOTIFICATION, (int)(maxVolume * (volume / 100.0)), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopSound(final int idx, final ImageView iv)
|
||||
{
|
||||
if (mPlayers[idx] != null && mPlayers[idx].isPlaying())
|
||||
{
|
||||
mPlayers[idx].stop();
|
||||
mPlayers[idx].release();
|
||||
iv.setImageResource(R.drawable.ic_play);
|
||||
mPlayers[idx] = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void playTestSound(final int idx, final ImageView iv, String src, int volume)
|
||||
{
|
||||
if (mPlayers[idx] != null && mPlayers[idx].isPlaying())
|
||||
{
|
||||
mPlayers[idx].stop();
|
||||
mPlayers[idx].release();
|
||||
iv.setImageResource(R.drawable.ic_play);
|
||||
mPlayers[idx] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Str.isNullOrWhitespace(src)) return;
|
||||
if (volume == 0) return;
|
||||
|
||||
Context ctxt = getContext();
|
||||
if (ctxt == null) return;
|
||||
|
||||
iv.setImageResource(R.drawable.ic_pause);
|
||||
|
||||
AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE);
|
||||
int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
|
||||
aman.setStreamVolume(AudioManager.STREAM_NOTIFICATION, (int)(maxVolume * (volume / 100.0)), 0);
|
||||
|
||||
MediaPlayer player = mPlayers[idx] = new MediaPlayer();
|
||||
player.setAudioAttributes(new AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_NOTIFICATION).build());
|
||||
player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
|
||||
|
||||
try
|
||||
{
|
||||
player.setDataSource(ctxt, Uri.parse(src));
|
||||
player.setLooping(false);
|
||||
player.setOnCompletionListener( mp -> SCNApp.runOnUiThread(() -> { mp.stop(); iv.setImageResource(R.drawable.ic_play); mPlayers[idx]=null; mp.release(); }));
|
||||
player.setOnSeekCompleteListener(mp -> SCNApp.runOnUiThread(() -> { mp.stop(); iv.setImageResource(R.drawable.ic_play); mPlayers[idx]=null; mp.release(); }));
|
||||
player.prepare();
|
||||
player.start();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.e("SFRAG:play", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveAndUpdate()
|
||||
{
|
||||
SCNSettings.inst().save();
|
||||
updateUI();
|
||||
NotificationService.inst().updateChannels();
|
||||
SCNApp.getMainActivity().adpTabs.tab1.touchHelper.updateEnabled();
|
||||
}
|
||||
|
||||
private void onUpgradeAccount()
|
||||
{
|
||||
//TODO
|
||||
IABService.inst().purchase(getActivity(), IABService.IAB_PRO_MODE);
|
||||
}
|
||||
|
||||
public void updateProState()
|
||||
{
|
||||
Purchase p = IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE);
|
||||
|
||||
if (prefUpgradeAccount != null) prefUpgradeAccount.setVisibility( p != null ? View.GONE : View.VISIBLE);
|
||||
if (prefUpgradeAccount_info != null) prefUpgradeAccount_info.setVisibility(p != null ? View.GONE : View.VISIBLE);
|
||||
if (prefUpgradeAccount_msg != null) prefUpgradeAccount_msg.setVisibility( p != null ? View.VISIBLE : View.GONE );
|
||||
}
|
||||
|
||||
private int getCacheSizeIndex(int value)
|
||||
@@ -215,7 +373,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
UltimateMusicPicker ump = new UltimateMusicPicker();
|
||||
ump.windowTitle("Choose notification sound");
|
||||
ump.removeSilent();
|
||||
ump.streamType(AudioManager.STREAM_ALARM);
|
||||
ump.streamType(AudioManager.STREAM_NOTIFICATION);
|
||||
ump.ringtone();
|
||||
ump.notification();
|
||||
ump.alarm();
|
||||
@@ -230,7 +388,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
UltimateMusicPicker ump = new UltimateMusicPicker();
|
||||
ump.windowTitle("Choose notification sound");
|
||||
ump.removeSilent();
|
||||
ump.streamType(AudioManager.STREAM_ALARM);
|
||||
ump.streamType(AudioManager.STREAM_NOTIFICATION);
|
||||
ump.ringtone();
|
||||
ump.notification();
|
||||
ump.alarm();
|
||||
@@ -245,7 +403,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
UltimateMusicPicker ump = new UltimateMusicPicker();
|
||||
ump.windowTitle("Choose notification sound");
|
||||
ump.removeSilent();
|
||||
ump.streamType(AudioManager.STREAM_ALARM);
|
||||
ump.streamType(AudioManager.STREAM_NOTIFICATION);
|
||||
ump.ringtone();
|
||||
ump.notification();
|
||||
ump.alarm();
|
||||
@@ -338,4 +496,11 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
|
||||
{
|
||||
musicPickerSwitch = -1;
|
||||
}
|
||||
|
||||
public void onViewpagerHide()
|
||||
{
|
||||
stopSound(0, prefMsgLowVolumeTest);
|
||||
stopSound(1, prefMsgNormVolumeTest);
|
||||
stopSound(2, prefMsgHighVolumeTest);
|
||||
}
|
||||
}
|
@@ -35,7 +35,7 @@ public class TabAdapter extends FragmentStatePagerAdapter {
|
||||
{
|
||||
switch (position)
|
||||
{
|
||||
case 0: return "Notifications";
|
||||
case 0: return "Messages";
|
||||
case 1: return "Account";
|
||||
case 2: return "Settings";
|
||||
default: return null;
|
||||
|
BIN
android/app/src/main/res/drawable-hdpi/ic_notification_white.png
Normal file
After Width: | Height: | Size: 472 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_notification_white.png
Normal file
After Width: | Height: | Size: 320 B |
After Width: | Height: | Size: 551 B |
After Width: | Height: | Size: 949 B |
After Width: | Height: | Size: 1.0 KiB |
@@ -1,15 +0,0 @@
|
||||
<vector android:height="24dp" android:viewportHeight="1000"
|
||||
android:viewportWidth="1000" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M500,500m-500,0a500,500 0,1 1,1000 0a500,500 0,1 1,-1000 0"
|
||||
android:strokeColor="#000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#ffffff" android:pathData="M300,694L700,694L500,136L300,694"/>
|
||||
<path android:fillColor="#ffffff" android:pathData="M473,559L527,559L527,774L473,774"/>
|
||||
<path android:fillColor="#000000" android:pathData="M376,640L624,640L500,295L376,640"/>
|
||||
<path android:fillColor="#ffffff" android:pathData="M100,730L500,730L300,172L100,730"/>
|
||||
<path android:fillColor="#000000" android:pathData="M176,676L424,676L300,331L176,676"/>
|
||||
<path android:fillColor="#ffffff" android:pathData="M273,595L327,595L327,810L273,810"/>
|
||||
<path android:fillColor="#ffffff" android:pathData="M500,730L900,730L700,172L500,730"/>
|
||||
<path android:fillColor="#000000" android:pathData="M576,676L824,676L700,331L576,676"/>
|
||||
<path android:fillColor="#ffffff" android:pathData="M673,595L727,595L727,810L673,810"/>
|
||||
</vector>
|
16
android/app/src/main/res/drawable/ic_pause.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<vector
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportHeight="438.536"
|
||||
android:viewportWidth="438.536"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<path
|
||||
android:fillColor="#FF3F51B5"
|
||||
android:pathData="M164.453,0H18.276C13.324,0 9.041,1.807 5.425,5.424C1.808,9.04 0.001,13.322 0.001,18.271v401.991c0,4.948 1.807,9.233 5.424,12.847c3.619,3.617 7.902,5.428 12.851,5.428h146.181c4.949,0 9.231,-1.811 12.847,-5.428c3.617,-3.613 5.424,-7.898 5.424,-12.847V18.271c0,-4.952 -1.807,-9.231 -5.428,-12.847C173.685,1.807 169.402,0 164.453,0z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#FF3F51B5"
|
||||
android:pathData="M433.113,5.424C429.496,1.807 425.215,0 420.267,0H274.086c-4.949,0 -9.237,1.807 -12.847,5.424c-3.621,3.615 -5.432,7.898 -5.432,12.847v401.991c0,4.948 1.811,9.233 5.432,12.847c3.609,3.617 7.897,5.428 12.847,5.428h146.181c4.948,0 9.229,-1.811 12.847,-5.428c3.614,-3.613 5.421,-7.898 5.421,-12.847V18.271C438.534,13.319 436.73,9.04 433.113,5.424z"/>
|
||||
|
||||
</vector>
|
12
android/app/src/main/res/drawable/ic_play.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector
|
||||
android:height="24dp"
|
||||
android:viewportHeight="41.999"
|
||||
android:viewportWidth="41.999"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<path
|
||||
android:fillColor="#FF3F51B5"
|
||||
android:pathData="M36.068,20.176l-29,-20C6.761,-0.035 6.363,-0.057 6.035,0.114C5.706,0.287 5.5,0.627 5.5,0.999v40c0,0.372 0.206,0.713 0.535,0.886c0.146,0.076 0.306,0.114 0.465,0.114c0.199,0 0.397,-0.06 0.568,-0.177l29,-20c0.271,-0.187 0.432,-0.494 0.432,-0.823S36.338,20.363 36.068,20.176z"/>
|
||||
|
||||
</vector>
|
16
android/app/src/main/res/drawable/ic_trash.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<vector
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
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>
|
12
android/app/src/main/res/drawable/ic_volume.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector
|
||||
android:height="24dp"
|
||||
android:viewportHeight="459"
|
||||
android:viewportWidth="459"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<path
|
||||
android:fillColor="#FF757575"
|
||||
android:pathData="M0,153v153h102l127.5,127.5v-408L102,153H0zM344.25,229.5c0,-45.9 -25.5,-84.15 -63.75,-102v204C318.75,313.65 344.25,275.4 344.25,229.5zM280.5,5.1v53.55C354.45,81.6 408,147.899 408,229.5S354.45,377.4 280.5,400.35V453.9C382.5,430.949 459,339.15 459,229.5C459,119.85 382.5,28.049 280.5,5.1z"/>
|
||||
|
||||
</vector>
|
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
android:id="@+id/layoutRoot"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
|
@@ -4,7 +4,6 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="TooManyViews"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".view.SettingsFragment">
|
||||
@@ -33,23 +32,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<Switch
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefAppEnabled"
|
||||
android:text="@string/str_enabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -67,7 +57,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLocalCacheSize"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/str_localcachesize"
|
||||
android:textColor="#000"
|
||||
@@ -96,23 +86,54 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="#c0c0c0"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<Switch
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:id="@+id/prefEnableDeleteSwipe"
|
||||
android:text="@string/str_deleteswipe"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="#c0c0c0"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|center"
|
||||
android:minHeight="48dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/prefUpgradeAccount"
|
||||
android:text="@string/str_upgrade_account"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<TextView
|
||||
android:id="@+id/prefUpgradeAccount_info"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/str_promode_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/prefUpgradeAccount2"
|
||||
android:textColor="#FF4D00"
|
||||
android:textStyle="bold"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/str_promode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -137,23 +158,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgLowEnableSound"
|
||||
android:text="@string/str_msg_enablesound"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -162,20 +174,15 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="#c0c0c0"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/prefMsgLowRingtone_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvMsgLowRingtone"
|
||||
@@ -192,9 +199,8 @@
|
||||
android:spinnerMode="dialog"
|
||||
android:text="Whatever"
|
||||
tools:ignore="HardcodedText" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -203,22 +209,67 @@
|
||||
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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgLowRepeatSound"
|
||||
android:text="@string/str_repeatnotificationsound"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="#c0c0c0"/>
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgLowForceVolume"
|
||||
android:text="@string/str_forcevolume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="3"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icnLowVolume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/volume_icon"
|
||||
android:src="@drawable/ic_volume"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/prefMsgLowVolume"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnLowVolumeTest"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/icnLowVolume"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnLowVolumeTest"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_play"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:contentDescription="@string/play_test_sound" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
@@ -228,23 +279,14 @@
|
||||
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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgLowEnableLED"
|
||||
android:text="@string/str_enable_led"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -294,23 +336,15 @@
|
||||
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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgLowEnableVibrations"
|
||||
android:text="@string/str_enable_vibration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -335,23 +369,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgNormEnableSound"
|
||||
android:text="@string/str_msg_enablesound"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -360,20 +385,15 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="#c0c0c0"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/prefMsgNormRingtone_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvMsgNormRingtone"
|
||||
@@ -392,6 +412,58 @@
|
||||
tools:ignore="HardcodedText" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="#c0c0c0"/>
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgNormForceVolume"
|
||||
android:text="@string/str_forcevolume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="3"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icnNormVolume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/volume_icon"
|
||||
android:src="@drawable/ic_volume"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/prefMsgNormVolume"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnNormVolumeTest"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/icnNormVolume"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnNormVolumeTest"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_play"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:contentDescription="@string/play_test_sound" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
@@ -401,23 +473,14 @@
|
||||
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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgNormRepeatSound"
|
||||
android:text="@string/str_repeatnotificationsound"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -426,23 +489,14 @@
|
||||
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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgNormEnableLED"
|
||||
android:text="@string/str_enable_led"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -492,23 +546,14 @@
|
||||
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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgNormEnableVibrations"
|
||||
android:text="@string/str_enable_vibration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -533,23 +578,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgHighEnableSound"
|
||||
android:text="@string/str_msg_enablesound"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -558,18 +594,15 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="#c0c0c0"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/prefMsgHighRingtone_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
@@ -590,6 +623,51 @@
|
||||
tools:ignore="HardcodedText" />
|
||||
</LinearLayout>
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgHighForceVolume"
|
||||
android:text="@string/str_forcevolume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="3"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icnHighVolume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/volume_icon"
|
||||
android:src="@drawable/ic_volume"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/prefMsgHighVolume"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnHighVolumeTest"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/icnHighVolume"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnHighVolumeTest"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_play"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:contentDescription="@string/play_test_sound" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
@@ -599,23 +677,14 @@
|
||||
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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgHighRepeatSound"
|
||||
android:text="@string/str_repeatnotificationsound"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -624,23 +693,14 @@
|
||||
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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgHighEnableLED"
|
||||
android:text="@string/str_enable_led"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -690,23 +750,14 @@
|
||||
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">
|
||||
|
||||
<Switch
|
||||
android:id="@+id/prefMsgHighEnableVibrations"
|
||||
android:text="@string/str_enable_vibration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@@ -1,9 +1,43 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layoutBack"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/card_margin"
|
||||
android:background="@color/bg_row_background">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/delete_icon"
|
||||
android:layout_width="@dimen/ic_delete"
|
||||
android:layout_height="@dimen/ic_delete"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="@dimen/padd_10"
|
||||
android:src="@drawable/ic_trash"
|
||||
android:contentDescription="@string/delete" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="@dimen/padd_10"
|
||||
android:layout_toLeftOf="@id/delete_icon"
|
||||
android:text="@string/delete"
|
||||
android:textColor="#fff"
|
||||
android:textSize="13dp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layoutFront"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card_view"
|
||||
android:layout_width="match_parent"
|
||||
@@ -55,8 +89,8 @@
|
||||
app:layout_constraintRight_toLeftOf="@+id/ivPriority"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_margin="4sp"
|
||||
android:ellipsize="none"
|
||||
android:maxLines="32"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="6"
|
||||
android:scrollHorizontally="false"
|
||||
|
||||
android:text="asdasd asdasd asdasd a" />
|
||||
@@ -78,4 +112,6 @@
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
</FrameLayout>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_notification_full.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_notification_full.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_notification_full.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_notification_full.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_notification_full.png
Normal file
After Width: | Height: | Size: 17 KiB |
@@ -7,4 +7,6 @@
|
||||
<color name="colorHeaderForeground">#FFFFFF</color>
|
||||
|
||||
<color name="colorBlack">#000</color>
|
||||
|
||||
<color name="bg_row_background">#fa315b</color>
|
||||
</resources>
|
||||
|
@@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<dimen name="card_margin">5dp</dimen>
|
||||
<dimen name="card_album_radius">0dp</dimen>
|
||||
|
||||
<dimen name="padd_10">10dp</dimen>
|
||||
<dimen name="ic_delete">30dp</dimen>
|
||||
<dimen name="thumbnail">90dp</dimen>
|
||||
</resources>
|
@@ -11,21 +11,28 @@
|
||||
<string name="ic_img_fuel_desc">Icon Fuel</string>
|
||||
<string name="str_qr_code">QR Code</string>
|
||||
<string name="str_reset_account">Reset Account</string>
|
||||
<string name="no_notifications">No notifications</string>
|
||||
<string name="no_notifications">No messages</string>
|
||||
<string name="str_not_connected">not connected</string>
|
||||
<string name="str_reload">reload</string>
|
||||
<string name="desc_priority_icon">Priority icon</string>
|
||||
<string name="str_common_settings">Common Settings</string>
|
||||
<string name="str_enabled">Enabled</string>
|
||||
<string name="str_localcachesize">Remember the last x notifications locally</string>
|
||||
<string name="str_header_prio0">Notifications (low priority)</string>
|
||||
<string name="str_header_prio1">Notifications (normal priority)</string>
|
||||
<string name="str_header_prio2">Notifications (high priority)</string>
|
||||
<string name="str_localcachesize">Remember the last x messages locally</string>
|
||||
<string name="str_header_prio0">Notifications (priority 0 - Low)</string>
|
||||
<string name="str_header_prio1">Notifications (priority 1 - Normal)</string>
|
||||
<string name="str_header_prio2">Notifications (priority 2 - High)</string>
|
||||
<string name="str_msg_enablesound">Enable notification sound</string>
|
||||
<string name="str_notificationsound">Notification sound</string>
|
||||
<string name="str_repeatnotificationsound">Repeat notification sound</string>
|
||||
<string name="str_forcevolume">Automatically set the volume</string>
|
||||
<string name="str_enable_led">Enable notification light</string>
|
||||
<string name="str_ledcolor">Notification light color</string>
|
||||
<string name="str_enable_vibration">Enable notification vibration</string>
|
||||
<string name="str_upgrade_account">Upgrade account</string>
|
||||
<string name="str_deleteswipe">Delete messages by swiping left</string>
|
||||
<string name="str_promode">Thank you for supporting the app and using the pro mode</string>
|
||||
<string name="str_promode_info">Increase your daily quota, remove the ad banner and support the developer (that\'s me)</string>
|
||||
<string name="volume_icon">Volume icon</string>
|
||||
<string name="play_test_sound">Play test sound</string>
|
||||
<string name="delete">DELETE</string>
|
||||
</resources>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
#Mon Oct 22 15:29:01 CEST 2018
|
||||
VERSION_NAME=0.0.4
|
||||
VERSION_CODE=4
|
||||
#Sat Nov 17 18:59:44 CET 2018
|
||||
VERSION_NAME=0.0.11
|
||||
VERSION_CODE=11
|
||||
|
@@ -8,7 +8,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
classpath 'com.google.gms:google-services:4.0.1'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
data/icon_512_nobox.png
Normal file
After Width: | Height: | Size: 237 KiB |
BIN
data/icon_512_transparent.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
data/icon_web.pdn
Normal file
77
examples/scn_send.sh
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Call with `scn_send title`
|
||||
# or `scn_send title content`
|
||||
# or `scn_send title content priority`
|
||||
#
|
||||
#
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "no title supplied via parameter"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# INSERT YOUR DATA HERE #
|
||||
################################################################################
|
||||
user_id=999
|
||||
user_key="????????????????????????????????????????????????????????????????"
|
||||
################################################################################
|
||||
|
||||
title=$1
|
||||
content=""
|
||||
|
||||
if [ "$#" -gt 1 ]; then
|
||||
content=$2
|
||||
fi
|
||||
|
||||
priority=1
|
||||
|
||||
if [ "$#" -gt 2 ]; then
|
||||
priority=$3
|
||||
fi
|
||||
|
||||
usr_msg_id=$(uuidgen)
|
||||
|
||||
while true ; do
|
||||
|
||||
curlresp=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-d "user_id=$user_id" -d "user_key=$user_key" -d "title=$title" \
|
||||
-d "content=$content" -d "priority=$priority" -d "msg_id=$usr_msg_id" \
|
||||
https://scn.blackforestbytes.com/send.php)
|
||||
|
||||
if [ "$curlresp" == 200 ] ; then
|
||||
echo "Successfully send"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$curlresp" == 400 ] ; then
|
||||
echo "Bad request - something went wrong"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$curlresp" == 401 ] ; then
|
||||
echo "Unauthorized - wrong userid/userkey"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$curlresp" == 403 ] ; then
|
||||
echo "Quota exceeded - wait one hour before re-try"
|
||||
sleep 3600
|
||||
fi
|
||||
|
||||
if [ "$curlresp" == 412 ] ; then
|
||||
echo "Precondition Failed - No device linked"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$curlresp" == 500 ] ; then
|
||||
echo "Internal server error - waiting for better times"
|
||||
sleep 60
|
||||
fi
|
||||
|
||||
# if none of the above matched we probably hav no network ...
|
||||
echo "Send failed (response code $curlresp) ... try again in 5s"
|
||||
sleep 5
|
||||
done
|
BIN
store/screenshot_1.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
store/screenshot_2.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
store/screenshot_3.png
Normal file
After Width: | Height: | Size: 55 KiB |
1
web/.gitignore
vendored
@@ -181,3 +181,4 @@ $RECYCLE.BIN/
|
||||
#################
|
||||
|
||||
config.php
|
||||
.verify_accesstoken
|
47
web/api.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
|
||||
<title>Simple Cloud Notifications - API</title>
|
||||
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||
</div>
|
||||
|
||||
<div id="mainpnl">
|
||||
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
|
||||
|
||||
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||
|
||||
<p>Get your user-id and user-key from the app and send notifications to your phone by performing a POST request against <code>https://simplecloudnotifier.blackforestbytes.com/send.php</code></p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
--data "content={message_body}" \
|
||||
--data "priority={0|1|2}" \
|
||||
--data "msg_id={unique_message_id}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
<p>The <code>content</code>, <code>priority</code> and <code>msg_id</code> parameters are optional, you can also send message with only a title and the default priority</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
|
||||
<a href="/api_more.php" class="button bordered tertiary" style="float: right; min-width: 100px; text-align: center">More</a>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
2
web/api/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
config.php
|
||||
.verify_accesstoken
|
56
web/api/__config_example.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
// insert your values here and rename to config.php
|
||||
|
||||
return
|
||||
[
|
||||
'global' =>
|
||||
[
|
||||
'prod' => true,
|
||||
],
|
||||
|
||||
'database' =>
|
||||
[
|
||||
'host' => '?',
|
||||
'database' => '?',
|
||||
'user' => '?',
|
||||
'password' => '?',
|
||||
],
|
||||
|
||||
'firebase' =>
|
||||
[
|
||||
'type' => 'service_account',
|
||||
'project_id' => '?',
|
||||
'private_key_id' => '???',
|
||||
'client_email' => '???.iam.gserviceaccount.com',
|
||||
'client_id' => '???',
|
||||
'auth_uri' => 'https://accounts.google.com/o/oauth2/auth',
|
||||
'token_uri' => 'https://oauth2.googleapis.com/token',
|
||||
'auth_provider_x509_cert_url' => 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
'client_x509_cert_url' => 'https://www.googleapis.com/robot/v1/metadata/x509/???f.iam.gserviceaccount.com',
|
||||
'private_key' => "-----BEGIN PRIVATE KEY-----\n"
|
||||
. "??????????\n"
|
||||
. "-----END PRIVATE KEY-----\n",
|
||||
'server_key' => '????',
|
||||
],
|
||||
|
||||
'verify_api' =>
|
||||
[
|
||||
'package_name' => 'com.blackforestbytes.simplecloudnotifier',
|
||||
'product_id' => '???',
|
||||
|
||||
'clientid' => '???.apps.googleusercontent.com',
|
||||
'clientsecret' => '???',
|
||||
'accesstoken' => file_exists('.verify_accesstoken') ? file_get_contents('.verify_accesstoken') : '',
|
||||
'refreshtoken' => '???',
|
||||
'scope' => 'https://www.googleapis.com/auth/androidpublisher',
|
||||
],
|
||||
|
||||
'error_reporting' =>
|
||||
[
|
||||
'send-mail' => true,
|
||||
'email-error-target' => '???@???.com',
|
||||
'email-error-sender' => '???@???.com',
|
||||
],
|
||||
|
||||
];
|
46
web/api/ack.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errid'=>101, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errid'=>102, 'message' => 'Missing parameter [[user_key]]']));
|
||||
if (!isset($INPUT['scn_msg_id'])) die(json_encode(['success' => false, 'errid'=>103, 'message' => 'Missing parameter [[scn_msg_id]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
$scn_msg_id = $INPUT['scn_msg_id'];
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>201, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
|
||||
|
||||
$stmt = $pdo->prepare('SELECT ack FROM messages WHERE scn_message_id=:smid AND sender_user_id=:uid LIMIT 1');
|
||||
$stmt->execute(['smid' => $scn_msg_id, 'uid' => $user_id]);
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>301, 'message' => 'Message not found']));
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE messages SET ack=1 WHERE scn_message_id=:smid AND sender_user_id=:uid');
|
||||
$stmt->execute(['smid' => $scn_msg_id, 'uid' => $user_id]);
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'prev_ack' => $datas[0]['ack'],
|
||||
'new_ack' => 1,
|
||||
'message' => 'ok'
|
||||
]);
|
53
web/api/expand.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errid'=>101, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errid'=>102, 'message' => 'Missing parameter [[user_key]]']));
|
||||
if (!isset($INPUT['scn_msg_id'])) die(json_encode(['success' => false, 'errid'=>103, 'message' => 'Missing parameter [[scn_msg_id]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
$scn_msg_id = $INPUT['scn_msg_id'];
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>201, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM messages WHERE scn_message_id=:smid AND sender_user_id=:uid LIMIT 1');
|
||||
$stmt->execute(['smid' => $scn_msg_id, 'uid' => $user_id]);
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>301, 'message' => 'Message not found']));
|
||||
|
||||
$msg = $datas[0];
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'data' =>
|
||||
[
|
||||
'title' => $msg['title'],
|
||||
'body' => $msg['content'],
|
||||
'trimmed' => false,
|
||||
'priority' => $msg['priority'],
|
||||
'timestamp' => strtotime($msg['timestamp']),
|
||||
'usr_msg_id' => $msg['usr_message_id'],
|
||||
'scn_msg_id' => $msg['scn_message_id'],
|
||||
],
|
||||
'message' => 'ok'
|
||||
]);
|
@@ -15,7 +15,7 @@ $user_key = $INPUT['user_key'];
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_max, quota_day FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -26,17 +26,26 @@ if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message'
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
|
||||
|
||||
|
||||
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM messages WHERE ack=0 AND sender_user_id=:uid');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
$nack_count = $stmt->fetch(PDO::FETCH_NUM)[0];
|
||||
|
||||
|
||||
$quota = $data['quota_today'];
|
||||
$quota_max = $data['quota_max'];
|
||||
$is_pro = $data['is_pro'];
|
||||
|
||||
if ($data['quota_day'] === null || $data['quota_day'] !== date("Y-m-d")) $quota=0;
|
||||
|
||||
echo json_encode(
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'message' => 'ok',
|
||||
'user_id' => $user_id,
|
||||
'quota' => $quota,
|
||||
'quota_max'=> $quota_max,
|
||||
'message' => 'ok'
|
||||
'quota_max' => Statics::quota_max($is_pro),
|
||||
'is_pro' => $is_pro,
|
||||
'fcm_token_set' => ($data['fcm_token'] != null),
|
||||
'unack_count' => $nack_count,
|
||||
]);
|
||||
return 0;
|
269
web/api/model.php
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
include('lib/httpful.phar');
|
||||
|
||||
class ERR
|
||||
{
|
||||
const NO_ERROR = 0000;
|
||||
|
||||
const MISSING_UID = 1101;
|
||||
const MISSING_TOK = 1102;
|
||||
const MISSING_TITLE = 1103;
|
||||
const INVALID_PRIO = 1104;
|
||||
const REQ_METHOD = 1105;
|
||||
|
||||
const NO_TITLE = 1201;
|
||||
const TITLE_TOO_LONG = 1202;
|
||||
const CONTENT_TOO_LONG = 1203;
|
||||
const USR_MSG_ID_TOO_LONG = 1204;
|
||||
|
||||
const USER_NOT_FOUND = 1301;
|
||||
const USER_AUTH_FAILED = 1302;
|
||||
|
||||
const NO_DEVICE_LINKED = 1401;
|
||||
|
||||
const QUOTA_REACHED = 2101;
|
||||
|
||||
const FIREBASE_COM_FAILED = 9901;
|
||||
const FIREBASE_COM_ERRORED = 9902;
|
||||
const INTERNAL_EXCEPTION = 9903;
|
||||
}
|
||||
|
||||
class Statics
|
||||
{
|
||||
public static $DB = NULL;
|
||||
public static $CFG = NULL;
|
||||
|
||||
public static function quota_max($is_pro) { return $is_pro ? 1000 : 50; }
|
||||
|
||||
public static function contentlen_max($is_pro) { return $is_pro ? 16384 : 2048; }
|
||||
}
|
||||
|
||||
function str_limit($str, $len)
|
||||
{
|
||||
if (strlen($str)>$len) return substr($str, 0, $len-3)."...";
|
||||
return $str;
|
||||
}
|
||||
|
||||
function getConfig()
|
||||
{
|
||||
if (Statics::$CFG !== NULL) return Statics::$CFG;
|
||||
|
||||
return Statics::$CFG = require "config.php";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String $msg
|
||||
* @param Exception $e
|
||||
*/
|
||||
function reportError($msg, $e = null)
|
||||
{
|
||||
if ($e != null) $msg = ($msg."\n\n[[EXCEPTION]]\n" . $e . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString());
|
||||
|
||||
$subject = "SCN_Server has encountered an Error at " . date("Y-m-d H:i:s") . "] ";
|
||||
|
||||
$content = "";
|
||||
|
||||
$content .= 'HTTP_HOST: ' . ParamServerOrUndef('HTTP_HOST') . "\n";
|
||||
$content .= 'REQUEST_URI: ' . ParamServerOrUndef('REQUEST_URI') . "\n";
|
||||
$content .= 'TIME: ' . date('Y-m-d H:i:s') . "\n";
|
||||
$content .= 'REMOTE_ADDR: ' . ParamServerOrUndef('REMOTE_ADDR') . "\n";
|
||||
$content .= 'HTTP_X_FORWARDED_FOR: ' . ParamServerOrUndef('HTTP_X_FORWARDED_FOR') . "\n";
|
||||
$content .= 'HTTP_USER_AGENT: ' . ParamServerOrUndef('HTTP_USER_AGENT') . "\n";
|
||||
$content .= 'MESSAGE:' . "\n" . $msg . "\n";
|
||||
$content .= '$_GET:' . "\n" . print_r($_GET, true) . "\n";
|
||||
$content .= '$_POST:' . "\n" . print_r($_POST, true) . "\n";
|
||||
$content .= '$_FILES:' . "\n" . print_r($_FILES, true) . "\n";
|
||||
|
||||
if (getConfig()['error_reporting']['send-mail']) sendMail($subject, $content, getConfig()['error_reporting']['email-error-target'], getConfig()['error_reporting']['email-error-sender']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $subject
|
||||
* @param string $content
|
||||
* @param string $to
|
||||
* @param string $from
|
||||
*/
|
||||
function sendMail($subject, $content, $to, $from) {
|
||||
mail($to, $subject, $content, 'From: ' . $from);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $idx
|
||||
* @return string
|
||||
*/
|
||||
function ParamServerOrUndef($idx) {
|
||||
return isset($_SERVER[$idx]) ? $_SERVER[$idx] : 'NOT_SET';
|
||||
}
|
||||
|
||||
function getDatabase()
|
||||
{
|
||||
if (Statics::$DB !== NULL) return Statics::$DB;
|
||||
|
||||
$_config = getConfig()['database'];
|
||||
|
||||
$dsn = "mysql:host=" . $_config['host'] . ";dbname=" . $_config['database'] . ";charset=utf8";
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
return Statics::$DB = new PDO($dsn, $_config['user'], $_config['password'], $opt);
|
||||
}
|
||||
|
||||
function generateRandomAuthKey()
|
||||
{
|
||||
$random = '';
|
||||
for ($i = 0; $i < 64; $i++)
|
||||
|
||||
try {
|
||||
switch (random_int(1, 3)) {
|
||||
case 1:
|
||||
$random .= chr(random_int(ord('0'), ord('9')));
|
||||
break;
|
||||
case 2:
|
||||
$random .= chr(random_int(ord('A'), ord('Z')));
|
||||
break;
|
||||
case 3:
|
||||
$random .= chr(random_int(ord('a'), ord('z')));
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
die(json_encode(['success' => false, 'message' => 'Internal error - no randomness']));
|
||||
}
|
||||
return $random;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $url
|
||||
* @param $body
|
||||
* @param $header
|
||||
* @return array|object|string
|
||||
* @throws \Httpful\Exception\ConnectionErrorException
|
||||
* @throws Exception
|
||||
*/
|
||||
function sendPOST($url, $body, $header)
|
||||
{
|
||||
$builder = \Httpful\Request::post($url);
|
||||
|
||||
$builder->body($body);
|
||||
|
||||
foreach ($header as $k => $v) $builder->addHeader($k, $v);
|
||||
|
||||
$response = $builder->send();
|
||||
|
||||
if ($response->code != 200) throw new Exception("Repsponse code: " . $response->code);
|
||||
|
||||
return $response->raw_body;
|
||||
}
|
||||
|
||||
function verifyOrderToken($tok)
|
||||
{
|
||||
// https://developers.google.com/android-publisher/api-ref/purchases/products/get
|
||||
|
||||
try
|
||||
{
|
||||
$package = getConfig()['verify_api']['package_name'];
|
||||
$product = getConfig()['verify_api']['product_id'];
|
||||
$acctoken = getConfig()['verify_api']['accesstoken'];
|
||||
|
||||
if ($acctoken == '' || $acctoken == null || $acctoken == false) $acctoken = refreshVerifyToken();
|
||||
|
||||
$url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$package.'/purchases/products/'.$product.'/tokens/'.$tok.'?access_token='.$acctoken;
|
||||
$response = $builder = \Httpful\Request::get($url)->send();
|
||||
$obj = json_decode($response->raw_body, true);
|
||||
|
||||
if ($response->code != 401 && ($obj === null || $obj === false))
|
||||
{
|
||||
reportError('verify-token returned NULL');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($response->code == 401 || isset($obj['error']) && isset($obj['error']['code']) && $obj['error']['code'] == 401) // "Invalid Credentials" -- refresh acces_token
|
||||
{
|
||||
$acctoken = refreshVerifyToken();
|
||||
|
||||
$url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$package.'/purchases/products/'.$product.'/tokens/'.$tok.'?access_token='.$acctoken;
|
||||
$response = $builder = \Httpful\Request::get($url)->send();
|
||||
$obj = json_decode($response->raw_body, true);
|
||||
|
||||
if ($obj === null || $obj === false)
|
||||
{
|
||||
reportError('verify-token returned NULL');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($obj['purchaseState']) && $obj['purchaseState'] === 0) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
reportError("VerifyOrder token threw exception", $e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
function refreshVerifyToken()
|
||||
{
|
||||
$url = 'https://accounts.google.com/o/oauth2/token'.
|
||||
'?grant_type=refresh_token'.
|
||||
'&refresh_token='.getConfig()['verify_api']['refreshtoken'].
|
||||
'&client_id='.getConfig()['verify_api']['clientid'].
|
||||
'&client_secret='.getConfig()['verify_api']['clientsecret'];
|
||||
|
||||
$json = sendPOST($url, "", []);
|
||||
$obj = json_decode($json, true);
|
||||
file_put_contents('.verify_accesstoken', $obj['access_token']);
|
||||
|
||||
return $obj['access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $http_code
|
||||
* @param array $message
|
||||
*/
|
||||
function api_return($http_code, $message)
|
||||
{
|
||||
http_response_code($http_code);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($message);
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String $str
|
||||
* @param String[] $path
|
||||
* @return mixed|null
|
||||
*/
|
||||
function try_json($str, $path)
|
||||
{
|
||||
try
|
||||
{
|
||||
$o = json_decode($str, true);
|
||||
foreach ($path as $p) $o = $o[$p];
|
||||
return $o;
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//#################################################################################################################
|
||||
|
||||
if (getConfig()['global']['prod']) {
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
} else {
|
||||
error_reporting(E_STRICT);
|
||||
ini_set('display_errors', 1);
|
||||
}
|
||||
|
||||
//#################################################################################################################
|
50
web/api/register.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
if (!isset($INPUT['fcm_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[fcm_token]]']));
|
||||
if (!isset($INPUT['pro'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro]]']));
|
||||
if (!isset($INPUT['pro_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro_token]]']));
|
||||
|
||||
$fcmtoken = $INPUT['fcm_token'];
|
||||
$ispro = $INPUT['pro'] == 'true';
|
||||
$pro_token = $INPUT['pro_token'];
|
||||
$user_key = generateRandomAuthKey();
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($ispro)
|
||||
{
|
||||
if (!verifyOrderToken($pro_token))
|
||||
{
|
||||
$pdo->rollBack();
|
||||
die(json_encode(['success' => false, 'message' => 'Purchase token could not be verified']));
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> :uid AND pro_token = :ptk');
|
||||
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO users (user_key, fcm_token, is_pro, pro_token, timestamp_accessed) VALUES (:key, :token, :bpro, :spro, NOW())');
|
||||
$stmt->execute(['key' => $user_key, 'token' => $fcmtoken, 'bpro' => $ispro, 'spro' => $ispro ? $pro_token : null]);
|
||||
$user_id = $pdo->lastInsertId('user_id');
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET fcm_token=NULL WHERE user_id <> :uid AND fcm_token=:ft');
|
||||
$stmt->execute(['uid' => $user_id, 'ft' => $fcm_token]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'user_key' => $user_key,
|
||||
'quota' => 0,
|
||||
'quota_max' => Statics::quota_max($ispro),
|
||||
'is_pro' => $ispro,
|
||||
'message' => 'New user registered'
|
||||
]);
|
56
web/api/requery.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errid'=>101, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errid'=>102, 'message' => 'Missing parameter [[user_key]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>201, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
|
||||
|
||||
//-------------------
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM messages WHERE ack=0 AND sender_user_id=:uid ORDER BY `timestamp` DESC LIMIT 16');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
$nonacks_sql = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$nonacks = [];
|
||||
|
||||
foreach ($nonacks_sql as $nack)
|
||||
{
|
||||
$nonacks []=
|
||||
[
|
||||
'title' => $nack['title'],
|
||||
'body' => $nack['content'],
|
||||
'priority' => $nack['priority'],
|
||||
'timestamp' => strtotime($nack['timestamp']),
|
||||
'usr_msg_id' => $nack['usr_message_id'],
|
||||
'scn_msg_id' => $nack['scn_message_id'],
|
||||
];
|
||||
}
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'message' => 'ok',
|
||||
'count' => count($nonacks),
|
||||
'data' => $nonacks,
|
||||
]);
|
37
web/api/schema.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
CREATE TABLE `users`
|
||||
(
|
||||
`user_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_key` VARCHAR(64) NOT NULL,
|
||||
`fcm_token` VARCHAR(256) NULL DEFAULT NULL,
|
||||
`messages_sent` INT(11) NOT NULL DEFAULT '0',
|
||||
`timestamp_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`timestamp_accessed` DATETIME NULL DEFAULT NULL,
|
||||
|
||||
`quota_today` INT(11) NOT NULL DEFAULT '0',
|
||||
`quota_day` DATE NULL DEFAULT NULL,
|
||||
|
||||
`is_pro` BIT NOT NULL DEFAULT 0,
|
||||
`pro_token` VARCHAR(256) NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (`user_id`)
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS `messages`;
|
||||
CREATE TABLE `messages`
|
||||
(
|
||||
`scn_message_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`sender_user_id` INT(11) NOT NULL,
|
||||
|
||||
`timestamp` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`ack` BIT NOT NULL DEFAULT 0,
|
||||
|
||||
`title` VARCHAR(256) NOT NULL,
|
||||
`content` VARCHAR(12288) NULL,
|
||||
`priority` INT(11) NOT NULL,
|
||||
|
||||
`fcm_message_id` VARCHAR(256) NULL,
|
||||
`usr_message_id` VARCHAR(256) NULL,
|
||||
|
||||
PRIMARY KEY (`scn_message_id`)
|
||||
);
|
@@ -16,7 +16,7 @@ $fcm_token = isset($INPUT['fcm_token']) ? $INPUT['fcm_token'] : null;
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_max, quota_day FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_day, is_pro FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -28,39 +28,46 @@ if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'me
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'message' => 'Authentification failed']));
|
||||
|
||||
$quota = $data['quota_today'];
|
||||
$quota_max = $data['quota_max'];
|
||||
$is_pro = $data['is_pro'];
|
||||
|
||||
$new_userkey = generateRandomAuthKey();
|
||||
|
||||
if ($fcm_token === null)
|
||||
{
|
||||
// only gen new user_secret
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), user_key=:at WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $user_id, 'at' => $new_userkey]);
|
||||
|
||||
echo json_encode(
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'user_key' => $new_userkey,
|
||||
'quota' => $quota,
|
||||
'quota_max'=> $quota_max,
|
||||
'quota_max'=> Statics::quota_max($data['is_pro']),
|
||||
'is_pro' => $is_pro,
|
||||
'message' => 'user updated'
|
||||
]);
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// update fcm and gen new user_secret
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), fcm_token=:ft, user_key=:at WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $user_id, 'ft' => $fcm_token, 'at' => $new_userkey]);
|
||||
|
||||
echo json_encode(
|
||||
$stmt = $pdo->prepare('UPDATE users SET fcm_token=NULL WHERE user_id <> :uid AND fcm_token=:ft');
|
||||
$stmt->execute(['uid' => $user_id, 'ft' => $fcm_token]);
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'user_key' => $new_userkey,
|
||||
'quota' => $quota,
|
||||
'quota_max'=> $quota_max,
|
||||
'quota_max'=> Statics::quota_max($data['is_pro']),
|
||||
'is_pro' => $is_pro,
|
||||
'message' => 'user updated'
|
||||
]);
|
||||
return 0;
|
||||
}
|
74
web/api/upgrade.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
|
||||
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_id]]']));
|
||||
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_key]]']));
|
||||
if (!isset($INPUT['pro'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro]]']));
|
||||
if (!isset($INPUT['pro_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro_token]]']));
|
||||
|
||||
$user_id = $INPUT['user_id'];
|
||||
$user_key = $INPUT['user_key'];
|
||||
$ispro = $INPUT['pro'] == 'true';
|
||||
$pro_token = $INPUT['pro_token'];
|
||||
|
||||
//----------------------
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_day, is_pro, pro_token FROM users WHERE user_id = :uid LIMIT 1');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (count($datas)<=0) die(json_encode(['success' => false, 'message' => 'User not found']));
|
||||
$data = $datas[0];
|
||||
|
||||
if ($data === null) die(json_encode(['success' => false, 'message' => 'User not found']));
|
||||
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'message' => 'UserID not found']));
|
||||
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'message' => 'Authentification failed']));
|
||||
|
||||
if ($ispro)
|
||||
{
|
||||
// set pro=true
|
||||
|
||||
if ($data['pro_token'] != $pro_token)
|
||||
{
|
||||
if (!verifyOrderToken($pro_token)) die(json_encode(['success' => false, 'message' => 'Purchase token could not be verified']));
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), is_pro=1, pro_token=:ptk WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> :uid AND pro_token = :ptk');
|
||||
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'quota' => $data['quota_today'],
|
||||
'quota_max'=> Statics::quota_max(true),
|
||||
'is_pro' => true,
|
||||
'message' => 'user updated'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set pro=false
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), is_pro=0, pro_token=NULL WHERE user_id = :uid');
|
||||
$stmt->execute(['uid' => $user_id]);
|
||||
|
||||
api_return(200,
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'quota' => $data['quota_today'],
|
||||
'quota_max'=> Statics::quota_max(false),
|
||||
'is_pro' => false,
|
||||
'message' => 'user updated'
|
||||
]);
|
||||
}
|
282
web/api_more.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
|
||||
<title>Simple Cloud Notifications - API</title>
|
||||
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||
</div>
|
||||
|
||||
<div id="mainpnl">
|
||||
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
|
||||
|
||||
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||
|
||||
<h2>Introduction</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
With this API you can send push notifications to your phone.
|
||||
</p>
|
||||
<p>
|
||||
To recieve them you will need to install the <a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier">SimpleCloudNotifier</a> app from the play store.
|
||||
When you open the app you can click on the account tab to see you unique <code>user_id</code> and <code>user_key</code>.
|
||||
These two values are used to identify and authenticate your device so that send messages can be routed to your phone.
|
||||
</p>
|
||||
<p>
|
||||
You can at any time generate a new <code>user_key</code> in the app and invalidate the old one.
|
||||
</p>
|
||||
<p>
|
||||
There is also a <a href="/index.php">web interface</a> for this API to manually send notifications to your phone or to test your setup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>Quota</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
By default you can send up to 100 messages per day per device.
|
||||
If you need more you can upgrade your account in the app to get 1000 messages per day, this has the additional benefit of removing ads and supporting the development of the app (and making sure I can pay the server costs).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>API Requests</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
To send a new notification you send a <code>POST</code> request to the URL <code>https://scn.blackforestbytes.com/send.php</code>.
|
||||
All Parameters can either directly be submitted as URL parameters or they can be put into the POST body.
|
||||
</p>
|
||||
<p>
|
||||
You <i>need</i> to supply a valid <code>user_id</code> - <code>user_key</code> pair and a <code>title</code> for your message, all other parameter are optional.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>API Response</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
If the operation was successful the API will respond with an HTTP statuscode 200 and an JSON payload indicating the send message and your remaining quota
|
||||
</p>
|
||||
<pre class="red-code">{
|
||||
"success":true,
|
||||
"message":"Message sent",
|
||||
"response":
|
||||
{
|
||||
"multicast_id":8000000000000000006,
|
||||
"success":1,
|
||||
"failure":0,
|
||||
"canonical_ids":0,
|
||||
"results": [{"message_id":"0:10000000000000000000000000000000d"}]
|
||||
},
|
||||
"quota":17,
|
||||
"quota_max":100
|
||||
}</pre>
|
||||
<p>
|
||||
If the operation is <b>not</b> successful the API will respond with an 4xx HTTP statuscode.
|
||||
</p>
|
||||
<table class="scode_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Statuscode</th>
|
||||
<th>Explanation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td data-label="Statuscode">200 (OK)</td>
|
||||
<td data-label="Explanation">Message sent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">400 (Bad Request)</td>
|
||||
<td data-label="Explanation">The request is invalid (missing parameters or wrong values)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">401 (Unauthorized)</td>
|
||||
<td data-label="Explanation">The user_id was not found or the user_key is wrong</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">403 (Forbidden)</td>
|
||||
<td data-label="Explanation">The user has exceeded its daily quota - wait 24 hours or upgrade your account</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">412 (Precondition Failed)</td>
|
||||
<td data-label="Explanation">There is no device connected with this account - open the app and press the refresh button in the account tab</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Statuscode">500 (Internal Server Error)</td>
|
||||
<td data-label="Explanation">There was an internal error while sending your data - try again later</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
There is also always a JSON payload with additional information.
|
||||
The <code>success</code> field is always there and in the error state you the <code>message</code> field to get a descritpion of the problem.
|
||||
</p>
|
||||
<pre class="red-code">{
|
||||
"success":false,
|
||||
"error":2101,
|
||||
"errhighlight":-1,
|
||||
"message":"Daily quota reached (100)"
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<h2>Message Content</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
Every message must have a title set.
|
||||
But you also (optionally) add more content, while the title has a max length of 120 characters, the conntent can be up to 10.000 characters.
|
||||
You can see the whole message with title and content in the app or when clicking on the notification.
|
||||
</p>
|
||||
<p>
|
||||
If needed the content can be supplied in the <code>content</code> parameter.
|
||||
</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
--data "content={message_content}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
</div>
|
||||
|
||||
<h2>Message Priority</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
Currently you can send a message with three different priorities: 0 (low), 1 (normal) and 2 (high).
|
||||
In the app you can then configure a different behaviour for different priorities, e.g. only playing a sound if the notification is high priority.
|
||||
</p>
|
||||
<p>
|
||||
Priorites are either 0, 1 or 2 and are supplied in the <code>priority</code> parameter.
|
||||
If no priority is supplied the message will get the default priority of 1.
|
||||
</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
--data "priority={0|1|2}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
</div>
|
||||
|
||||
<h2>Message Uniqueness</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
Sometimes your script can run in an environment with an unstable connection and you want to implement an automatic re-try mechanism to send a message again if the last try failed due to bad connectivity.
|
||||
</p>
|
||||
<p>
|
||||
To ensure that a message is only send once you can generate a unique id for your message (I would recommend a simple <code>uuidgen</code>).
|
||||
If you send a message with an UUID that was already used in the near past the API still returns OK, but no new message is sent.
|
||||
</p>
|
||||
<p>
|
||||
The message_id is optional - but if you want to use it you need to supply it via the <code>msg_id</code> parameter.
|
||||
</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
--data "msg_id={message_id}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
<p>
|
||||
Be aware that the server only saves send messages for a short amount of time. Because of that you can only use this to prevent duplicates in a short time-frame, older messages with the same ID are probably already deleted and the message will be send again.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>Bash script example</h2>
|
||||
<div class="section">
|
||||
<p>
|
||||
Depending on your use case it can be useful to create a bash script that handles things like resending messages if you have connection problems or waiting if there is no quota left.<br/>
|
||||
Here is an example how such a scrippt could look like, you can put it into <code>/usr/local/sbin</code> and call it with <code>scn_send "title" "content"</code>
|
||||
</p>
|
||||
<pre style="color:#000000;" class="yellow-code"><span style="color:#3f7f59; font-weight:bold;">#!/usr/bin/env bash</span>
|
||||
|
||||
<span style="color:#3f7f59; ">#</span>
|
||||
<span style="color:#3f7f59; "># Call with `scn_send title`</span>
|
||||
<span style="color:#3f7f59; "># or `scn_send title content`</span>
|
||||
<span style="color:#3f7f59; "># or `scn_send title content priority`</span>
|
||||
<span style="color:#3f7f59; ">#</span>
|
||||
<span style="color:#3f7f59; ">#</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -lt 1 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"no title supplied via parameter"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#3f7f59; ">################################################################################</span>
|
||||
<span style="color:#3f7f59; "># INSERT YOUR DATA HERE #</span>
|
||||
<span style="color:#3f7f59; ">################################################################################</span>
|
||||
user_id=999
|
||||
user_key=<span style="color:#2a00ff; ">"????????????????????????????????????????????????????????????????"</span>
|
||||
<span style="color:#3f7f59; ">################################################################################</span>
|
||||
|
||||
title=$1
|
||||
content=<span style="color:#2a00ff; ">""</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -gt 1 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
content=$2
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
priority=1
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -gt 2 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
priority=$3
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
usr_msg_id=$(uuidgen)
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">while</span> true ; <span style="color:#7f0055; font-weight:bold; ">do</span>
|
||||
|
||||
curlresp=$(curl -s -o <span style="color:#3f3fbf; ">/dev/null</span> -w <span style="color:#2a00ff; ">"%{http_code}"</span> <span style="color:#2a00ff; ">\</span>
|
||||
-d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">user_id</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$user_id</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">user_key</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$user_key</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">title</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$title</span><span style="color:#2a00ff; ">"</span> <span style="color:#2a00ff; ">\</span>
|
||||
-d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">content</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$content</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">priority</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$priority</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">msg_id</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$usr_msg_id</span><span style="color:#2a00ff; ">"</span> <span style="color:#2a00ff; ">\</span>
|
||||
https:<span style="color:#3f3fbf; ">/</span><span style="color:#3f3fbf; ">/scn.blackforestbytes.com/send.php</span>)
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 200 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Successfully send"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 0
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 400 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Bad request - something went wrong"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 401 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Unauthorized - wrong </span><span style="color:#3f3fbf; ">userid/userkey</span><span style="color:#2a00ff; ">"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 403 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Quota exceeded - wait one hour before re-try"</span>
|
||||
sleep 3600
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 412 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Precondition Failed - No device linked"</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 500 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Internal server error - waiting for better times"</span>
|
||||
sleep 60
|
||||
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||
|
||||
<span style="color:#3f7f59; "># if none of the above matched we probably hav no network ...</span>
|
||||
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Send failed (response code </span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">) ... try again in 5s"</span>
|
||||
sleep 5
|
||||
<span style="color:#7f0055; font-weight:bold; ">done</span>
|
||||
</pre>
|
||||
<p>
|
||||
Be aware that the server only saves send messages for a short amount of time. Because of that you can only use this to prevent duplicates in a short time-frame, older messages with the same ID are probably already deleted and the message will be send again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -22,12 +22,29 @@ body
|
||||
#mainpnl
|
||||
{
|
||||
box-shadow: 0 0 32px #DDD;
|
||||
animation:blink-shadow ease-in-out 4s infinite;
|
||||
//animation:blink-shadow ease-in-out 4s infinite;
|
||||
width: 87%;
|
||||
min-width: 300px;
|
||||
max-width: 900px;
|
||||
position: relative;
|
||||
min-height: 485px;
|
||||
min-height: 570px;
|
||||
|
||||
background: var(--form-back-color);
|
||||
color: var(--form-fore-color);
|
||||
border: .0625rem solid var(--form-border-color);
|
||||
border-radius: var(--universal-border-radius);
|
||||
margin: 32px .5rem;
|
||||
padding: calc(2 * var(--universal-padding)) var(--universal-padding);
|
||||
}
|
||||
|
||||
.red-code
|
||||
{
|
||||
border-left: .25rem solid #E53935;
|
||||
}
|
||||
|
||||
.yellow-code
|
||||
{
|
||||
border-left: .25rem solid #FFCB05;
|
||||
}
|
||||
|
||||
#mainpnl input,
|
||||
@@ -74,13 +91,10 @@ body
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: -999;
|
||||
}
|
||||
|
||||
#copyinfo a:hover
|
||||
{
|
||||
font-family: "Courier New", monospace;
|
||||
color: #00F;
|
||||
//z-index: -999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#copyinfo a,
|
||||
@@ -90,6 +104,14 @@ body
|
||||
font-family: "Courier New", monospace;
|
||||
color: #AAA;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
#copyinfo a:hover
|
||||
{
|
||||
font-family: "Courier New", monospace;
|
||||
color: #0288D1;
|
||||
}
|
||||
|
||||
#tr_link
|
||||
@@ -178,6 +200,12 @@ input::-webkit-inner-spin-button {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fullcenterflex .card
|
||||
{
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
a.card,
|
||||
@@ -193,3 +221,30 @@ a.card:hover
|
||||
{
|
||||
box-shadow: 0 0 16px #AAA;
|
||||
}
|
||||
|
||||
table.scode_table {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
table.scode_table td:nth-child(2) {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
table.scode_table th:nth-child(2) {
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
#mainpnl h2 {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.linkcaption:hover,
|
||||
.linkcaption:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre, pre span
|
||||
{
|
||||
font-family: Menlo, Consolas, monospace;
|
||||
background: #F9F9F9;;
|
||||
}
|
BIN
web/favicon.ico
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
web/favicon.png
Normal file
After Width: | Height: | Size: 90 KiB |
@@ -9,14 +9,22 @@
|
||||
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||
</div>
|
||||
|
||||
<form id="mainpnl">
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a href="/index_api.php" class="button bordered" id="tr_link">API</a>
|
||||
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a tabindex="-1" href="/api.php" class="button bordered" id="tr_link">API</a>
|
||||
|
||||
<h1>Simple Cloud Notifier</h1>
|
||||
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||
|
||||
<div class="row responsive-label">
|
||||
<div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div>
|
||||
@@ -46,7 +54,7 @@
|
||||
|
||||
<div class="row responsive-label">
|
||||
<div class="col-sm-12 col-md-3"><label for="txt" class="doc">Message Content</label></div>
|
||||
<div class="col-sm-12 col-md"><textarea id="txt" class="doc" <?php echo (isset($_GET['preset_content']) ? (' value="'.$_GET['preset_content'].'" '):(''));?> rows="5"></textarea></div>
|
||||
<div class="col-sm-12 col-md"><textarea id="txt" class="doc" <?php echo (isset($_GET['preset_content']) ? (' value="'.$_GET['preset_content'].'" '):(''));?> rows="8" maxlength="2048"></textarea></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@@ -55,10 +63,6 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
</div>
|
||||
|
||||
<script src="/js/logic.js" type="text/javascript" ></script>
|
||||
<script src="/js/toastify.js"></script>
|
||||
</body>
|
||||
|
@@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="/css/mini-default.min.css">
|
||||
<title>Simple Cloud Notifications - API</title>
|
||||
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<form id="mainpnl">
|
||||
<a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a href="/index.php" class="button bordered" id="tr_link">Send</a>
|
||||
|
||||
<h1>Simple Cloud Notifier</h1>
|
||||
|
||||
<p>Get your user-id and user-key from the app and send notifications to your phone by performing a POST request against <code>https://simplecloudnotifier.blackforestbytes.com/send.php</code></p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "priority={0|1|2}" \
|
||||
--data "title={message_title}" \
|
||||
--data "content={message_content}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
<p>The <code>content</code> and <code>priority</code> parameters are optional, you can also send message with only a title and the default priority</p>
|
||||
<pre>curl \
|
||||
--data "user_id={userid}" \
|
||||
--data "user_key={userkey}" \
|
||||
--data "title={message_title}" \
|
||||
https://scn.blackforestbytes.com/send.php</pre>
|
||||
</form>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -34,10 +34,10 @@ function send()
|
||||
if (xhr.readyState !== 4) return;
|
||||
|
||||
console.log('Status: ' + xhr.status);
|
||||
if (xhr.status === 200)
|
||||
if (xhr.status === 200 || xhr.status === 401 || xhr.status === 403 || xhr.status === 412)
|
||||
{
|
||||
let resp = JSON.parse(xhr.responseText);
|
||||
if (!resp.success)
|
||||
if (!resp.success || xhr.status !== 200)
|
||||
{
|
||||
if (resp.errhighlight === 101) uid.classList.add('input-invalid');
|
||||
if (resp.errhighlight === 102) key.classList.add('input-invalid');
|
||||
@@ -55,7 +55,7 @@ function send()
|
||||
else
|
||||
{
|
||||
window.location.href =
|
||||
'/index_sent.php' +
|
||||
'/message_sent.php' +
|
||||
'?ok=' + 1 +
|
||||
'&message_count=' + resp.messagecount +
|
||||
'"a=' + resp.quota +
|
||||
|
@@ -3,16 +3,22 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Simple Cloud Notifications</title>
|
||||
<link rel="stylesheet" href="/css/mini-default.min.css">
|
||||
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
|
||||
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||
</div>
|
||||
|
||||
<form id="mainpnl">
|
||||
<div id="mainpnl">
|
||||
|
||||
<div class="fullcenterflex">
|
||||
|
||||
@@ -39,15 +45,11 @@
|
||||
|
||||
</div>
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a href="/index.php" class="button bordered" id="tr_link">Send</a>
|
||||
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
|
||||
|
||||
<h1>Simple Cloud Notifier</h1>
|
||||
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||
|
||||
</form>
|
||||
|
||||
<div id="copyinfo">
|
||||
<a href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||
</div>
|
||||
|
||||
</body>
|
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
include('lib/httpful.phar');
|
||||
|
||||
class Statics
|
||||
{
|
||||
public static $DB = NULL;
|
||||
public static $CFG = NULL;
|
||||
}
|
||||
|
||||
function getConfig()
|
||||
{
|
||||
if (Statics::$CFG !== NULL) return Statics::$CFG;
|
||||
|
||||
return Statics::$CFG = require "config.php";
|
||||
}
|
||||
|
||||
function getDatabase()
|
||||
{
|
||||
if (Statics::$DB !== NULL) return Statics::$DB;
|
||||
|
||||
$_config = getConfig()['database'];
|
||||
|
||||
$dsn = "mysql:host=" . $_config['host'] . ";dbname=" . $_config['database'] . ";charset=utf8";
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
return Statics::$DB = new PDO($dsn, $_config['user'], $_config['password'], $opt);
|
||||
}
|
||||
|
||||
function generateRandomAuthKey()
|
||||
{
|
||||
$random = '';
|
||||
for ($i = 0; $i < 64; $i++)
|
||||
|
||||
try {
|
||||
switch (random_int(1, 3)) {
|
||||
case 1:
|
||||
$random .= chr(random_int(ord('0'), ord('9')));
|
||||
break;
|
||||
case 2:
|
||||
$random .= chr(random_int(ord('A'), ord('Z')));
|
||||
break;
|
||||
case 3:
|
||||
$random .= chr(random_int(ord('a'), ord('z')));
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
die(json_encode(['success' => false, 'message' => 'Internal error - no randomness']));
|
||||
}
|
||||
return $random;
|
||||
}
|
||||
|
||||
function sendPOST($url, $body, $header)
|
||||
{
|
||||
$builder = \Httpful\Request::post($url);
|
||||
|
||||
$builder->body($body);
|
||||
|
||||
foreach ($header as $k => $v) $builder->addHeader($k, $v);
|
||||
|
||||
$response = $builder->send();
|
||||
|
||||
if ($response->code != 200) throw new Exception("Repsponse code: " . $response->code);
|
||||
|
||||
return $response->body;
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
include_once 'model.php';
|
||||
|
||||
$INPUT = array_merge($_GET, $_POST);
|
||||
|
||||
if (!isset($INPUT['fcm_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[fcm_token]]']));
|
||||
|
||||
$fcmtoken = $INPUT['fcm_token'];
|
||||
$user_key = generateRandomAuthKey();
|
||||
|
||||
$pdo = getDatabase();
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO users (user_key, fcm_token, timestamp_accessed) VALUES (:key, :token, NOW())');
|
||||
$stmt->execute(['key' => $user_key, 'token' => $fcmtoken]);
|
||||
$user_id = $pdo->lastInsertId('user_id');
|
||||
|
||||
echo json_encode(
|
||||
[
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'user_key' => $user_key,
|
||||
'quota' => 0,
|
||||
'quota_max'=> 100,
|
||||
'message' => 'New user registered'
|
||||
]);
|
||||
|
||||
return 0;
|