in-app-purchase: pro-mode

This commit is contained in:
2018-11-12 00:15:50 +01:00
parent e69b1232d7
commit 083945852b
38 changed files with 1134 additions and 62 deletions

View File

@@ -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'

View File

@@ -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;

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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() { }
}

View File

@@ -0,0 +1,10 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public class NInt
{
public int Value;
public NInt(int v) { Value = v; }
private NInt() { }
}

View File

@@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public final class Nothing
{
public final static Nothing Inst = new Nothing();
}

View File

@@ -0,0 +1,11 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public class Tuple1<T1>
{
public final T1 Item1;
public Tuple1(T1 i1)
{
Item1 = i1;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func0to0 {
Func0to0 EMPTY = ()->{};
void invoke();
}

View File

@@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func0to1<TResult> {
TResult invoke();
}

View File

@@ -0,0 +1,8 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
import java.io.IOException;
@FunctionalInterface
public interface Func0to1WithIOException<TResult> {
TResult invoke() throws IOException;
}

View File

@@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func1to0<TInput1> {
void invoke(TInput1 value);
}

View File

@@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func1to1<TInput1, TResult> {
TResult invoke(TInput1 value);
}

View File

@@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func2to0<TInput1, TInput2> {
void invoke(TInput1 value1, TInput2 value2);
}

View File

@@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func2to1<TInput1, TInput2, TResult> {
TResult invoke(TInput1 value1, TInput2 value2);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>