Push notifications with back stack (Pending Intents, Kotlin) - kotlin

I'm facing a problem with passing the launch URL from one activity to another, without creating a new Intent for my MainActivity.
I have a webview, which is work with OneSignal push notifications. I wanted to modify the grouping notifications content.
If there's a way to get the result I want (modifying notifications group layout for OneSignal) That would be awesome. I'll simply use the One Signal default action and that would be the best solution for me.
If I have to implement it on that way:
https://developer.android.com/training/notify-user/group
The problem is, when I start a new child activity of the MainActivity, I don't use the "StartActivity / StartActivityForResults" functions.
This is the extension of OSRemoteNotificationReceivedHandler (OneSignal class)
It's outside of my MainActivity class.
class NotificationServiceExtension : OSRemoteNotificationReceivedHandler {
#RequiresApi(Build.VERSION_CODES.N)
override fun remoteNotificationReceived(
context: Context,
notificationReceivedEvent: OSNotificationReceivedEvent
) {
val notification = notificationReceivedEvent.notification
val bigText = Html.fromHtml(notification.body, FROM_HTML_MODE_LEGACY).toString()
var smallText = Html.fromHtml(notification.additionalData["cleantitle"] as String, FROM_HTML_MODE_LEGACY).toString()
val summaryStatistics = Html.fromHtml(notification.additionalData["setSummaryText"] as String, FROM_HTML_MODE_LEGACY).toString()
if (smallText == "test") {
smallText = Html.fromHtml(notification.additionalData["smalltitle"] as String, FROM_HTML_MODE_LEGACY).toString()
}
else{
val name = Html.fromHtml(notification.additionalData["text"] as String, FROM_HTML_MODE_LEGACY).toString()
smallText += " from $name"
}
val smallContent = RemoteViews("com.webviewapp.mywebviewapp", R.layout.small_layout_notification)
val sum = RemoteViews("com.webviewapp.mywebviewapp", R.layout.summary_layout_notification)
val bigContent = RemoteViews("com.webviewapp.mywebviewapp", R.layout.large_notification_layout)
bigContent.setTextViewText(R.id.notification_title, smallText)
bigContent.setTextViewText(R.id.notification_content, bigText)
smallContent.setTextViewText(R.id.notification_title, smallText)
sum.setTextViewText(R.id.notification_title, summaryStatistics)
notificationReceivedEvent.complete(null)
var bp: Bitmap? = null
try {
bp =Picasso.get().load(notification.largeIcon).get()
smallContent.setImageViewBitmap(R.id.noti_pic, bp)
bigContent.setImageViewBitmap(R.id.noti_pic, bp)
}
catch(e:Exception){
print(e)
}
try {
val fid = notification.additionalData["fid"] as String
notificationId = fid.toInt()
}
catch(e:java.lang.Exception){
notificationId += Date().time.toInt()
}
val notificationOpenActivity = Intent(context.applicationContext, MainActivity::class.java)
.putExtra("launchURL", notification.additionalData["pushURL"] as String)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
val resultPendingIntent: PendingIntent? = TaskStackBuilder.create(context.applicationContext).run {
// Add the intent, which inflates the back stack
addNextIntentWithParentStack(notificationOpenActivity)
// Get the PendingIntent containing the entire back stack
getPendingIntent(0,
PendingIntent.FLAG_UPDATE_CURRENT)
}
val receivedNotification = NotificationCompat.Builder(context.applicationContext, NOTIFICATION_GROUP)
.setSmallIcon(R.drawable.myIcon)
.setColor(ContextCompat.getColor(context.applicationContext,R.color.blue_primary))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setStyle(NotificationCompat.DecoratedCustomViewStyle()) // to expand button
.setAutoCancel(true)
.setVibrate(longArrayOf(500, 500, 500))
.setCustomBigContentView(bigContent)
.setCustomContentView(smallContent)
.setCustomHeadsUpContentView(sum)
.setChannelId(NOTIFICATION_CHANNEL)
.setGroup(NOTIFICATION_GROUP)
.setGroupSummary(false)
.setContentIntent(resultPendingIntent)
.build()
val summary = NotificationCompat.Builder(context.applicationContext, NOTIFICATION_GROUP)
.setSmallIcon(R.drawable.myIcon)
.setColor(ContextCompat.getColor(context.applicationContext,R.color.blue_primary))
//.setContentTitle(summaryStatistics.toString())
.setContentTitle(summaryStatistics)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setStyle(NotificationCompat.InboxStyle()
.setBigContentTitle(summaryStatistics)
.addLine(summaryStatistics)
.setSummaryText(summaryStatistics))
.setAutoCancel(true)
.setChannelId(NOTIFICATION_CHANNEL)
.setGroup(NOTIFICATION_GROUP)
.setGroupSummary(true)
.build()
NotificationManagerCompat.from(context.applicationContext).apply {
notify(notificationId, receivedNotification)
notify(SUMMARY_ID, summary)
}
}
}
And the notifications work good as I want. The problem is, How do I pass from the child activity the extra parameter to it's parent if I didn't create that child from the parent? I simply want to load it's URL into my webview, but also keep the back stack.
Also, how can I make sure I don't create multiple MainActivity if I won't use that child?
Thanks in advance.

Notification Back Stack
Android's documentation page "Start an Activity from a Notification" covers the back stack use case under the "Regular activity" suggestion.
Regular activity
This is an activity that exists as a part of your app's normal UX flow. So when the user arrives in the activity from the notification, the new task should include a complete back stack, allowing them to press Back and navigate up the app hierarchy.
I see you are using the addNextIntentWithParentStack method on TaskStackBuilder already in your code so looks like you may have already followed that page.
However there is one thing wrong with Google's docs here, the requestCode sent to getPendingIntent should be a unique value for your app.
Example:
getPendingIntent(
1234, // NOTE: Change this to a unique requestCode for your app
PendingIntent.FLAG_UPDATE_CURRENT
)
I have filed an docs issue with Google on this.
Lastly, since I didn't see this in your question make sure you have correctly added android:parentActivityName to your Activity in your AndroidManifest.xml per Android's "Define your app's Activity hierarchy"
OneSignal Details
Notification Tracking
Note that calling notificationReceivedEvent.complete(null) means OneSignal won't know anything about your notification you're displaying with NotificationManagerCompat. This changes a few things:
Click counts won't be sent to OneSignal
Notification won't be restored. (notifications are automatically cleaned when the app is "force stopped", device is rebooted, or app is updated)
Notification Groups
OneSignal can already do the grouping and summary you have in your code. Just set the "Group Key" on the dashboard, or android_group if you are sending the notification with the REST API.
Back stack
OneSignal doesn't allow you to control the back stack, it simply just always resumes that app and leaves the back stack un-effected. However you can disable this default behavior with com.onesignal.NotificationOpened.DEFAULT in your AndroidManifest.xml and use your own startActivity from the OneSignal.setNotificationOpenedHandler.

Related

How to set the google in-app comment optional?

The in-app dialog is working fine on my device but it's has required comment. the submit button will not be enable if the user does not have inputted a comment. I want the user have only optional comment on the app.
fun inAppReview() {
val reviewManager = ReviewManagerFactory.create(mContext)
val requestReviewFlow = reviewManager.requestReviewFlow()
requestReviewFlow.addOnCompleteListener { request ->
if (request.isSuccessful) {
// We got the ReviewInfo object
val reviewInfo = request.result
val flow = reviewManager.launchReviewFlow(mContext as Activity, reviewInfo)
flow.addOnCompleteListener {
// The flow has finished. The API does not indicate whether the user
// reviewed or not, or even whether the review dialog was shown. Thus, no
// matter the result, we continue our app flow.
}
} else {
Log.d("Error: ", request.exception.toString())
// There was some problem, continue regardless of the result.
}
}
}
Dependencies
implementation 'com.google.android.play:core:1.8.0'
implementation 'com.google.android.play:core-ktx:1.8.1'
As noted in the answer to a similar question https://stackoverflow.com/a/66568269/7867180: it is required only when you are a tester. Since, it is written "reviews are only visible to developers", it means that you are really the tester. In other cases, it will be optional as stated in docs.

NavController in Activity & Fragment Issue

In my Activity I setup the NavController in OnCreate:
navController = findNavController(this, R.id.NavHostFragment)
In my Fragments, I setup the NavController in onViewCreated:
navController = Navigation.findNavController(view)
The Activity only navigates to a few Fragments (e.g. SitesFragment, ContactsFragment, TasksFragment etc) from the NavigationView when item is clicked as below:
R.id.nav_sites_fragment -> {
navController.popBackStack(R.id.sitesFragment, true)
navController.navigate(R.id.sitesFragment)
}
In the Fragments, click events (in RecyclerViews mainly heading to other Fragments, such as SiteFragment, ContactFragment, TaskFragment etc) are handled as below:
if (!navController.popBackStack(R.id.siteFragment, false)) {
// not in BackStack
navController.navigate(R.id.action_contactFragment_to_siteFragment)
}
The problem is, Fragments from Fragment actions remain in the backstack even when I popBackStack in the Activity actions..
I think my understanding of backstack is not correct as I'm not sure what is going on - but that maybe I have created two separate instances of NavController?
EDIT: Although I have looked at this post How to clear navigation Stack after navigating to another fragment in Android, I am still finding same behaviour. PopBackStack in Activity is not clearing Fragments added to the backstack..
For example:
SITES -> SITE -> CONTACT -> CONTACTS should remove first three fragments, but backpress still returns to CONTACT..
Ok, so I found this post https://github.com/android/architecture-components-samples/issues/767.
I modified the code a bit and in my Activity I have the following function:
private fun navigateWithClearStack(destination: Int) {
val navController = findNavController(R.id.NavHostFragment)
val navHostFragment: NavHostFragment = supportFragmentManager.findFragmentById(R.id.NavHostFragment) as NavHostFragment
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(R.navigation.business_navigation)
graph.startDestination = destination
navController.graph = graph
}
Then, where I handle the NavigationView clicks:
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_sites_fragment -> navigateWithClearStack(R.id.sitesFragment)
R.id.nav_projects_fragment -> navigateWithClearStack(R.id.projectsFragment)
R.id.nav_contacts_fragment -> navigateWithClearStack(R.id.contactsFragment)
R.id.nav_tasks_fragment -> navigateWithClearStack(R.id.tasksFragment)
R.id.nav_profile_fragment -> makeToast("Todo: Profile Fragment")
R.id.nav_settings_fragment -> makeToast("Todo: Settings Fragment")
}
business_drawer_layout.closeDrawer(GravityCompat.START)
return true
}
So as users Navigate through app, Fragments are added to the BackStack (using popUpTo to deal with duplicates), but when user clicks shortcut back to one of 'starting' fragments, the graph is replaced, thus clearing the BackStack.. Which I think is kind of neat.

Handle To Remove/Add Live Data Observer to Observe On Button Click

I want to to observe a row in room database. it change after some period. but when we stop button click it need to be stop observing form database and when click start button it start observing again.
My current code is
To create Observer
private lateinit var recordObserver: Observer<Ride>
recordObserver= Observer<Ride> { rides ->
if (rides != null)
updateData(rides)
else
setDataToZero()
}
when(isState){
Constants.isrunning->{//need to start observer}
Constants.Stop->{//need to stop observer}
}
In order to start/stop observing LiveData you should use observe() / removeObserver() methods. As simple as that. If you have access to LifecycleOwner (Fragment, Activity) use fun observe(), if not - use fun observeForever().
Your code will look like this:
val liveData = database.observeRides() // get your live data
when(isState){
Constants.isrunning -> {
liveData.observe(this, recordObserver)
}
Constants.Stop -> {
liveData.removeObserver(recordObserver)
}
}

Kotlin-Firebase-cannot utilize the string read from database further down in the code

I have managed to read data from my firebase database but cant seem to re-use the String which has been read.
My successful read is as per below. When i check the logcat for the Log.d("Brand") it actually shows the String as expected.
brandchosenRef=FirebaseDatabase.getInstance().reference
val brandsRef = brandchosenRef.child("CarList2").orderByChild("Car").equalTo(searchable_spinner_brand.selectedItem.toString())
val valueEventListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for(ds in dataSnapshot.children){
Log.d("spinner brand",searchable_spinner_brand.selectedItem.toString())
val Brand = ds.child("Brand").getValue(String::class.java)
val brandselected= Brand.toString()
Log.d("Brand","$brandselected")
selectedbrand== brandselected
Log.d("selected brand",selectedbrand)
}
}
override fun onCancelled(databaseError: DatabaseError) {
Log.d("Branderror","error on brand")
}
}
brandsRef.addListenerForSingleValueEvent(valueEventListener)
What i am trying to do is write "selectedbrand" into a separate node using the following:
val carselected = searchable_spinner_brand.selectedItem.toString()
val dealref = FirebaseDatabase.getInstance().getReference("Deal_Summary2")
val dealsummayId = dealref.push().key
val summaryArray = DealSummaryArray(dealsummayId.toString(),"manual input for testing","brand","Deal_ID",carselected,extrastext.text.toString(),otherinfo.text.toString(),Gauteng,WC,KZN,"Open")
dealref.child(dealsummayId.toString()).setValue(summaryArray).addOnCompleteListener{
}
Note, in the above i was inputting "manual input for testing" to check that my write to Firebase was working and it works as expected. if i replace that with selectedbrand, then i get the below error.
kotlin.UninitializedPropertyAccessException: lateinit property selectedbrand has not been initialized
the summary array indicated above is defined in a separate class as follows. and as seen "manual input for testing is declared as String.
class DealSummaryArray(val id:String,val brand:String,val Buyer_ID:String,val Deal_ID:String,val Car:String,val extras:String,val other_info:String,val Gauteng:String,val Western_Cape:String,val KZN:String,val Status:String) {
constructor():this("","","","","","","","","","",""){
}
}
My question simply put, it why can i not re-use the value i read from the database? even if i was not trying to re-write it to a new node i cannot seem to utilize the value outside of the firebase query.
I seem to get this problem everywhere in my activities and have to find strange work around's like write to a textview and then reference the textview. please assist.
Data is loaded from Firebase asynchronously, as it may take some time before you get a response from the server. To prevent blocking the application (which would be a bad experience for your users), your main code continues to run while the data is being loaded. And then when the data is available, Firebase calls your onDataChange method.
What this means in practice is that any code that needs the data from the database, needs to be inside the onDataChange method or be called from there. So any code that requires selectedbrand needs to be inside onDataChange or called from there (typically through a callback interface).
Also see:
How to check a certain data already exists in firestore or not, which contains example code including of the callback interface, in Java.
getContactsFromFirebase() method return an empty list, which contains a similar example for the Firebase Realtime Database.
Setting Singleton property value in Firebase Listener, which shows a way to make the code behave more synchronous, and explains shows that this may not work on various Android versions.

Open Android activity automatically on receiving notification

I have to launch the app on receiving notification. The following piece of code works fine when the app is killed and notification is received (i.e the code inside if condition). But when the app is running in foreground or background, multiple instances of the activity gets created(i.e snippet in else condition). It's not the MainActivity that has to be launched on receiving the notification, instead it's some other activity containing the broadcast Receiver.
I have added the following lines in the onMessage of GCMintentService class.
if (currentPackage.equalsIgnoreCase(context.getPackageName()
.toString())) {
broadcastMessage(context, message);
} else {
Intent mIntent = new Intent(context, MainActivity.class);
mIntent.setAction(Intent.ACTION_MAIN);
mIntent.addCategory(Intent.CATEGORY_LAUNCHER);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(mIntent);
}
In the activity, under onReceive method of BroadcastReceiver, i am starting the activity again.
private final BroadcastReceiver mHandleMessageReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
startActivity(getIntent().setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
}
};
I also had this requirement in one of my application. We can achieve it if we call
Intent mIntent = new Intent(context, MainActivity.class);
mIntent.setAction(Intent.ACTION_MAIN);
mIntent.addCategory(Intent.CATEGORY_LAUNCHER);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(mIntent);
broadcastMessage(context, message);
In the main activity use the following in the broadcast receiver that would receive the broadcasted message above.
WakeLock wakeLock = null;
KeyguardManager kgMgr = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
boolean locked = kgMgr.inKeyguardRestrictedInputMode();
PowerManager pm = (PowerManager) context
.getSystemService(Context.POWER_SERVICE);
if (!pm.isScreenOn()) {
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK
| PowerManager.ACQUIRE_CAUSES_WAKEUP, "MyWakeLock");
wakeLock.acquire();
}
if (locked) {
Window mWindow = getWindow();
mWindow.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
mWindow.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
}
Personally I feel that this is not the best of the answers and also best of the ideas to open the app directly when received a notification as there will be many functions like onCreate onResume, will be triggered automatically, spoil the users work if they are in a really important work by opening another app directly, also we need to put a lot of flags or use any other method to manage the flow of the application, when user open the app, app comes from background, app opened due notification and all such cases. Avoid it as it spoils the whole user experience.