I build a to-do list app that should show notifications to remind about tasks. In order to be able to schedule the notifications to the exact minute of the deadline, I pass the notifications data from flutter to kotlin, and showing the notification from a Broadcast receiver.
Here I am sending the notifications data to kotlin:
await platform.invokeMethod('setNextNotification', {
'tasksNames': tasksNames,
'notificationsTimeInMillis': notificationsTimeInMillis
});
This is how I get the data inside FlutterActivity:
private const val CHANNEL = "flutter.native/helper"
class MainActivity : FlutterActivity() {
companion object {
const val TASKS_NAMES_EXTRA = "tasksNames"
const val NOTIFICATIONS_TIME_IN_MILLIS_EXTRA = "notificationsTimeInMillis"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
// Init the AlarmManager.
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
// We got here from the setNotifications() method in flutter...
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "setNextNotification") {
// Get the time till next notification
val notificationsTimeInMillis: ArrayList<Long> = call.argument(NOTIFICATIONS_TIME_IN_MILLIS_EXTRA)
?: ArrayList()
// Create a pending intent for the notifications
val pIntent: PendingIntent? = createPendingIntent(call.argument(TASKS_NAMES_EXTRA), call.argument(TIME_LEFT_TEXTS_EXTRA), notificationsTimeInMillis, this)
// Cancel all alarms
while (alarmManager.nextAlarmClock != null)
alarmManager.cancel(alarmManager.nextAlarmClock.showIntent)
// Set the alarm
setAlarm(notificationsTimeInMillis[0], pIntent, alarmManager)
}
}
}
private fun setAlarm(notificationTime: Long, pIntent: PendingIntent?, alarmManager: AlarmManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // The API is 23 or higher...
alarmManager.setAlarmClock(AlarmManager.AlarmClockInfo(notificationTime, pIntent), pIntent)
} else { // The API is 19 - 22...
// We want the alarm to go of on the exact time it scheduled for so we use the setExact method.
alarmManager.setExact(AlarmManager.RTC_WAKEUP, notificationTime, pIntent)
}
}
private fun createPendingIntent(tasksNames: ArrayList<String>?, timeTillNotificationsInMillis: ArrayList<Long>?,
context: Context): android.app.PendingIntent? {
return try {
val intent: android.content.Intent = android.content.Intent(context, AlarmManagerHelperWakeful::class.java)
intent.action = "notification"
intent.putStringArrayListExtra(TASKS_NAMES_EXTRA, tasksNames)
intent.putStringArrayListExtra(NOTIFICATIONS_TIME_IN_MILLIS_EXTRA, timeTillNotificationsInMillisAsString)
android.app.PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} catch (e: java.lang.Exception) {
null
}
}
}
And this is how I show the notification on the BroadcastReceiver, and after that sets the next notification:
Class AlarmManagerHelperWakeful : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent != null && intent.action == "notification" && context != null) {
val tasksLabels: ArrayList<String> = intent.getStringArrayListExtra(MainActivity.TASKS_NAMES_EXTRA)
?: ArrayList()
val notificationsTimeInMillisAsString: ArrayList<String> = intent.getStringArrayListExtra(MainActivity.NOTIFICATIONS_TIME_IN_MILLIS_EXTRA)
?: ArrayList()
if (tasksLabels.size > 0) {
// Create a notification manager.
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
var builder = NotificationCompat.Builder(context) // The initialization is for api 25 or lower so it is deprecated.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // This is API 26 or higher...
// Create a channel for API 26 or higher;
val channelId = "channel_01" // The id of the channel.
if (notificationManager.getNotificationChannel(channelId) == null) {
val channel = NotificationChannel(channelId,
context.getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(channel)
}
// Update the builder to a no deprecated one.
builder = NotificationCompat.Builder(context, channelId)
}
// Set the notification details.
builder.setSmallIcon(android.R.drawable.ic_notification_overlay)
builder.setContentTitle(tasksLabels[0])
builder.setContentText(someText)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
notificationId = someUniqueId
// Show the notification.
notificationManager.notify(notificationId.toInt(), builder.build())
// Remove this notification from the notifications lists.
tasksLabels.removeAt(0)
notificationsTimeInMillisAsString.removeAt(0)
// There are more notifications...
if (tasksLabels.size > 0) {
// Init the AlarmManager.
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
// Cancel all alarms
while (alarmManager.nextAlarmClock != null)
alarmManager.cancel(alarmManager.nextAlarmClock.showIntent)
// Create a pending intent for the notifications
val pIntent: PendingIntent? = createPendingIntent(tasksLabels, cnotificationsTimeInMillisAsString, context)
// Set the alarm
setAlarm(notificationsTimeInMillisAsString[0].toLong(), pIntent, alarmManager)
}
}
} else {
if (intent == null) {
Log.d("Debug", "Checking: intent == null")
} else if ( intent.action != "notification") {
Log.d("Debug", "Checking: intent.action != notification")
val tasksLabels: ArrayList<String> = intent.getStringArrayListExtra(MainActivity.TASKS_NAMES_EXTRA)
?: ArrayList()
Log.d("Debug", "Checking: tasksNames.size inside else if" + tasksLabels.size)
}
}
}
}
Everything works perfectly fine unless I restart my device. Then the Broadcast receiver gets an intent without any data. For the BoradcastReceiver to get intent with notifications data, I need to invoke the method from the flutter code (the method that sends notifications data to the kotlin code), which means that for now, the user has to enter the app for that. Otherwise, user will not see notifications until entering my app and reinvoking the flutter code.
How can I overcome that issue?
You should use Push Notification instead of sending local notification to your broadcast receiver. There're lot of cases that make your app cannot send local notification. ex: user closed app (lot of users alway close app after using), OS closed app or clean memory, Dart method is crashing.
Firebase FCM is pretty simple, it's much more simple than you solution that's using broadcast receiver. Also totally free.
https://pub.dev/packages/firebase_messaging
Pushwoosh is good too, it has schedule notification
https://pub.dev/packages/pushwoosh
Using push notification also has other pros that you app will also work on iOS, doesn't need to keep your app running on background which is very bad idea if your app doesn't have any special features that need to run background (music player, geo location, VOIP)
In case you don't want to use Push Notification. Take a look at this lib:
https://pub.dev/packages/flutter_local_notifications
Right now you are sending data from dart to the native plugin. You might try it the other way around. This example shows how you can get the native android reboot event. Next, you can use this example to fetch the required data. After fetching the data you can set the notification.
You could also try to store the information of the last notification in SharedPreferences, fetch that on boot, and set the notification.
Related
I am trying to retrieve OTP in Huawei device. but it is not working.
I have created the app in Huawei developer console with all the requirements.
Below is the code i am using to retrieve the sms.
private fun initSmsManager() {
val task = ReadSmsManager.start(this#MainActivity)
task.addOnCompleteListener {
if (task.isSuccessful) {
// The service is enabled successfully. Continue with the process.
Toast.makeText(this, "ReadSms service has been enabled.", Toast.LENGTH_LONG).show()
} else
Toast.makeText(this, "The service failed to be enabled.", Toast.LENGTH_LONG).show()
}
task.addOnSuccessListener(this, OnSuccessListener {
if(task.isSuccessful){
Toast.makeText(this, "ReadSms service has been enabled.", Toast.LENGTH_LONG).show()
myReceiver = MyBroadcastReceiver();
val intentFilter = IntentFilter(READ_SMS_BROADCAST_ACTION)
registerReceiver(myReceiver, intentFilter)
}
})
task.addOnFailureListener(this, OnFailureListener {
Toast.makeText(this,it.message,Toast.LENGTH_SHORT).show();
})
}
Broadcast receiver
class MyBroadcastReceiver : BroadcastReceiver() {
companion object {
val TAG = MyBroadcastReceiver::class.java.simpleName
}
override fun onReceive(context: Context?, intent: Intent?) {
val bundle = intent!!.extras
if (bundle != null) {
val status: Status? = bundle.getParcelable(ReadSmsConstant.EXTRA_STATUS)
if (status?.statusCode == CommonStatusCodes.TIMEOUT) {
// Service has timed out and no SMS message that meets the requirement is read. Service ended.
// doSomethingWhenTimeOut()
} else if (status?.statusCode == CommonStatusCodes.SUCCESS) {
if (bundle.containsKey(ReadSmsConstant.EXTRA_SMS_MESSAGE)) {
// An SMS message that meets the requirement is read. Service ended.
//doSomethingWhenGetMessage(bundle.getString(ReadSmsConstant.EXTRA_SMS_MESSAGE))
bundle.getString(ReadSmsConstant.EXTRA_SMS_MESSAGE)?.let {
Log.d(TAG, it)
val local = Intent()
local.action = "service.to.activity.transfer"
local.putExtra("sms", it)
context!!.sendBroadcast(local)
}
}
}
}
}
}
Any help in this would be beneficial.
Please confirm the following points:
Check whether the broadcast for receiving SMS verification codes is enabled. You can do that by performing breakpoint debugging or recording logs.
Check whether the SMS message format meets the rules for automatically reading SMS messages.
For details,See Docs.
Check whether the hash_value field is correct.
If no error occurs during the preceding check, could you pls provide a complete log trace then i will try to find out what can be wrong about this issue. :)
All code is working fine it works well on debug mode when I build released apk then keystore changed also hash changes for release mode. If you know anything about how to run on release mode , please let us know. Maybe some changes are made in Huawei developer account
Is there a way to wait for a future to complete without blocking the event loop?
An example of a use case with querying Mongo:
Future<Result> dbFut = Future.future();
mongo.findOne("myusers", myQuery, new JsonObject(), res -> {
if(res.succeeded()) {
...
dbFut.complete(res.result());
}
else {
...
dbFut.fail(res.cause());
}
}
});
// Here I need the result of the DB query
if(dbFut.succeeded()) {
doSomethingWith(dbFut.result());
}
else {
error();
}
I know the doSomethingWith(dbFut.result()); can be moved to the handler, yet if it's long, the code will get unreadable (Callback hell ?) It that the right solution ? Is that the omny solution without additional libraries ?
I'm aware that rxJava simplifies the code, but as I don't know it, learning Vert.x and rxJava is just too much.
I also wanted to give a try to vertx-sync. I put the dependency in the pom.xml; everything got downloaded fine but when I started my app, I got the following error
maurice#mickey> java \
-javaagent:~/.m2/repository/co/paralleluniverse/quasar-core/0.7.5/quasar-core-0.7.5-jdk8.jar \
-jar target/app-dev-0.1-fat.jar \
-conf conf/config.json
Error opening zip file or JAR manifest missing : ~/.m2/repository/co/paralleluniverse/quasar-core/0.7.5/quasar-core-0.7.5-jdk8.jar
Error occurred during initialization of VM
agent library failed to init: instrument
I know what the error means in general, but I don't know in that context... I tried to google for it but didn't find any clear explanation about which manifest to put where. And as previously, unless mandatory, I prefer to learn one thing at a time.
So, back to the question : is there a way with "basic" Vert.x to wait for a future without perturbation on the event loop ?
You can set a handler for the future to be executed upon completion or failure:
Future<Result> dbFut = Future.future();
mongo.findOne("myusers", myQuery, new JsonObject(), res -> {
if(res.succeeded()) {
...
dbFut.complete(res.result());
}
else {
...
dbFut.fail(res.cause());
}
}
});
dbFut.setHandler(asyncResult -> {
if(asyncResult.succeeded()) {
// your logic here
}
});
This is a pure Vert.x way that doesn't block the event loop
I agree that you should not block in the Vertx processing pipeline, but I make one exception to that rule: Start-up. By design, I want to block while my HTTP server is initialising.
This code might help you:
/**
* #return null when waiting on {#code Future<Void>}
*/
#Nullable
public static <T>
T awaitComplete(Future<T> f)
throws Throwable
{
final Object lock = new Object();
final AtomicReference<AsyncResult<T>> resultRef = new AtomicReference<>(null);
synchronized (lock)
{
// We *must* be locked before registering a callback.
// If result is ready, the callback is called immediately!
f.onComplete(
(AsyncResult<T> result) ->
{
resultRef.set(result);
synchronized (lock) {
lock.notify();
}
});
do {
// Nested sync on lock is fine. If we get a spurious wake-up before resultRef is set, we need to
// reacquire the lock, then wait again.
// Ref: https://stackoverflow.com/a/249907/257299
synchronized (lock)
{
// #Blocking
lock.wait();
}
}
while (null == resultRef.get());
}
final AsyncResult<T> result = resultRef.get();
#Nullable
final Throwable t = result.cause();
if (null != t) {
throw t;
}
#Nullable
final T x = result.result();
return x;
}
I am having trouble subscribing to a socketcluster (http://socketcluster.io/) channel when using a redux-saga generator in my chat app. The socketcluster backend is setup in a way where any messages are saved in the database then published into the receiving user's personal channel, which is named after the user's id. For example, User A has an id '123abc' and would subscribe to the channel named '123abc' for their realtime messages.
The code below does receive new messages that are published to a channel but it throws a "TypeError: Converting circular structure to JSON" onload and breaks all of my other redux-saga generators in the app. I've done digging in Chrome Devtools and my theory is that it has something to do with queue created in the createChannel function. Also, I've tried returning a deferred promise in the subscribeToChannel function but that also caused a Circular Conversion Error, I can post that code on request.
I referred to this answer at first: https://stackoverflow.com/a/35288877/5068616 and it helped me get the below code in place but I cannot find any similar issues on the internet. Also something to note, I am utilizing redux-socket-cluster (https://github.com/mattkrick/redux-socket-cluster) to sync up the socket and state, but I don't think it is the root of the problem
sagas.js
export default function* root() {
yield [
fork(startSubscription),
]
}
function* startSubscription(getState) {
while (true) {
const {
userId
} = yield take(actions.SUBSCRIBE_TO_MY_CHANNEL);
yield call(monitorChangeEvents, subscribeToChannel(userId))
}
}
function* monitorChangeEvents(channel) {
while (true) {
const info = yield call(channel.take) // Blocks until the promise resolves
console.log(info)
}
}
function subscribeToChannel(channelName) {
const channel = createChannel();
const socket = socketCluster.connect(socketConfig);
const c = socket.subscribe(channelName);
c.watch(event => {
channel.put(event)
})
return channel;
}
function createChannel() {
const messageQueue = []
const resolveQueue = []
function put(msg) {
// anyone waiting for a message ?
if (resolveQueue.length) {
// deliver the message to the oldest one waiting (First In First Out)
const nextResolve = resolveQueue.shift()
nextResolve(msg)
} else {
// no one is waiting ? queue the event
messageQueue.push(msg)
}
}
// returns a Promise resolved with the next message
function take() {
// do we have queued messages ?
if (messageQueue.length) {
// deliver the oldest queued message
return Promise.resolve(messageQueue.shift())
} else {
// no queued messages ? queue the taker until a message arrives
return new Promise((resolve) => resolveQueue.push(resolve))
}
}
return {
take,
put
}
}
Thanks for the help!
i've an observable that I create with the following code.
Observable.create(new Observable.OnSubscribe<ReturnType>() {
#Override
public void call(Subscriber<? super ReturnType> subscriber) {
try {
if (!subscriber.isUnsubscribed()) {
subscriber.onNext(performRequest());
}
subscriber.onCompleted();
} catch (Exception e) {
subscriber.onError(e);
}
}
});
performRequest() will perform a long running task as you might expect.
Now, since i might be launching the same Observable twice or more in a very short amount of time, I decided to write such transformer:
protected Observable.Transformer<ReturnType, ReturnType> attachToRunningTaskIfAvailable() {
return origObservable -> {
synchronized (mapOfRunningTasks) {
// If not in maps
if ( ! mapOfRunningTasks.containsKey(getCacheKey()) ) {
Timber.d("Cache miss for %s", getCacheKey());
mapOfRunningTasks.put(
getCacheKey(),
origObservable
.doOnTerminate(() -> {
Timber.d("Removed from tasks %s", getCacheKey());
synchronized (mapOfRunningTasks) {
mapOfRunningTasks.remove(getCacheKey());
}
})
.cache()
);
} else {
Timber.d("Cache Hit for %s", getCacheKey());
}
return mapOfRunningTasks.get(getCacheKey());
}
};
}
Which basically puts the original .cache observable in a HashMap<String, Observable>.
This basically disallows multiple requests with the same getCacheKey() (Example login) to call performRequest() in parallel. Instead, if a second login request arrives while another is in progress, the second request observable gets "discarded" and the already-running will be used instead. => All the calls to onNext are going to be cached and sent to both subscribers actually hitting my backend only once.
Now, suppouse this code:
// Observable loginTask
public void doLogin(Observable<UserInfo> loginTask) {
loginTask.subscribe(
(userInfo) -> {},
(throwable) -> {
if (userWantsToRetry()) {
doLogin(loinTask);
}
}
);
}
Where loginTask was composed with the previous transformer. Well, when an error occurs (might be connectivity) and the userWantsToRetry() then i'll basically re-call the method with the same observable. Unfortunately that has been cached and I'll receive the same error without hitting performRequest() again since the sequence gets replayed.
Is there a way I could have both the "same requests grouping" behavior that the transformer provides me AND the retry button?
Your question has a lot going on and it's hard to put it into direct terms. I can make a couple recommendations though. Firstly your Observable.create can be simplified by using an Observable.defer(Func0<Observable<T>>). This will run the func every time a new subscriber is subscribed and catch and channel any exceptions to the subscriber's onError.
Observable.defer(() -> {
return Observable.just(performRequest());
});
Next, you can use observable.repeatWhen(Func1<Observable<Void>, Observable<?>>) to decide when you want to retry. Repeat operators will re-subscribe to the observable after an onComplete event. This particular overload will send an event to a subject when an onComplete event is received. The function you provide will receive this subject. Your function should call something like takeWhile(predicate) and onComplete when you do not want to retry again.
Observable.just(1,2,3).flatMap((Integer num) -> {
final AtomicInteger tryCount = new AtomicInteger(0);
return Observable.just(num)
.repeatWhen((Observable<? extends Void> notifications) ->
notifications.takeWhile((x) -> num == 2 && tryCount.incrementAndGet() != 3));
})
.subscribe(System.out::println);
Output:
1
2
2
2
3
The above example shows that retries are aloud when the event is not 2 and up to a max of 22 retries. If you switch to a repeatWhen then the flatMap would contain your decision as to use a cached observable or the realWork observable. Hope this helps!
i am building a chat application with the help of GoogleCloudMessaging(GCM).i am able to send and receive messages. my problem is when i receive messages from multiple devices at the same time, all those messages are appended to the same list-view in my broadcast receiver class. how can i separate the message based on the senders and append the current chat message to the listview. and make separate notifications for other messages based on the senders and when i click on the notification it should open the same list view with messages according to sender.
can any one give me an efficient way of doing this.if you have any sample code to handle this situation please post the code.
My current code:
public class Serious extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
String action=intent.getAction();
if (action.equals("com.google.android.c2dm.intent.REGISTRATION"))
{
String registrationID=intent.getStringExtra("registration_id");
// Log.i("uo",registrationID);
String error=intent.getStringExtra("error");
String unregisterd=intent.getStringExtra("unregistered");
}
else if(action.equals("com.google.android.c2dm.intent.RECEIVE"))
{
String data1=intent.getStringExtra("data1");
String data2=intent.getStringExtra("data2");
addNewMessage(new Message(data2, false));
/* PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class), 0);
NotificationCompat.Builder mBuilder =
new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle("My Notification")
.setDefaults(Notification.DEFAULT_SOUND)
.setAutoCancel(true)
.setContentText(data1+data2);
mBuilder.setContentIntent(contentIntent);
NotificationManager mNotificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.notify(1, mBuilder.build());*/
}
}
void addNewMessage(Message m)
{
MainActivity.messages.add(m);
MainActivity.adapter.notifyDataSetChanged();
//MainActivity.getListView().setSelection(MainActivity.messages.size()-1);
}
}
You should pass in your gcm message a parameter that contains the sender id. Then, when you handle the arrived message, use that sender id to decide where to add that message. In order to show multiple notifications, pass different int values to notify. Currently you always pass 1, so a new notification overrides the old one.