From 083945852b4b7444c342f4ac2c136fd27c6e24e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Mon, 12 Nov 2018 00:15:50 +0100 Subject: [PATCH] in-app-purchase: pro-mode --- android/app/build.gradle | 5 +- .../simplecloudnotifier/SCNApp.java | 1 + .../lib/android/ThreadUtils.java | 23 ++ .../lib/collections/CollectionHelper.java | 38 +++ .../lib/datatypes/IntRange.java | 14 ++ .../lib/datatypes/NInt.java | 10 + .../lib/datatypes/Nothing.java | 6 + .../lib/datatypes/Tuple1.java | 11 + .../lib/datatypes/Tuple2.java | 13 + .../lib/datatypes/Tuple3.java | 15 ++ .../lib/datatypes/Tuple4.java | 17 ++ .../lib/lambda/Func0to0.java | 9 + .../lib/lambda/Func0to1.java | 6 + .../lib/lambda/Func0to1WithIOException.java | 8 + .../lib/lambda/Func1to0.java | 6 + .../lib/lambda/Func1to1.java | 6 + .../lib/lambda/Func2to0.java | 6 + .../lib/lambda/Func2to1.java | 6 + .../lib/string/CompactJsonFormatter.java | 231 ++++++++++++++++++ .../simplecloudnotifier/lib/string/Str.java | 98 ++++++++ .../model/SCNSettings.java | 47 +++- .../model/ServerCommunication.java | 82 ++++++- .../service/IABService.java | 195 +++++++++++++++ .../service/NotificationService.java | 2 - .../view/MainActivity.java | 16 +- .../view/NotificationsFragment.java | 14 +- .../view/SettingsFragment.java | 22 +- .../src/main/res/layout/fragment_settings.xml | 26 +- android/app/src/main/res/values/strings.xml | 2 + android/build.gradle | 2 +- web/.gitignore | 3 +- web/info.php | 15 +- web/model.php | 102 ++++++++ web/register.php | 26 +- web/schema.sql | 4 +- web/send.php | 15 +- web/update.php | 16 +- web/upgrade.php | 78 ++++++ 38 files changed, 1134 insertions(+), 62 deletions(-) create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/android/ThreadUtils.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/collections/CollectionHelper.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/IntRange.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/NInt.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Nothing.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple1.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple2.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple3.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple4.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to0.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to1.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to1WithIOException.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func1to0.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func1to1.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func2to0.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func2to1.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/string/CompactJsonFormatter.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/string/Str.java create mode 100644 android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/IABService.java create mode 100644 web/upgrade.php diff --git a/android/app/build.gradle b/android/app/build.gradle index 62e1160..697c783 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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' diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/SCNApp.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/SCNApp.java index f57198c..21b2284 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/SCNApp.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/SCNApp.java @@ -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; diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/android/ThreadUtils.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/android/ThreadUtils.java new file mode 100644 index 0000000..745b9e4 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/android/ThreadUtils.java @@ -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()); + } + } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/collections/CollectionHelper.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/collections/CollectionHelper.java new file mode 100644 index 0000000..4fd2eae --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/collections/CollectionHelper.java @@ -0,0 +1,38 @@ +package com.blackforestbytes.simplecloudnotifier.lib.collections; + +import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func1to1; + +import java.util.*; + +public final class CollectionHelper +{ + public static List unique(List input, Func1to1 mapping) + { + List output = new ArrayList<>(input.size()); + + HashSet seen = new HashSet<>(); + + for (T v : input) if (seen.add(mapping.invoke(v))) output.add(v); + + return output; + } + + public static List sort(List input, Comparator comparator) + { + List output = new ArrayList<>(input); + Collections.sort(output, comparator); + return output; + } + + public static > List sort(List input, Func1to1 mapper) + { + return sort(input, mapper, 1); + } + + public static > List sort(List input, Func1to1 mapper, int sortMod) + { + List output = new ArrayList<>(input); + Collections.sort(output, (o1, o2) -> sortMod * mapper.invoke(o1).compareTo(mapper.invoke(o2))); + return output; + } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/IntRange.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/IntRange.java new file mode 100644 index 0000000..8eab9fd --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/IntRange.java @@ -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() { } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/NInt.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/NInt.java new file mode 100644 index 0000000..bb0aabc --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/NInt.java @@ -0,0 +1,10 @@ +package com.blackforestbytes.simplecloudnotifier.lib.datatypes; + +public class NInt +{ + public int Value; + + public NInt(int v) { Value = v; } + + private NInt() { } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Nothing.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Nothing.java new file mode 100644 index 0000000..41750e8 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Nothing.java @@ -0,0 +1,6 @@ +package com.blackforestbytes.simplecloudnotifier.lib.datatypes; + +public final class Nothing +{ + public final static Nothing Inst = new Nothing(); +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple1.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple1.java new file mode 100644 index 0000000..2b0c62a --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple1.java @@ -0,0 +1,11 @@ +package com.blackforestbytes.simplecloudnotifier.lib.datatypes; + +public class Tuple1 +{ + public final T1 Item1; + + public Tuple1(T1 i1) + { + Item1 = i1; + } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple2.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple2.java new file mode 100644 index 0000000..a9a9f19 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple2.java @@ -0,0 +1,13 @@ +package com.blackforestbytes.simplecloudnotifier.lib.datatypes; + +public class Tuple2 +{ + public final T1 Item1; + public final T2 Item2; + + public Tuple2(T1 i1, T2 i2) + { + Item1 = i1; + Item2 = i2; + } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple3.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple3.java new file mode 100644 index 0000000..263e2db --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple3.java @@ -0,0 +1,15 @@ +package com.blackforestbytes.simplecloudnotifier.lib.datatypes; + +public class Tuple3 +{ + 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; + } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple4.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple4.java new file mode 100644 index 0000000..17986ba --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/datatypes/Tuple4.java @@ -0,0 +1,17 @@ +package com.blackforestbytes.simplecloudnotifier.lib.datatypes; + +public class Tuple4 +{ + 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; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to0.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to0.java new file mode 100644 index 0000000..1cd57c4 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to0.java @@ -0,0 +1,9 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +@FunctionalInterface +public interface Func0to0 { + + Func0to0 EMPTY = ()->{}; + + void invoke(); +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to1.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to1.java new file mode 100644 index 0000000..5b5b190 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to1.java @@ -0,0 +1,6 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +@FunctionalInterface +public interface Func0to1 { + TResult invoke(); +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to1WithIOException.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to1WithIOException.java new file mode 100644 index 0000000..bcc2a66 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func0to1WithIOException.java @@ -0,0 +1,8 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +import java.io.IOException; + +@FunctionalInterface +public interface Func0to1WithIOException { + TResult invoke() throws IOException; +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func1to0.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func1to0.java new file mode 100644 index 0000000..6ca36cb --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func1to0.java @@ -0,0 +1,6 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +@FunctionalInterface +public interface Func1to0 { + void invoke(TInput1 value); +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func1to1.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func1to1.java new file mode 100644 index 0000000..3f77730 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func1to1.java @@ -0,0 +1,6 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +@FunctionalInterface +public interface Func1to1 { + TResult invoke(TInput1 value); +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func2to0.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func2to0.java new file mode 100644 index 0000000..41ec717 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func2to0.java @@ -0,0 +1,6 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +@FunctionalInterface +public interface Func2to0 { + void invoke(TInput1 value1, TInput2 value2); +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func2to1.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func2to1.java new file mode 100644 index 0000000..254cddf --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/lambda/Func2to1.java @@ -0,0 +1,6 @@ +package com.blackforestbytes.simplecloudnotifier.lib.lambda; + +@FunctionalInterface +public interface Func2to1 { + TResult invoke(TInput1 value1, TInput2 value2); +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/string/CompactJsonFormatter.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/string/CompactJsonFormatter.java new file mode 100644 index 0000000..2cbfd60 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/string/CompactJsonFormatter.java @@ -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; + } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/string/Str.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/string/Str.java new file mode 100644 index 0000000..a5577c5 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/lib/string/Str.java @@ -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 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 String join(String sep, List list, Func1to1 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; + } + } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SCNSettings.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SCNSettings.java index 6c08eab..a38569a 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SCNSettings.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/SCNSettings.java @@ -6,7 +6,9 @@ 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.service.IABService; import com.google.firebase.iid.FirebaseInstanceId; public class SCNSettings @@ -36,6 +38,10 @@ 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; @@ -57,6 +63,9 @@ 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); @@ -151,10 +160,12 @@ public class SCNSettings { 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 -> @@ -166,8 +177,11 @@ public class SCNSettings { if (isConnected()) ServerCommunication.info(user_id, user_key, null); }); + + updateProState(null); } + // reset account key public void reset(View loader) { if (!isConnected()) return; @@ -175,23 +189,50 @@ public class SCNSettings ServerCommunication.update(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); + } } 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); + 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); + } } diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/ServerCommunication.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/ServerCommunication.java index acebde3..6c87f0d 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/ServerCommunication.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/model/ServerCommunication.java @@ -9,6 +9,7 @@ import org.json.JSONObject; import org.json.JSONTokener; import java.io.IOException; +import java.net.URLEncoder; import okhttp3.Call; import okhttp3.Callback; @@ -25,12 +26,12 @@ public class ServerCommunication 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() @@ -67,6 +68,7 @@ public class ServerCommunication SCNSettings.inst().fcm_token_server = token; SCNSettings.inst().quota_curr = json.getInt("quota"); SCNSettings.inst().quota_max = json.getInt("quota_max"); + SCNSettings.inst().promode_server = json.getBoolean("is_pro"); SCNSettings.inst().save(); SCNApp.refreshAccountTab(); @@ -132,6 +134,7 @@ public class ServerCommunication SCNSettings.inst().fcm_token_server = token; SCNSettings.inst().quota_curr = json.getInt("quota"); SCNSettings.inst().quota_max = json.getInt("quota_max"); + SCNSettings.inst().promode_server = json.getBoolean("is_pro"); SCNSettings.inst().save(); SCNApp.refreshAccountTab(); @@ -187,10 +190,11 @@ public class ServerCommunication 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.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().promode_server = json.getBoolean("is_pro"); SCNSettings.inst().save(); SCNApp.refreshAccountTab(); @@ -247,9 +251,10 @@ public class ServerCommunication return; } - SCNSettings.inst().user_id = json.getInt("user_id"); - SCNSettings.inst().quota_curr = json.getInt("quota"); - SCNSettings.inst().quota_max = json.getInt("quota_max"); + SCNSettings.inst().user_id = json.getInt("user_id"); + SCNSettings.inst().quota_curr = json.getInt("quota"); + SCNSettings.inst().quota_max = json.getInt("quota_max"); + SCNSettings.inst().promode_server = json.getBoolean("is_pro"); SCNSettings.inst().save(); SCNApp.refreshAccountTab(); @@ -270,4 +275,63 @@ public class ServerCommunication 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) { + e.printStackTrace(); + 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", r); + + JSONObject json = (JSONObject) new JSONTokener(r).nextValue(); + + if (!json.getBoolean("success")) { + SCNApp.showToast(json.getString("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().promode_server = json.getBoolean("is_pro"); + SCNSettings.inst().save(); + + SCNApp.refreshAccountTab(); + } catch (Exception e) { + e.printStackTrace(); + 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); + } + } + } diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/IABService.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/IABService.java new file mode 100644 index 0000000..7e896d2 --- /dev/null +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/IABService.java @@ -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 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 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; + } +} diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/NotificationService.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/NotificationService.java index 51dd89a..b87bd1d 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/NotificationService.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/service/NotificationService.java @@ -6,8 +6,6 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.graphics.Color; -import android.media.AudioAttributes; import android.media.AudioManager; import android.net.Uri; import android.os.Build; diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/MainActivity.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/MainActivity.java index 9836113..ee6828d 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/MainActivity.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/MainActivity.java @@ -1,17 +1,15 @@ package com.blackforestbytes.simplecloudnotifier.view; -import android.net.Uri; import android.os.Bundle; 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; @@ -42,7 +40,7 @@ public class MainActivity extends AppCompatActivity tabLayout.setupWithViewPager(viewPager); SCNApp.register(this); - + IABService.startup(this); SCNSettings.inst().work(this); } @@ -51,6 +49,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(); + } } diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/NotificationsFragment.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/NotificationsFragment.java index 46b73a2..f752ccc 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/NotificationsFragment.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/NotificationsFragment.java @@ -6,6 +6,8 @@ import android.view.View; import android.view.ViewGroup; import com.blackforestbytes.simplecloudnotifier.R; +import com.blackforestbytes.simplecloudnotifier.model.SCNSettings; +import com.blackforestbytes.simplecloudnotifier.service.IABService; import com.google.android.gms.ads.doubleclick.PublisherAdRequest; import com.google.android.gms.ads.doubleclick.PublisherAdView; @@ -16,6 +18,8 @@ import androidx.recyclerview.widget.RecyclerView; public class NotificationsFragment extends Fragment { + private PublisherAdView adView; + public NotificationsFragment() { // Required empty public constructor @@ -30,11 +34,17 @@ public class NotificationsFragment extends Fragment rvMessages.setLayoutManager(new LinearLayoutManager(this.getContext(), RecyclerView.VERTICAL, true)); rvMessages.setAdapter(new MessageAdapter(v.findViewById(R.id.tvNoElements))); - PublisherAdView mPublisherAdView = v.findViewById(R.id.adBanner); + 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() + { + adView.setVisibility(IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE) != null ? View.GONE : View.VISIBLE); + } } diff --git a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/SettingsFragment.java b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/SettingsFragment.java index 935aac3..43676ca 100644 --- a/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/SettingsFragment.java +++ b/android/app/src/main/java/com/blackforestbytes/simplecloudnotifier/view/SettingsFragment.java @@ -1,6 +1,5 @@ package com.blackforestbytes.simplecloudnotifier.view; -import android.app.NotificationManager; import android.content.Context; import android.media.AudioManager; import android.net.Uri; @@ -16,8 +15,10 @@ import android.widget.Spinner; import android.widget.Switch; import android.widget.TextView; +import com.android.billingclient.api.Purchase; import com.blackforestbytes.simplecloudnotifier.R; import com.blackforestbytes.simplecloudnotifier.model.SCNSettings; +import com.blackforestbytes.simplecloudnotifier.service.IABService; import com.blackforestbytes.simplecloudnotifier.service.NotificationService; import org.jetbrains.annotations.NotNull; @@ -33,6 +34,8 @@ 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 prefMsgLowEnableSound; private TextView prefMsgLowRingtone_value; @@ -85,6 +88,8 @@ 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); prefMsgLowEnableSound = v.findViewById(R.id.prefMsgLowEnableSound); prefMsgLowRingtone_value = v.findViewById(R.id.prefMsgLowRingtone_value); @@ -122,6 +127,10 @@ public class SettingsFragment extends Fragment implements MusicPickerListener if (prefAppEnabled.isChecked() != s.Enabled) prefAppEnabled.setChecked(s.Enabled); + 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 plcsa = new ArrayAdapter<>(c, android.R.layout.simple_spinner_item, SCNSettings.CHOOSABLE_CACHE_SIZES); plcsa.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); prefLocalCacheSize.setAdapter(plcsa); @@ -197,7 +206,16 @@ public class SettingsFragment extends Fragment implements MusicPickerListener private void onUpgradeAccount() { - //TODO + IABService.inst().purchase(getActivity(), IABService.IAB_PRO_MODE); + } + + public void updateProState() + { + Purchase p = IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE); + + prefUpgradeAccount.setVisibility( p != null ? View.GONE : View.VISIBLE); + prefUpgradeAccount_info.setVisibility(p != null ? View.GONE : View.VISIBLE); + prefUpgradeAccount_msg.setVisibility( p != null ? View.VISIBLE : View.GONE ); } private int getCacheSizeIndex(int value) diff --git a/android/app/src/main/res/layout/fragment_settings.xml b/android/app/src/main/res/layout/fragment_settings.xml index c077a60..b689e3e 100644 --- a/android/app/src/main/res/layout/fragment_settings.xml +++ b/android/app/src/main/res/layout/fragment_settings.xml @@ -96,7 +96,8 @@ android:layout_marginTop="2dp" android:background="#c0c0c0"/> - + android:layout_height="wrap_content" /> - + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index f4d4172..d4adb57 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -28,4 +28,6 @@ Notification light color Enable notification vibration Upgrade account + Thank you for supporting the app and using the pro mode + Increase your daily quota, remove the ad banner and support the developer (that\'s me) diff --git a/android/build.gradle b/android/build.gradle index b674840..d21b66f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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' } } diff --git a/web/.gitignore b/web/.gitignore index d692dff..c96a770 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -180,4 +180,5 @@ $RECYCLE.BIN/ ################# -config.php \ No newline at end of file +config.php +.verify_accesstoken \ No newline at end of file diff --git a/web/info.php b/web/info.php index ac3cb10..471e7cb 100644 --- a/web/info.php +++ b/web/info.php @@ -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 FROM users WHERE user_id = :uid LIMIT 1'); $stmt->execute(['uid' => $user_id]); $datas = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -27,16 +27,17 @@ if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'er if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed'])); $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( [ - 'success' => true, - 'user_id' => $user_id, - 'quota' => $quota, - 'quota_max'=> $quota_max, - 'message' => 'ok' + 'success' => true, + 'user_id' => $user_id, + 'quota' => $quota, + 'quota_max' => Statics::quota_max($is_pro), + 'is_pro' => $is_pro, + 'message' => 'ok' ]); return 0; \ No newline at end of file diff --git a/web/model.php b/web/model.php index 7182c40..d06d3e9 100644 --- a/web/model.php +++ b/web/model.php @@ -6,6 +6,8 @@ class Statics { public static $DB = NULL; public static $CFG = NULL; + + public static function quota_max($is_pro) { return $is_pro ? 1000 : 100; } } function getConfig() @@ -15,6 +17,34 @@ function getConfig() return Statics::$CFG = require "config.php"; } +function reportError($msg) +{ + $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 $idx + * @return string + */ +function ParamServerOrUndef($idx) { + return isset($_SERVER[$idx]) ? $_SERVER[$idx] : 'NOT_SET'; +} + function getDatabase() { if (Statics::$DB !== NULL) return Statics::$DB; @@ -57,6 +87,13 @@ function generateRandomAuthKey() return $random; } +/** + * @param $url + * @param $body + * @param $header + * @return array|object|string + * @throws \Httpful\Exception\ConnectionErrorException + */ function sendPOST($url, $body, $header) { $builder = \Httpful\Request::post($url); @@ -71,3 +108,68 @@ function sendPOST($url, $body, $header) return $response->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 = refreshVerifyToken(); + + $url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$package.'/purchases/products/'.$product.'/tokens/'.$tok.'?access_token='.$acctoken; + + $json = sendPOST($url, "", []); + $obj = json_decode($json); + + if ($obj === null || $obj === false) + { + reportError('verify-token returned NULL'); + return false; + } + + if (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; + $json = sendPOST($url, "", []); + $obj = json_decode($json); + + 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 . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString()); + 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); + file_put_contents('.verify_accesstoken', $obj['access_token']); + + return $obj->access_token; +} \ No newline at end of file diff --git a/web/register.php b/web/register.php index c089e3a..cf7eea8 100644 --- a/web/register.php +++ b/web/register.php @@ -5,24 +5,34 @@ 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(); -$stmt = $pdo->prepare('INSERT INTO users (user_key, fcm_token, timestamp_accessed) VALUES (:key, :token, NOW())'); -$stmt->execute(['key' => $user_key, 'token' => $fcmtoken]); +if ($ispro) +{ + if (!verifyOrderToken($pro_token)) die(json_encode(['success' => false, 'message' => 'Purchase token could not be verified'])); +} + +$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'); echo json_encode( [ - 'success' => true, - 'user_id' => $user_id, - 'user_key' => $user_key, - 'quota' => 0, - 'quota_max'=> 100, - 'message' => 'New user registered' + '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' ]); return 0; \ No newline at end of file diff --git a/web/schema.sql b/web/schema.sql index 2e177fb..0dbc3bf 100644 --- a/web/schema.sql +++ b/web/schema.sql @@ -9,7 +9,9 @@ CREATE TABLE `users` `quota_today` INT(11) NOT NULL DEFAULT '0', `quota_day` DATE NULL DEFAULT NULL, - `quota_max` INT(11) NOT NULL DEFAULT '100', + + `is_pro` BIT NOT NULL DEFAULT 0, + `pro_token` VARCHAR(256) NULL DEFAULT NULL, PRIMARY KEY (`user_id`) ); diff --git a/web/send.php b/web/send.php index 1b54ce9..d13acc1 100644 --- a/web/send.php +++ b/web/send.php @@ -32,7 +32,7 @@ if (strlen($content) > 10000) die(json_encode(['success' => false, 'errhighli $pdo = getDatabase(); -$stmt = $pdo->prepare('SELECT user_id, user_key, fcm_token, messages_sent, quota_today, quota_max, quota_day FROM users WHERE user_id = :uid LIMIT 1'); +$stmt = $pdo->prepare('SELECT user_id, user_key, fcm_token, messages_sent, quota_today, is_pro, quota_day FROM users WHERE user_id = :uid LIMIT 1'); $stmt->execute(['uid' => $user_id]); $datas = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -47,7 +47,7 @@ $fcm = $data['fcm_token']; $new_quota = $data['quota_today'] + 1; if ($data['quota_day'] === null || $data['quota_day'] !== date("Y-m-d")) $new_quota=1; -if ($new_quota > $data['quota_max']) die(json_encode(['success' => false, 'errhighlight' => -1, 'message' => 'Daily quota reached ('.$data['quota_max'].')'])); +if ($new_quota > Statics::quota_max($data['is_pro'])) die(json_encode(['success' => false, 'errhighlight' => -1, 'message' => 'Daily quota reached ('.Statics::quota_max($data['is_pro']).')'])); //------------------------------------------------------------------ @@ -90,11 +90,12 @@ $stmt->execute(['uid' => $user_id, 'q' => $new_quota]); echo (json_encode( [ - 'success' => true, - 'message' => 'Message sent', - 'response' => $httpresult, + 'success' => true, + 'message' => 'Message sent', + 'response' => $httpresult, 'messagecount' => $data['messages_sent']+1, - 'quota'=>$new_quota, - 'quota_max'=>$data['quota_max'], + 'quota' => $new_quota, + 'is_pro' => $data['is_pro'], + 'quota_max' => Statics::quota_max($data['is_pro']), ])); return 0; \ No newline at end of file diff --git a/web/update.php b/web/update.php index 2b95aad..f643d21 100644 --- a/web/update.php +++ b/web/update.php @@ -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,7 +28,7 @@ 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(); @@ -39,12 +39,13 @@ if ($fcm_token === null) echo json_encode( [ - 'success' => true, - 'user_id' => $user_id, + 'success' => true, + 'user_id' => $user_id, 'user_key' => $new_userkey, 'quota' => $quota, - 'quota_max'=> $quota_max, - 'message' => 'user updated' + 'quota_max'=> Statics::quota_max($data['is_pro']), + 'is_pro' => $is_pro, + 'message' => 'user updated' ]); return 0; } @@ -59,7 +60,8 @@ else '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; diff --git a/web/upgrade.php b/web/upgrade.php new file mode 100644 index 0000000..3c1dbf5 --- /dev/null +++ b/web/upgrade.php @@ -0,0 +1,78 @@ + 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]); + + echo json_encode( + [ + 'success' => true, + 'user_id' => $user_id, + 'user_key' => $new_userkey, + 'quota' => $data['quota'], + 'quota_max'=> Statics::quota_max(true), + 'is_pro' => true, + 'message' => 'user updated' + ]); + return 0; +} +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]); + + echo json_encode( + [ + 'success' => true, + 'user_id' => $user_id, + 'user_key' => $new_userkey, + 'quota' => $data['quota'], + 'quota_max'=> Statics::quota_max(false), + 'is_pro' => false, + 'message' => 'user updated' + ]); + return 0; +}