52 Commits

Author SHA1 Message Date
90f4270b74 expand on click 2018-11-17 03:24:10 +01:00
f68d75c226 swipe to delete 2018-11-17 03:09:18 +01:00
9cf5133469 get nonack on info-call 2018-11-17 02:09:43 +01:00
75929aad48 rename php files 2018-11-17 01:30:41 +01:00
dca149ec4e move api to sub-dir 2018-11-17 01:29:12 +01:00
75a7b97d24 audio playback on notification stream 2018-11-17 01:21:53 +01:00
eb62873fb6 Android O repeat sound 2018-11-17 00:52:44 +01:00
f8effe7d1e no LED with android O 2018-11-17 00:24:22 +01:00
87a3d34315 scroll to newest message 2018-11-17 00:07:41 +01:00
0f38ad6e5c default_quota=50 2018-11-16 23:53:47 +01:00
0ec6ab71b2 fixed layout wrap in settings 2018-11-16 23:51:46 +01:00
b42204c87d fixed NPE on message recieve if app is not active 2018-11-16 23:28:48 +01:00
53cb259ec1 stupid sound shit 2018-11-13 18:11:17 +01:00
0150cc9e8e force POST in send.php 2018-11-13 16:23:04 +01:00
20214473f1 html styling 2018-11-13 16:06:22 +01:00
jenkins
586ac07ef3 [Jenkins] Increment version 2018-11-12 18:27:08 +01:00
66f82f26d5 channel bullshit 2018-11-12 18:25:41 +01:00
6cb2aa00fb intermed 2018-11-12 17:23:19 +01:00
96a236803e simplify settings layout 2018-11-12 16:29:43 +01:00
9d24044eb3 center pro message 2018-11-12 16:11:31 +01:00
90248bcb54 confirm-dialog 2018-11-12 16:04:59 +01:00
a3b2a1b14f only one account per fcm_token (2) 2018-11-12 16:01:06 +01:00
b21b159b95 fix user_key reset in promode 2018-11-12 15:58:59 +01:00
93ec261dc0 ack messages on recieve 2018-11-12 15:51:06 +01:00
ae246e9219 favicon 2018-11-12 15:28:07 +01:00
3f18fdd35a only one account per fcm_token 2018-11-12 15:22:21 +01:00
faf5207478 fixed wrong sql key 2018-11-12 15:11:26 +01:00
71f003dd66 fix error in send.php 2018-11-12 15:07:48 +01:00
3f85ab514e prevent purchase re-use 2018-11-12 14:57:54 +01:00
9eb5a6b1b9 fix NPE 2018-11-12 14:57:44 +01:00
8e26cd6078 fixed order verify 2018-11-12 14:49:09 +01:00
36b9263730 fix json_decode problems 2018-11-12 14:24:11 +01:00
3d29fecaec gracefully handle user_not_found 2018-11-12 14:23:50 +01:00
jenkins
92ac05f1e3 [Jenkins] Increment version 2018-11-12 13:06:38 +01:00
cae6ff6271 implement usr_msg_id 2018-11-12 13:03:56 +01:00
2163ae4dbf index_more.php 2018-11-12 12:41:59 +01:00
083945852b in-app-purchase: pro-mode 2018-11-12 00:28:57 +01:00
jenkins
e69b1232d7 [Jenkins] Increment version 2018-11-11 19:38:22 +01:00
ac681065fa billing permission 2018-11-11 19:36:58 +01:00
90d8b46179 actually use settings 2018-10-22 16:29:37 +02:00
jenkins
280b5ca4aa [Jenkins] Increment version 2018-10-22 15:29:28 +02:00
54a171bd5e ads 2018-10-22 15:16:39 +02:00
c043367810 icon 2018-10-22 14:37:45 +02:00
5494df1c56 settings kinda fin 2018-10-22 14:24:35 +02:00
f9d04e38a0 ColorPicker 2018-10-22 14:04:50 +02:00
6dfd7e016b setting stuff 2018-10-22 13:53:21 +02:00
96a3cbc40a colors 2018-10-22 01:32:21 +02:00
7261795c99 UltimateMusicPicker 2018-10-22 01:23:25 +02:00
d6becd15c1 switch to androidx 2018-10-22 01:05:05 +02:00
0b22a18088 preferences (1) 2018-10-21 22:58:26 +02:00
39fcddfaf0 small stuff 2018-10-20 19:10:10 +02:00
jenkins
38eaa0c132 [Jenkins] Increment version 2018-10-20 18:59:26 +02:00
89 changed files with 4492 additions and 611 deletions

View File

@@ -14,8 +14,8 @@
<option name="values"> <option name="values">
<map> <map>
<entry key="assetSourceType" value="FILE" /> <entry key="assetSourceType" value="FILE" />
<entry key="outputName" value="priority_low" /> <entry key="outputName" value="ic_garbage" />
<entry key="sourceFile" value="C:\Users\Mike\Downloads\Low Priority-595b40b75ba036ed117d9842.svg" /> <entry key="sourceFile" value="C:\Users\Mike\Downloads\garbage.svg" />
</map> </map>
</option> </option>
</PersistentState> </PersistentState>

View File

@@ -35,23 +35,23 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:support-v4:28.0.0' implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.android.support:cardview-v7:28.0.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.android.support:recyclerview-v7:28.0.0' implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.android.support:design:28.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'com.takisoft.fix:preference-v7:28.0.0.0'
implementation 'com.takisoft.fix:preference-v7-extras:28.0.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.firebase:firebase-core:16.0.4' implementation 'com.google.firebase:firebase-core:16.0.4'
implementation 'com.google.firebase:firebase-messaging:17.3.3' implementation 'com.google.firebase:firebase-messaging:17.3.4'
implementation 'com.google.android.gms:play-services-ads:17.1.0'
implementation "android.arch.lifecycle:extensions:1.1.1" implementation 'com.android.billingclient:billing:1.2'
implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.github.kenglxn.QRGen:android:2.5.0' implementation 'com.github.kenglxn.QRGen:android:2.5.0'
implementation "com.github.DeweyReed:UltimateMusicPicker:2.0.0"
implementation 'com.github.duanhong169:colorpicker:1.1.5'
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'

View File

@@ -1,15 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.blackforestbytes.simplecloudnotifier"> package="com.blackforestbytes.simplecloudnotifier">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.android.vending.BILLING" />
<application <application
android:allowBackup="false" android:allowBackup="false"
android:name="SCNApp" android:name="SCNApp"
android:icon="@mipmap/ic_launcher" android:icon="@drawable/icon"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".view.MainActivity"> <activity android:name=".view.MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -19,13 +27,17 @@
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/icon" /> <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/icon" />
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorAccent" /> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/colorAccent" />
<meta-data android:name="com.google.android.gms.ads.AD_MANAGER_APP" android:value="true"/>
<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-3320562328966175~7579972005"/>
<service android:name=".service.FBMService"> <service android:name=".service.FBMService" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" /> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<receiver android:name=".service.BroadcastReceiverService" android:exported="false" />
</application> </application>
</manifest> </manifest>

View File

@@ -1,28 +1,29 @@
package com.blackforestbytes.simplecloudnotifier; package com.blackforestbytes.simplecloudnotifier;
import android.app.Application; import android.app.Application;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleObserver;
import android.arch.lifecycle.OnLifecycleEvent;
import android.arch.lifecycle.ProcessLifecycleOwner;
import android.content.Context; import android.content.Context;
import android.widget.Toast; import android.widget.Toast;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList; import com.android.billingclient.api.BillingClient;
import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
import com.blackforestbytes.simplecloudnotifier.view.AccountFragment; import com.blackforestbytes.simplecloudnotifier.view.AccountFragment;
import com.blackforestbytes.simplecloudnotifier.view.MainActivity; import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
import com.blackforestbytes.simplecloudnotifier.view.TabAdapter; import com.blackforestbytes.simplecloudnotifier.view.TabAdapter;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.lifecycle.ProcessLifecycleOwner;
public class SCNApp extends Application implements LifecycleObserver public class SCNApp extends Application implements LifecycleObserver
{ {
private static SCNApp instance; private static SCNApp instance;
private static WeakReference<MainActivity> mainActivity; private static WeakReference<MainActivity> mainActivity = new WeakReference<>(null);
public static final boolean DEBUG = BuildConfig.DEBUG || !BuildConfig.VERSION_NAME.endsWith(".0"); public static final boolean LOCAL_DEBUG = BuildConfig.DEBUG;
public static final boolean RELEASE = !DEBUG; public static final boolean DEBUG = BuildConfig.DEBUG || !BuildConfig.VERSION_NAME.endsWith(".0");
public static final boolean RELEASE = !DEBUG;
private static boolean isBackground = true; private static boolean isBackground = true;
@@ -37,6 +38,11 @@ public class SCNApp extends Application implements LifecycleObserver
return instance; return instance;
} }
public static MainActivity getMainActivity()
{
return mainActivity.get();
}
public static boolean isBackground() public static boolean isBackground()
{ {
return isBackground; return isBackground;
@@ -92,3 +98,15 @@ public class SCNApp extends Application implements LifecycleObserver
isBackground = false; isBackground = false;
} }
} }
/*
==TODO==
[ ] - test notification channels
[ ] - startup time
[ ] - periodically get non-ack (option - even when not in-app)
[ ] - publish (+ HN post ?)
*/

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,29 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
import android.widget.SeekBar;
public final class FI
{
private FI() throws InstantiationException { throw new InstantiationException(); }
public static SeekBar.OnSeekBarChangeListener SeekBarChanged(Func3to0<SeekBar, Integer, Boolean> action)
{
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
action.invoke(seekBar, progress, fromUser);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
};
}
}

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,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func3to0<TInput1, TInput2, TInput3> {
void invoke(TInput1 value1, TInput2 value2, TInput3 value3);
}

View File

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

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

@@ -9,7 +9,8 @@ import java.util.TimeZone;
public class CMessage public class CMessage
{ {
public final long Timestamp ; public final long SCN_ID;
public final long Timestamp;
public final String Title; public final String Title;
public final String Content; public final String Content;
public final PriorityEnum Priority; public final PriorityEnum Priority;
@@ -21,8 +22,9 @@ public class CMessage
_format.setTimeZone(TimeZone.getDefault()); _format.setTimeZone(TimeZone.getDefault());
} }
public CMessage(long t, String mt, String mc, PriorityEnum p) public CMessage(long id, long t, String mt, String mc, PriorityEnum p)
{ {
SCN_ID = id;
Timestamp = t; Timestamp = t;
Title = mt; Title = mt;
Content = mc; Content = mc;

View File

@@ -3,15 +3,19 @@ package com.blackforestbytes.simplecloudnotifier.model;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.view.MessageAdapter; import com.blackforestbytes.simplecloudnotifier.view.MessageAdapter;
import com.blackforestbytes.simplecloudnotifier.SCNApp; import com.blackforestbytes.simplecloudnotifier.SCNApp;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
public class CMessageList public class CMessageList
{ {
public ArrayList<CMessage> Messages; public ArrayList<CMessage> Messages;
public Set<String> AllAcks;
private ArrayList<WeakReference<MessageAdapter>> _listener = new ArrayList<>(); private ArrayList<WeakReference<MessageAdapter>> _listener = new ArrayList<>();
@@ -29,6 +33,7 @@ public class CMessageList
private CMessageList() private CMessageList()
{ {
Messages = new ArrayList<>(); Messages = new ArrayList<>();
AllAcks = new HashSet<>();
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE); SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
int count = sharedPref.getInt("message_count", 0); int count = sharedPref.getInt("message_count", 0);
@@ -38,14 +43,17 @@ public class CMessageList
String title = sharedPref.getString("message["+i+"].title", ""); String title = sharedPref.getString("message["+i+"].title", "");
String content = sharedPref.getString("message["+i+"].content", ""); String content = sharedPref.getString("message["+i+"].content", "");
PriorityEnum prio = PriorityEnum.parseAPI(sharedPref.getInt("message["+i+"].priority", 1)); PriorityEnum prio = PriorityEnum.parseAPI(sharedPref.getInt("message["+i+"].priority", 1));
long scnid = sharedPref.getLong("message["+i+"].scnid", 0);
Messages.add(new CMessage(time, title, content, prio)); Messages.add(new CMessage(scnid, time, title, content, prio));
} }
AllAcks = sharedPref.getStringSet("acks", new HashSet<>());
} }
public CMessage add(final long time, final String title, final String content, final PriorityEnum pe) public CMessage add(final long scnid, final long time, final String title, final String content, final PriorityEnum pe)
{ {
CMessage msg = new CMessage(time, title, content, pe); CMessage msg = new CMessage(scnid, time, title, content, pe);
boolean run = SCNApp.runOnUiThread(() -> boolean run = SCNApp.runOnUiThread(() ->
{ {
@@ -55,11 +63,18 @@ public class CMessageList
SharedPreferences.Editor e = sharedPref.edit(); SharedPreferences.Editor e = sharedPref.edit();
Messages.add(msg); Messages.add(msg);
e.putInt("message_count", count+1); AllAcks.add(Long.toHexString(msg.SCN_ID));
e.putLong("message["+count+"].timestamp", time);
e.putString("message["+count+"].title", title); while (Messages.size()>SCNSettings.inst().LocalCacheSize) Messages.remove(0);
e.putString("message["+count+"].content", content);
e.putInt("message["+count+"].priority", pe.ID); e.putInt( "message_count", count+1);
e.putLong( "message["+count+"].timestamp", time);
e.putString("message["+count+"].title", title);
e.putString("message["+count+"].content", content);
e.putInt( "message["+count+"].priority", pe.ID);
e.putLong( "message["+count+"].scnid", scnid);
e.putStringSet("acks", AllAcks);
e.apply(); e.apply();
@@ -68,13 +83,15 @@ public class CMessageList
MessageAdapter a = ref.get(); MessageAdapter a = ref.get();
if (a == null) continue; if (a == null) continue;
a.customNotifyItemInserted(count); a.customNotifyItemInserted(count);
a.scrollToTop();
} }
CleanUpListener(); CleanUpListener();
}); });
if (!run) if (!run)
{ {
Messages.add(new CMessage(time, title, content, pe)); Messages.add(new CMessage(scnid, time, title, content, pe));
AllAcks.add(Long.toHexString(msg.SCN_ID));
fullSave(); fullSave();
} }
@@ -106,12 +123,15 @@ public class CMessageList
for (int i = 0; i < Messages.size(); i++) for (int i = 0; i < Messages.size(); i++)
{ {
e.putLong("message["+i+"].timestamp", Messages.get(i).Timestamp); e.putLong( "message["+i+"].timestamp", Messages.get(i).Timestamp);
e.putString("message["+i+"].title", Messages.get(i).Title); e.putString("message["+i+"].title", Messages.get(i).Title);
e.putString("message["+i+"].content", Messages.get(i).Content); e.putString("message["+i+"].content", Messages.get(i).Content);
e.putInt("message["+i+"].priority", Messages.get(i).Priority.ID); e.putInt( "message["+i+"].priority", Messages.get(i).Priority.ID);
e.putLong( "message["+i+"].scnid", Messages.get(i).SCN_ID);
} }
e.putStringSet("acks", AllAcks);
e.apply(); e.apply();
} }
@@ -121,6 +141,11 @@ public class CMessageList
return Messages.get(pos); return Messages.get(pos);
} }
public CMessage tryGetFromBack(int pos)
{
return tryGet(Messages.size() - pos - 1);
}
public int size() public int size()
{ {
return Messages.size(); return Messages.size();
@@ -139,4 +164,21 @@ public class CMessageList
if (_listener.get(i).get() == null) _listener.remove(i); if (_listener.get(i).get() == null) _listener.remove(i);
} }
} }
public boolean isAck(long id)
{
return AllAcks.contains(Long.toHexString(id));
}
public void remove(int index)
{
Messages.remove(index);
fullSave();
}
public void insert(int index, CMessage item)
{
Messages.add(index, item);
fullSave();
}
} }

View File

@@ -0,0 +1,34 @@
package com.blackforestbytes.simplecloudnotifier.model;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.net.Uri;
public class NotificationSettings
{
public boolean EnableSound;
public String SoundName;
public String SoundSource;
public boolean RepeatSound;
public boolean ForceVolume;
public int ForceVolumeValue;
public boolean EnableLED;
public int LEDColor;
public boolean EnableVibration;
public NotificationSettings(PriorityEnum p)
{
EnableSound = (p == PriorityEnum.HIGH);
SoundName = "Default";
SoundSource = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION).toString();
RepeatSound = false;
EnableLED = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL);
LEDColor = Color.BLUE;
EnableVibration = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL);
ForceVolume = false;
ForceVolumeValue = 50;
}
}

View File

@@ -6,7 +6,10 @@ import android.content.SharedPreferences;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import com.android.billingclient.api.Purchase;
import com.blackforestbytes.simplecloudnotifier.SCNApp; import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.iid.FirebaseInstanceId;
public class SCNSettings public class SCNSettings
@@ -22,6 +25,12 @@ public class SCNSettings
} }
} }
// ------------------------------------------------------------
public final static Integer[] CHOOSABLE_CACHE_SIZES = new Integer[]{20, 50, 100, 200, 500, 1000, 2000, 5000};
// ------------------------------------------------------------
public int quota_curr; public int quota_curr;
public int quota_max; public int quota_max;
public int user_id; public int user_id;
@@ -30,16 +39,67 @@ public class SCNSettings
public String fcm_token_local; public String fcm_token_local;
public String fcm_token_server; public String fcm_token_server;
public String promode_token;
public boolean promode_local;
public boolean promode_server;
// ------------------------------------------------------------
public boolean Enabled = true;
public int LocalCacheSize = 500;
public final NotificationSettings PriorityLow = new NotificationSettings(PriorityEnum.LOW);
public final NotificationSettings PriorityNorm = new NotificationSettings(PriorityEnum.NORMAL);
public final NotificationSettings PriorityHigh = new NotificationSettings(PriorityEnum.HIGH);
// ------------------------------------------------------------
public SCNSettings() public SCNSettings()
{ {
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("Config", Context.MODE_PRIVATE); SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("Config", Context.MODE_PRIVATE);
quota_curr = sharedPref.getInt("quota_curr", 0); quota_curr = sharedPref.getInt( "quota_curr", 0);
quota_max = sharedPref.getInt("quota_max", 0); quota_max = sharedPref.getInt( "quota_max", 0);
user_id = sharedPref.getInt("user_id", -1); user_id = sharedPref.getInt( "user_id", -1);
user_key = sharedPref.getString("user_key", ""); user_key = sharedPref.getString("user_key", "");
fcm_token_local = sharedPref.getString("fcm_token_local", ""); fcm_token_local = sharedPref.getString("fcm_token_local", "");
fcm_token_server = sharedPref.getString("fcm_token_server", ""); 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);
PriorityLow.EnableLED = sharedPref.getBoolean("priority_low:enabled_led", PriorityLow.EnableLED);
PriorityLow.EnableSound = sharedPref.getBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
PriorityLow.EnableVibration = sharedPref.getBoolean("priority_low:enabled_vibration", PriorityLow.EnableVibration);
PriorityLow.RepeatSound = sharedPref.getBoolean("priority_low:repeat_sound", PriorityLow.RepeatSound);
PriorityLow.SoundName = sharedPref.getString( "priority_low:sound_name", PriorityLow.SoundName);
PriorityLow.SoundSource = sharedPref.getString( "priority_low:sound_source", PriorityLow.SoundSource);
PriorityLow.LEDColor = sharedPref.getInt( "priority_low:led_color", PriorityLow.LEDColor);
PriorityLow.ForceVolume = sharedPref.getBoolean("priority_low:force_volume", PriorityLow.ForceVolume);
PriorityLow.ForceVolumeValue = sharedPref.getInt( "priority_low:force_volume_value", PriorityLow.ForceVolumeValue);
PriorityNorm.EnableLED = sharedPref.getBoolean("priority_norm:enabled_led", PriorityNorm.EnableLED);
PriorityNorm.EnableSound = sharedPref.getBoolean("priority_norm:enabled_sound", PriorityNorm.EnableSound);
PriorityNorm.EnableVibration = sharedPref.getBoolean("priority_norm:enabled_vibration", PriorityNorm.EnableVibration);
PriorityNorm.RepeatSound = sharedPref.getBoolean("priority_norm:repeat_sound", PriorityNorm.RepeatSound);
PriorityNorm.SoundName = sharedPref.getString( "priority_norm:sound_name", PriorityNorm.SoundName);
PriorityNorm.SoundSource = sharedPref.getString( "priority_norm:sound_source", PriorityNorm.SoundSource);
PriorityNorm.LEDColor = sharedPref.getInt( "priority_norm:led_color", PriorityNorm.LEDColor);
PriorityNorm.ForceVolume = sharedPref.getBoolean("priority_norm:force_volume", PriorityNorm.ForceVolume);
PriorityNorm.ForceVolumeValue = sharedPref.getInt( "priority_norm:force_volume_value", PriorityNorm.ForceVolumeValue);
PriorityHigh.EnableLED = sharedPref.getBoolean("priority_high:enabled_led", PriorityHigh.EnableLED);
PriorityHigh.EnableSound = sharedPref.getBoolean("priority_high:enabled_sound", PriorityHigh.EnableSound);
PriorityHigh.EnableVibration = sharedPref.getBoolean("priority_high:enabled_vibration", PriorityHigh.EnableVibration);
PriorityHigh.RepeatSound = sharedPref.getBoolean("priority_high:repeat_sound", PriorityHigh.RepeatSound);
PriorityHigh.SoundName = sharedPref.getString( "priority_high:sound_name", PriorityHigh.SoundName);
PriorityHigh.SoundSource = sharedPref.getString( "priority_high:sound_source", PriorityHigh.SoundSource);
PriorityHigh.LEDColor = sharedPref.getInt( "priority_high:led_color", PriorityHigh.LEDColor);
PriorityHigh.ForceVolume = sharedPref.getBoolean("priority_high:force_volume", PriorityHigh.ForceVolume);
PriorityHigh.ForceVolumeValue = sharedPref.getInt( "priority_high:force_volume_value", PriorityHigh.ForceVolumeValue);
} }
public void save() public void save()
@@ -47,12 +107,45 @@ public class SCNSettings
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("Config", Context.MODE_PRIVATE); SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("Config", Context.MODE_PRIVATE);
SharedPreferences.Editor e = sharedPref.edit(); SharedPreferences.Editor e = sharedPref.edit();
e.putInt("quota_curr", quota_curr); e.putInt( "quota_curr", quota_curr);
e.putInt("quota_max", quota_max); e.putInt( "quota_max", quota_max);
e.putInt("user_id", user_id); e.putInt( "user_id", user_id);
e.putString("user_key", user_key); e.putString( "user_key", user_key);
e.putString("fcm_token_local", fcm_token_local); e.putString( "fcm_token_local", fcm_token_local);
e.putString("fcm_token_server", fcm_token_server); e.putString( "fcm_token_server", fcm_token_server);
e.putBoolean("app_enabled", Enabled);
e.putInt( "local_cache_size", LocalCacheSize);
e.putBoolean("priority_low:enabled_led", PriorityLow.EnableLED);
e.putBoolean("priority_low:enabled_sound", PriorityLow.EnableSound);
e.putBoolean("priority_low:enabled_vibration", PriorityLow.EnableVibration);
e.putBoolean("priority_low:repeat_sound", PriorityLow.RepeatSound);
e.putString( "priority_low:sound_name", PriorityLow.SoundName);
e.putString( "priority_low:sound_source", PriorityLow.SoundSource);
e.putInt( "priority_low:led_color", PriorityLow.LEDColor);
e.putBoolean("priority_low:force_volume", PriorityLow.ForceVolume);
e.putInt( "priority_low:force_volume_value", PriorityLow.ForceVolumeValue);
e.putBoolean("priority_norm:enabled_led", PriorityNorm.EnableLED);
e.putBoolean("priority_norm:enabled_sound", PriorityNorm.EnableSound);
e.putBoolean("priority_norm:enabled_vibration", PriorityNorm.EnableVibration);
e.putBoolean("priority_norm:repeat_sound", PriorityNorm.RepeatSound);
e.putString( "priority_norm:sound_name", PriorityNorm.SoundName);
e.putString( "priority_norm:sound_source", PriorityNorm.SoundSource);
e.putInt( "priority_norm:led_color", PriorityNorm.LEDColor);
e.putBoolean("priority_norm:force_volume", PriorityNorm.ForceVolume);
e.putInt( "priority_norm:force_volume_value", PriorityNorm.ForceVolumeValue);
e.putBoolean("priority_high:enabled_led", PriorityHigh.EnableLED);
e.putBoolean("priority_high:enabled_sound", PriorityHigh.EnableSound);
e.putBoolean("priority_high:enabled_vibration", PriorityHigh.EnableVibration);
e.putBoolean("priority_high:repeat_sound", PriorityHigh.RepeatSound);
e.putString( "priority_high:sound_name", PriorityHigh.SoundName);
e.putString( "priority_high:sound_source", PriorityHigh.SoundSource);
e.putInt( "priority_high:led_color", PriorityHigh.LEDColor);
e.putBoolean("priority_high:force_volume", PriorityHigh.ForceVolume);
e.putInt( "priority_high:force_volume_value", PriorityHigh.ForceVolumeValue);
e.apply(); e.apply();
} }
@@ -74,16 +167,18 @@ public class SCNSettings
{ {
fcm_token_local = token; fcm_token_local = token;
save(); save();
if (!fcm_token_local.equals(fcm_token_server)) ServerCommunication.update(user_id, user_key, fcm_token_local, loader); if (!fcm_token_local.equals(fcm_token_server)) ServerCommunication.updateFCMToken(user_id, user_key, fcm_token_local, loader);
} }
else else
{ {
fcm_token_local = token; fcm_token_local = token;
save(); 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) public void work(Activity a)
{ {
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult -> FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult ->
@@ -95,32 +190,62 @@ public class SCNSettings
{ {
if (isConnected()) ServerCommunication.info(user_id, user_key, null); if (isConnected()) ServerCommunication.info(user_id, user_key, null);
}); });
updateProState(null);
} }
// reset account key
public void reset(View loader) public void reset(View loader)
{ {
if (!isConnected()) return; if (!isConnected()) return;
ServerCommunication.update(user_id, user_key, loader); ServerCommunication.resetSecret(user_id, user_key, loader);
} }
// refresh account data
public void refresh(View loader, Activity a) public void refresh(View loader, Activity a)
{ {
if (isConnected()) if (isConnected())
{ {
ServerCommunication.info(user_id, user_key, loader); ServerCommunication.info(user_id, user_key, loader);
if (promode_server != promode_local) updateProState(loader);
if (!Str.equals(fcm_token_local, fcm_token_server)) work(a);
} }
else else
{ {
// get token then register
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult -> FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult ->
{ {
String newToken = instanceIdResult.getToken(); String newToken = instanceIdResult.getToken();
Log.e("FB::GetInstanceId", newToken); Log.e("FB::GetInstanceId", newToken);
SCNSettings.inst().setServerToken(newToken, loader); SCNSettings.inst().setServerToken(newToken, loader); // does register in here
}).addOnCompleteListener(r -> }).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

@@ -4,11 +4,16 @@ import android.util.Log;
import android.view.View; import android.view.View;
import com.blackforestbytes.simplecloudnotifier.SCNApp; import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.service.FBMService;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONTokener; import org.json.JSONTokener;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Callback; import okhttp3.Callback;
@@ -19,18 +24,18 @@ import okhttp3.ResponseBody;
public class ServerCommunication public class ServerCommunication
{ {
public static final String BASE_URL = "https://scn.blackforestbytes.com/"; public static final String BASE_URL = /*SCNApp.LOCAL_DEBUG ? "http://localhost:1010/" : */"https://scn.blackforestbytes.com/api/";
private static final OkHttpClient client = new OkHttpClient(); private static final OkHttpClient client = new OkHttpClient();
private ServerCommunication(){ throw new Error("no."); } 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 try
{ {
Request request = new Request.Builder() 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(); .build();
client.newCall(request).enqueue(new Callback() client.newCall(request).enqueue(new Callback()
@@ -38,7 +43,7 @@ public class ServerCommunication
@Override @Override
public void onFailure(Call call, IOException e) public void onFailure(Call call, IOException e)
{ {
e.printStackTrace(); Log.e("SC:register", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); }); SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
} }
@@ -56,24 +61,25 @@ public class ServerCommunication
JSONObject json = (JSONObject) new JSONTokener(r).nextValue(); JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json.getBoolean("success")) if (!json_bool(json, "success"))
{ {
SCNApp.showToast(json.getString("message"), 4000); SCNApp.showToast(json_str(json, "message"), 4000);
return; return;
} }
SCNSettings.inst().user_id = json.getInt("user_id"); SCNSettings.inst().user_id = json_int(json, "user_id");
SCNSettings.inst().user_key = json.getString("user_key"); SCNSettings.inst().user_key = json_str(json, "user_key");
SCNSettings.inst().fcm_token_server = token; SCNSettings.inst().fcm_token_server = token;
SCNSettings.inst().quota_curr = json.getInt("quota"); SCNSettings.inst().quota_curr = json_int(json, "quota");
SCNSettings.inst().quota_max = json.getInt("quota_max"); SCNSettings.inst().quota_max = json_int(json, "quota_max");
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
SCNSettings.inst().save(); SCNSettings.inst().save();
SCNApp.refreshAccountTab(); SCNApp.refreshAccountTab();
} }
catch (Exception e) catch (Exception e)
{ {
e.printStackTrace(); Log.e("SC:register", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
} }
finally finally
@@ -85,17 +91,17 @@ public class ServerCommunication
} }
catch (Exception e) catch (Exception e)
{ {
e.printStackTrace(); Log.e("SC:register", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
} }
} }
public static void update(int id, String key, String token, View loader) public static void updateFCMToken(int id, String key, String token, View loader)
{ {
try try
{ {
Request request = new Request.Builder() Request request = new Request.Builder()
.url(BASE_URL + "update.php?user_id="+id+"&user_key="+key+"&fcm_token="+token) .url(BASE_URL + "updateFCMToken.php?user_id="+id+"&user_key="+key+"&fcm_token="+token)
.build(); .build();
client.newCall(request).enqueue(new Callback() client.newCall(request).enqueue(new Callback()
@@ -103,7 +109,7 @@ public class ServerCommunication
@Override @Override
public void onFailure(Call call, IOException e) public void onFailure(Call call, IOException e)
{ {
e.printStackTrace(); Log.e("SC:update_1", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); }); SCNApp.runOnUiThread(() -> { if (loader!=null)loader.setVisibility(View.GONE); });
} }
@@ -121,24 +127,25 @@ public class ServerCommunication
JSONObject json = (JSONObject) new JSONTokener(r).nextValue(); JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json.getBoolean("success")) if (!json_bool(json, "success"))
{ {
SCNApp.showToast(json.getString("message"), 4000); SCNApp.showToast(json_str(json, "message"), 4000);
return; return;
} }
SCNSettings.inst().user_id = json.getInt("user_id"); SCNSettings.inst().user_id = json_int(json, "user_id");
SCNSettings.inst().user_key = json.getString("user_key"); SCNSettings.inst().user_key = json_str(json, "user_key");
SCNSettings.inst().fcm_token_server = token; SCNSettings.inst().fcm_token_server = token;
SCNSettings.inst().quota_curr = json.getInt("quota"); SCNSettings.inst().quota_curr = json_int(json, "quota");
SCNSettings.inst().quota_max = json.getInt("quota_max"); SCNSettings.inst().quota_max = json_int(json, "quota_max");
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
SCNSettings.inst().save(); SCNSettings.inst().save();
SCNApp.refreshAccountTab(); SCNApp.refreshAccountTab();
} }
catch (Exception e) catch (Exception e)
{ {
e.printStackTrace(); Log.e("SC:update_1", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
} }
finally finally
@@ -150,23 +157,23 @@ public class ServerCommunication
} }
catch (Exception e) catch (Exception e)
{ {
e.printStackTrace(); Log.e("SC:update_1", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
} }
} }
public static void update(int id, String key, View loader) public static void resetSecret(int id, String key, View loader)
{ {
try try
{ {
Request request = new Request.Builder() Request request = new Request.Builder()
.url(BASE_URL + "update.php?user_id=" + id + "&user_key=" + key) .url(BASE_URL + "updateFCMToken.php?user_id=" + id + "&user_key=" + key)
.build(); .build();
client.newCall(request).enqueue(new Callback() { client.newCall(request).enqueue(new Callback() {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(Call call, IOException e) {
e.printStackTrace(); Log.e("SC:update_2", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
} }
@@ -182,20 +189,21 @@ public class ServerCommunication
JSONObject json = (JSONObject) new JSONTokener(r).nextValue(); JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json.getBoolean("success")) { if (!json_bool(json, "success")) {
SCNApp.showToast(json.getString("message"), 4000); SCNApp.showToast(json_str(json, "message"), 4000);
return; return;
} }
SCNSettings.inst().user_id = json.getInt("user_id"); SCNSettings.inst().user_id = json_int(json, "user_id");
SCNSettings.inst().user_key = json.getString("user_key"); SCNSettings.inst().user_key = json_str(json, "user_key");
SCNSettings.inst().quota_curr = json.getInt("quota"); SCNSettings.inst().quota_curr = json_int(json, "quota");
SCNSettings.inst().quota_max = json.getInt("quota_max"); SCNSettings.inst().quota_max = json_int(json, "quota_max");
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
SCNSettings.inst().save(); SCNSettings.inst().save();
SCNApp.refreshAccountTab(); SCNApp.refreshAccountTab();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); Log.e("SC:update_2", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
} finally { } finally {
SCNApp.runOnUiThread(() -> { SCNApp.runOnUiThread(() -> {
@@ -207,7 +215,7 @@ public class ServerCommunication
} }
catch (Exception e) catch (Exception e)
{ {
e.printStackTrace(); Log.e("SC:update_2", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
} }
} }
@@ -223,7 +231,7 @@ public class ServerCommunication
client.newCall(request).enqueue(new Callback() { client.newCall(request).enqueue(new Callback() {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(Call call, IOException e) {
e.printStackTrace(); Log.e("SC:info", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
SCNApp.runOnUiThread(() -> { SCNApp.runOnUiThread(() -> {
if (loader != null) loader.setVisibility(View.GONE); if (loader != null) loader.setVisibility(View.GONE);
@@ -242,19 +250,46 @@ public class ServerCommunication
JSONObject json = (JSONObject) new JSONTokener(r).nextValue(); JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json.getBoolean("success")) { if (!json_bool(json, "success"))
SCNApp.showToast(json.getString("message"), 4000); {
SCNApp.showToast(json_str(json, "message"), 4000);
int errid = json.optInt("errid", 0);
if (errid == 201 || errid == 202 || errid == 203 || errid == 204)
{
// user not found or auth failed
SCNSettings.inst().user_id = -1;
SCNSettings.inst().user_key = "";
SCNSettings.inst().quota_curr = 0;
SCNSettings.inst().quota_max = 0;
SCNSettings.inst().promode_server = false;
SCNSettings.inst().fcm_token_server = "";
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
}
return; return;
} }
SCNSettings.inst().user_id = json.getInt("user_id"); SCNSettings.inst().user_id = json_int(json, "user_id");
SCNSettings.inst().quota_curr = json.getInt("quota"); SCNSettings.inst().quota_curr = json_int(json, "quota");
SCNSettings.inst().quota_max = json.getInt("quota_max"); SCNSettings.inst().quota_max = json_int(json, "quota_max");
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
if (!json_bool(json, "fcm_token_set")) SCNSettings.inst().fcm_token_server = "";
SCNSettings.inst().save(); SCNSettings.inst().save();
SCNApp.refreshAccountTab(); SCNApp.refreshAccountTab();
if (json_int(json, "unack_count")>0)
{
ServerCommunication.requery(id, key, loader);
}
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); Log.e("SC:info", e.toString());
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
} finally { } finally {
SCNApp.runOnUiThread(() -> { SCNApp.runOnUiThread(() -> {
@@ -265,9 +300,216 @@ public class ServerCommunication
}); });
} }
catch (Exception e) catch (Exception e)
{
Log.e("SC:info", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
}
}
public static void requery(int id, String key, View loader)
{
try
{
Request request = new Request.Builder()
.url(BASE_URL + "requery.php?user_id=" + id + "&user_key=" + key)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("SC:requery", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
SCNApp.runOnUiThread(() -> {
if (loader != null) loader.setVisibility(View.GONE);
});
}
@Override
public void onResponse(Call call, Response response) {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success"))
{
SCNApp.showToast(json_str(json, "message"), 4000);
return;
}
int count = json_int(json, "count");
JSONArray arr = json.getJSONArray("data");
for (int i = 0; i < count; i++)
{
JSONObject o = arr.getJSONObject(0);
long time = json_lng(o, "timestamp");
String title = json_str(o, "title");
String content = json_str(o, "body");
PriorityEnum prio = PriorityEnum.parseAPI(json_int(o, "priority"));
long scn_id = json_lng(o, "scn_msg_id");
FBMService.recieveData(time, title, content, prio, scn_id, true);
}
SCNSettings.inst().user_id = json_int(json, "user_id");
SCNSettings.inst().quota_curr = json_int(json, "quota");
SCNSettings.inst().quota_max = json_int(json, "quota_max");
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
if (!json_bool(json, "fcm_token_set")) SCNSettings.inst().fcm_token_server = "";
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
if (json_int(json, "unack_count")>0)
{
ServerCommunication.requery(id, key, loader);
}
} catch (Exception e) {
Log.e("SC:info", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
} finally {
SCNApp.runOnUiThread(() -> {
if (loader != null) loader.setVisibility(View.GONE);
});
}
}
});
}
catch (Exception e)
{
Log.e("SC:requery", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
}
}
public static void upgrade(int id, String key, View loader, boolean pro, String pro_token)
{
try
{
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
Request request = new Request.Builder()
.url(BASE_URL + "upgrade.php?user_id=" + id + "&user_key=" + key + "&pro=" + pro + "&pro_token=" + URLEncoder.encode(pro_token, "utf-8"))
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("SC:upgrade", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
}
@Override
public void onResponse(Call call, Response response) {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success")) {
SCNApp.showToast(json_str(json, "message"), 4000);
return;
}
SCNSettings.inst().user_id = json_int(json, "user_id");
SCNSettings.inst().quota_curr = json_int(json, "quota");
SCNSettings.inst().quota_max = json_int(json, "quota_max");
SCNSettings.inst().promode_server = json_bool(json, "is_pro");
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
} catch (Exception e) {
Log.e("SC:upgrade", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
} finally {
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
}
}
});
}
catch (Exception e)
{ {
e.printStackTrace(); e.printStackTrace();
SCNApp.showToast("Communication with server failed", 4000); SCNApp.showToast("Communication with server failed", 4000);
} }
} }
public static void ack(int id, String key, CMessage msg)
{
try
{
Request request = new Request.Builder()
.url(BASE_URL + "ack.php?user_id=" + id + "&user_key=" + key + "&scn_msg_id=" + msg.SCN_ID)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("SC:ack", e.toString());
}
@Override
public void onResponse(Call call, Response response)
{
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json_bool(json, "success")) SCNApp.showToast(json_str(json, "message"), 4000);
} catch (Exception e) {
Log.e("SC:ack", e.toString());
SCNApp.showToast("Communication with server failed", 4000);
}
}
});
}
catch (Exception e)
{
Log.e("SC:ack", e.toString());
}
}
private static boolean json_bool(JSONObject o, String key) throws JSONException
{
Object v = o.get(key);
if (v instanceof Integer) return ((int)v) != 0;
if (v instanceof Boolean) return ((boolean)v);
if (v instanceof String) return !Str.equals(((String)v), "0") && !Str.equals(((String)v), "false");
return o.getBoolean(key);
}
private static int json_int(JSONObject o, String key) throws JSONException
{
return o.getInt(key);
}
private static long json_lng(JSONObject o, String key) throws JSONException
{
return o.getLong(key);
}
private static String json_str(JSONObject o, String key) throws JSONException
{
return o.getString(key);
}
} }

View File

@@ -0,0 +1,42 @@
package com.blackforestbytes.simplecloudnotifier.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
public class BroadcastReceiverService extends BroadcastReceiver
{
public static final int NOTIF_SHOW_MAIN = 10021;
public static final int NOTIF_STOP_SOUND = 10022;
public static final String ID_KEY = "com.blackforestbytes.simplecloudnotifier.BroadcastID";
@Override
public void onReceive(Context context, Intent intent)
{
if (intent == null) return;
Bundle extras = intent.getExtras();
if (extras == null) return;
int notificationId = extras.getInt(ID_KEY, 0);
if (notificationId == 0) return;
else if (notificationId == NOTIF_SHOW_MAIN) showMain(context);
else if (notificationId == NOTIF_STOP_SOUND) stopNotificationSound();
else return;
}
private void stopNotificationSound()
{
SoundService.stop();
}
private void showMain(Context ctxt)
{
SoundService.stop();
Intent intent = new Intent(ctxt, MainActivity.class);
ctxt.startActivity(intent);
}
}

View File

@@ -1,20 +1,14 @@
package com.blackforestbytes.simplecloudnotifier.service; package com.blackforestbytes.simplecloudnotifier.service;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp; import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.model.CMessage; import com.blackforestbytes.simplecloudnotifier.model.CMessage;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList; import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum; import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings; import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.view.MainActivity; import com.blackforestbytes.simplecloudnotifier.model.ServerCommunication;
import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage; import com.google.firebase.messaging.RemoteMessage;
@@ -32,6 +26,8 @@ public class FBMService extends FirebaseMessagingService
{ {
try try
{ {
if (!SCNSettings.inst().Enabled) return;
Log.i("FB::MessageReceived", "From: " + remoteMessage.getFrom()); Log.i("FB::MessageReceived", "From: " + remoteMessage.getFrom());
Log.i("FB::MessageReceived", "Payload: " + remoteMessage.getData()); Log.i("FB::MessageReceived", "Payload: " + remoteMessage.getData());
if (remoteMessage.getNotification() != null) Log.i("FB::MessageReceived", "Notify_Title: " + remoteMessage.getNotification().getTitle()); if (remoteMessage.getNotification() != null) Log.i("FB::MessageReceived", "Notify_Title: " + remoteMessage.getNotification().getTitle());
@@ -41,18 +37,9 @@ public class FBMService extends FirebaseMessagingService
String title = remoteMessage.getData().get("title"); String title = remoteMessage.getData().get("title");
String content = remoteMessage.getData().get("body"); String content = remoteMessage.getData().get("body");
PriorityEnum prio = PriorityEnum.parseAPI(remoteMessage.getData().get("priority")); PriorityEnum prio = PriorityEnum.parseAPI(remoteMessage.getData().get("priority"));
long scn_id = Long.parseLong(remoteMessage.getData().get("scn_msg_id"));
CMessage msg = CMessageList.inst().add(time, title, content, prio); recieveData(time, title, content, prio, scn_id, false);
if (SCNApp.isBackground())
{
NotificationService.inst().show(msg);
}
else
{
SCNApp.showToast("Message recieved: " + title, Toast.LENGTH_LONG);
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -60,4 +47,27 @@ public class FBMService extends FirebaseMessagingService
SCNApp.showToast("Recieved invalid message from server", Toast.LENGTH_LONG); SCNApp.showToast("Recieved invalid message from server", Toast.LENGTH_LONG);
} }
} }
public static void recieveData(long time, String title, String content, PriorityEnum prio, long scn_id, boolean alwaysAck)
{
CMessage msg = CMessageList.inst().add(scn_id, time, title, content, prio);
if (CMessageList.inst().isAck(scn_id))
{
Log.w("FB::MessageReceived", "Recieved ack-ed message: " + scn_id);
if (alwaysAck) ServerCommunication.ack(SCNSettings.inst().user_id, SCNSettings.inst().user_key, msg);
return;
}
if (SCNApp.isBackground())
{
NotificationService.inst().showBackground(msg);
}
else
{
NotificationService.inst().showForeground(msg);
}
ServerCommunication.ack(SCNSettings.inst().user_id, SCNSettings.inst().user_key, msg);
}
} }

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

@@ -1,22 +1,36 @@
package com.blackforestbytes.simplecloudnotifier.service; package com.blackforestbytes.simplecloudnotifier.service;
import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.support.v4.app.NotificationCompat; import android.os.VibrationEffect;
import android.os.Vibrator;
import android.widget.Toast;
import com.blackforestbytes.simplecloudnotifier.R; import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp; import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.model.CMessage; import com.blackforestbytes.simplecloudnotifier.model.CMessage;
import com.blackforestbytes.simplecloudnotifier.model.NotificationSettings;
import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.view.MainActivity; import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
public class NotificationService public class NotificationService
{ {
private final static String CHANNEL_ID = "CHAN_BFB_SCN_MESSAGES"; private final static String CHANNEL_P0_ID = "CHAN_BFB_SCN_MESSAGES_P0";
private final static String CHANNEL_P1_ID = "CHAN_BFB_SCN_MESSAGES_P1";
private final static String CHANNEL_P2_ID = "CHAN_BFB_SCN_MESSAGES_P2";
private final static Object _lock = new Object(); private final static Object _lock = new Object();
private static NotificationService _inst = null; private static NotificationService _inst = null;
@@ -31,39 +45,212 @@ public class NotificationService
private NotificationService() private NotificationService()
{ {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannels();
{
Context ctxt = SCNApp.getContext();
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Push notifications", NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("Messages from the API");
channel.setLightColor(Color.rgb(255, 0, 0));
channel.setVibrationPattern(new long[]{200});
channel.enableLights(true);
channel.enableVibration(true);
NotificationManager notificationManager = ctxt.getSystemService(NotificationManager.class);
if (notificationManager != null) notificationManager.createNotificationChannel(channel);
}
} }
public void show(CMessage msg) private void createChannels()
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
Context ctxt = SCNApp.getContext();
NotificationManager notifman = ctxt.getSystemService(NotificationManager.class);
if (notifman == null) return;
{
NotificationChannel channel0 = notifman.getNotificationChannel(CHANNEL_P0_ID);
if (channel0 == null)
{
channel0 = new NotificationChannel(CHANNEL_P0_ID, "Push notifications (low priority)", NotificationManager.IMPORTANCE_DEFAULT);
channel0.setDescription("Push notifications from the server with low priority.\nGo to the in-app settings to configure ringtone, volume and vibrations");
channel0.setSound(null, null);
channel0.setVibrationPattern(null);
channel0.setLightColor(Color.BLUE);
notifman.createNotificationChannel(channel0);
}
}
{
NotificationChannel channel1 = notifman.getNotificationChannel(CHANNEL_P1_ID);
if (channel1 == null)
{
channel1 = new NotificationChannel(CHANNEL_P1_ID, "Push notifications (normal priority)", NotificationManager.IMPORTANCE_DEFAULT);
channel1.setDescription("Push notifications from the server with low priority.\nGo to the in-app settings to configure ringtone, volume and vibrations");
channel1.setSound(null, null);
channel1.setVibrationPattern(null);
channel1.setLightColor(Color.BLUE);
notifman.createNotificationChannel(channel1);
}
}
{
NotificationChannel channel2 = notifman.getNotificationChannel(CHANNEL_P2_ID);
if (channel2 == null)
{
channel2 = new NotificationChannel(CHANNEL_P1_ID, "Push notifications (high priority)", NotificationManager.IMPORTANCE_DEFAULT);
channel2.setDescription("Push notifications from the server with low priority.\nGo to the in-app settings to configure ringtone, volume and vibrations");
channel2.setSound(null, null);
channel2.setVibrationPattern(null);
channel2.setLightColor(Color.BLUE);
notifman.createNotificationChannel(channel2);
}
}
}
public void showForeground(CMessage msg)
{
SCNApp.showToast("Message recieved: " + msg.Title, Toast.LENGTH_LONG);
try
{
NotificationSettings ns = SCNSettings.inst().PriorityNorm;
switch (msg.Priority)
{
case LOW: ns = SCNSettings.inst().PriorityLow; break;
case NORMAL: ns = SCNSettings.inst().PriorityNorm; break;
case HIGH: ns = SCNSettings.inst().PriorityHigh; break;
}
SoundService.play(ns.EnableSound, ns.SoundSource, ns.ForceVolume, ns.ForceVolumeValue, false);
if (ns.EnableVibration)
{
Vibrator v = (Vibrator) SCNApp.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
v.vibrate(VibrationEffect.createOneShot(1500, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
v.vibrate(1500);
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
public void showBackground(CMessage msg)
{ {
Context ctxt = SCNApp.getContext(); Context ctxt = SCNApp.getContext();
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, CHANNEL_ID) NotificationSettings ns = SCNSettings.inst().PriorityNorm;
.setSmallIcon(R.drawable.ic_bfb) switch (msg.Priority)
.setContentTitle(msg.Title) {
.setContentText(msg.Content) case LOW: ns = SCNSettings.inst().PriorityLow; break;
.setShowWhen(true) case NORMAL: ns = SCNSettings.inst().PriorityNorm; break;
.setWhen(msg.Timestamp) case HIGH: ns = SCNSettings.inst().PriorityHigh; break;
.setPriority(NotificationCompat.PRIORITY_HIGH) }
.setAutoCancel(true);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
{
// old
showBackground_old(msg, ctxt, ns, msg.Priority);
}
else
{
// new
showBackground_new(msg, ctxt, ns, msg.Priority);
}
}
private String getChannel(PriorityEnum p)
{
switch (p)
{
case LOW: return CHANNEL_P0_ID;
case NORMAL: return CHANNEL_P1_ID;
case HIGH: return CHANNEL_P2_ID;
default: return CHANNEL_P0_ID;
}
}
private void showBackground_old(CMessage msg, Context ctxt, NotificationSettings ns, PriorityEnum prio)
{
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, getChannel(prio));
mBuilder.setSmallIcon(R.drawable.ic_bfb);
mBuilder.setContentTitle(msg.Title);
mBuilder.setContentText(msg.Content);
mBuilder.setShowWhen(true);
mBuilder.setWhen(msg.Timestamp * 1000);
mBuilder.setAutoCancel(true);
if (msg.Priority == PriorityEnum.LOW) mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
if (ns.EnableVibration) mBuilder.setVibrate(new long[]{500});
if (ns.EnableLED) mBuilder.setLights(ns.LEDColor, 500, 500);
if (ns.EnableSound && !ns.SoundSource.isEmpty() && !ns.RepeatSound) mBuilder.setSound(Uri.parse(ns.SoundSource), AudioManager.STREAM_NOTIFICATION);
Intent intent = new Intent(ctxt, MainActivity.class); Intent intent = new Intent(ctxt, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0); PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0);
mBuilder.setContentIntent(pi); mBuilder.setContentIntent(pi);
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE); NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
if (mNotificationManager != null) mNotificationManager.notify(0, mBuilder.build());
if (ns.EnableSound && !ns.SoundSource.isEmpty() && ns.RepeatSound)
{
Intent intnt_stop = new Intent(SCNApp.getContext(), BroadcastReceiverService.class);
intnt_stop.putExtra(BroadcastReceiverService.ID_KEY, BroadcastReceiverService.NOTIF_STOP_SOUND);
PendingIntent pi_stop = PendingIntent.getBroadcast(SCNApp.getContext().getApplicationContext(), BroadcastReceiverService.NOTIF_STOP_SOUND, intnt_stop, 0);
mBuilder.addAction(new NotificationCompat.Action(-1, "Stop", pi_stop));
mBuilder.setDeleteIntent(pi_stop);
SoundService.play(ns.EnableSound, ns.SoundSource, ns.ForceVolume, ns.ForceVolumeValue, ns.RepeatSound);
}
Notification n = mBuilder.build();
if (mNotificationManager != null) mNotificationManager.notify(0, n);
} }
@RequiresApi(api = Build.VERSION_CODES.O)
private void showBackground_new(CMessage msg, Context ctxt, NotificationSettings ns, PriorityEnum prio)
{
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, getChannel(prio));
mBuilder.setSmallIcon(R.drawable.ic_bfb);
mBuilder.setContentTitle(msg.Title);
mBuilder.setContentText(msg.Content);
mBuilder.setShowWhen(true);
mBuilder.setWhen(msg.Timestamp * 1000);
mBuilder.setAutoCancel(true);
if (ns.EnableLED) mBuilder.setLights(ns.LEDColor, 500, 500);
if (msg.Priority == PriorityEnum.LOW) mBuilder.setPriority(NotificationCompat.PRIORITY_LOW);
if (msg.Priority == PriorityEnum.NORMAL) mBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT);
if (msg.Priority == PriorityEnum.HIGH) mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
Intent intnt_click = new Intent(SCNApp.getContext(), BroadcastReceiverService.class);
intnt_click.putExtra(BroadcastReceiverService.ID_KEY, BroadcastReceiverService.NOTIF_SHOW_MAIN);
PendingIntent pi = PendingIntent.getBroadcast(ctxt, 0, intnt_click, 0);
mBuilder.setContentIntent(pi);
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
if (mNotificationManager == null) return;
if (ns.EnableSound && !Str.isNullOrWhitespace(ns.SoundSource))
{
if (ns.RepeatSound)
{
Intent intnt_stop = new Intent(SCNApp.getContext(), BroadcastReceiverService.class);
intnt_stop.putExtra(BroadcastReceiverService.ID_KEY, BroadcastReceiverService.NOTIF_STOP_SOUND);
PendingIntent pi_stop = PendingIntent.getBroadcast(ctxt, BroadcastReceiverService.NOTIF_STOP_SOUND, intnt_stop, 0);
mBuilder.addAction(new NotificationCompat.Action(-1, "Stop", pi_stop));
mBuilder.setDeleteIntent(pi_stop);
}
SoundService.play(ns.EnableSound, ns.SoundSource, ns.ForceVolume, ns.ForceVolumeValue, ns.RepeatSound);
}
Notification n = mBuilder.build();
n.flags |= Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(0, n);
if (ns.EnableVibration)
{
Vibrator v = (Vibrator) SCNApp.getContext().getSystemService(Context.VIBRATOR_SERVICE);
v.vibrate(VibrationEffect.createOneShot(1500, VibrationEffect.DEFAULT_AMPLITUDE));
}
//if (ns.EnableLED) { } // no LED in Android-O -- configure via Channel
}
} }

View File

@@ -0,0 +1,56 @@
package com.blackforestbytes.simplecloudnotifier.service;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.util.Log;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import java.io.IOException;
public class SoundService
{
private static MediaPlayer mpLast = null;
public static void play(boolean enableSound, String soundSource, boolean forceVolume, int forceVolumeValue, boolean loop)
{
if (!enableSound) return;
if (Str.isNullOrWhitespace(soundSource)) return;
stop();
if (forceVolume)
{
AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE);
int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
aman.setStreamVolume(AudioManager.STREAM_NOTIFICATION, (int)(maxVolume * (forceVolumeValue / 100.0)), 0);
}
try
{
MediaPlayer player = new MediaPlayer();
player.setAudioAttributes(new AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_NOTIFICATION).build());
player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
player.setDataSource(SCNApp.getContext(), Uri.parse(soundSource));
player.setLooping(loop);
player.setOnCompletionListener( mp -> { mp.stop(); mp.release(); });
player.setOnSeekCompleteListener(mp -> { mp.stop(); mp.release(); });
player.prepare();
player.start();
mpLast = player;
}
catch (IOException e)
{
Log.e("Sound::play", e.toString());
}
}
public static void stop()
{
if (mpLast != null && mpLast.isPlaying()) { mpLast.stop(); mpLast.release(); mpLast = null; }
}
}

View File

@@ -0,0 +1,77 @@
package com.blackforestbytes.simplecloudnotifier.util;
import android.graphics.Canvas;
import android.view.View;
import com.blackforestbytes.simplecloudnotifier.view.MessageAdapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
public class MessageAdapterTouchHelper extends ItemTouchHelper.SimpleCallback
{
private MessageAdapterTouchHelperListener listener;
public MessageAdapterTouchHelper(int dragDirs, int swipeDirs, MessageAdapterTouchHelperListener listener)
{
super(dragDirs, swipeDirs);
this.listener = listener;
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target)
{
return true;
}
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)
{
if (viewHolder != null)
{
final View foregroundView = ((MessageAdapter.MessagePresenter) viewHolder).viewForeground;
getDefaultUIUtil().onSelected(foregroundView);
}
}
@Override
public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)
{
final View foregroundView = ((MessageAdapter.MessagePresenter) viewHolder).viewForeground;
getDefaultUIUtil().onDrawOver(c, recyclerView, foregroundView, dX, dY, actionState, isCurrentlyActive);
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder)
{
final View foregroundView = ((MessageAdapter.MessagePresenter) viewHolder).viewForeground;
getDefaultUIUtil().clearView(foregroundView);
}
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)
{
final View foregroundView = ((MessageAdapter.MessagePresenter) viewHolder).viewForeground;
getDefaultUIUtil().onDraw(c, recyclerView, foregroundView, dX, dY, actionState, isCurrentlyActive);
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction)
{
listener.onSwiped(viewHolder, direction, viewHolder.getAdapterPosition());
}
@Override
public int convertToAbsoluteDirection(int flags, int layoutDirection)
{
return super.convertToAbsoluteDirection(flags, layoutDirection);
}
public interface MessageAdapterTouchHelperListener
{
void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position);
}
}

View File

@@ -1,13 +1,13 @@
package com.blackforestbytes.simplecloudnotifier.view; package com.blackforestbytes.simplecloudnotifier.view;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -22,6 +22,10 @@ import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import net.glxn.qrgen.android.QRCode; import net.glxn.qrgen.android.QRCode;
import net.glxn.qrgen.core.image.ImageType; import net.glxn.qrgen.core.image.ImageType;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import static android.content.Context.CLIPBOARD_SERVICE; import static android.content.Context.CLIPBOARD_SERVICE;
public class AccountFragment extends Fragment public class AccountFragment extends Fragment
@@ -54,15 +58,47 @@ public class AccountFragment extends Fragment
v.findViewById(R.id.btnAccountReset).setOnClickListener(cv -> v.findViewById(R.id.btnAccountReset).setOnClickListener(cv ->
{ {
View lpnl = v.findViewById(R.id.loadingPanel); Activity a = getActivity();
lpnl.setVisibility(View.VISIBLE); if (a == null) return;
SCNSettings.inst().reset(lpnl);
AlertDialog.Builder builder = new AlertDialog.Builder(a);
builder.setTitle("Confirm");
builder.setMessage("Reset account key?");
builder.setPositiveButton("YES", (dialog, which) -> {
View lpnl = v.findViewById(R.id.loadingPanel);
lpnl.setVisibility(View.VISIBLE);
SCNSettings.inst().reset(lpnl);
dialog.dismiss();
});
builder.setNegativeButton("NO", (dialog, which) -> dialog.dismiss());
AlertDialog alert = builder.create();
alert.show();
}); });
v.findViewById(R.id.btnClearLocalStorage).setOnClickListener(cv -> v.findViewById(R.id.btnClearLocalStorage).setOnClickListener(cv ->
{ {
CMessageList.inst().clear(); Activity a = getActivity();
SCNApp.showToast("Notifications cleared", 1000); if (a == null) return;
AlertDialog.Builder builder = new AlertDialog.Builder(a);
builder.setTitle("Confirm");
builder.setMessage("Clear local messages?");
builder.setPositiveButton("YES", (dialog, which) -> {
CMessageList.inst().clear();
SCNApp.showToast("Notifications cleared", 1000);
dialog.dismiss();
});
builder.setNegativeButton("NO", (dialog, which) -> dialog.dismiss());
AlertDialog alert = builder.create();
alert.show();
}); });
v.findViewById(R.id.btnQR).setOnClickListener(cv -> v.findViewById(R.id.btnQR).setOnClickListener(cv ->

View File

@@ -1,28 +1,25 @@
package com.blackforestbytes.simplecloudnotifier.view; package com.blackforestbytes.simplecloudnotifier.view;
import android.support.design.widget.TabLayout;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager; import android.widget.RelativeLayout;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import com.blackforestbytes.simplecloudnotifier.R; import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp; import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList; import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings; import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.model.ServerCommunication; import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.blackforestbytes.simplecloudnotifier.service.NotificationService; import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
import com.google.firebase.iid.FirebaseInstanceId; import com.google.android.material.tabs.TabLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
public class MainActivity extends AppCompatActivity public class MainActivity extends AppCompatActivity
{ {
public TabAdapter adpTabs; public TabAdapter adpTabs;
public RelativeLayout layoutRoot;
@Override @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
@@ -33,6 +30,8 @@ public class MainActivity extends AppCompatActivity
NotificationService.inst(); NotificationService.inst();
CMessageList.inst(); CMessageList.inst();
layoutRoot = findViewById(R.id.layoutRoot);
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
@@ -44,8 +43,24 @@ public class MainActivity extends AppCompatActivity
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
tabLayout.setupWithViewPager(viewPager); tabLayout.setupWithViewPager(viewPager);
SCNApp.register(this); viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { /* */ }
@Override
public void onPageSelected(int position)
{
if (position != 2) adpTabs.tab3.onViewpagerHide();
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
SCNApp.register(this);
IABService.startup(this);
SCNSettings.inst().work(this); SCNSettings.inst().work(this);
} }
@@ -54,6 +69,16 @@ public class MainActivity extends AppCompatActivity
{ {
super.onStop(); super.onStop();
SCNSettings.inst().save();
CMessageList.inst().fullSave(); CMessageList.inst().fullSave();
} }
@Override
protected void onDestroy()
{
super.onDestroy();
CMessageList.inst().fullSave();
IABService.inst().destroy();
}
} }

View File

@@ -1,26 +1,39 @@
package com.blackforestbytes.simplecloudnotifier.view; package com.blackforestbytes.simplecloudnotifier.view;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import com.blackforestbytes.simplecloudnotifier.R; import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.model.CMessage; import com.blackforestbytes.simplecloudnotifier.model.CMessage;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList; import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class MessageAdapter extends RecyclerView.Adapter public class MessageAdapter extends RecyclerView.Adapter
{ {
private final View vNoElements; private final View vNoElements;
private final LinearLayoutManager manLayout;
private final RecyclerView viewRecycler;
public MessageAdapter(View noElementsView) private WeakHashMap<MessagePresenter, Boolean> viewHolders = new WeakHashMap<>();
public MessageAdapter(View noElementsView, LinearLayoutManager layout, RecyclerView recycler)
{ {
vNoElements = noElementsView; vNoElements = noElementsView;
manLayout = layout;
viewRecycler = recycler;
CMessageList.inst().register(this); CMessageList.inst().register(this);
vNoElements.setVisibility(getItemCount()>0 ? View.GONE : View.VISIBLE); vNoElements.setVisibility(getItemCount()>0 ? View.GONE : View.VISIBLE);
@@ -37,9 +50,18 @@ public class MessageAdapter extends RecyclerView.Adapter
@Override @Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position)
{ {
CMessage msg = CMessageList.inst().tryGet(position); CMessage msg = CMessageList.inst().tryGetFromBack(position);
MessagePresenter view = (MessagePresenter) holder; MessagePresenter view = (MessagePresenter) holder;
view.setMessage(msg); view.setMessage(msg);
viewHolders.put(view, true);
}
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder)
{
if (holder instanceof MessagePresenter) viewHolders.remove(holder);
} }
@Override @Override
@@ -60,23 +82,51 @@ public class MessageAdapter extends RecyclerView.Adapter
vNoElements.setVisibility(getItemCount()>0 ? View.GONE : View.VISIBLE); vNoElements.setVisibility(getItemCount()>0 ? View.GONE : View.VISIBLE);
} }
private class MessagePresenter extends RecyclerView.ViewHolder implements View.OnClickListener public void scrollToTop()
{
manLayout.smoothScrollToPosition(viewRecycler, null, 0);
}
public void removeItem(int position)
{
CMessageList.inst().remove(position);
notifyItemRemoved(position);
}
public void restoreItem(CMessage item, int position)
{
CMessageList.inst().insert(position, item);
notifyItemInserted(position);
}
public class MessagePresenter extends RecyclerView.ViewHolder implements View.OnClickListener
{ {
private TextView tvTimestamp; private TextView tvTimestamp;
private TextView tvTitle; private TextView tvTitle;
private TextView tvMessage; private TextView tvMessage;
private ImageView ivPriority; private ImageView ivPriority;
public RelativeLayout viewForeground;
public RelativeLayout viewBackground;
private CMessage data; private CMessage data;
MessagePresenter(View itemView) MessagePresenter(View itemView)
{ {
super(itemView); super(itemView);
tvTimestamp = itemView.findViewById(R.id.tvTimestamp); tvTimestamp = itemView.findViewById(R.id.tvTimestamp);
tvTitle = itemView.findViewById(R.id.tvTitle); tvTitle = itemView.findViewById(R.id.tvTitle);
tvMessage = itemView.findViewById(R.id.tvMessage); tvMessage = itemView.findViewById(R.id.tvMessage);
ivPriority = itemView.findViewById(R.id.ivPriority); ivPriority = itemView.findViewById(R.id.ivPriority);
viewForeground = itemView.findViewById(R.id.layoutFront);
viewBackground = itemView.findViewById(R.id.layoutBack);
itemView.setOnClickListener(this); itemView.setOnClickListener(this);
tvTimestamp.setOnClickListener(this);
tvTitle.setOnClickListener(this);
tvMessage.setOnClickListener(this);
ivPriority.setOnClickListener(this);
viewForeground.setOnClickListener(this);
} }
void setMessage(CMessage msg) void setMessage(CMessage msg)
@@ -106,7 +156,16 @@ public class MessageAdapter extends RecyclerView.Adapter
@Override @Override
public void onClick(View v) public void onClick(View v)
{ {
//SCNApp.showToast(data.Title, Toast.LENGTH_LONG); for (MessagePresenter holder : MessageAdapter.this.viewHolders.keySet())
{
if (holder == null) continue;
if (holder == this) continue;
if (holder.tvMessage == null) continue;
if (holder.tvMessage.getMaxLines() == 6) continue;
holder.tvMessage.setMaxLines(6);
}
tvMessage.setMaxLines(9999);
} }
} }
} }

View File

@@ -1,20 +1,33 @@
package com.blackforestbytes.simplecloudnotifier.view; package com.blackforestbytes.simplecloudnotifier.view;
import android.content.Context; import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.blackforestbytes.simplecloudnotifier.R; import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.blackforestbytes.simplecloudnotifier.util.MessageAdapterTouchHelper;
import com.google.android.gms.ads.doubleclick.PublisherAdRequest;
import com.google.android.gms.ads.doubleclick.PublisherAdView;
import com.google.android.material.snackbar.Snackbar;
public class NotificationsFragment extends Fragment import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class NotificationsFragment extends Fragment implements MessageAdapterTouchHelper.MessageAdapterTouchHelperListener
{ {
private PublisherAdView adView;
private MessageAdapter adpMessages;
public NotificationsFragment() public NotificationsFragment()
{ {
// Required empty public constructor // Required empty public constructor
@@ -26,9 +39,43 @@ public class NotificationsFragment extends Fragment
View v = inflater.inflate(R.layout.fragment_notifications, container, false); View v = inflater.inflate(R.layout.fragment_notifications, container, false);
RecyclerView rvMessages = v.findViewById(R.id.rvMessages); RecyclerView rvMessages = v.findViewById(R.id.rvMessages);
rvMessages.setLayoutManager(new LinearLayoutManager(this.getContext(), RecyclerView.VERTICAL, true)); LinearLayoutManager lman = new LinearLayoutManager(this.getContext(), RecyclerView.VERTICAL, false);
rvMessages.setAdapter(new MessageAdapter(v.findViewById(R.id.tvNoElements))); rvMessages.setLayoutManager(lman);
rvMessages.setAdapter(adpMessages = new MessageAdapter(v.findViewById(R.id.tvNoElements), lman, rvMessages));
ItemTouchHelper.SimpleCallback itemTouchHelperCallback = new MessageAdapterTouchHelper(0, ItemTouchHelper.LEFT, this);
new ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(rvMessages);
adView = v.findViewById(R.id.adBanner);
PublisherAdRequest adRequest = new PublisherAdRequest.Builder().build();
adView.loadAd(adRequest);
adView.setVisibility(SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
return v; return v;
} }
public void updateProState()
{
if (adView != null) adView.setVisibility(IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE) != null ? View.GONE : View.VISIBLE);
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position)
{
if (viewHolder instanceof MessageAdapter.MessagePresenter)
{
final CMessage deletedItem = CMessageList.inst().tryGet(viewHolder.getAdapterPosition());
final int deletedIndex = viewHolder.getAdapterPosition();
String name = deletedItem.Title;
adpMessages.removeItem(viewHolder.getAdapterPosition());
Snackbar snackbar = Snackbar.make(SCNApp.getMainActivity().layoutRoot, name + " removed", Snackbar.LENGTH_LONG);
snackbar.setAction("UNDO", view -> adpMessages.restoreItem(deletedItem, deletedIndex));
snackbar.setActionTextColor(Color.YELLOW);
snackbar.show();
}
}
} }

View File

@@ -1,15 +1,488 @@
package com.blackforestbytes.simplecloudnotifier.view; package com.blackforestbytes.simplecloudnotifier.view;
import android.content.Context;
import android.graphics.Color;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.preference.PreferenceFragmentCompat; import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import com.android.billingclient.api.Purchase;
import com.blackforestbytes.simplecloudnotifier.R; import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.android.ThreadUtils;
import com.blackforestbytes.simplecloudnotifier.lib.lambda.FI;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
public class SettingsFragment extends PreferenceFragmentCompat import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import top.defaults.colorpicker.ColorPickerPopup;
import xyz.aprildown.ultimatemusicpicker.MusicPickerListener;
import xyz.aprildown.ultimatemusicpicker.UltimateMusicPicker;
public class SettingsFragment extends Fragment implements MusicPickerListener
{ {
@Override private Switch prefAppEnabled;
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) private Spinner prefLocalCacheSize;
private Button prefUpgradeAccount;
private TextView prefUpgradeAccount_msg;
private TextView prefUpgradeAccount_info;
private Switch prefMsgLowEnableSound;
private TextView prefMsgLowRingtone_value;
private View prefMsgLowRingtone_container;
private Switch prefMsgLowRepeatSound;
private Switch prefMsgLowEnableLED;
private View prefMsgLowLedColor_container;
private ImageView prefMsgLowLedColor_value;
private Switch prefMsgLowEnableVibrations;
private Switch prefMsgLowForceVolume;
private SeekBar prefMsgLowVolume;
private ImageView prefMsgLowVolumeTest;
private Switch prefMsgNormEnableSound;
private TextView prefMsgNormRingtone_value;
private View prefMsgNormRingtone_container;
private Switch prefMsgNormRepeatSound;
private Switch prefMsgNormEnableLED;
private View prefMsgNormLedColor_container;
private ImageView prefMsgNormLedColor_value;
private Switch prefMsgNormEnableVibrations;
private Switch prefMsgNormForceVolume;
private SeekBar prefMsgNormVolume;
private ImageView prefMsgNormVolumeTest;
private Switch prefMsgHighEnableSound;
private TextView prefMsgHighRingtone_value;
private View prefMsgHighRingtone_container;
private Switch prefMsgHighRepeatSound;
private Switch prefMsgHighEnableLED;
private View prefMsgHighLedColor_container;
private ImageView prefMsgHighLedColor_value;
private Switch prefMsgHighEnableVibrations;
private Switch prefMsgHighForceVolume;
private SeekBar prefMsgHighVolume;
private ImageView prefMsgHighVolumeTest;
private int musicPickerSwitch = -1;
private MediaPlayer[] mPlayers = new MediaPlayer[3];
public SettingsFragment()
{ {
setPreferencesFromResource(R.xml.preferences, rootKey); // Required empty public constructor
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View v = inflater.inflate(R.layout.fragment_settings, container, false);
initFields(v);
updateUI();
initListener();
return v;
}
private void initFields(View v)
{
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);
prefMsgLowRingtone_container = v.findViewById(R.id.prefMsgLowRingtone_container);
prefMsgLowRepeatSound = v.findViewById(R.id.prefMsgLowRepeatSound);
prefMsgLowEnableLED = v.findViewById(R.id.prefMsgLowEnableLED);
prefMsgLowLedColor_value = v.findViewById(R.id.prefMsgLowLedColor_value);
prefMsgLowLedColor_container = v.findViewById(R.id.prefMsgLowLedColor_container);
prefMsgLowEnableVibrations = v.findViewById(R.id.prefMsgLowEnableVibrations);
prefMsgLowForceVolume = v.findViewById(R.id.prefMsgLowForceVolume);
prefMsgLowVolume = v.findViewById(R.id.prefMsgLowVolume);
prefMsgLowVolumeTest = v.findViewById(R.id.btnLowVolumeTest);
prefMsgNormEnableSound = v.findViewById(R.id.prefMsgNormEnableSound);
prefMsgNormRingtone_value = v.findViewById(R.id.prefMsgNormRingtone_value);
prefMsgNormRingtone_container = v.findViewById(R.id.prefMsgNormRingtone_container);
prefMsgNormRepeatSound = v.findViewById(R.id.prefMsgNormRepeatSound);
prefMsgNormEnableLED = v.findViewById(R.id.prefMsgNormEnableLED);
prefMsgNormLedColor_value = v.findViewById(R.id.prefMsgNormLedColor_value);
prefMsgNormLedColor_container = v.findViewById(R.id.prefMsgNormLedColor_container);
prefMsgNormEnableVibrations = v.findViewById(R.id.prefMsgNormEnableVibrations);
prefMsgNormForceVolume = v.findViewById(R.id.prefMsgNormForceVolume);
prefMsgNormVolume = v.findViewById(R.id.prefMsgNormVolume);
prefMsgNormVolumeTest = v.findViewById(R.id.btnNormVolumeTest);
prefMsgHighEnableSound = v.findViewById(R.id.prefMsgHighEnableSound);
prefMsgHighRingtone_value = v.findViewById(R.id.prefMsgHighRingtone_value);
prefMsgHighRingtone_container = v.findViewById(R.id.prefMsgHighRingtone_container);
prefMsgHighRepeatSound = v.findViewById(R.id.prefMsgHighRepeatSound);
prefMsgHighEnableLED = v.findViewById(R.id.prefMsgHighEnableLED);
prefMsgHighLedColor_value = v.findViewById(R.id.prefMsgHighLedColor_value);
prefMsgHighLedColor_container = v.findViewById(R.id.prefMsgHighLedColor_container);
prefMsgHighEnableVibrations = v.findViewById(R.id.prefMsgHighEnableVibrations);
prefMsgHighForceVolume = v.findViewById(R.id.prefMsgHighForceVolume);
prefMsgHighVolume = v.findViewById(R.id.prefMsgHighVolume);
prefMsgHighVolumeTest = v.findViewById(R.id.btnHighVolumeTest);
}
private void updateUI()
{
SCNSettings s = SCNSettings.inst();
Context c = getContext();
if (c == null) return;
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);
prefLocalCacheSize.setSelection(getCacheSizeIndex(s.LocalCacheSize));
if (prefMsgLowEnableSound.isChecked() != s.PriorityLow.EnableSound) prefMsgLowEnableSound.setChecked(s.PriorityLow.EnableSound);
if (!prefMsgLowRingtone_value.getText().equals(s.PriorityLow.SoundName)) prefMsgLowRingtone_value.setText(s.PriorityLow.SoundName);
if (prefMsgLowRepeatSound.isChecked() != s.PriorityLow.RepeatSound) prefMsgLowRepeatSound.setChecked(s.PriorityLow.RepeatSound);
if (prefMsgLowEnableLED.isChecked() != s.PriorityLow.EnableLED) prefMsgLowEnableLED.setChecked(s.PriorityLow.EnableLED);
prefMsgLowLedColor_value.setColorFilter(s.PriorityLow.LEDColor);
if (prefMsgLowEnableVibrations.isChecked() != s.PriorityLow.EnableVibration) prefMsgLowEnableVibrations.setChecked(s.PriorityLow.EnableVibration);
if (prefMsgLowForceVolume.isChecked() != s.PriorityLow.ForceVolume) prefMsgLowForceVolume.setChecked(s.PriorityLow.ForceVolume);
if (prefMsgLowVolume.getMax() != 100) prefMsgLowVolume.setMax(100);
if (prefMsgLowVolume.getProgress() != s.PriorityLow.ForceVolumeValue) prefMsgLowVolume.setProgress(s.PriorityLow.ForceVolumeValue);
if (prefMsgLowVolume.isEnabled() != s.PriorityLow.ForceVolume) prefMsgLowVolume.setEnabled(s.PriorityLow.ForceVolume);
if (prefMsgLowVolumeTest.isEnabled() != s.PriorityLow.ForceVolume) prefMsgLowVolumeTest.setEnabled(s.PriorityLow.ForceVolume);
if (s.PriorityLow.ForceVolume) prefMsgLowVolumeTest.setColorFilter(null); else prefMsgLowVolumeTest.setColorFilter(Color.argb(150,200,200,200));
if (prefMsgNormEnableSound.isChecked() != s.PriorityNorm.EnableSound) prefMsgNormEnableSound.setChecked(s.PriorityNorm.EnableSound);
if (!prefMsgNormRingtone_value.getText().equals(s.PriorityNorm.SoundName)) prefMsgNormRingtone_value.setText(s.PriorityNorm.SoundName);
if (prefMsgNormRepeatSound.isChecked() != s.PriorityNorm.RepeatSound) prefMsgNormRepeatSound.setChecked(s.PriorityNorm.RepeatSound);
if (prefMsgNormEnableLED.isChecked() != s.PriorityNorm.EnableLED) prefMsgNormEnableLED.setChecked(s.PriorityNorm.EnableLED);
prefMsgNormLedColor_value.setColorFilter(s.PriorityNorm.LEDColor);
if (prefMsgNormEnableVibrations.isChecked() != s.PriorityNorm.EnableVibration) prefMsgNormEnableVibrations.setChecked(s.PriorityNorm.EnableVibration);
if (prefMsgNormForceVolume.isChecked() != s.PriorityNorm.ForceVolume) prefMsgNormForceVolume.setChecked(s.PriorityNorm.ForceVolume);
if (prefMsgNormVolume.getMax() != 100) prefMsgNormVolume.setMax(100);
if (prefMsgNormVolume.getProgress() != s.PriorityNorm.ForceVolumeValue) prefMsgNormVolume.setProgress(s.PriorityNorm.ForceVolumeValue);
if (prefMsgNormVolume.isEnabled() != s.PriorityNorm.ForceVolume) prefMsgNormVolume.setEnabled(s.PriorityNorm.ForceVolume);
if (prefMsgNormVolumeTest.isEnabled() != s.PriorityNorm.ForceVolume) prefMsgNormVolumeTest.setEnabled(s.PriorityNorm.ForceVolume);
if (s.PriorityNorm.ForceVolume) prefMsgNormVolumeTest.setColorFilter(null); else prefMsgNormVolumeTest.setColorFilter(Color.argb(150,200,200,200));
if (prefMsgHighEnableSound.isChecked() != s.PriorityHigh.EnableSound) prefMsgHighEnableSound.setChecked(s.PriorityHigh.EnableSound);
if (!prefMsgHighRingtone_value.getText().equals(s.PriorityHigh.SoundName)) prefMsgHighRingtone_value.setText(s.PriorityHigh.SoundName);
if (prefMsgHighRepeatSound.isChecked() != s.PriorityHigh.RepeatSound) prefMsgHighRepeatSound.setChecked(s.PriorityHigh.RepeatSound);
if (prefMsgHighEnableLED.isChecked() != s.PriorityHigh.EnableLED) prefMsgHighEnableLED.setChecked(s.PriorityHigh.EnableLED);
prefMsgHighLedColor_value.setColorFilter(s.PriorityHigh.LEDColor);
if (prefMsgHighEnableVibrations.isChecked() != s.PriorityHigh.EnableVibration) prefMsgHighEnableVibrations.setChecked(s.PriorityHigh.EnableVibration);
if (prefMsgHighForceVolume.isChecked() != s.PriorityHigh.ForceVolume) prefMsgHighForceVolume.setChecked(s.PriorityHigh.ForceVolume);
if (prefMsgHighVolume.getMax() != 100) prefMsgHighVolume.setMax(100);
if (prefMsgHighVolume.getProgress() != s.PriorityHigh.ForceVolumeValue) prefMsgHighVolume.setProgress(s.PriorityHigh.ForceVolumeValue);
if (prefMsgHighVolume.isEnabled() != s.PriorityHigh.ForceVolume) prefMsgHighVolume.setEnabled(s.PriorityHigh.ForceVolume);
if (prefMsgHighVolumeTest.isEnabled() != s.PriorityHigh.ForceVolume) prefMsgHighVolumeTest.setEnabled(s.PriorityHigh.ForceVolume);
if (s.PriorityHigh.ForceVolume) prefMsgHighVolumeTest.setColorFilter(null); else prefMsgHighVolumeTest.setColorFilter(Color.argb(150,200,200,200));
}
private void initListener()
{
SCNSettings s = SCNSettings.inst();
prefAppEnabled.setOnCheckedChangeListener((a,b) -> { s.Enabled=b; saveAndUpdate(); });
prefLocalCacheSize.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
{
@Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
s.LocalCacheSize = prefLocalCacheSize.getSelectedItemPosition()>=0 ? SCNSettings.CHOOSABLE_CACHE_SIZES[prefLocalCacheSize.getSelectedItemPosition()] : 100;
saveAndUpdate();
}
@Override public void onNothingSelected(AdapterView<?> parent) { /* */ }
});
prefUpgradeAccount.setOnClickListener(a -> onUpgradeAccount());
prefMsgLowEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableSound=b; saveAndUpdate(); });
prefMsgLowRingtone_container.setOnClickListener(a -> chooseRingtoneLow());
prefMsgLowRepeatSound.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.RepeatSound=b; saveAndUpdate(); });
prefMsgLowEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableLED=b; saveAndUpdate(); });
prefMsgLowLedColor_container.setOnClickListener(a -> chooseLEDColorLow());
prefMsgLowEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableVibration=b; saveAndUpdate(); });
prefMsgLowForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.ForceVolume=b; saveAndUpdate(); });
prefMsgLowVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityLow.ForceVolumeValue=b; saveAndUpdate(); updateVolume(0, b); } }));
prefMsgLowVolumeTest.setOnClickListener((v) -> { if (s.PriorityLow.ForceVolume) playTestSound(0, prefMsgLowVolumeTest, s.PriorityLow.SoundSource, s.PriorityLow.ForceVolumeValue); });
prefMsgNormEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableSound=b; saveAndUpdate(); });
prefMsgNormRingtone_container.setOnClickListener(a -> chooseRingtoneNorm());
prefMsgNormRepeatSound.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.RepeatSound=b; saveAndUpdate(); });
prefMsgNormEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableLED=b; saveAndUpdate(); });
prefMsgNormLedColor_container.setOnClickListener(a -> chooseLEDColorNorm());
prefMsgNormEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableVibration=b; saveAndUpdate(); });
prefMsgNormForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.ForceVolume=b; saveAndUpdate(); });
prefMsgNormVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityNorm.ForceVolumeValue=b; saveAndUpdate(); updateVolume(1, b); } }));
prefMsgNormVolumeTest.setOnClickListener((v) -> { if (s.PriorityNorm.ForceVolume) playTestSound(1, prefMsgNormVolumeTest, s.PriorityNorm.SoundSource, s.PriorityNorm.ForceVolumeValue); });
prefMsgHighEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableSound=b; saveAndUpdate(); });
prefMsgHighRingtone_container.setOnClickListener(a -> chooseRingtoneHigh());
prefMsgHighRepeatSound.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.RepeatSound=b; saveAndUpdate(); });
prefMsgHighEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableLED=b; saveAndUpdate(); });
prefMsgHighLedColor_container.setOnClickListener(a -> chooseLEDColorHigh());
prefMsgHighEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableVibration=b; saveAndUpdate(); });
prefMsgHighForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.ForceVolume=b; saveAndUpdate(); });
prefMsgHighVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityHigh.ForceVolumeValue=b; saveAndUpdate(); updateVolume(2, b); } }));
prefMsgHighVolumeTest.setOnClickListener((v) -> { if (s.PriorityHigh.ForceVolume) playTestSound(2, prefMsgHighVolumeTest, s.PriorityHigh.SoundSource, s.PriorityHigh.ForceVolumeValue); });
}
private void updateVolume(int idx, int volume)
{
if (mPlayers[idx] != null && mPlayers[idx].isPlaying())
{
AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE);
int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
aman.setStreamVolume(AudioManager.STREAM_NOTIFICATION, (int)(maxVolume * (volume / 100.0)), 0);
}
}
private void stopSound(final int idx, final ImageView iv)
{
if (mPlayers[idx] != null && mPlayers[idx].isPlaying())
{
mPlayers[idx].stop();
mPlayers[idx].release();
iv.setImageResource(R.drawable.ic_play);
mPlayers[idx] = null;
}
}
private void playTestSound(final int idx, final ImageView iv, String src, int volume)
{
if (mPlayers[idx] != null && mPlayers[idx].isPlaying())
{
mPlayers[idx].stop();
mPlayers[idx].release();
iv.setImageResource(R.drawable.ic_play);
mPlayers[idx] = null;
return;
}
if (Str.isNullOrWhitespace(src)) return;
if (volume == 0) return;
Context ctxt = getContext();
if (ctxt == null) return;
iv.setImageResource(R.drawable.ic_pause);
AudioManager aman = (AudioManager) SCNApp.getContext().getSystemService(Context.AUDIO_SERVICE);
int maxVolume = aman.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
aman.setStreamVolume(AudioManager.STREAM_NOTIFICATION, (int)(maxVolume * (volume / 100.0)), 0);
MediaPlayer player = mPlayers[idx] = new MediaPlayer();
player.setAudioAttributes(new AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_NOTIFICATION).build());
player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
try
{
player.setDataSource(ctxt, Uri.parse(src));
player.setLooping(false);
player.setOnCompletionListener( mp -> SCNApp.runOnUiThread(() -> { mp.stop(); iv.setImageResource(R.drawable.ic_play); mPlayers[idx]=null; mp.release(); }));
player.setOnSeekCompleteListener(mp -> SCNApp.runOnUiThread(() -> { mp.stop(); iv.setImageResource(R.drawable.ic_play); mPlayers[idx]=null; mp.release(); }));
player.prepare();
player.start();
}
catch (IOException e)
{
Log.e("SFRAG:play", e.toString());
}
}
private void saveAndUpdate()
{
SCNSettings.inst().save();
updateUI();
}
private void onUpgradeAccount()
{
IABService.inst().purchase(getActivity(), IABService.IAB_PRO_MODE);
}
public void updateProState()
{
Purchase p = IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE);
if (prefUpgradeAccount != null) prefUpgradeAccount.setVisibility( p != null ? View.GONE : View.VISIBLE);
if (prefUpgradeAccount_info != null) prefUpgradeAccount_info.setVisibility(p != null ? View.GONE : View.VISIBLE);
if (prefUpgradeAccount_msg != null) prefUpgradeAccount_msg.setVisibility( p != null ? View.VISIBLE : View.GONE );
}
private int getCacheSizeIndex(int value)
{
for (int i = 0; i < SCNSettings.CHOOSABLE_CACHE_SIZES.length; i++)
{
if (SCNSettings.CHOOSABLE_CACHE_SIZES[i] == value) return i;
}
return 2;
}
private void chooseRingtoneLow()
{
musicPickerSwitch = 1;
UltimateMusicPicker ump = new UltimateMusicPicker();
ump.windowTitle("Choose notification sound");
ump.removeSilent();
ump.streamType(AudioManager.STREAM_NOTIFICATION);
ump.ringtone();
ump.notification();
ump.alarm();
ump.music();
if (!SCNSettings.inst().PriorityLow.SoundSource.isEmpty())ump.selectUri(Uri.parse(SCNSettings.inst().PriorityLow.SoundSource));
ump.goWithDialog(getChildFragmentManager());
}
private void chooseRingtoneNorm()
{
musicPickerSwitch = 2;
UltimateMusicPicker ump = new UltimateMusicPicker();
ump.windowTitle("Choose notification sound");
ump.removeSilent();
ump.streamType(AudioManager.STREAM_NOTIFICATION);
ump.ringtone();
ump.notification();
ump.alarm();
ump.music();
if (!SCNSettings.inst().PriorityNorm.SoundSource.isEmpty())ump.defaultUri(Uri.parse(SCNSettings.inst().PriorityNorm.SoundSource));
ump.goWithDialog(getChildFragmentManager());
}
private void chooseRingtoneHigh()
{
musicPickerSwitch = 3;
UltimateMusicPicker ump = new UltimateMusicPicker();
ump.windowTitle("Choose notification sound");
ump.removeSilent();
ump.streamType(AudioManager.STREAM_NOTIFICATION);
ump.ringtone();
ump.notification();
ump.alarm();
ump.music();
if (!SCNSettings.inst().PriorityHigh.SoundSource.isEmpty())ump.defaultUri(Uri.parse(SCNSettings.inst().PriorityHigh.SoundSource));
ump.goWithDialog(getChildFragmentManager());
}
private void chooseLEDColorLow()
{
new ColorPickerPopup.Builder(getContext())
.initialColor(SCNSettings.inst().PriorityLow.LEDColor) // Set initial color
.enableBrightness(true) // Enable brightness slider or not
.okTitle("Choose")
.cancelTitle("Cancel")
.showIndicator(true)
.showValue(false)
.build()
.show(getView(), new ColorPickerPopup.ColorPickerObserver()
{
@Override
public void onColorPicked(int color) {
SCNSettings.inst().PriorityLow.LEDColor = color;
saveAndUpdate();
}
@Override
public void onColor(int color, boolean fromUser) { }
});
}
private void chooseLEDColorNorm()
{
new ColorPickerPopup.Builder(getContext())
.initialColor(SCNSettings.inst().PriorityNorm.LEDColor) // Set initial color
.enableBrightness(true) // Enable brightness slider or not
.okTitle("Choose")
.cancelTitle("Cancel")
.showIndicator(true)
.showValue(false)
.build()
.show(getView(), new ColorPickerPopup.ColorPickerObserver()
{
@Override
public void onColorPicked(int color) {
SCNSettings.inst().PriorityNorm.LEDColor = color;
saveAndUpdate();
}
@Override
public void onColor(int color, boolean fromUser) { }
});
}
private void chooseLEDColorHigh()
{
new ColorPickerPopup.Builder(getContext())
.initialColor(SCNSettings.inst().PriorityHigh.LEDColor) // Set initial color
.enableBrightness(true) // Enable brightness slider or not
.okTitle("Choose")
.cancelTitle("Cancel")
.showIndicator(true)
.showValue(false)
.build()
.show(getView(), new ColorPickerPopup.ColorPickerObserver()
{
@Override
public void onColorPicked(int color) {
SCNSettings.inst().PriorityHigh.LEDColor = color;
saveAndUpdate();
}
@Override
public void onColor(int color, boolean fromUser) { }
});
}
@Override
public void onMusicPick(@NotNull Uri uri, @NotNull String s)
{
if (musicPickerSwitch == 1) { SCNSettings.inst().PriorityLow.SoundSource =uri.toString(); SCNSettings.inst().PriorityLow.SoundName =s; saveAndUpdate(); }
if (musicPickerSwitch == 2) { SCNSettings.inst().PriorityNorm.SoundSource=uri.toString(); SCNSettings.inst().PriorityNorm.SoundName=s; saveAndUpdate(); }
if (musicPickerSwitch == 3) { SCNSettings.inst().PriorityHigh.SoundSource=uri.toString(); SCNSettings.inst().PriorityHigh.SoundName=s; saveAndUpdate(); }
musicPickerSwitch = -1;
}
@Override
public void onPickCanceled()
{
musicPickerSwitch = -1;
}
public void onViewpagerHide()
{
stopSound(0, prefMsgLowVolumeTest);
stopSound(1, prefMsgNormVolumeTest);
stopSound(2, prefMsgHighVolumeTest);
} }
} }

View File

@@ -1,8 +1,8 @@
package com.blackforestbytes.simplecloudnotifier.view; package com.blackforestbytes.simplecloudnotifier.view;
import android.support.v4.app.Fragment; import androidx.fragment.app.Fragment;
import android.support.v4.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter; import androidx.fragment.app.FragmentStatePagerAdapter;
public class TabAdapter extends FragmentStatePagerAdapter { public class TabAdapter extends FragmentStatePagerAdapter {

View File

@@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="1000"
android:viewportWidth="1000" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000"
android:pathData="M500,500m-500,0a500,500 0,1 1,1000 0a500,500 0,1 1,-1000 0"
android:strokeColor="#000000" android:strokeWidth="1"/>
</vector>

View File

@@ -0,0 +1,16 @@
<vector
android:height="24dp"
android:width="24dp"
android:viewportHeight="438.536"
android:viewportWidth="438.536"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FF3F51B5"
android:pathData="M164.453,0H18.276C13.324,0 9.041,1.807 5.425,5.424C1.808,9.04 0.001,13.322 0.001,18.271v401.991c0,4.948 1.807,9.233 5.424,12.847c3.619,3.617 7.902,5.428 12.851,5.428h146.181c4.949,0 9.231,-1.811 12.847,-5.428c3.617,-3.613 5.424,-7.898 5.424,-12.847V18.271c0,-4.952 -1.807,-9.231 -5.428,-12.847C173.685,1.807 169.402,0 164.453,0z"/>
<path
android:fillColor="#FF3F51B5"
android:pathData="M433.113,5.424C429.496,1.807 425.215,0 420.267,0H274.086c-4.949,0 -9.237,1.807 -12.847,5.424c-3.621,3.615 -5.432,7.898 -5.432,12.847v401.991c0,4.948 1.811,9.233 5.432,12.847c3.609,3.617 7.897,5.428 12.847,5.428h146.181c4.948,0 9.229,-1.811 12.847,-5.428c3.614,-3.613 5.421,-7.898 5.421,-12.847V18.271C438.534,13.319 436.73,9.04 433.113,5.424z"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector
android:height="24dp"
android:viewportHeight="41.999"
android:viewportWidth="41.999"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FF3F51B5"
android:pathData="M36.068,20.176l-29,-20C6.761,-0.035 6.363,-0.057 6.035,0.114C5.706,0.287 5.5,0.627 5.5,0.999v40c0,0.372 0.206,0.713 0.535,0.886c0.146,0.076 0.306,0.114 0.465,0.114c0.199,0 0.397,-0.06 0.568,-0.177l29,-20c0.271,-0.187 0.432,-0.494 0.432,-0.823S36.338,20.363 36.068,20.176z"/>
</vector>

View File

@@ -0,0 +1,16 @@
<vector
android:height="24dp"
android:width="24dp"
android:viewportHeight="53"
android:viewportWidth="53"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FFFFFF"
android:pathData="M42.943,6H33.5V3c0,-1.654 -1.346,-3 -3,-3h-8c-1.654,0 -3,1.346 -3,3v3h-9.443C8.096,6 6.5,7.596 6.5,9.557V14h2h36h2V9.557C46.5,7.596 44.904,6 42.943,6zM31.5,6h-10V3c0,-0.552 0.449,-1 1,-1h8c0.551,0 1,0.448 1,1V6z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M8.5,49.271C8.5,51.327 10.173,53 12.229,53h28.541c2.057,0 3.729,-1.673 3.729,-3.729V16h-36V49.271z"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector
android:height="24dp"
android:viewportHeight="459"
android:viewportWidth="459"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FF757575"
android:pathData="M0,153v153h102l127.5,127.5v-408L102,153H0zM344.25,229.5c0,-45.9 -25.5,-84.15 -63.75,-102v204C318.75,313.65 344.25,275.4 344.25,229.5zM280.5,5.1v53.55C354.45,81.6 408,147.899 408,229.5S354.45,377.4 280.5,400.35V453.9C382.5,430.949 459,339.15 459,229.5C459,119.85 382.5,28.049 280.5,5.1z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <RelativeLayout
android:id="@+id/layoutRoot"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:showIn="@layout/activity_main"> tools:showIn="@layout/activity_main">
<android.support.v7.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -16,7 +17,7 @@
android:minHeight="?attr/actionBarSize" android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" /> android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
<android.support.design.widget.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout" android:id="@+id/tab_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -26,7 +27,7 @@
android:minHeight="?attr/actionBarSize" android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/> android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>
<android.support.v4.view.ViewPager <androidx.viewpager.widget.ViewPager
android:id="@+id/pager" android:id="@+id/pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"

View File

@@ -7,7 +7,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".view.AccountFragment"> tools:context=".view.AccountFragment">
<android.support.constraint.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -216,7 +216,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
tools:layout_editor_absoluteY="352dp" /> tools:layout_editor_absoluteY="352dp" />
</android.support.constraint.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<RelativeLayout <RelativeLayout
android:id="@+id/loadingPanel" android:id="@+id/loadingPanel"

View File

@@ -3,20 +3,49 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:ads="http://schemas.android.com/apk/res-auto"
tools:context=".view.NotificationsFragment"> tools:context=".view.NotificationsFragment">
<android.support.v7.widget.RecyclerView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rvMessages"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:clipToPadding="false"
android:scrollbars="vertical" /> <RelativeLayout
android:id="@+id/pnlMessages"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/adBanner"
android:layout_width="match_parent"
android:layout_height="0dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvMessages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:scrollbars="vertical" />
<TextView
android:id="@+id/tvNoElements"
android:textAlignment="center"
android:gravity="center"
android:text="@string/no_notifications"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
<com.google.android.gms.ads.doubleclick.PublisherAdView
android:id="@+id/adBanner"
app:layout_constraintTop_toBottomOf="@+id/pnlMessages"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
ads:adSize="SMART_BANNER"
ads:adUnitId="ca-app-pub-3320562328966175/5524654300" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/tvNoElements"
android:textAlignment="center"
android:gravity="center"
android:text="@string/no_notifications"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout> </FrameLayout>

View File

@@ -0,0 +1,752 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.SettingsFragment">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:background="@color/colorHeader"
android:textColor="@color/colorHeaderForeground"
android:textSize="16sp"
android:padding="4dp"
android:text="@string/str_common_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:id="@+id/prefAppEnabled"
android:text="@string/str_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp">
<TextView
android:id="@+id/tvLocalCacheSize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/str_localcachesize"
android:textColor="#000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/prefLocalCacheSize"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Spinner
android:minWidth="64dp"
android:id="@+id/prefLocalCacheSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnerMode="dialog"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<LinearLayout
android:orientation="vertical"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|center"
android:minHeight="48dp">
<Button
android:id="@+id/prefUpgradeAccount"
android:text="@string/str_upgrade_account"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<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>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:background="@color/colorHeader"
android:textColor="@color/colorHeaderForeground"
android:textSize="16sp"
android:padding="4dp"
android:text="@string/str_header_prio0"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/prefMsgLowEnableSound"
android:text="@string/str_msg_enablesound"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<LinearLayout
android:id="@+id/prefMsgLowRingtone_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp"
android:gravity="center">
<TextView
android:id="@+id/tvMsgLowRingtone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/str_notificationsound"
android:textColor="#000" />
<TextView
android:id="@+id/prefMsgLowRingtone_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="64dp"
android:spinnerMode="dialog"
android:text="Whatever"
tools:ignore="HardcodedText" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgLowRepeatSound"
android:text="@string/str_repeatnotificationsound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgLowForceVolume"
android:text="@string/str_forcevolume"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"
android:orientation="horizontal"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp">
<ImageView
android:id="@+id/icnLowVolume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/volume_icon"
android:src="@drawable/ic_volume"
app:layout_constraintStart_toStartOf="parent" />
<SeekBar
android:id="@+id/prefMsgLowVolume"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnLowVolumeTest"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/icnLowVolume"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/btnLowVolumeTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_play"
app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="@string/play_test_sound" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgLowEnableLED"
android:text="@string/str_enable_led"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/prefMsgLowLedColor_container"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/str_ledcolor"
android:textColor="#000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/prefMsgLowLedColor_value"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:src="@drawable/circle"
android:layout_marginEnd="4dp"
android:id="@+id/prefMsgLowLedColor_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnerMode="dialog"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgLowEnableVibrations"
android:text="@string/str_enable_vibration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:background="@color/colorHeader"
android:textColor="@color/colorHeaderForeground"
android:textSize="16sp"
android:padding="4dp"
android:text="@string/str_header_prio1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/prefMsgNormEnableSound"
android:text="@string/str_msg_enablesound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<LinearLayout
android:id="@+id/prefMsgNormRingtone_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp"
android:gravity="center">
<TextView
android:id="@+id/tvMsgNormRingtone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/str_notificationsound"
android:textColor="#000" />
<TextView
android:id="@+id/prefMsgNormRingtone_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="64dp"
android:spinnerMode="dialog"
android:text="Whatever"
tools:ignore="HardcodedText" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgNormForceVolume"
android:text="@string/str_forcevolume"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"
android:orientation="horizontal"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp">
<ImageView
android:id="@+id/icnNormVolume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/volume_icon"
android:src="@drawable/ic_volume"
app:layout_constraintStart_toStartOf="parent" />
<SeekBar
android:id="@+id/prefMsgNormVolume"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnNormVolumeTest"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/icnNormVolume"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/btnNormVolumeTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_play"
app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="@string/play_test_sound" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgNormRepeatSound"
android:text="@string/str_repeatnotificationsound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgNormEnableLED"
android:text="@string/str_enable_led"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/prefMsgNormLedColor_container"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/str_ledcolor"
android:textColor="#000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/prefMsgNormLedColor_value"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:src="@drawable/circle"
android:layout_marginEnd="4dp"
android:id="@+id/prefMsgNormLedColor_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnerMode="dialog"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgNormEnableVibrations"
android:text="@string/str_enable_vibration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:background="@color/colorHeader"
android:textColor="@color/colorHeaderForeground"
android:textSize="16sp"
android:padding="4dp"
android:text="@string/str_header_prio2"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/prefMsgHighEnableSound"
android:text="@string/str_msg_enablesound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<LinearLayout
android:id="@+id/prefMsgHighRingtone_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp"
android:orientation="vertical"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tvMsgHighRingtone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/str_notificationsound"
android:textColor="#000" />
<TextView
android:id="@+id/prefMsgHighRingtone_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="64dp"
android:spinnerMode="dialog"
android:text="Whatever"
tools:ignore="HardcodedText" />
</LinearLayout>
<Switch
android:id="@+id/prefMsgHighForceVolume"
android:text="@string/str_forcevolume"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"
android:orientation="horizontal"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp">
<ImageView
android:id="@+id/icnHighVolume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/volume_icon"
android:src="@drawable/ic_volume"
app:layout_constraintStart_toStartOf="parent" />
<SeekBar
android:id="@+id/prefMsgHighVolume"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnHighVolumeTest"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/icnHighVolume"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/btnHighVolumeTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_play"
app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="@string/play_test_sound" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgHighRepeatSound"
android:text="@string/str_repeatnotificationsound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgHighEnableLED"
android:text="@string/str_enable_led"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/prefMsgHighLedColor_container"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/str_ledcolor"
android:textColor="#000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/prefMsgHighLedColor_value"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:src="@drawable/circle"
android:layout_marginEnd="4dp"
android:id="@+id/prefMsgHighLedColor_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnerMode="dialog"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<Switch
android:id="@+id/prefMsgHighEnableVibrations"
android:text="@string/str_enable_vibration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:minHeight="48dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -1,80 +1,117 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto" xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.CardView <RelativeLayout
android:id="@+id/card_view" android:id="@+id/layoutBack"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="@dimen/card_margin" android:layout_margin="@dimen/card_margin"
android:elevation="3dp" android:background="@color/bg_row_background">
card_view:cardCornerRadius="@dimen/card_album_radius">
<ImageView
android:id="@+id/delete_icon"
android:layout_width="@dimen/ic_delete"
android:layout_height="@dimen/ic_delete"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="@dimen/padd_10"
android:src="@drawable/ic_trash"
android:contentDescription="@string/delete" />
<android.support.constraint.ConstraintLayout <TextView
android:background="#FFFFFFFF" android:layout_width="wrap_content"
android:layout_margin="3dp" android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="@dimen/padd_10"
android:layout_toLeftOf="@id/delete_icon"
android:text="@string/delete"
android:textColor="#fff"
android:textSize="13dp" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layoutFront"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/card_margin"
android:elevation="3dp"
card_view:cardCornerRadius="@dimen/card_album_radius">
<TextView
android:id="@+id/tvTimestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:textSize="12sp"
android:textStyle="italic"
android:text="2018-09-11 20:22:32" /> <androidx.constraintlayout.widget.ConstraintLayout
android:background="#FFFFFFFF"
android:layout_margin="3dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/tvTitle" android:id="@+id/tvTimestamp"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintRight_toLeftOf="@+id/tvTimestamp" android:textSize="12sp"
android:layout_marginEnd="4sp" android:textStyle="italic"
android:textSize="16sp"
android:textColor="@color/colorBlack"
android:textStyle="bold"
android:ellipsize="none"
android:maxLines="6"
android:text="Message from me"/> android:text="2018-09-11 20:22:32" />
<TextView <TextView
android:id="@+id/tvMessage" android:id="@+id/tvTitle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tvTitle" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@+id/ivPriority" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/tvTimestamp"
android:layout_margin="4sp" android:layout_marginEnd="4sp"
android:ellipsize="none" android:textSize="16sp"
android:maxLines="32" android:textColor="@color/colorBlack"
android:scrollHorizontally="false" android:textStyle="bold"
android:ellipsize="none"
android:maxLines="6"
android:text="asdasd asdasd asdasd a" /> android:text="Message from me"/>
<ImageView <TextView
android:id="@+id/ivPriority" android:id="@+id/tvMessage"
android:visibility="gone" android:layout_width="0dp"
android:layout_width="24dp" android:layout_height="wrap_content"
android:layout_height="24dp" app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:layout_constraintTop_toBottomOf="@+id/tvTimestamp" app:layout_constraintRight_toLeftOf="@+id/ivPriority"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" android:layout_margin="4sp"
android:layout_margin="4sp" android:ellipsize="end"
android:paddingTop="3dp" android:maxLines="6"
android:contentDescription="@string/desc_priority_icon" /> android:scrollHorizontally="false"
</android.support.constraint.ConstraintLayout> android:text="asdasd asdasd asdasd a" />
</android.support.v7.widget.CardView> <ImageView
android:id="@+id/ivPriority"
android:tint="#BBB"
android:visibility="gone"
android:layout_width="24dp"
android:layout_height="24dp"
app:layout_constraintTop_toBottomOf="@+id/tvTimestamp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="4sp"
android:paddingTop="3dp"
android:contentDescription="@string/desc_priority_icon" />
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</RelativeLayout>
</FrameLayout>

View File

@@ -3,6 +3,10 @@
<color name="colorPrimary">#3F51B5</color> <color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color> <color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color> <color name="colorAccent">#FF4081</color>
<color name="colorHeader">#3F51B5</color>
<color name="colorHeaderForeground">#FFFFFF</color>
<color name="colorBlack">#000</color> <color name="colorBlack">#000</color>
<color name="bg_row_background">#fa315b</color>
</resources> </resources>

View File

@@ -2,4 +2,8 @@
<resources> <resources>
<dimen name="card_margin">5dp</dimen> <dimen name="card_margin">5dp</dimen>
<dimen name="card_album_radius">0dp</dimen> <dimen name="card_album_radius">0dp</dimen>
<dimen name="padd_10">10dp</dimen>
<dimen name="ic_delete">30dp</dimen>
<dimen name="thumbnail">90dp</dimen>
</resources> </resources>

View File

@@ -15,4 +15,23 @@
<string name="str_not_connected">not connected</string> <string name="str_not_connected">not connected</string>
<string name="str_reload">reload</string> <string name="str_reload">reload</string>
<string name="desc_priority_icon">Priority icon</string> <string name="desc_priority_icon">Priority icon</string>
<string name="str_common_settings">Common Settings</string>
<string name="str_enabled">Enabled</string>
<string name="str_localcachesize">Remember the last x notifications locally</string>
<string name="str_header_prio0">Notifications (priority 0 - Low)</string>
<string name="str_header_prio1">Notifications (priority 1 - Normal)</string>
<string name="str_header_prio2">Notifications (priority 2 - High)</string>
<string name="str_msg_enablesound">Enable notification sound</string>
<string name="str_notificationsound">Notification sound</string>
<string name="str_repeatnotificationsound">Repeat notification sound</string>
<string name="str_forcevolume">Automatically set the volume</string>
<string name="str_enable_led">Enable notification light</string>
<string name="str_ledcolor">Notification light color</string>
<string name="str_enable_vibration">Enable notification vibration</string>
<string name="str_upgrade_account">Upgrade account</string>
<string name="str_promode">Thank you for supporting the app and using the pro mode</string>
<string name="str_promode_info">Increase your daily quota, remove the ad banner and support the developer (that\'s me)</string>
<string name="volume_icon">Volume icon</string>
<string name="play_test_sound">Play test sound</string>
<string name="delete">DELETE</string>
</resources> </resources>

View File

@@ -1,7 +1,7 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="@style/PreferenceFixTheme.Light.NoActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- your app branding color for the app bar --> <!-- your app branding color for the app bar -->
<item name="colorPrimary">#3F51B5</item> <item name="colorPrimary">#3F51B5</item>
<!-- darker variant for the status bar and contextual app bars --> <!-- darker variant for the status bar and contextual app bars -->

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<EditTextPreference
android:key="message_count"
android:title="Keep x notifications"
android:summary="Remember tha last x notifications locally"
android:defaultValue="200" />
<PreferenceCategory
android:title="Notifications"
android:key="pref_key_notifications">
<CheckBoxPreference
android:key="notification_enable_sound"
android:title="Notification sound"
android:summary="Play a sound when a notification is recieved"
android:defaultValue="false" />
<CheckBoxPreference
android:key="notification_enable_light"
android:title="Notification light"
android:summary="Turn the notification LED on when a notification is recieved"
android:defaultValue="true" />
<CheckBoxPreference
android:key="notification_enable_vibrate"
android:title="Notification vibration"
android:summary="Vibrate when a notification is recieved"
android:defaultValue="false" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -1,3 +1,3 @@
#Sat Oct 20 02:47:36 CEST 2018 #Mon Nov 12 18:26:41 CET 2018
VERSION_NAME=0.0.2 VERSION_NAME=0.0.7
VERSION_CODE=2 VERSION_CODE=7

View File

@@ -8,9 +8,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.2.1' 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'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
} }
} }
@@ -20,6 +18,9 @@ allprojects {
jcenter() jcenter()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven { url "https://dl.bintray.com/gericop/maven" } maven { url "https://dl.bintray.com/gericop/maven" }
maven { url "https://maven.google.com" }
} }
} }

View File

@@ -11,3 +11,6 @@ org.gradle.jvmargs=-Xmx1536m
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 114 KiB

BIN
data/icon_web.pdn Normal file

Binary file not shown.

1
web/.gitignore vendored
View File

@@ -181,3 +181,4 @@ $RECYCLE.BIN/
################# #################
config.php config.php
.verify_accesstoken

47
web/api.php Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
<title>Simple Cloud Notifications - API</title>
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
<link rel="stylesheet" href="/css/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favicon.png"/>
<link rel="icon" type="image/png" href="/favicon.ico"/>
</head>
<body>
<div id="copyinfo">
<a tabindex="-1" href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schw&ouml;rer</a>
</div>
<div id="mainpnl">
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
<p>Get your user-id and user-key from the app and send notifications to your phone by performing a POST request against <code>https://simplecloudnotifier.blackforestbytes.com/send.php</code></p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "title={message_title}" \
--data "content={message_body}" \
--data "priority={0|1|2}" \
--data "msg_id={unique_message_id}" \
https://scn.blackforestbytes.com/send.php</pre>
<p>The <code>content</code>, <code>priority</code> and <code>msg_id</code> parameters are optional, you can also send message with only a title and the default priority</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "title={message_title}" \
https://scn.blackforestbytes.com/send.php</pre>
<a href="/api_more.php" class="button bordered tertiary" style="float: right; min-width: 100px; text-align: center">More</a>
</div>
</body>
</html>

2
web/api/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
config.php
.verify_accesstoken

46
web/api/ack.php Normal file
View File

@@ -0,0 +1,46 @@
<?php
include_once 'model.php';
$INPUT = array_merge($_GET, $_POST);
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errid'=>101, 'message' => 'Missing parameter [[user_id]]']));
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errid'=>102, 'message' => 'Missing parameter [[user_key]]']));
if (!isset($INPUT['scn_msg_id'])) die(json_encode(['success' => false, 'errid'=>103, 'message' => 'Missing parameter [[scn_msg_id]]']));
$user_id = $INPUT['user_id'];
$user_key = $INPUT['user_key'];
$scn_msg_id = $INPUT['scn_msg_id'];
//----------------------
$pdo = getDatabase();
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
$stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>201, 'message' => 'User not found']));
$data = $datas[0];
if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message' => 'User not found']));
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
$stmt = $pdo->prepare('SELECT ack FROM messages WHERE scn_message_id=:smid AND sender_user_id=:uid LIMIT 1');
$stmt->execute(['smid' => $scn_msg_id, 'uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>301, 'message' => 'Message not found']));
$stmt = $pdo->prepare('UPDATE messages SET ack=1 WHERE scn_message_id=:smid AND sender_user_id=:uid');
$stmt->execute(['smid' => $scn_msg_id, 'uid' => $user_id]);
api_return(200,
[
'success' => true,
'prev_ack' => $datas[0]['ack'],
'new_ack' => 1,
'message' => 'ok'
]);

View File

@@ -15,7 +15,7 @@ $user_key = $INPUT['user_key'];
$pdo = getDatabase(); $pdo = getDatabase();
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_max, quota_day FROM users WHERE user_id = :uid LIMIT 1'); $stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
$stmt->execute(['uid' => $user_id]); $stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC); $datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -26,17 +26,26 @@ if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message'
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found'])); if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed'])); if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
$stmt = $pdo->prepare('SELECT COUNT(*) FROM messages WHERE ack=0 AND sender_user_id=:uid');
$stmt->execute(['uid' => $user_id]);
$nack_count = $stmt->fetch(PDO::FETCH_NUM);
$quota = $data['quota_today']; $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; if ($data['quota_day'] === null || $data['quota_day'] !== date("Y-m-d")) $quota=0;
echo json_encode( api_return(200,
[ [
'success' => true, 'success' => true,
'user_id' => $user_id, 'message' => 'ok',
'quota' => $quota, 'user_id' => $user_id,
'quota_max'=> $quota_max, 'quota' => $quota,
'message' => 'ok' 'quota_max' => Statics::quota_max($is_pro),
'is_pro' => $is_pro,
'fcm_token_set' => ($data['fcm_token'] != null),
'unack_count' => $nack_count,
]); ]);
return 0;

261
web/api/model.php Normal file
View File

@@ -0,0 +1,261 @@
<?php
include('lib/httpful.phar');
class ERR
{
const NO_ERROR = 0000;
const MISSING_UID = 1101;
const MISSING_TOK = 1102;
const MISSING_TITLE = 1103;
const INVALID_PRIO = 1104;
const REQ_METHOD = 1105;
const NO_TITLE = 1201;
const TITLE_TOO_LONG = 1202;
const CONTENT_TOO_LONG = 1203;
const USR_MSG_ID_TOO_LONG = 1204;
const USER_NOT_FOUND = 1301;
const USER_AUTH_FAILED = 1302;
const NO_DEVICE_LINKED = 1401;
const QUOTA_REACHED = 2101;
const FIREBASE_COM_FAILED = 9901;
const FIREBASE_COM_ERRORED = 9902;
const INTERNAL_EXCEPTION = 9903;
}
class Statics
{
public static $DB = NULL;
public static $CFG = NULL;
public static function quota_max($is_pro) { return $is_pro ? 1000 : 50; }
}
function getConfig()
{
if (Statics::$CFG !== NULL) return Statics::$CFG;
return Statics::$CFG = require "config.php";
}
/**
* @param String $msg
* @param Exception $e
*/
function reportError($msg, $e = null)
{
if ($e != null) $msg = ($msg."\n\n[[EXCEPTION]]\n" . $e . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString());
$subject = "SCN_Server has encountered an Error at " . date("Y-m-d H:i:s") . "] ";
$content = "";
$content .= 'HTTP_HOST: ' . ParamServerOrUndef('HTTP_HOST') . "\n";
$content .= 'REQUEST_URI: ' . ParamServerOrUndef('REQUEST_URI') . "\n";
$content .= 'TIME: ' . date('Y-m-d H:i:s') . "\n";
$content .= 'REMOTE_ADDR: ' . ParamServerOrUndef('REMOTE_ADDR') . "\n";
$content .= 'HTTP_X_FORWARDED_FOR: ' . ParamServerOrUndef('HTTP_X_FORWARDED_FOR') . "\n";
$content .= 'HTTP_USER_AGENT: ' . ParamServerOrUndef('HTTP_USER_AGENT') . "\n";
$content .= 'MESSAGE:' . "\n" . $msg . "\n";
$content .= '$_GET:' . "\n" . print_r($_GET, true) . "\n";
$content .= '$_POST:' . "\n" . print_r($_POST, true) . "\n";
$content .= '$_FILES:' . "\n" . print_r($_FILES, true) . "\n";
if (getConfig()['error_reporting']['send-mail']) sendMail($subject, $content, getConfig()['error_reporting']['email-error-target'], getConfig()['error_reporting']['email-error-sender']);
}
/**
* @param string $subject
* @param string $content
* @param string $to
* @param string $from
*/
function sendMail($subject, $content, $to, $from) {
mail($to, $subject, $content, 'From: ' . $from);
}
/**
* @param string $idx
* @return string
*/
function ParamServerOrUndef($idx) {
return isset($_SERVER[$idx]) ? $_SERVER[$idx] : 'NOT_SET';
}
function getDatabase()
{
if (Statics::$DB !== NULL) return Statics::$DB;
$_config = getConfig()['database'];
$dsn = "mysql:host=" . $_config['host'] . ";dbname=" . $_config['database'] . ";charset=utf8";
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
return Statics::$DB = new PDO($dsn, $_config['user'], $_config['password'], $opt);
}
function generateRandomAuthKey()
{
$random = '';
for ($i = 0; $i < 64; $i++)
try {
switch (random_int(1, 3)) {
case 1:
$random .= chr(random_int(ord('0'), ord('9')));
break;
case 2:
$random .= chr(random_int(ord('A'), ord('Z')));
break;
case 3:
$random .= chr(random_int(ord('a'), ord('z')));
break;
}
}
catch (Exception $e)
{
die(json_encode(['success' => false, 'message' => 'Internal error - no randomness']));
}
return $random;
}
/**
* @param $url
* @param $body
* @param $header
* @return array|object|string
* @throws \Httpful\Exception\ConnectionErrorException
* @throws Exception
*/
function sendPOST($url, $body, $header)
{
$builder = \Httpful\Request::post($url);
$builder->body($body);
foreach ($header as $k => $v) $builder->addHeader($k, $v);
$response = $builder->send();
if ($response->code != 200) throw new Exception("Repsponse code: " . $response->code);
return $response->raw_body;
}
function verifyOrderToken($tok)
{
// https://developers.google.com/android-publisher/api-ref/purchases/products/get
try
{
$package = getConfig()['verify_api']['package_name'];
$product = getConfig()['verify_api']['product_id'];
$acctoken = getConfig()['verify_api']['accesstoken'];
if ($acctoken == '' || $acctoken == null || $acctoken == false) $acctoken = refreshVerifyToken();
$url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$package.'/purchases/products/'.$product.'/tokens/'.$tok.'?access_token='.$acctoken;
$response = $builder = \Httpful\Request::get($url)->send();
$obj = json_decode($response->raw_body, true);
if ($response->code != 401 && ($obj === null || $obj === false))
{
reportError('verify-token returned NULL');
return false;
}
if ($response->code == 401 || isset($obj['error']) && isset($obj['error']['code']) && $obj['error']['code'] == 401) // "Invalid Credentials" -- refresh acces_token
{
$acctoken = refreshVerifyToken();
$url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$package.'/purchases/products/'.$product.'/tokens/'.$tok.'?access_token='.$acctoken;
$response = $builder = \Httpful\Request::get($url)->send();
$obj = json_decode($response->raw_body, true);
if ($obj === null || $obj === false)
{
reportError('verify-token returned NULL');
return false;
}
}
if (isset($obj['purchaseState']) && $obj['purchaseState'] === 0) return true;
return false;
}
catch (Exception $e)
{
reportError("VerifyOrder token threw exception", $e);
return false;
}
}
/** @throws Exception */
function refreshVerifyToken()
{
$url = 'https://accounts.google.com/o/oauth2/token'.
'?grant_type=refresh_token'.
'&refresh_token='.getConfig()['verify_api']['refreshtoken'].
'&client_id='.getConfig()['verify_api']['clientid'].
'&client_secret='.getConfig()['verify_api']['clientsecret'];
$json = sendPOST($url, "", []);
$obj = json_decode($json, true);
file_put_contents('.verify_accesstoken', $obj['access_token']);
return $obj['access_token'];
}
/**
* @param int $http_code
* @param array $message
*/
function api_return($http_code, $message)
{
http_response_code($http_code);
header('Content-Type: application/json');
echo json_encode($message);
die();
}
/**
* @param String $str
* @param String[] $path
* @return mixed|null
*/
function try_json($str, $path)
{
try
{
$o = json_decode($str, true);
foreach ($path as $p) $o = $o[$p];
return $o;
}
catch (Exception $e)
{
return null;
}
}
//#################################################################################################################
if (getConfig()['global']['prod']) {
ini_set('display_errors', 0);
ini_set('log_errors', 1);
} else {
error_reporting(E_STRICT);
ini_set('display_errors', 1);
}
//#################################################################################################################

50
web/api/register.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
include_once 'model.php';
$INPUT = array_merge($_GET, $_POST);
if (!isset($INPUT['fcm_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[fcm_token]]']));
if (!isset($INPUT['pro'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro]]']));
if (!isset($INPUT['pro_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro_token]]']));
$fcmtoken = $INPUT['fcm_token'];
$ispro = $INPUT['pro'] == 'true';
$pro_token = $INPUT['pro_token'];
$user_key = generateRandomAuthKey();
$pdo = getDatabase();
$pdo->beginTransaction();
if ($ispro)
{
if (!verifyOrderToken($pro_token))
{
$pdo->rollBack();
die(json_encode(['success' => false, 'message' => 'Purchase token could not be verified']));
}
$stmt = $pdo->prepare('UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> :uid AND pro_token = :ptk');
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
}
$stmt = $pdo->prepare('INSERT INTO users (user_key, fcm_token, is_pro, pro_token, timestamp_accessed) VALUES (:key, :token, :bpro, :spro, NOW())');
$stmt->execute(['key' => $user_key, 'token' => $fcmtoken, 'bpro' => $ispro, 'spro' => $ispro ? $pro_token : null]);
$user_id = $pdo->lastInsertId('user_id');
$stmt = $pdo->prepare('UPDATE users SET fcm_token=NULL WHERE user_id <> :uid AND fcm_token=:ft');
$stmt->execute(['uid' => $user_id, 'ft' => $fcm_token]);
$pdo->commit();
api_return(200,
[
'success' => true,
'user_id' => $user_id,
'user_key' => $user_key,
'quota' => 0,
'quota_max' => Statics::quota_max($ispro),
'is_pro' => $ispro,
'message' => 'New user registered'
]);

56
web/api/requery.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
include_once 'model.php';
$INPUT = array_merge($_GET, $_POST);
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errid'=>101, 'message' => 'Missing parameter [[user_id]]']));
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errid'=>102, 'message' => 'Missing parameter [[user_key]]']));
$user_id = $INPUT['user_id'];
$user_key = $INPUT['user_key'];
//----------------------
$pdo = getDatabase();
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day, fcm_token FROM users WHERE user_id = :uid LIMIT 1');
$stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>201, 'message' => 'User not found']));
$data = $datas[0];
if ($data === null) die(json_encode(['success' => false, 'errid'=>202, 'message' => 'User not found']));
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errid'=>203, 'message' => 'UserID not found']));
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
//-------------------
$stmt = $pdo->prepare('SELECT * FROM messages WHERE ack=0 AND sender_user_id=:uid ORDER BY `timestamp` DESC LIMIT 16');
$stmt->execute(['uid' => $user_id]);
$nonacks_sql = $stmt->fetchAll(PDO::FETCH_ASSOC);
$nonacks = [];
foreach ($nonacks_sql as $nack)
{
$nonacks []=
[
'title' => $nack['title'],
'body' => $nack['content'],
'priority' => $nack['priority'],
'timestamp' => $nack['timestamp'],
'usr_msg_id' => $nack['usr_message_id'],
'scn_msg_id' => $nack['scn_message_id'],
];
}
api_return(200,
[
'success' => true,
'message' => 'ok',
'count' => count($nonacks),
'data' => $nonacks,
]);

37
web/api/schema.sql Normal file
View File

@@ -0,0 +1,37 @@
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users`
(
`user_id` INT(11) NOT NULL AUTO_INCREMENT,
`user_key` VARCHAR(64) NOT NULL,
`fcm_token` VARCHAR(256) NULL DEFAULT NULL,
`messages_sent` INT(11) NOT NULL DEFAULT '0',
`timestamp_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`timestamp_accessed` DATETIME NULL DEFAULT NULL,
`quota_today` INT(11) NOT NULL DEFAULT '0',
`quota_day` DATE NULL DEFAULT NULL,
`is_pro` BIT NOT NULL DEFAULT 0,
`pro_token` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`user_id`)
);
DROP TABLE IF EXISTS `messages`;
CREATE TABLE `messages`
(
`scn_message_id` INT(11) NOT NULL AUTO_INCREMENT,
`sender_user_id` INT(11) NOT NULL,
`timestamp` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ack` BIT NOT NULL DEFAULT 0,
`title` VARCHAR(256) NOT NULL,
`content` VARCHAR(12288) NULL,
`priority` INT(11) NOT NULL,
`fcm_message_id` VARCHAR(256) NULL,
`usr_message_id` VARCHAR(256) NULL,
PRIMARY KEY (`scn_message_id`)
);

View File

@@ -16,7 +16,7 @@ $fcm_token = isset($INPUT['fcm_token']) ? $INPUT['fcm_token'] : null;
$pdo = getDatabase(); $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]); $stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC); $datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -28,39 +28,46 @@ if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'me
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'message' => 'Authentification failed'])); if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'message' => 'Authentification failed']));
$quota = $data['quota_today']; $quota = $data['quota_today'];
$quota_max = $data['quota_max']; $is_pro = $data['is_pro'];
$new_userkey = generateRandomAuthKey(); $new_userkey = generateRandomAuthKey();
if ($fcm_token === null) if ($fcm_token === null)
{ {
// only gen new user_secret
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), user_key=:at WHERE user_id = :uid'); $stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), user_key=:at WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id, 'at' => $new_userkey]); $stmt->execute(['uid' => $user_id, 'at' => $new_userkey]);
echo json_encode( api_return(200,
[ [
'success' => true, 'success' => true,
'user_id' => $user_id, 'user_id' => $user_id,
'user_key' => $new_userkey, 'user_key' => $new_userkey,
'quota' => $quota, 'quota' => $quota,
'quota_max'=> $quota_max, 'quota_max'=> Statics::quota_max($data['is_pro']),
'message' => 'user updated' 'is_pro' => $is_pro,
'message' => 'user updated'
]); ]);
return 0;
} }
else else
{ {
// update fcm and gen new user_secret
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), fcm_token=:ft, user_key=:at WHERE user_id = :uid'); $stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), fcm_token=:ft, user_key=:at WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id, 'ft' => $fcm_token, 'at' => $new_userkey]); $stmt->execute(['uid' => $user_id, 'ft' => $fcm_token, 'at' => $new_userkey]);
echo json_encode( $stmt = $pdo->prepare('UPDATE users SET fcm_token=NULL WHERE user_id <> :uid AND fcm_token=:ft');
$stmt->execute(['uid' => $user_id, 'ft' => $fcm_token]);
api_return(200,
[ [
'success' => true, 'success' => true,
'user_id' => $user_id, 'user_id' => $user_id,
'user_key' => $new_userkey, 'user_key' => $new_userkey,
'quota' => $quota, 'quota' => $quota,
'quota_max'=> $quota_max, 'quota_max'=> Statics::quota_max($data['is_pro']),
'is_pro' => $is_pro,
'message' => 'user updated' 'message' => 'user updated'
]); ]);
return 0;
} }

74
web/api/upgrade.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
include_once 'model.php';
$INPUT = array_merge($_GET, $_POST);
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_id]]']));
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_key]]']));
if (!isset($INPUT['pro'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro]]']));
if (!isset($INPUT['pro_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro_token]]']));
$user_id = $INPUT['user_id'];
$user_key = $INPUT['user_key'];
$ispro = $INPUT['pro'] == 'true';
$pro_token = $INPUT['pro_token'];
//----------------------
$pdo = getDatabase();
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_day, is_pro, pro_token FROM users WHERE user_id = :uid LIMIT 1');
$stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($datas)<=0) die(json_encode(['success' => false, 'message' => 'User not found']));
$data = $datas[0];
if ($data === null) die(json_encode(['success' => false, 'message' => 'User not found']));
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'message' => 'UserID not found']));
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'message' => 'Authentification failed']));
if ($ispro)
{
// set pro=true
if ($data['pro_token'] != $pro_token)
{
if (!verifyOrderToken($pro_token)) die(json_encode(['success' => false, 'message' => 'Purchase token could not be verified']));
}
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), is_pro=1, pro_token=:ptk WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
$stmt = $pdo->prepare('UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> :uid AND pro_token = :ptk');
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
api_return(200,
[
'success' => true,
'user_id' => $user_id,
'quota' => $data['quota_today'],
'quota_max'=> Statics::quota_max(true),
'is_pro' => true,
'message' => 'user updated'
]);
}
else
{
// set pro=false
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), is_pro=0, pro_token=NULL WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id]);
api_return(200,
[
'success' => true,
'user_id' => $user_id,
'quota' => $data['quota_today'],
'quota_max'=> Statics::quota_max(false),
'is_pro' => false,
'message' => 'user updated'
]);
}

193
web/api_more.php Normal file
View File

@@ -0,0 +1,193 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
<title>Simple Cloud Notifications - API</title>
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
<link rel="stylesheet" href="/css/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favicon.png"/>
<link rel="icon" type="image/png" href="/favicon.ico"/>
</head>
<body>
<div id="copyinfo">
<a tabindex="-1" href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schw&ouml;rer</a>
</div>
<div id="mainpnl">
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
<h2>Introduction</h2>
<div class="section">
<p>
With this API you can send push notifications to your phone.
</p>
<p>
To recieve them you will need to install the <a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier">SimpleCloudNotifier</a> app from the play store.
When you open the app you can click on the account tab to see you unique <code>user_id</code> and <code>user_key</code>.
These two values are used to identify and authenticate your device so that send messages can be routed to your phone.
</p>
<p>
You can at any time generate a new <code>user_key</code> in the app and invalidate the old one.
</p>
<p>
There is also a <a href="/index.php">web interface</a> for this API to manually send notifications to your phone or to test your setup.
</p>
</div>
<h2>Quota</h2>
<div class="section">
<p>
By default you can send up to 100 messages per day per device.
If you need more you can upgrade your account in the app to get 1000 messages per day, this has the additional benefit of removing ads and supporting the development of the app (and making sure I can pay the server costs).
</p>
</div>
<h2>API Requests</h2>
<div class="section">
<p>
To send a new notification you send a <code>POST</code> request to the URL <code>https://scn.blackforestbytes.com/send.php</code>.
All Parameters can either directly be submitted as URL parameters or they can be put into the POST body.
</p>
<p>
You <i>need</i> to supply a valid <code>user_id</code> - <code>user_key</code> pair and a <code>title</code> for your message, all other parameter are optional.
</p>
</div>
<h2>API Response</h2>
<div class="section">
<p>
If the operation was successful the API will respond with an HTTP statuscode 200 and an JSON payload indicating the send message and your remaining quota
</p>
<pre class="red-code">{
"success":true,
"message":"Message sent",
"response":
{
"multicast_id":8000000000000000006,
"success":1,
"failure":0,
"canonical_ids":0,
"results": [{"message_id":"0:10000000000000000000000000000000d"}]
},
"quota":17,
"quota_max":100
}</pre>
<p>
If the operation is <b>not</b> successful the API will respond with an 4xx HTTP statuscode.
</p>
<table class="scode_table">
<thead>
<tr>
<th>Statuscode</th>
<th>Explanation</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Statuscode">200 (OK)</td>
<td data-label="Explanation">Message sent</td>
</tr>
<tr>
<td data-label="Statuscode">400 (Bad Request)</td>
<td data-label="Explanation">The request is invalid (missing parameters or wrong values)</td>
</tr>
<tr>
<td data-label="Statuscode">401 (Unauthorized)</td>
<td data-label="Explanation">The user_id was not found or the user_key is wrong</td>
</tr>
<tr>
<td data-label="Statuscode">403 (Forbidden)</td>
<td data-label="Explanation">The user has exceeded its daily quota - wait 24 hours or upgrade your account</td>
</tr>
<tr>
<td data-label="Statuscode">412 (Precondition Failed)</td>
<td data-label="Explanation">There is no device connected with this account - open the app and press the refresh button in the account tab</td>
</tr>
<tr>
<td data-label="Statuscode">500 (Internal Server Error)</td>
<td data-label="Explanation">There was an internal error while sending your data - try again later</td>
</tr>
</tbody>
</table>
<p>
There is also always a JSON payload with additional information.
The <code>success</code> field is always there and in the error state you the <code>message</code> field to get a descritpion of the problem.
</p>
<pre class="red-code">{
"success":false,
"error":2101,
"errhighlight":-1,
"message":"Daily quota reached (100)"
}</pre>
</div>
<h2>Message Content</h2>
<div class="section">
<p>
Every message must have a title set.
But you also (optionally) add more content, while the title has a max length of 120 characters, the conntent can be up to 10.000 characters.
You can see the whole message with title and content in the app or when clicking on the notification.
</p>
<p>
If needed the content can be supplied in the <code>content</code> parameter.
</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "title={message_title}" \
--data "content={message_content}" \
https://scn.blackforestbytes.com/send.php</pre>
</div>
<h2>Message Priority</h2>
<div class="section">
<p>
Currently you can send a message with three different priorities: 0 (low), 1 (normal) and 2 (high).
In the app you can then configure a different behaviour for different priorities, e.g. only playing a sound if the notification is high priority.
</p>
<p>
Priorites are either 0, 1 or 2 and are supplied in the <code>priority</code> parameter.
If no priority is supplied the message will get the default priority of 1.
</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "title={message_title}" \
--data "priority={0|1|2}" \
https://scn.blackforestbytes.com/send.php</pre>
</div>
<h2>Message Uniqueness</h2>
<div class="section">
<p>
Sometimes your script can run in an environment with an unstable connection and you want to implement an automatic re-try mechanism to send a message again if the last try failed due to bad connectivity.
</p>
<p>
To ensure that a message is only send once you can generate a unique id for your message (I would recommend a simple <code>uuidgen</code>).
If you send a message with an UUID that was already used in the near past the API still returns OK, but no new message is sent.
</p>
<p>
The message_id is optional - but if you want to use it you need to supply it via the <code>msg_id</code> parameter.
</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "title={message_title}" \
--data "msg_id={message_id}" \
https://scn.blackforestbytes.com/send.php</pre>
<p>
Be aware that the server only saves send messages for a short amount of time. Because of that you can only use this to prevent duplicates in a short time-frame, older messages with the same ID are probably already deleted and the message will be send again.
</p>
</div>
</div>
</body>
</html>

View File

@@ -22,12 +22,24 @@ body
#mainpnl #mainpnl
{ {
box-shadow: 0 0 32px #DDD; box-shadow: 0 0 32px #DDD;
animation:blink-shadow ease-in-out 4s infinite; //animation:blink-shadow ease-in-out 4s infinite;
width: 87%; width: 87%;
min-width: 300px; min-width: 300px;
max-width: 900px; max-width: 900px;
position: relative; position: relative;
min-height: 485px; min-height: 570px;
background: var(--form-back-color);
color: var(--form-fore-color);
border: .0625rem solid var(--form-border-color);
border-radius: var(--universal-border-radius);
margin: 32px .5rem;
padding: calc(2 * var(--universal-padding)) var(--universal-padding);
}
.red-code
{
border-left: .25rem solid #E53935;
} }
#mainpnl input, #mainpnl input,
@@ -74,13 +86,10 @@ body
position: fixed; position: fixed;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: -999; //z-index: -999;
} display: flex;
flex-direction: column;
#copyinfo a:hover text-align: right;
{
font-family: "Courier New", monospace;
color: #00F;
} }
#copyinfo a, #copyinfo a,
@@ -90,6 +99,14 @@ body
font-family: "Courier New", monospace; font-family: "Courier New", monospace;
color: #AAA; color: #AAA;
text-decoration: none; text-decoration: none;
display: block;
line-height: 1em;
}
#copyinfo a:hover
{
font-family: "Courier New", monospace;
color: #0288D1;
} }
#tr_link #tr_link
@@ -178,6 +195,12 @@ input::-webkit-inner-spin-button {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
align-content: center; align-content: center;
pointer-events: none;
}
.fullcenterflex .card
{
pointer-events: auto;
} }
a.card, a.card,
@@ -193,3 +216,23 @@ a.card:hover
{ {
box-shadow: 0 0 16px #AAA; box-shadow: 0 0 16px #AAA;
} }
table.scode_table {
max-height: none;
}
table.scode_table td:nth-child(2) {
flex-grow: 3;
}
table.scode_table th:nth-child(2) {
flex-grow: 3;
}
#mainpnl h2 {
margin-top: 1.75rem;
}
.linkcaption:hover {
text-decoration: none;
}

BIN
web/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -9,14 +9,22 @@
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">--> <!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favicon.png"/>
<link rel="icon" type="image/png" href="/favicon.ico"/>
</head> </head>
<body> <body>
<div id="copyinfo">
<a tabindex="-1" href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schw&ouml;rer</a>
</div>
<form id="mainpnl"> <form id="mainpnl">
<a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a> <a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a href="/index_api.php" class="button bordered" id="tr_link">API</a> <a tabindex="-1" href="/api.php" class="button bordered" id="tr_link">API</a>
<h1>Simple Cloud Notifier</h1> <a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div> <div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div>
@@ -46,7 +54,7 @@
<div class="row responsive-label"> <div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="txt" class="doc">Message Content</label></div> <div class="col-sm-12 col-md-3"><label for="txt" class="doc">Message Content</label></div>
<div class="col-sm-12 col-md"><textarea id="txt" class="doc" <?php echo (isset($_GET['preset_content']) ? (' value="'.$_GET['preset_content'].'" '):(''));?> rows="5"></textarea></div> <div class="col-sm-12 col-md"><textarea id="txt" class="doc" <?php echo (isset($_GET['preset_content']) ? (' value="'.$_GET['preset_content'].'" '):(''));?> rows="8"></textarea></div>
</div> </div>
<div class="row"> <div class="row">
@@ -55,10 +63,6 @@
</div> </div>
</form> </form>
<div id="copyinfo">
<a href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
</div>
<script src="/js/logic.js" type="text/javascript" ></script> <script src="/js/logic.js" type="text/javascript" ></script>
<script src="/js/toastify.js"></script> <script src="/js/toastify.js"></script>
</body> </body>

View File

@@ -1,39 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/css/mini-default.min.css">
<title>Simple Cloud Notifications - API</title>
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
<link rel="stylesheet" href="/css/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<form id="mainpnl">
<a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a href="/index.php" class="button bordered" id="tr_link">Send</a>
<h1>Simple Cloud Notifier</h1>
<p>Get your user-id and user-key from the app and send notifications to your phone by performing a POST request against <code>https://simplecloudnotifier.blackforestbytes.com/send.php</code></p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "priority={0|1|2}" \
--data "title={message_title}" \
--data "content={message_content}" \
https://scn.blackforestbytes.com/send.php</pre>
<p>The <code>content</code> and <code>priority</code> parameters are optional, you can also send message with only a title and the default priority</p>
<pre>curl \
--data "user_id={userid}" \
--data "user_key={userkey}" \
--data "title={message_title}" \
https://scn.blackforestbytes.com/send.php</pre>
</form>
<div id="copyinfo">
<a href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
</div>
</body>
</html>

View File

@@ -55,7 +55,7 @@ function send()
else else
{ {
window.location.href = window.location.href =
'/index_sent.php' + '/message_sent.php' +
'?ok=' + 1 + '?ok=' + 1 +
'&message_count=' + resp.messagecount + '&message_count=' + resp.messagecount +
'&quota=' + resp.quota + '&quota=' + resp.quota +

View File

@@ -3,16 +3,22 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Simple Cloud Notifications</title> <title>Simple Cloud Notifications</title>
<link rel="stylesheet" href="/css/mini-default.min.css"> <link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">--> <!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">--> <!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favicon.png"/>
<link rel="icon" type="image/png" href="/favicon.ico"/>
</head> </head>
<body> <body>
<div id="copyinfo">
<a tabindex="-1" href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schw&ouml;rer</a>
</div>
<form id="mainpnl"> <div id="mainpnl">
<div class="fullcenterflex"> <div class="fullcenterflex">
@@ -39,16 +45,12 @@
</div> </div>
<a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a> <a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a href="/index.php" class="button bordered" id="tr_link">Send</a> <a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
<h1>Simple Cloud Notifier</h1> <a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
</form> </div>
<div id="copyinfo">
<a href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
</div>
</body> </body>
</html> </html>

View File

@@ -1,73 +0,0 @@
<?php
include('lib/httpful.phar');
class Statics
{
public static $DB = NULL;
public static $CFG = NULL;
}
function getConfig()
{
if (Statics::$CFG !== NULL) return Statics::$CFG;
return Statics::$CFG = require "config.php";
}
function getDatabase()
{
if (Statics::$DB !== NULL) return Statics::$DB;
$_config = getConfig()['database'];
$dsn = "mysql:host=" . $_config['host'] . ";dbname=" . $_config['database'] . ";charset=utf8";
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
return Statics::$DB = new PDO($dsn, $_config['user'], $_config['password'], $opt);
}
function generateRandomAuthKey()
{
$random = '';
for ($i = 0; $i < 64; $i++)
try {
switch (random_int(1, 3)) {
case 1:
$random .= chr(random_int(ord('0'), ord('9')));
break;
case 2:
$random .= chr(random_int(ord('A'), ord('Z')));
break;
case 3:
$random .= chr(random_int(ord('a'), ord('z')));
break;
}
}
catch (Exception $e)
{
die(json_encode(['success' => false, 'message' => 'Internal error - no randomness']));
}
return $random;
}
function sendPOST($url, $body, $header)
{
$builder = \Httpful\Request::post($url);
$builder->body($body);
foreach ($header as $k => $v) $builder->addHeader($k, $v);
$response = $builder->send();
if ($response->code != 200) throw new Exception("Repsponse code: " . $response->code);
return $response->body;
}

View File

@@ -1,28 +0,0 @@
<?php
include_once 'model.php';
$INPUT = array_merge($_GET, $_POST);
if (!isset($INPUT['fcm_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[fcm_token]]']));
$fcmtoken = $INPUT['fcm_token'];
$user_key = generateRandomAuthKey();
$pdo = getDatabase();
$stmt = $pdo->prepare('INSERT INTO users (user_key, fcm_token, timestamp_accessed) VALUES (:key, :token, NOW())');
$stmt->execute(['key' => $user_key, 'token' => $fcmtoken]);
$user_id = $pdo->lastInsertId('user_id');
echo json_encode(
[
'success' => true,
'user_id' => $user_id,
'user_key' => $user_key,
'quota' => 0,
'quota_max'=> 100,
'message' => 'New user registered'
]);
return 0;

View File

@@ -1,15 +0,0 @@
CREATE TABLE `users`
(
`user_id` INT(11) NOT NULL AUTO_INCREMENT,
`user_key` VARCHAR(64) NOT NULL,
`fcm_token` VARCHAR(256) NULL DEFAULT NULL,
`messages_sent` INT(11) NOT NULL DEFAULT '0',
`timestamp_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`timestamp_accessed` DATETIME NULL DEFAULT NULL,
`quota_today` INT(11) NOT NULL DEFAULT '0',
`quota_day` DATE NULL DEFAULT NULL,
`quota_max` INT(11) NOT NULL DEFAULT '100',
PRIMARY KEY (`user_id`)
);

View File

@@ -1,100 +1,179 @@
<?php <?php
include_once 'model.php'; include_once 'api/model.php';
//------------------------------------------------------------------
sleep(1);
//------------------------------------------------------------------
$INPUT = array_merge($_GET, $_POST);
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'errhighlight' => 101, 'message' => 'Missing parameter [[user_id]]']));
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'errhighlight' => 102, 'message' => 'Missing parameter [[user_token]]']));
if (!isset($INPUT['title'])) die(json_encode(['success' => false, 'errhighlight' => 103, 'message' => 'Missing parameter [[title]]']));
//------------------------------------------------------------------
$user_id = $INPUT['user_id'];
$user_key = $INPUT['user_key'];
$message = $INPUT['title'];
$content = isset($INPUT['content']) ? $INPUT['content'] : '';
$priority = isset($INPUT['priority']) ? $INPUT['priority'] : '1';
//------------------------------------------------------------------
if ($priority !== '0' && $priority !== '1' && $priority !== '2') die(json_encode(['success' => false, 'errhighlight' => 105, 'message' => 'Invalid priority']));
if (strlen(trim($message)) == 0) die(json_encode(['success' => false, 'errhighlight' => 103, 'message' => 'No title specified']));
if (strlen($message) > 120) die(json_encode(['success' => false, 'errhighlight' => 103, 'message' => 'Title too long (120 characters)']));
if (strlen($content) > 10000) die(json_encode(['success' => false, 'errhighlight' => 104, 'message' => 'Content too long (10000 characters)']));
//------------------------------------------------------------------
$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->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($datas)<=0) die(json_encode(['success' => false, 'errhighlight' => 101, 'message' => 'User not found']));
$data = $datas[0];
if ($data === null) die(json_encode(['success' => false, 'errhighlight' => 101, 'message' => 'User not found']));
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'errhighlight' => 101, 'message' => 'UserID not found']));
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errhighlight' => 102, 'message' => 'Authentification failed']));
$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'].')']));
//------------------------------------------------------------------
$url = "https://fcm.googleapis.com/fcm/send";
$payload = json_encode(
[
'to' => $fcm,
//'dry_run' => true,
'android' => [ 'priority' => 'high' ],
//'notification' =>
//[
// 'title' => $message,
// 'body' => $content,
//],
'data' =>
[
'title' => $message,
'body' => $content,
'priority' => $priority,
'timestamp' => time(),
]
]);
$header=
[
'Authorization' => 'key=' . getConfig()['firebase']['server_key'],
'Content-Type' => 'application/json',
];
try try
{ {
$httpresult = sendPOST($url, $payload, $header);
//------------------------------------------------------------------
//sleep(1);
//------------------------------------------------------------------
if ($_SERVER['REQUEST_METHOD'] !== 'POST') api_return(400, ['success' => false, 'error' => ERR::REQ_METHOD, 'errhighlight' => -1, 'message' => 'Invalid request method (must be POST)']);
$INPUT = array_merge($_GET, $_POST);
if (!isset($INPUT['user_id'])) api_return(400, ['success' => false, 'error' => ERR::MISSING_UID, 'errhighlight' => 101, 'message' => 'Missing parameter [[user_id]]']);
if (!isset($INPUT['user_key'])) api_return(400, ['success' => false, 'error' => ERR::MISSING_TOK, 'errhighlight' => 102, 'message' => 'Missing parameter [[user_token]]']);
if (!isset($INPUT['title'])) api_return(400, ['success' => false, 'error' => ERR::MISSING_TITLE, 'errhighlight' => 103, 'message' => 'Missing parameter [[title]]']);
//------------------------------------------------------------------
$user_id = $INPUT['user_id'];
$user_key = $INPUT['user_key'];
$message = $INPUT['title'];
$content = isset($INPUT['content']) ? $INPUT['content'] : '';
$priority = isset($INPUT['priority']) ? $INPUT['priority'] : '1';
$usrmsgid = isset($INPUT['msg_id']) ? $INPUT['msg_id'] : null;
//------------------------------------------------------------------
if ($priority !== '0' && $priority !== '1' && $priority !== '2') api_return(400, ['success' => false, 'error' => ERR::INVALID_PRIO, 'errhighlight' => 105, 'message' => 'Invalid priority']);
if (strlen(trim($message)) == 0) api_return(400, ['success' => false, 'error' => ERR::NO_TITLE, 'errhighlight' => 103, 'message' => 'No title specified']);
if (strlen($message) > 120) api_return(400, ['success' => false, 'error' => ERR::TITLE_TOO_LONG, 'errhighlight' => 103, 'message' => 'Title too long (120 characters)']);
if (strlen($content) > 10000) api_return(400, ['success' => false, 'error' => ERR::CONTENT_TOO_LONG, 'errhighlight' => 104, 'message' => 'Content too long (10000 characters)']);
if ($usrmsgid != null && strlen($usrmsgid) > 64) api_return(400, ['success' => false, 'error' => ERR::USR_MSG_ID_TOO_LONG, 'errhighlight' => -1, 'message' => 'MessageID too long (64 characters)']);
//------------------------------------------------------------------
$pdo = getDatabase();
$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);
if (count($datas)<=0) die(json_encode(['success' => false, 'error' => ERR::USER_NOT_FOUND, 'errhighlight' => 101, 'message' => 'User not found']));
$data = $datas[0];
if ($data === null) api_return(401, ['success' => false, 'error' => ERR::USER_NOT_FOUND, 'errhighlight' => 101, 'message' => 'User not found']);
if ($data['user_id'] !== (int)$user_id) api_return(401, ['success' => false, 'error' => ERR::USER_NOT_FOUND, 'errhighlight' => 101, 'message' => 'UserID not found']);
if ($data['user_key'] !== $user_key) api_return(401, ['success' => false, 'error' => ERR::USER_AUTH_FAILED, 'errhighlight' => 102, 'message' => 'Authentification failed']);
$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 > Statics::quota_max($data['is_pro'])) api_return(403, ['success' => false, 'error' => ERR::QUOTA_REACHED, 'errhighlight' => -1, 'message' => 'Daily quota reached ('.Statics::quota_max($data['is_pro']).')']);
if ($fcm == null || $fcm == '' || $fcm == false)
{
api_return(412, ['success' => false, 'error' => ERR::NO_DEVICE_LINKED, 'errhighlight' => -1, 'message' => 'No device linked with this account']);
}
//------------------------------------------------------------------
if ($usrmsgid != null)
{
$stmt = $pdo->prepare('SELECT scn_message_id FROM messages WHERE sender_user_id=:uid AND usr_message_id IS NOT NULL AND usr_message_id=:umid LIMIT 1');
$stmt->execute(['uid' => $user_id, 'umid' => $usrmsgid]);
if (count($stmt->fetchAll(PDO::FETCH_ASSOC))>0)
{
api_return(200,
[
'success' => true,
'message' => 'Message already sent',
'suppress_send' => true,
'response' => '',
'messagecount' => $data['messages_sent']+1,
'quota' => $data['quota_today'],
'is_pro' => $data['is_pro'],
'quota_max' => Statics::quota_max($data['is_pro']),
]);
}
}
//------------------------------------------------------------------
$pdo->beginTransaction();
$stmt = $pdo->prepare('INSERT INTO messages (sender_user_id, title, content, priority, fcm_message_id, usr_message_id) VALUES (:suid, :t, :c, :p, :fmid, :umid)');
$stmt->execute(
[
'suid' => $user_id,
't' => $message,
'c' => $content,
'p' => $priority,
'fmid' => null,
'umid' => $usrmsgid,
]);
$scn_msg_id = $pdo->lastInsertId();
$url = "https://fcm.googleapis.com/fcm/send";
$payload = json_encode(
[
'to' => $fcm,
//'dry_run' => true,
'android' => [ 'priority' => 'high' ],
//'notification' =>
//[
// 'title' => $message,
// 'body' => $content,
//],
'data' =>
[
'title' => $message,
'body' => $content,
'priority' => $priority,
'timestamp' => time(),
'usr_msg_id' => $usrmsgid,
'scn_msg_id' => $scn_msg_id,
]
]);
$header=
[
'Authorization' => 'key=' . getConfig()['firebase']['server_key'],
'Content-Type' => 'application/json',
];
try
{
$httpresult = sendPOST($url, $payload, $header);
if (try_json($httpresult, ['success']) != 1)
{
reportError("FCM communication failed (success_1 <> true)\n\n".$httpresult);
$pdo->rollBack();
api_return(500, ['success' => false, 'error' => ERR::FIREBASE_COM_ERRORED, 'errhighlight' => -1, 'message' => 'Communication with firebase service failed.']);
}
}
catch (Exception $e)
{
reportError("FCM communication failed", $e);
$pdo->rollBack();
api_return(500, ['success' => false, 'error' => ERR::FIREBASE_COM_FAILED, 'errhighlight' => -1, 'message' => 'Communication with firebase service failed.'."\n\n".'Exception: ' . $e->getMessage()]);
}
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), messages_sent=messages_sent+1, quota_today=:q, quota_day=NOW() WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id, 'q' => $new_quota]);
$stmt = $pdo->prepare('UPDATE messages SET fcm_message_id=:fmid WHERE scn_message_id=:smid');
$stmt->execute([ 'fmid' => try_json($httpresult, ['results', 0, 'message_id']), 'smid' => $scn_msg_id ]);
$pdo->commit();
api_return(200,
[
'success' => true,
'error' => ERR::NO_ERROR,
'errhighlight' => -1,
'message' => 'Message sent',
'suppress_send' => false,
'response' => $httpresult,
'messagecount' => $data['messages_sent']+1,
'quota' => $new_quota,
'is_pro' => $data['is_pro'],
'quota_max' => Statics::quota_max($data['is_pro']),
'scn_msg_id' => $scn_msg_id,
]);
} }
catch (Exception $e) catch (Exception $mex)
{ {
die(json_encode(['success' => false, 'message' => 'Exception: ' . $e->getMessage()])); reportError("Root try-catch triggered", $mex);
if ($pdo->inTransaction()) $pdo->rollBack();
api_return(500, ['success' => false, 'error' => ERR::INTERNAL_EXCEPTION, 'errhighlight' => -1, 'message' => 'PHP script threw exception.'."\n\n".'Exception: ' . $e->getMessage()]);
} }
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), messages_sent=messages_sent+1, quota_today=:q, quota_day=NOW() WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id, 'q' => $new_quota]);
echo (json_encode(
[
'success' => true,
'message' => 'Message sent',
'response' => $httpresult,
'messagecount' => $data['messages_sent']+1,
'quota'=>$new_quota,
'quota_max'=>$data['quota_max'],
]));
return 0;