22 Commits

Author SHA1 Message Date
b703853b28 send.bash example 2018-11-17 14:26:29 +01:00
6f7529fc9b fixed duplicate-recieve 2018-11-17 14:01:51 +01:00
27b45098f0 ups 2018-11-17 13:46:23 +01:00
21f66d99cd fixed timestamp format in requery 2018-11-17 13:44:42 +01:00
1745051e6e fixed info.php return value 2018-11-17 12:51:20 +01:00
jenkins
b395e054e6 [Jenkins] Increment version 2018-11-17 03:25:54 +01:00
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
49 changed files with 1740 additions and 540 deletions

View File

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

View File

@@ -36,6 +36,8 @@
</intent-filter>
</service>
<receiver android:name=".service.BroadcastReceiverService" android:exported="false" />
</application>
</manifest>

View File

@@ -19,7 +19,7 @@ import androidx.lifecycle.ProcessLifecycleOwner;
public class SCNApp extends Application implements LifecycleObserver
{
private static SCNApp instance;
private static WeakReference<MainActivity> mainActivity;
private static WeakReference<MainActivity> mainActivity = new WeakReference<>(null);
public static final boolean LOCAL_DEBUG = BuildConfig.DEBUG;
public static final boolean DEBUG = BuildConfig.DEBUG || !BuildConfig.VERSION_NAME.endsWith(".0");
@@ -102,33 +102,11 @@ public class SCNApp extends Application implements LifecycleObserver
/*
==TODO==
[X] - Pro mode
[X] - no ads
[X] - more quota
[X] - restore pro mode
[X] - send pro state to server
[X] - prevent duplicate-send
[X] - send custom msg-id in API
[X] - prevent second ack on same msg-id
[X] - more in-depth API doc on website (?)
[X] - perhaps response codes in api (?)
[X] - verify recieve
[ ] - Android O repeat sound
[ ] - test notification channels
[ ] - startup time
[ ] - periodically get non-ack (option - even when not in-app)
[ ] - publish (+ HN post ?)
[ ] - publish (+ HN post ?)
[ ] - Use for mscom server errrors
[ ] - Use for bfb server errors
[ ] - Use for transmission state
[ ] - Message on connnection lost (seperate process - resend until succ)
[ ] - Message on connnection regained
[ ] - Message on seed-count changed
*/

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

@@ -3,15 +3,19 @@ package com.blackforestbytes.simplecloudnotifier.model;
import android.content.Context;
import android.content.SharedPreferences;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.view.MessageAdapter;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
public class CMessageList
{
public ArrayList<CMessage> Messages;
public Set<String> AllAcks;
private ArrayList<WeakReference<MessageAdapter>> _listener = new ArrayList<>();
@@ -29,6 +33,7 @@ public class CMessageList
private CMessageList()
{
Messages = new ArrayList<>();
AllAcks = new HashSet<>();
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("CMessageList", Context.MODE_PRIVATE);
int count = sharedPref.getInt("message_count", 0);
@@ -42,6 +47,8 @@ public class CMessageList
Messages.add(new CMessage(scnid, time, title, content, prio));
}
AllAcks = sharedPref.getStringSet("acks", new HashSet<>());
}
public CMessage add(final long scnid, final long time, final String title, final String content, final PriorityEnum pe)
@@ -56,6 +63,7 @@ public class CMessageList
SharedPreferences.Editor e = sharedPref.edit();
Messages.add(msg);
AllAcks.add(Long.toHexString(msg.SCN_ID));
while (Messages.size()>SCNSettings.inst().LocalCacheSize) Messages.remove(0);
@@ -66,6 +74,8 @@ public class CMessageList
e.putInt( "message["+count+"].priority", pe.ID);
e.putLong( "message["+count+"].scnid", scnid);
e.putStringSet("acks", AllAcks);
e.apply();
for (WeakReference<MessageAdapter> ref : _listener)
@@ -73,6 +83,7 @@ public class CMessageList
MessageAdapter a = ref.get();
if (a == null) continue;
a.customNotifyItemInserted(count);
a.scrollToTop();
}
CleanUpListener();
});
@@ -80,6 +91,7 @@ public class CMessageList
if (!run)
{
Messages.add(new CMessage(scnid, time, title, content, pe));
AllAcks.add(Long.toHexString(msg.SCN_ID));
fullSave();
}
@@ -118,6 +130,8 @@ public class CMessageList
e.putLong( "message["+i+"].scnid", Messages.get(i).SCN_ID);
}
e.putStringSet("acks", AllAcks);
e.apply();
}
@@ -127,6 +141,11 @@ public class CMessageList
return Messages.get(pos);
}
public CMessage tryGetFromBack(int pos)
{
return tryGet(Messages.size() - pos - 1);
}
public int size()
{
return Messages.size();
@@ -145,4 +164,21 @@ public class CMessageList
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

@@ -11,6 +11,9 @@ public class NotificationSettings
public String SoundSource;
public boolean RepeatSound;
public boolean ForceVolume;
public int ForceVolumeValue;
public boolean EnableLED;
public int LEDColor;
@@ -18,12 +21,14 @@ public class NotificationSettings
public NotificationSettings(PriorityEnum p)
{
EnableSound = (p == PriorityEnum.HIGH);
SoundName = (p == PriorityEnum.HIGH) ? "Default" : "";
SoundSource = (p == PriorityEnum.HIGH) ? RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION).toString() : Uri.EMPTY.toString();
RepeatSound = false;
EnableLED = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL);
LEDColor = Color.BLUE;
EnableVibration = (p == PriorityEnum.HIGH) || (p == PriorityEnum.NORMAL);
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

@@ -71,29 +71,35 @@ public class SCNSettings
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.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.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.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()
@@ -101,39 +107,45 @@ public class SCNSettings
SharedPreferences sharedPref = SCNApp.getContext().getSharedPreferences("Config", Context.MODE_PRIVATE);
SharedPreferences.Editor e = sharedPref.edit();
e.putInt( "quota_curr", quota_curr);
e.putInt( "quota_max", quota_max);
e.putInt( "user_id", user_id);
e.putString( "user_key", user_key);
e.putString( "fcm_token_local", fcm_token_local);
e.putString( "fcm_token_server", fcm_token_server);
e.putInt( "quota_curr", quota_curr);
e.putInt( "quota_max", quota_max);
e.putInt( "user_id", user_id);
e.putString( "user_key", user_key);
e.putString( "fcm_token_local", fcm_token_local);
e.putString( "fcm_token_server", fcm_token_server);
e.putBoolean("app_enabled", Enabled);
e.putInt( "local_cache_size", LocalCacheSize);
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: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: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: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();
}
@@ -155,7 +167,7 @@ public class SCNSettings
{
fcm_token_local = token;
save();
if (!fcm_token_local.equals(fcm_token_server)) ServerCommunication.update(user_id, user_key, fcm_token_local, loader);
if (!fcm_token_local.equals(fcm_token_server)) ServerCommunication.updateFCMToken(user_id, user_key, fcm_token_local, loader);
}
else
{
@@ -187,7 +199,7 @@ public class SCNSettings
{
if (!isConnected()) return;
ServerCommunication.update(user_id, user_key, loader);
ServerCommunication.resetSecret(user_id, user_key, loader);
}
// refresh account data

View File

@@ -5,7 +5,9 @@ import android.view.View;
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.JSONTokener;
@@ -22,7 +24,7 @@ import okhttp3.ResponseBody;
public class ServerCommunication
{
public static final String BASE_URL = /*SCNApp.LOCAL_DEBUG ? "http://localhost:1010/" : */"https://scn.blackforestbytes.com/";
public static final String BASE_URL = /*SCNApp.LOCAL_DEBUG ? "http://localhost:1010/" : */"https://scn.blackforestbytes.com/api/";
private static final OkHttpClient client = new OkHttpClient();
@@ -94,12 +96,12 @@ public class ServerCommunication
}
}
public static void update(int id, String key, String token, View loader)
public static void updateFCMToken(int id, String key, String token, View loader)
{
try
{
Request request = new Request.Builder()
.url(BASE_URL + "update.php?user_id="+id+"&user_key="+key+"&fcm_token="+token)
.url(BASE_URL + "updateFCMToken.php?user_id="+id+"&user_key="+key+"&fcm_token="+token)
.build();
client.newCall(request).enqueue(new Callback()
@@ -160,12 +162,12 @@ public class ServerCommunication
}
}
public static void update(int id, String key, View loader)
public static void resetSecret(int id, String key, View loader)
{
try
{
Request request = new Request.Builder()
.url(BASE_URL + "update.php?user_id=" + id + "&user_key=" + key)
.url(BASE_URL + "updateFCMToken.php?user_id=" + id + "&user_key=" + key)
.build();
client.newCall(request).enqueue(new Callback() {
@@ -280,6 +282,9 @@ public class ServerCommunication
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);
@@ -298,6 +303,75 @@ public class ServerCommunication
}
}
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);
}
} 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
@@ -355,12 +429,12 @@ public class ServerCommunication
}
}
public static void ack(int id, String key, CMessage msg)
public static void ack(int id, String key, long msg_scn_id)
{
try
{
Request request = new Request.Builder()
.url(BASE_URL + "ack.php?user_id=" + id + "&user_key=" + key + "&scn_msg_id=" + msg.SCN_ID)
.url(BASE_URL + "ack.php?user_id=" + id + "&user_key=" + key + "&scn_msg_id=" + msg_scn_id)
.build();
client.newCall(request).enqueue(new Callback() {
@@ -370,8 +444,24 @@ public class ServerCommunication
}
@Override
public void onResponse(Call call, Response response) {
// ????
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);
}
}
});
}
@@ -396,6 +486,11 @@ public class ServerCommunication
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

@@ -39,18 +39,7 @@ public class FBMService extends FirebaseMessagingService
PriorityEnum prio = PriorityEnum.parseAPI(remoteMessage.getData().get("priority"));
long scn_id = Long.parseLong(remoteMessage.getData().get("scn_msg_id"));
CMessage msg = CMessageList.inst().add(scn_id, time, title, content, prio);
if (SCNApp.isBackground())
{
NotificationService.inst().showBackground(msg);
}
else
{
NotificationService.inst().showForeground(msg);
}
ServerCommunication.ack(SCNSettings.inst().user_id, SCNSettings.inst().user_key, msg);
recieveData(time, title, content, prio, scn_id, false);
}
catch (Exception e)
{
@@ -58,4 +47,27 @@ public class FBMService extends FirebaseMessagingService
SCNApp.showToast("Recieved invalid message from server", Toast.LENGTH_LONG);
}
}
public static void recieveData(long time, String title, String content, PriorityEnum prio, long scn_id, boolean alwaysAck)
{
if (CMessageList.inst().isAck(scn_id))
{
Log.w("FB::MessageReceived", "Recieved ack-ed message: " + scn_id);
if (alwaysAck) ServerCommunication.ack(SCNSettings.inst().user_id, SCNSettings.inst().user_key, scn_id);
return;
}
CMessage msg = CMessageList.inst().add(scn_id, time, title, content, prio);
if (SCNApp.isBackground())
{
NotificationService.inst().showBackground(msg);
}
else
{
NotificationService.inst().showForeground(msg);
}
ServerCommunication.ack(SCNSettings.inst().user_id, SCNSettings.inst().user_key, scn_id);
}
}

View File

@@ -6,10 +6,8 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.VibrationEffect;
@@ -18,17 +16,21 @@ import android.widget.Toast;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
import com.blackforestbytes.simplecloudnotifier.model.NotificationSettings;
import com.blackforestbytes.simplecloudnotifier.model.PriorityEnum;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
public class NotificationService
{
private final static String CHANNEL_ID = "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 static NotificationService _inst = null;
@@ -43,10 +45,10 @@ public class NotificationService
private NotificationService()
{
updateChannels();
createChannels();
}
public void updateChannels()
private void createChannels()
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
@@ -54,14 +56,41 @@ public class NotificationService
NotificationManager notifman = ctxt.getSystemService(NotificationManager.class);
if (notifman == null) return;
NotificationChannel channel = notifman.getNotificationChannel(CHANNEL_ID);
if (channel == null)
{
channel = new NotificationChannel(CHANNEL_ID, "Push notifications", NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("Push notifications from the server");
channel.setSound(null, null);
channel.setVibrationPattern(null);
notifman.createNotificationChannel(channel);
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);
}
}
}
@@ -79,13 +108,7 @@ public class NotificationService
case HIGH: ns = SCNSettings.inst().PriorityHigh; break;
}
if (ns.EnableSound && !ns.SoundSource.isEmpty())
{
Ringtone rt = RingtoneManager.getRingtone(SCNApp.getContext(), Uri.parse(ns.SoundSource));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) rt.setLooping(false);
rt.play();
new Thread(() -> { try { Thread.sleep(5*1000); } catch (InterruptedException e) { /* */ } rt.stop(); }).start();
}
SoundService.play(ns.EnableSound, ns.SoundSource, ns.ForceVolume, ns.ForceVolumeValue, false);
if (ns.EnableVibration)
{
@@ -118,75 +141,116 @@ public class NotificationService
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
{
// old
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, CHANNEL_ID);
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()) mBuilder.setSound(Uri.parse(ns.SoundSource), AudioManager.STREAM_NOTIFICATION);
Intent intent = new Intent(ctxt, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0);
mBuilder.setContentIntent(pi);
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
Notification n = mBuilder.build();
if (ns.EnableSound && !ns.SoundSource.isEmpty() && ns.RepeatSound) n.flags |= Notification.FLAG_INSISTENT;
if (mNotificationManager != null) mNotificationManager.notify(0, n);
showBackground_old(msg, ctxt, ns, msg.Priority);
}
else
{
// new
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(ctxt, CHANNEL_ID);
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 intent = new Intent(ctxt, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0);
mBuilder.setContentIntent(pi);
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
if (mNotificationManager == null) return;
Notification n = mBuilder.build();
n.flags |= Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(0, n);
if (ns.EnableSound && !ns.SoundSource.isEmpty())
{
Ringtone rt = RingtoneManager.getRingtone(SCNApp.getContext(), Uri.parse(ns.SoundSource));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) rt.setLooping(false);
rt.play();
new Thread(() -> { try { Thread.sleep(5*1000); } catch (InterruptedException e) { /* */ } rt.stop(); }).start();
}
if (ns.EnableVibration)
{
Vibrator v = (Vibrator) SCNApp.getContext().getSystemService(Context.VIBRATOR_SERVICE);
v.vibrate(VibrationEffect.createOneShot(1500, VibrationEffect.DEFAULT_AMPLITUDE));
}
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);
PendingIntent pi = PendingIntent.getActivity(ctxt, 0, intent, 0);
mBuilder.setContentIntent(pi);
NotificationManager mNotificationManager = (NotificationManager) ctxt.getSystemService(Context.NOTIFICATION_SERVICE);
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,6 +1,7 @@
package com.blackforestbytes.simplecloudnotifier.view;
import android.os.Bundle;
import android.widget.RelativeLayout;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
@@ -18,6 +19,7 @@ import androidx.viewpager.widget.ViewPager;
public class MainActivity extends AppCompatActivity
{
public TabAdapter adpTabs;
public RelativeLayout layoutRoot;
@Override
protected void onCreate(Bundle savedInstanceState)
@@ -28,6 +30,8 @@ public class MainActivity extends AppCompatActivity
NotificationService.inst();
CMessageList.inst();
layoutRoot = findViewById(R.id.layoutRoot);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@@ -39,6 +43,22 @@ public class MainActivity extends AppCompatActivity
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
tabLayout.setupWithViewPager(viewPager);
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);

View File

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

View File

@@ -1,24 +1,32 @@
package com.blackforestbytes.simplecloudnotifier.view;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.model.CMessage;
import com.blackforestbytes.simplecloudnotifier.model.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;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class NotificationsFragment extends Fragment
public class NotificationsFragment extends Fragment implements MessageAdapterTouchHelper.MessageAdapterTouchHelperListener
{
private PublisherAdView adView;
private MessageAdapter adpMessages;
public NotificationsFragment()
{
@@ -31,8 +39,12 @@ public class NotificationsFragment extends Fragment
View v = inflater.inflate(R.layout.fragment_notifications, container, false);
RecyclerView rvMessages = v.findViewById(R.id.rvMessages);
rvMessages.setLayoutManager(new LinearLayoutManager(this.getContext(), RecyclerView.VERTICAL, true));
rvMessages.setAdapter(new MessageAdapter(v.findViewById(R.id.tvNoElements)));
LinearLayoutManager lman = new LinearLayoutManager(this.getContext(), RecyclerView.VERTICAL, false);
rvMessages.setLayoutManager(lman);
rvMessages.setAdapter(adpMessages = new MessageAdapter(v.findViewById(R.id.tvNoElements), lman, rvMessages));
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();
@@ -47,4 +59,23 @@ public class NotificationsFragment extends Fragment
{
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,9 +1,16 @@
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.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -11,18 +18,24 @@ 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.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;
import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import top.defaults.colorpicker.ColorPickerPopup;
@@ -45,6 +58,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
private View prefMsgLowLedColor_container;
private ImageView prefMsgLowLedColor_value;
private Switch prefMsgLowEnableVibrations;
private Switch prefMsgLowForceVolume;
private SeekBar prefMsgLowVolume;
private ImageView prefMsgLowVolumeTest;
private Switch prefMsgNormEnableSound;
private TextView prefMsgNormRingtone_value;
@@ -54,6 +70,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
private View prefMsgNormLedColor_container;
private ImageView prefMsgNormLedColor_value;
private Switch prefMsgNormEnableVibrations;
private Switch prefMsgNormForceVolume;
private SeekBar prefMsgNormVolume;
private ImageView prefMsgNormVolumeTest;
private Switch prefMsgHighEnableSound;
private TextView prefMsgHighRingtone_value;
@@ -63,9 +82,14 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
private View prefMsgHighLedColor_container;
private ImageView prefMsgHighLedColor_value;
private Switch prefMsgHighEnableVibrations;
private Switch prefMsgHighForceVolume;
private SeekBar prefMsgHighVolume;
private ImageView prefMsgHighVolumeTest;
private int musicPickerSwitch = -1;
private MediaPlayer[] mPlayers = new MediaPlayer[3];
public SettingsFragment()
{
// Required empty public constructor
@@ -99,6 +123,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefMsgLowLedColor_value = v.findViewById(R.id.prefMsgLowLedColor_value);
prefMsgLowLedColor_container = v.findViewById(R.id.prefMsgLowLedColor_container);
prefMsgLowEnableVibrations = v.findViewById(R.id.prefMsgLowEnableVibrations);
prefMsgLowForceVolume = v.findViewById(R.id.prefMsgLowForceVolume);
prefMsgLowVolume = v.findViewById(R.id.prefMsgLowVolume);
prefMsgLowVolumeTest = v.findViewById(R.id.btnLowVolumeTest);
prefMsgNormEnableSound = v.findViewById(R.id.prefMsgNormEnableSound);
prefMsgNormRingtone_value = v.findViewById(R.id.prefMsgNormRingtone_value);
@@ -108,6 +135,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefMsgNormLedColor_value = v.findViewById(R.id.prefMsgNormLedColor_value);
prefMsgNormLedColor_container = v.findViewById(R.id.prefMsgNormLedColor_container);
prefMsgNormEnableVibrations = v.findViewById(R.id.prefMsgNormEnableVibrations);
prefMsgNormForceVolume = v.findViewById(R.id.prefMsgNormForceVolume);
prefMsgNormVolume = v.findViewById(R.id.prefMsgNormVolume);
prefMsgNormVolumeTest = v.findViewById(R.id.btnNormVolumeTest);
prefMsgHighEnableSound = v.findViewById(R.id.prefMsgHighEnableSound);
prefMsgHighRingtone_value = v.findViewById(R.id.prefMsgHighRingtone_value);
@@ -117,6 +147,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefMsgHighLedColor_value = v.findViewById(R.id.prefMsgHighLedColor_value);
prefMsgHighLedColor_container = v.findViewById(R.id.prefMsgHighLedColor_container);
prefMsgHighEnableVibrations = v.findViewById(R.id.prefMsgHighEnableVibrations);
prefMsgHighForceVolume = v.findViewById(R.id.prefMsgHighForceVolume);
prefMsgHighVolume = v.findViewById(R.id.prefMsgHighVolume);
prefMsgHighVolumeTest = v.findViewById(R.id.btnHighVolumeTest);
}
private void updateUI()
@@ -142,6 +175,12 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
if (prefMsgLowEnableLED.isChecked() != s.PriorityLow.EnableLED) prefMsgLowEnableLED.setChecked(s.PriorityLow.EnableLED);
prefMsgLowLedColor_value.setColorFilter(s.PriorityLow.LEDColor);
if (prefMsgLowEnableVibrations.isChecked() != s.PriorityLow.EnableVibration) prefMsgLowEnableVibrations.setChecked(s.PriorityLow.EnableVibration);
if (prefMsgLowForceVolume.isChecked() != s.PriorityLow.ForceVolume) prefMsgLowForceVolume.setChecked(s.PriorityLow.ForceVolume);
if (prefMsgLowVolume.getMax() != 100) prefMsgLowVolume.setMax(100);
if (prefMsgLowVolume.getProgress() != s.PriorityLow.ForceVolumeValue) prefMsgLowVolume.setProgress(s.PriorityLow.ForceVolumeValue);
if (prefMsgLowVolume.isEnabled() != s.PriorityLow.ForceVolume) prefMsgLowVolume.setEnabled(s.PriorityLow.ForceVolume);
if (prefMsgLowVolumeTest.isEnabled() != s.PriorityLow.ForceVolume) prefMsgLowVolumeTest.setEnabled(s.PriorityLow.ForceVolume);
if (s.PriorityLow.ForceVolume) prefMsgLowVolumeTest.setColorFilter(null); else prefMsgLowVolumeTest.setColorFilter(Color.argb(150,200,200,200));
if (prefMsgNormEnableSound.isChecked() != s.PriorityNorm.EnableSound) prefMsgNormEnableSound.setChecked(s.PriorityNorm.EnableSound);
if (!prefMsgNormRingtone_value.getText().equals(s.PriorityNorm.SoundName)) prefMsgNormRingtone_value.setText(s.PriorityNorm.SoundName);
@@ -149,6 +188,12 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
if (prefMsgNormEnableLED.isChecked() != s.PriorityNorm.EnableLED) prefMsgNormEnableLED.setChecked(s.PriorityNorm.EnableLED);
prefMsgNormLedColor_value.setColorFilter(s.PriorityNorm.LEDColor);
if (prefMsgNormEnableVibrations.isChecked() != s.PriorityNorm.EnableVibration) prefMsgNormEnableVibrations.setChecked(s.PriorityNorm.EnableVibration);
if (prefMsgNormForceVolume.isChecked() != s.PriorityNorm.ForceVolume) prefMsgNormForceVolume.setChecked(s.PriorityNorm.ForceVolume);
if (prefMsgNormVolume.getMax() != 100) prefMsgNormVolume.setMax(100);
if (prefMsgNormVolume.getProgress() != s.PriorityNorm.ForceVolumeValue) prefMsgNormVolume.setProgress(s.PriorityNorm.ForceVolumeValue);
if (prefMsgNormVolume.isEnabled() != s.PriorityNorm.ForceVolume) prefMsgNormVolume.setEnabled(s.PriorityNorm.ForceVolume);
if (prefMsgNormVolumeTest.isEnabled() != s.PriorityNorm.ForceVolume) prefMsgNormVolumeTest.setEnabled(s.PriorityNorm.ForceVolume);
if (s.PriorityNorm.ForceVolume) prefMsgNormVolumeTest.setColorFilter(null); else prefMsgNormVolumeTest.setColorFilter(Color.argb(150,200,200,200));
if (prefMsgHighEnableSound.isChecked() != s.PriorityHigh.EnableSound) prefMsgHighEnableSound.setChecked(s.PriorityHigh.EnableSound);
if (!prefMsgHighRingtone_value.getText().equals(s.PriorityHigh.SoundName)) prefMsgHighRingtone_value.setText(s.PriorityHigh.SoundName);
@@ -156,6 +201,12 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
if (prefMsgHighEnableLED.isChecked() != s.PriorityHigh.EnableLED) prefMsgHighEnableLED.setChecked(s.PriorityHigh.EnableLED);
prefMsgHighLedColor_value.setColorFilter(s.PriorityHigh.LEDColor);
if (prefMsgHighEnableVibrations.isChecked() != s.PriorityHigh.EnableVibration) prefMsgHighEnableVibrations.setChecked(s.PriorityHigh.EnableVibration);
if (prefMsgHighForceVolume.isChecked() != s.PriorityHigh.ForceVolume) prefMsgHighForceVolume.setChecked(s.PriorityHigh.ForceVolume);
if (prefMsgHighVolume.getMax() != 100) prefMsgHighVolume.setMax(100);
if (prefMsgHighVolume.getProgress() != s.PriorityHigh.ForceVolumeValue) prefMsgHighVolume.setProgress(s.PriorityHigh.ForceVolumeValue);
if (prefMsgHighVolume.isEnabled() != s.PriorityHigh.ForceVolume) prefMsgHighVolume.setEnabled(s.PriorityHigh.ForceVolume);
if (prefMsgHighVolumeTest.isEnabled() != s.PriorityHigh.ForceVolume) prefMsgHighVolumeTest.setEnabled(s.PriorityHigh.ForceVolume);
if (s.PriorityHigh.ForceVolume) prefMsgHighVolumeTest.setColorFilter(null); else prefMsgHighVolumeTest.setColorFilter(Color.argb(150,200,200,200));
}
private void initListener()
@@ -181,6 +232,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefMsgLowEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableLED=b; saveAndUpdate(); });
prefMsgLowLedColor_container.setOnClickListener(a -> chooseLEDColorLow());
prefMsgLowEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.EnableVibration=b; saveAndUpdate(); });
prefMsgLowForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityLow.ForceVolume=b; saveAndUpdate(); });
prefMsgLowVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityLow.ForceVolumeValue=b; saveAndUpdate(); updateVolume(0, b); } }));
prefMsgLowVolumeTest.setOnClickListener((v) -> { if (s.PriorityLow.ForceVolume) playTestSound(0, prefMsgLowVolumeTest, s.PriorityLow.SoundSource, s.PriorityLow.ForceVolumeValue); });
prefMsgNormEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableSound=b; saveAndUpdate(); });
prefMsgNormRingtone_container.setOnClickListener(a -> chooseRingtoneNorm());
@@ -188,6 +242,9 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefMsgNormEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableLED=b; saveAndUpdate(); });
prefMsgNormLedColor_container.setOnClickListener(a -> chooseLEDColorNorm());
prefMsgNormEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.EnableVibration=b; saveAndUpdate(); });
prefMsgNormForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityNorm.ForceVolume=b; saveAndUpdate(); });
prefMsgNormVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityNorm.ForceVolumeValue=b; saveAndUpdate(); updateVolume(1, b); } }));
prefMsgNormVolumeTest.setOnClickListener((v) -> { if (s.PriorityNorm.ForceVolume) playTestSound(1, prefMsgNormVolumeTest, s.PriorityNorm.SoundSource, s.PriorityNorm.ForceVolumeValue); });
prefMsgHighEnableSound.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableSound=b; saveAndUpdate(); });
prefMsgHighRingtone_container.setOnClickListener(a -> chooseRingtoneHigh());
@@ -195,6 +252,72 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefMsgHighEnableLED.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableLED=b; saveAndUpdate(); });
prefMsgHighLedColor_container.setOnClickListener(a -> chooseLEDColorHigh());
prefMsgHighEnableVibrations.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.EnableVibration=b; saveAndUpdate(); });
prefMsgHighForceVolume.setOnCheckedChangeListener((a,b) -> { s.PriorityHigh.ForceVolume=b; saveAndUpdate(); });
prefMsgHighVolume.setOnSeekBarChangeListener(FI.SeekBarChanged((a,b,c) -> { if (c) { s.PriorityHigh.ForceVolumeValue=b; saveAndUpdate(); updateVolume(2, b); } }));
prefMsgHighVolumeTest.setOnClickListener((v) -> { if (s.PriorityHigh.ForceVolume) playTestSound(2, prefMsgHighVolumeTest, s.PriorityHigh.SoundSource, s.PriorityHigh.ForceVolumeValue); });
}
private void 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()
@@ -232,7 +355,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
UltimateMusicPicker ump = new UltimateMusicPicker();
ump.windowTitle("Choose notification sound");
ump.removeSilent();
ump.streamType(AudioManager.STREAM_ALARM);
ump.streamType(AudioManager.STREAM_NOTIFICATION);
ump.ringtone();
ump.notification();
ump.alarm();
@@ -247,7 +370,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
UltimateMusicPicker ump = new UltimateMusicPicker();
ump.windowTitle("Choose notification sound");
ump.removeSilent();
ump.streamType(AudioManager.STREAM_ALARM);
ump.streamType(AudioManager.STREAM_NOTIFICATION);
ump.ringtone();
ump.notification();
ump.alarm();
@@ -262,7 +385,7 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
UltimateMusicPicker ump = new UltimateMusicPicker();
ump.windowTitle("Choose notification sound");
ump.removeSilent();
ump.streamType(AudioManager.STREAM_ALARM);
ump.streamType(AudioManager.STREAM_NOTIFICATION);
ump.ringtone();
ump.notification();
ump.alarm();
@@ -355,4 +478,11 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
{
musicPickerSwitch = -1;
}
public void onViewpagerHide()
{
stopSound(0, prefMsgLowVolumeTest);
stopSound(1, prefMsgNormVolumeTest);
stopSound(2, prefMsgHighVolumeTest);
}
}

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>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:id="@+id/layoutRoot"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"

View File

@@ -57,7 +57,7 @@
<TextView
android:id="@+id/tvLocalCacheSize"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/str_localcachesize"
android:textColor="#000"
@@ -202,6 +202,60 @@
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"
@@ -342,6 +396,60 @@
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"
@@ -499,6 +607,53 @@
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"

View File

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

@@ -7,4 +7,6 @@
<color name="colorHeaderForeground">#FFFFFF</color>
<color name="colorBlack">#000</color>
<color name="bg_row_background">#fa315b</color>
</resources>

View File

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

View File

@@ -24,10 +24,14 @@
<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>

View File

@@ -1,3 +1,3 @@
#Mon Nov 12 13:06:09 CET 2018
VERSION_NAME=0.0.6
VERSION_CODE=6
#Sat Nov 17 03:25:30 CET 2018
VERSION_NAME=0.0.8
VERSION_CODE=8

77
examples/scn_send.sh Normal file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
#
# Call with `scn_send title`
# or `scn_send title content`
# or `scn_send title content priority`
#
#
if [ "$#" -lt 1 ]; then
echo "no title supplied via parameter"
exit 1
fi
################################################################################
# INSERT YOUR DATA HERE #
################################################################################
user_id=999
user_key="????????????????????????????????????????????????????????????????"
################################################################################
title=$1
content=""
if [ "$#" -gt 1 ]; then
content=$2
fi
priority=1
if [ "$#" -gt 2 ]; then
priority=$3
fi
usr_msg_id=$(uuidgen)
while true ; do
curlresp=$(curl -s -o /dev/null -w "%{http_code}" \
-d "user_id=$user_id" -d "user_key=$user_key" -d "title=$title" \
-d "content=$content" -d "priority=$priority" -d "msg_id=$usr_msg_id" \
https://scn.blackforestbytes.com/send.php)
if [ "$curlresp" == 200 ] ; then
echo "Successfully send"
exit 0
fi
if [ "$curlresp" == 400 ] ; then
echo "Bad request - something went wrong"
exit 1
fi
if [ "$curlresp" == 401 ] ; then
echo "Unauthorized - wrong userid/userkey"
exit 1
fi
if [ "$curlresp" == 403 ] ; then
echo "Quota exceeded - wait one hour before re-try"
sleep 3600
fi
if [ "$curlresp" == 412 ] ; then
echo "Precondition Failed - No device linked"
exit 1
fi
if [ "$curlresp" == 500 ] ; then
echo "Internal server error - waiting for better times"
sleep 60
fi
# if none of the above matched we probably hav no network ...
echo "Send failed (response code $curlresp) ... try again in 5s"
sleep 5
done

View File

@@ -12,11 +12,17 @@
<link rel="icon" type="image/png" href="/favicon.ico"/>
</head>
<body>
<div 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>
<a href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
<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 \
@@ -34,12 +40,8 @@
--data "title={message_title}" \
https://scn.blackforestbytes.com/send.php</pre>
<a href="/index_more.php" class="button bordered tertiary" style="float: right; min-width: 100px; text-align: center">More</a>
<a href="/api_more.php" class="button bordered tertiary" style="float: right; min-width: 100px; text-align: center">More</a>
</div>
<div id="copyinfo">
<a href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
</div>
</body>
</html>

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

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

View File

@@ -37,11 +37,10 @@ if (count($datas)<=0) die(json_encode(['success' => false, 'errid'=>301, 'messag
$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]);
echo json_encode(
api_return(200,
[
'success' => true,
'prev_ack' => $datas[0]['ack'],
'new_ack' => 1,
'message' => 'ok'
]);
return 0;
]);

View File

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

View File

@@ -2,12 +2,39 @@
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 : 100; }
public static function quota_max($is_pro) { return $is_pro ? 1000 : 50; }
}
function getConfig()
@@ -190,10 +217,15 @@ function refreshVerifyToken()
return $obj['access_token'];
}
/**
* @param int $http_code
* @param array $message
*/
function api_return($http_code, $message)
{
http_response_code($http_code);
echo $message;
header('Content-Type: application/json');
echo json_encode($message);
die();
}

View File

@@ -38,7 +38,7 @@ $stmt->execute(['uid' => $user_id, 'ft' => $fcm_token]);
$pdo->commit();
echo json_encode(
api_return(200,
[
'success' => true,
'user_id' => $user_id,
@@ -47,6 +47,4 @@ echo json_encode(
'quota_max' => Statics::quota_max($ispro),
'is_pro' => $ispro,
'message' => 'New user registered'
]);
return 0;
]);

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' => strtotime($nack['timestamp']),
'usr_msg_id' => $nack['usr_message_id'],
'scn_msg_id' => $nack['scn_message_id'],
];
}
api_return(200,
[
'success' => true,
'message' => 'ok',
'count' => count($nonacks),
'data' => $nonacks,
]);

View File

@@ -39,7 +39,7 @@ if ($fcm_token === null)
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), user_key=:at WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id, 'at' => $new_userkey]);
echo json_encode(
api_return(200,
[
'success' => true,
'user_id' => $user_id,
@@ -49,7 +49,6 @@ if ($fcm_token === null)
'is_pro' => $is_pro,
'message' => 'user updated'
]);
return 0;
}
else
{
@@ -61,7 +60,7 @@ else
$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]);
echo json_encode(
api_return(200,
[
'success' => true,
'user_id' => $user_id,
@@ -71,5 +70,4 @@ else
'is_pro' => $is_pro,
'message' => 'user updated'
]);
return 0;
}

View File

@@ -45,7 +45,7 @@ if ($ispro)
$stmt = $pdo->prepare('UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> :uid AND pro_token = :ptk');
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
echo json_encode(
api_return(200,
[
'success' => true,
'user_id' => $user_id,
@@ -54,7 +54,6 @@ if ($ispro)
'is_pro' => true,
'message' => 'user updated'
]);
return 0;
}
else
{
@@ -63,7 +62,7 @@ else
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), is_pro=0, pro_token=NULL WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id]);
echo json_encode(
api_return(200,
[
'success' => true,
'user_id' => $user_id,
@@ -72,5 +71,4 @@ else
'is_pro' => false,
'message' => 'user updated'
]);
return 0;
}

282
web/api_more.php Normal file
View File

@@ -0,0 +1,282 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
<title>Simple Cloud Notifications - API</title>
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
<link rel="stylesheet" href="/css/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favicon.png"/>
<link rel="icon" type="image/png" href="/favicon.ico"/>
</head>
<body>
<div id="copyinfo">
<a tabindex="-1" href="https://www.blackforestbytes.com">&#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>
<h2>Bash script example</h2>
<div class="section">
<p>
Depending on your use case it can be useful to create a bash script that handles things like resending messages if you have connection problems or waiting if there is no quota left.<br/>
Here is an example how such a scrippt could look like, you can put it into <code>/usr/local/sbin</code> and call it with <code>scn_send "title" "content"</code>
</p>
<pre style="color:#000000;" class="yellow-code"><span style="color:#3f7f59; font-weight:bold;">#!/usr/bin/env bash</span>
<span style="color:#3f7f59; ">#</span>
<span style="color:#3f7f59; "># Call with `scn_send title`</span>
<span style="color:#3f7f59; "># or `scn_send title content`</span>
<span style="color:#3f7f59; "># or `scn_send title content priority`</span>
<span style="color:#3f7f59; ">#</span>
<span style="color:#3f7f59; ">#</span>
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -lt 1 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"no title supplied via parameter"</span>
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
<span style="color:#7f0055; font-weight:bold; ">fi</span>
<span style="color:#3f7f59; ">################################################################################</span>
<span style="color:#3f7f59; "># INSERT YOUR DATA HERE #</span>
<span style="color:#3f7f59; ">################################################################################</span>
user_id=999
user_key=<span style="color:#2a00ff; ">"????????????????????????????????????????????????????????????????"</span>
<span style="color:#3f7f59; ">################################################################################</span>
title=$1
content=<span style="color:#2a00ff; ">""</span>
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -gt 1 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
content=$2
<span style="color:#7f0055; font-weight:bold; ">fi</span>
priority=1
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -gt 2 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
priority=$3
<span style="color:#7f0055; font-weight:bold; ">fi</span>
usr_msg_id=$(uuidgen)
<span style="color:#7f0055; font-weight:bold; ">while</span> true ; <span style="color:#7f0055; font-weight:bold; ">do</span>
curlresp=$(curl -s -o <span style="color:#3f3fbf; ">/dev/null</span> -w <span style="color:#2a00ff; ">"%{http_code}"</span> <span style="color:#2a00ff; ">\</span>
-d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">user_id</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$user_id</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">user_key</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$user_key</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">title</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$title</span><span style="color:#2a00ff; ">"</span> <span style="color:#2a00ff; ">\</span>
-d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">content</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$content</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">priority</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$priority</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">msg_id</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$usr_msg_id</span><span style="color:#2a00ff; ">"</span> <span style="color:#2a00ff; ">\</span>
https:<span style="color:#3f3fbf; ">/</span><span style="color:#3f3fbf; ">/scn.blackforestbytes.com/send.php</span>)
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 200 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Successfully send"</span>
<span style="color:#7f0055; font-weight:bold; ">exit</span> 0
<span style="color:#7f0055; font-weight:bold; ">fi</span>
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 400 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Bad request - something went wrong"</span>
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
<span style="color:#7f0055; font-weight:bold; ">fi</span>
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 401 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Unauthorized - wrong </span><span style="color:#3f3fbf; ">userid/userkey</span><span style="color:#2a00ff; ">"</span>
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
<span style="color:#7f0055; font-weight:bold; ">fi</span>
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 403 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Quota exceeded - wait one hour before re-try"</span>
sleep 3600
<span style="color:#7f0055; font-weight:bold; ">fi</span>
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 412 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Precondition Failed - No device linked"</span>
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
<span style="color:#7f0055; font-weight:bold; ">fi</span>
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 500 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Internal server error - waiting for better times"</span>
sleep 60
<span style="color:#7f0055; font-weight:bold; ">fi</span>
<span style="color:#3f7f59; "># if none of the above matched we probably hav no network ...</span>
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Send failed (response code </span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">) ... try again in 5s"</span>
sleep 5
<span style="color:#7f0055; font-weight:bold; ">done</span>
</pre>
<p>
Be aware that the server only saves send messages for a short amount of time. Because of that you can only use this to prevent duplicates in a short time-frame, older messages with the same ID are probably already deleted and the message will be send again.
</p>
</div>
</div>
</body>
</html>

View File

@@ -42,6 +42,11 @@ body
border-left: .25rem solid #E53935;
}
.yellow-code
{
border-left: .25rem solid #FFCB05;
}
#mainpnl input,
#mainpnl textarea
{
@@ -86,13 +91,10 @@ body
position: fixed;
bottom: 0;
right: 0;
z-index: -999;
}
#copyinfo a:hover
{
font-family: "Courier New", monospace;
color: #00F;
//z-index: -999;
display: flex;
flex-direction: column;
text-align: right;
}
#copyinfo a,
@@ -102,6 +104,14 @@ body
font-family: "Courier New", monospace;
color: #AAA;
text-decoration: none;
display: block;
line-height: 1em;
}
#copyinfo a:hover
{
font-family: "Courier New", monospace;
color: #0288D1;
}
#tr_link
@@ -230,4 +240,10 @@ table.scode_table th:nth-child(2) {
.linkcaption:hover {
text-decoration: none;
}
pre, pre span
{
font-family: Menlo, Consolas, monospace;
background: #F9F9F9;;
}

View File

@@ -13,12 +13,18 @@
<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>
<form id="mainpnl">
<a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a href="/index_api.php" class="button bordered" id="tr_link">API</a>
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a tabindex="-1" href="/api.php" class="button bordered" id="tr_link">API</a>
<a href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
<div class="row responsive-label">
<div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div>
@@ -57,10 +63,6 @@
</div>
</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/toastify.js"></script>
</body>

View File

@@ -1,192 +0,0 @@
<!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="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>
<a 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"}]
},
"messagecount":623,
"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>
<div id="copyinfo">
<a href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
</div>
</body>
</html>

View File

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

View File

@@ -13,6 +13,10 @@
</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">
@@ -41,16 +45,12 @@
</div>
<a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a href="/index.php" class="button bordered" id="tr_link">Send</a>
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
<a href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
</div>
<div id="copyinfo">
<a href="https://www.blackforestbytes.com">&#169; blackforestbytes</a>
</div>
</body>
</html>

View File

@@ -1,6 +1,6 @@
<?php
include_once 'model.php';
include_once 'api/model.php';
try
{
@@ -9,11 +9,13 @@ try
//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, json_encode(['success' => false, 'error' => 1101, 'errhighlight' => 101, 'message' => 'Missing parameter [[user_id]]']));
if (!isset($INPUT['user_key'])) api_return(400, json_encode(['success' => false, 'error' => 1102, 'errhighlight' => 102, 'message' => 'Missing parameter [[user_token]]']));
if (!isset($INPUT['title'])) api_return(400, json_encode(['success' => false, 'error' => 1103, 'errhighlight' => 103, 'message' => 'Missing parameter [[title]]']));
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]]']);
//------------------------------------------------------------------
@@ -27,12 +29,12 @@ try
//------------------------------------------------------------------
if ($priority !== '0' && $priority !== '1' && $priority !== '2') api_return(400, json_encode(['success' => false, 'error' => 1104, 'errhighlight' => 105, 'message' => 'Invalid priority']));
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, json_encode(['success' => false, 'error' => 1201, 'errhighlight' => 103, 'message' => 'No title specified']));
if (strlen($message) > 120) api_return(400, json_encode(['success' => false, 'error' => 1202, 'errhighlight' => 103, 'message' => 'Title too long (120 characters)']));
if (strlen($content) > 10000) api_return(400, json_encode(['success' => false, 'error' => 1203, 'errhighlight' => 104, 'message' => 'Content too long (10000 characters)']));
if ($usrmsgid != null && strlen($usrmsgid) > 64) api_return(400, json_encode(['success' => false, 'error' => 1204, 'errhighlight' => -1, 'message' => 'MessageID too long (64 characters)']));
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)']);
//------------------------------------------------------------------
@@ -42,22 +44,22 @@ try
$stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($datas)<=0) die(json_encode(['success' => false, 'error' => 1301, 'errhighlight' => 101, 'message' => 'User not found']));
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, json_encode(['success' => false, 'error' => 1301, 'errhighlight' => 101, 'message' => 'User not found']));
if ($data['user_id'] !== (int)$user_id) api_return(401, json_encode(['success' => false, 'error' => 1302, 'errhighlight' => 101, 'message' => 'UserID not found']));
if ($data['user_key'] !== $user_key) api_return(401, json_encode(['success' => false, 'error' => 1303, 'errhighlight' => 102, 'message' => 'Authentification failed']));
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, json_encode(['success' => false, 'error' => 2101, 'errhighlight' => -1, 'message' => 'Daily quota reached ('.Statics::quota_max($data['is_pro']).')']));
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, json_encode(['success' => false, 'error' => 1401, 'errhighlight' => -1, 'message' => 'No device linked with this account']));
api_return(412, ['success' => false, 'error' => ERR::NO_DEVICE_LINKED, 'errhighlight' => -1, 'message' => 'No device linked with this account']);
}
//------------------------------------------------------------------
@@ -69,7 +71,7 @@ try
if (count($stmt->fetchAll(PDO::FETCH_ASSOC))>0)
{
api_return(200, json_encode(
api_return(200,
[
'success' => true,
'message' => 'Message already sent',
@@ -79,7 +81,7 @@ try
'quota' => $data['quota_today'],
'is_pro' => $data['is_pro'],
'quota_max' => Statics::quota_max($data['is_pro']),
]));
]);
}
}
@@ -136,14 +138,14 @@ try
{
reportError("FCM communication failed (success_1 <> true)\n\n".$httpresult);
$pdo->rollBack();
api_return(500, json_encode(['success' => false, 'error' => 9902, 'errhighlight' => -1, 'message' => 'Communication with firebase service failed.']));
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, json_encode(['success' => false, 'error' => 9901, 'errhighlight' => -1, 'message' => 'Communication with firebase service failed.'."\n\n".'Exception: ' . $e->getMessage()]));
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');
@@ -154,9 +156,11 @@ try
$pdo->commit();
api_return(200, json_encode(
api_return(200,
[
'success' => true,
'error' => ERR::NO_ERROR,
'errhighlight' => -1,
'message' => 'Message sent',
'suppress_send' => false,
'response' => $httpresult,
@@ -165,11 +169,11 @@ try
'is_pro' => $data['is_pro'],
'quota_max' => Statics::quota_max($data['is_pro']),
'scn_msg_id' => $scn_msg_id,
]));
]);
}
catch (Exception $mex)
{
reportError("Root try-catch triggered", $mex);
if ($pdo->inTransaction()) $pdo->rollBack();
api_return(500, json_encode(['success' => false, 'error' => 9903, 'errhighlight' => -1, 'message' => 'PHP script threw exception.'."\n\n".'Exception: ' . $e->getMessage()]));
api_return(500, ['success' => false, 'error' => ERR::INTERNAL_EXCEPTION, 'errhighlight' => -1, 'message' => 'PHP script threw exception.'."\n\n".'Exception: ' . $e->getMessage()]);
}