in-app-purchase: pro-mode
This commit is contained in:
parent
e69b1232d7
commit
083945852b
@ -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'
|
||||
|
@ -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;
|
||||
|
@ -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,38 @@
|
||||
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, 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,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,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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
@ -191,6 +194,7 @@ public class ServerCommunication
|
||||
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();
|
||||
@ -250,6 +254,7 @@ public class ServerCommunication
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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<Integer> plcsa = new ArrayAdapter<>(c, android.R.layout.simple_spinner_item, SCNSettings.CHOOSABLE_CACHE_SIZES);
|
||||
plcsa.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
prefLocalCacheSize.setAdapter(plcsa);
|
||||
@ -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)
|
||||
|
@ -96,7 +96,8 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="#c0c0c0"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_width="match_parent"
|
||||
@ -107,12 +108,25 @@
|
||||
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>
|
||||
|
||||
|
@ -28,4 +28,6 @@
|
||||
<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_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>
|
||||
</resources>
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
1
web/.gitignore
vendored
1
web/.gitignore
vendored
@ -181,3 +181,4 @@ $RECYCLE.BIN/
|
||||
#################
|
||||
|
||||
config.php
|
||||
.verify_accesstoken
|
@ -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,7 +27,7 @@ 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;
|
||||
|
||||
@ -36,7 +36,8 @@ echo json_encode(
|
||||
'success' => true,
|
||||
'user_id' => $user_id,
|
||||
'quota' => $quota,
|
||||
'quota_max'=> $quota_max,
|
||||
'quota_max' => Statics::quota_max($is_pro),
|
||||
'is_pro' => $is_pro,
|
||||
'message' => 'ok'
|
||||
]);
|
||||
return 0;
|
102
web/model.php
102
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;
|
||||
}
|
@ -5,14 +5,23 @@ 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(
|
||||
@ -21,7 +30,8 @@ echo json_encode(
|
||||
'user_id' => $user_id,
|
||||
'user_key' => $user_key,
|
||||
'quota' => 0,
|
||||
'quota_max'=> 100,
|
||||
'quota_max' => Statics::quota_max($ispro),
|
||||
'is_pro' => $ispro,
|
||||
'message' => 'New user registered'
|
||||
]);
|
||||
|
||||
|
@ -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`)
|
||||
);
|
||||
|
@ -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']).')']));
|
||||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
@ -94,7 +94,8 @@ echo (json_encode(
|
||||
'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;
|
@ -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();
|
||||
|
||||
@ -43,7 +43,8 @@ if ($fcm_token === null)
|
||||
'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;
|
||||
@ -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;
|
||||
|
78
web/upgrade.php
Normal file
78
web/upgrade.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?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]);
|
||||
|
||||
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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user