invertase / notifee

⚛️ A feature rich notifications library for React Native.
https://notifee.app
Apache License 2.0
1.77k stars 205 forks source link

Android: Fullscreen notification bypasses lockscreen and presents entire app (Incoming Calls) #501

Closed frozencap closed 1 year ago

frozencap commented 1 year ago

Hi everyone and thanks to the authors for open-sourcing and maintaining this

Use case: Incoming Call on Lock Screen.

After this exact sequence 1- killing the app 2- awakening it with server-sent high importance notification below 3- dismissing the notification 4- locking the screen 5- resending the same notification

The notification bypasses lockscreen and just opens the app on top of it, every time, without entering passcode, with full access to the app (but not past the lock screen). Seems like a big footgun.

Is this a bug or a feature ?

More generally: Is it possible today with notifee to get basic Incoming Call notifications going? With similar behavior users expect: turns on lock screen, occupies it full screen and displays/vibrates/rings until interacted with.

mainComponent sounds like a good lead but it isn't quite clear in the docs what it can or cannot do (props? navigate back to main app?)

const displayCallNotificationAndroid = async ({
  callId,
  callerName,
  hasVideo,
}: {
  callId: string;
  callerName: string;
  hasVideo: boolean;
}) => {
  console.log('📞 📥  displayCallNotificationAndroid: ', callId);

  const channelId = await notifee.createChannel({
    id: 'nugget-calls',
    name: 'nugget-calls',
    importance: AndroidImportance.HIGH,
  });

  const dnr = await notifee.displayNotification({
    title: callerName,
    body: `is calling you on ${hasVideo ? 'video' : 'voice'}...`,
    id: callId,
    android: {
      channelId,
      smallIcon: 'ic_launcher_round',
      color: '#dedede',
      category: AndroidCategory.CALL,
      importance: AndroidImportance.HIGH,
      fullScreenAction: {
        id: 'default',
      },
      actions: [
        {
          title: 'Decline',
          pressAction: {
            id: 'decline-call',
          },
        },
        {
          title: 'Answer',
          pressAction: {
            id: 'answer-call',
          },
        },
      ],
      lightUpScreen: true,
      // asForegroundService: true,
      colorized: true,
    },
  });
  console.log('🔭 displayNotification result: ', dnr);
};
Samsung s10e 
Android 12
compileSdkVersion = 31
"react-native": "^0.68.0",
"@react-native-firebase/app": "^13.0.1",
"@react-native-firebase/messaging": "^13.0.1",
"@notifee/react-native": "^5.4.1",

Thanks!

jgarplind commented 9 months ago

that the passcode lockscreen gets presented in between?

Thanks so much for your code examples and help here, @pierguinzani ! I too am very curious how to switch activities automatically as my goal is to show the user an incoming call screen in the custom activity opened by the full screen notification and in that component show buttons like "accept" or "decline". When the user taps "decline", BackHandler.exitApp() does fine to close the activity, but when the user taps "accept", I'm not sure how to close the current (custom) activity, present the keyguard, and then open the main (default) activity afterwards? From your response it seems that it's matter of setting the properties on the different activities in the Manifest but I don't see how that alone would do it? I've tried but nothing happens. Pseudo-coding here, it almost seems like I would need something in my component like:

<Button onPress={() => {
   CustomModule.presentKeyguard().then(() => 
     BackHandler.exitApp();
     CustomModule.startMainActivity(); ??
   );
})>Accept</Button>

Sorry I'm sure there's plenty of reasons why that pseudocode would never work, my native Android is almost non-existent so it's more an expression of my goals.

It's been a few months, so I assume this is no longer relevant for you, but I discovered (yet to refine) a plausible solution that I would like to share with you and others:

To start the MainActivity from the CustomActivity, I wrote a NativeModule exporting this method:

    @ReactMethod
    void navigateToMainActivity() {
        Activity activity = getCurrentActivity();
        if (activity != null) {
            Intent intent = new Intent(activity, MainActivity.class);
            activity.startActivity(intent);
        }
    }

I was stuck here for some time, as the MainActivity was still hidden behind the lock screen, and there was no prompt to unlock the device.

Now, I'm definitely no Android developer, but I managed to figure out that one way to prompt to unlock the keyguard was to include this code in the MainActivity:

  public void onNewIntent(Intent intent) {
    KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
    keyguardManager.requestDismissKeyguard(this, null);
    super.onNewIntent(intent);
  }

I'm planning to work more on this shortly, but I think I have finally found a solution that is satisfactory for my use case, and hopefully others' too!

badhrin commented 6 months ago

@shawarmaz

package com.myapp;

import com.facebook.react.ReactActivity;

public class CustomActivity extends ReactActivity {
  @Override
  protected String getMainComponentName() {
    return "custom";
  }
}

which folder did you place this activity in

im not able to launch this with an expo managed project with dev client. do you have any clues to how i could prevail with expo.

Emuter / jgarplind / shawarmaz / Anyone - Any help on getting this working with Expo Managed Project (Dev Client) ?

jgarplind commented 6 months ago

Emuter / jgarplind / shawarmaz / Anyone - Any help on getting this working with Expo Managed Project (Dev Client)?

I think you are confusing terminology.

Managed project != Dev client

Managed workflow is the opposite of bare workflow. Only with a bare workflow it is possible to add custom Android code, which I have found to be required for this use case.

Using a bare workflow, you can then set up a "development client" which is a version of your app that you will be using instead of the Expo Go app for development purposes.

Check Expo docs, e.g. https://docs.expo.dev/workflow/customizing/

badhrin commented 6 months ago

jgarplind - Thank you for taking the time to respond. Yes, it's a Expo project with a Dev client. I did not use the word 'bare workflow' as I believe it means directly accessing native, as opposed to doing it through Config Plugins. Anyway, am pretty new to all this & hence will defer to you. And really appreciate the Notifee maintainers & contributors for this excellent work !!

Related to my question, I was wondering if there was any config plugin that can help with implementing what's described above in Expo (without having to write ANY android code myself). I am just a junior RN & Expo dev & can hardly think of doing Android. Hence the ask.

Additionally, based on my research & trials over the last few days trying to handle Incoming calls using Fullscreen Notification & Expo, sharing a few findings. Hope this helps others who land here.

  1. Full Screen Notification in Expo won't work OOB just because the below will NOT be set in the MainActivity: <activity ... android:showWhenLocked="true" android:turnScreenOn="true" />

  2. However, it's pretty easy to build a Config Plugin for this, thanks to a page in Expo Community that gives the exact code. Here's the link - https://forums.expo.dev/t/how-to-edit-android-manifest-was-build/65663/5. I have tried it & it works like a charm.

  3. Post implementing & rebuilding the Development Build using EAS (or local), the FullScreen notification will work as in the Notifee docs. However, if you still find that on Lock Screen, it does not show up as a Full Screen notification, then it mostly has to do with the phone. It worked for me on 1 phone, but not on the other. I was unable to solve it ever on the phone. But my guess is that it has to do with some phone setting (pointer - maybe the batter setting is set to 'restricted' or 'optimized' & an 'unrestricted' setting update will help)

  4. I will keep working on Notifee for an Incoming Call use case (using Expo only )and will update this thread, if I am able to make it work consistently across phones.

Prafulkumar0512 commented 5 months ago

that the passcode lockscreen gets presented in between?

Thanks so much for your code examples and help here, @pierguinzani ! I too am very curious how to switch activities automatically as my goal is to show the user an incoming call screen in the custom activity opened by the full screen notification and in that component show buttons like "accept" or "decline". When the user taps "decline", BackHandler.exitApp() does fine to close the activity, but when the user taps "accept", I'm not sure how to close the current (custom) activity, present the keyguard, and then open the main (default) activity afterwards? From your response it seems that it's matter of setting the properties on the different activities in the Manifest but I don't see how that alone would do it? I've tried but nothing happens. Pseudo-coding here, it almost seems like I would need something in my component like:

<Button onPress={() => {
   CustomModule.presentKeyguard().then(() => 
     BackHandler.exitApp();
     CustomModule.startMainActivity(); ??
   );
})>Accept</Button>

Sorry I'm sure there's plenty of reasons why that pseudocode would never work, my native Android is almost non-existent so it's more an expression of my goals.

It's been a few months, so I assume this is no longer relevant for you, but I discovered (yet to refine) a plausible solution that I would like to share with you and others:

To start the MainActivity from the CustomActivity, I wrote a NativeModule exporting this method:

    @ReactMethod
    void navigateToMainActivity() {
        Activity activity = getCurrentActivity();
        if (activity != null) {
            Intent intent = new Intent(activity, MainActivity.class);
            activity.startActivity(intent);
        }
    }

I was stuck here for some time, as the MainActivity was still hidden behind the lock screen, and there was no prompt to unlock the device.

Now, I'm definitely no Android developer, but I managed to figure out that one way to prompt to unlock the keyguard was to include this code in the MainActivity:

  public void onNewIntent(Intent intent) {
    KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
    keyguardManager.requestDismissKeyguard(this, null);
    super.onNewIntent(intent);
  }

I'm planning to work more on this shortly, but I think I have finally found a solution that is satisfactory for my use case, and hopefully others' too!

@jgarplind Thanks man for your solution. can you explain where to add thenavigatetoMainActivity method in mainActivity or custom Activity?

One more query is there any method to open the mainactivity and redirect to a specific screen when the user clicks on the action of fullscreen notification

jgarplind commented 5 months ago

@jgarplind Thanks man for your solution. can you explain where to add thenavigatetoMainActivity method in mainActivity or custom Activity?

In our case it looks like this:

public class IncomingCallModule extends ReactContextBaseJavaModule {
    IncomingCallModule(ReactApplicationContext context) {
        super(context);
    }

    @Override
    public String getName() {
        return "IncomingCallModule";
    }

    @ReactMethod
    void navigateToMainActivity() {
        Activity activity = getCurrentActivity();
        if (activity != null) {
            Intent intent = new Intent(activity, MainActivity.class);
            activity.startActivity(intent);
        }
    }
}

One more query is there any method to open the mainactivity and redirect to a specific screen when the user clicks on the action of fullscreen notification

I think you will need to figure out some case in the app logic.

For us it looks something like:

    notifee.onBackgroundEvent(async ({ type, detail }) => {
      if (
        type === EventType.DELIVERED &&
        detail.notification?.android?.channelId === "incomingcall"
      ) {
        // navigate to call UI
      }
    }
ReemKrauss commented 5 months ago

I'm just wrapping up a release for my app and will get to writing an article on Incoming Calls with notifee based on @pierguinzani's suggestion

@shawarmaz did you end up writing that article ?

I'm new to react native and I'm trying to get full screen notification for my video call feature I'm using FCM with Notifee and android: { fullScreenAction: { id: 'default', }, } Just doesn't work and there is almost no documentation on the subject. I would very much appreciate it if you could give me any guidance.

badhrin commented 4 months ago

@ReemKrauss - Working on a similar calling feature now & have the same issue - Notifee full screen does not work on Android. On analysis, I found that the issue was only in Android 14 & above. Full screen works until Android 13. The Solution was to enable the "Full Screen Alerts" in "Special App Access" settings. However, the problem is how to request OR enable this programatically ? I tried to add the Full Screen Permission in Android Manifest, but beyond that how do we make the User provide specific permission for this. This "Full Screen Alerts" is not part of the PermissionsAndroid of React Native as it's a Special permission. Hope this is helpful to you. Do let me know if you find a final solution for this. And please feel free to DM - would be good to compare notes as we work on similar feature

mikehardy commented 4 months ago

Looks like full screen intents for Android 14 will need an implementation - some information: https://stackoverflow.com/questions/77253322/jump-to-full-screen-notification-settings-android-14

This should be in a new issue I think, and will likely need a community member to provide a PR - I'll do my best to get it merged and released though I know this repo is a bit behind on that front (my fault, sorry)

I typically recommend people use react-native-permissions but it does not appear to implement this yet. A PR there would likely be most welcome https://github.com/zoontek/react-native-permissions/pulls?q=USE_FULL_SCREEN_INTENT

badhrin commented 4 months ago

@mikehardy - Thank you for the inputs. I unfortunately do not have skills to contribute to PR for this, but here are some additional thoughts & some learnings which may be useful for others:

  1. While this can surely be a PR in react-native-permissions, should this also not become part of Notifee itself i.e. a part of notifee.requestPermission() ? As an end user,
  2. Thanks to the link that you posted, I was able to find a way to launch the Full screen Alert permission screen from the RN app. This can be done using RN's Linking.sendIntent. The intent - "android.settings.MANAGE_APP_USE_FULL_SCREEN_INTENT" opens up the general FSI settings screen for all apps. This can also be done using Expo to launch a very App specific FSI setting screen using Expo Intent Launcher. The only catch is that the data field must show the uri in this format - "package:my.package.name". Thanks to https://stackoverflow.com/questions/76565961/does-the-settings-page-for-action-manage-app-use-full-screen-intent-work-androi for the pointer.
  3. However, I could still find NO way of checking if the user has enabled this permission. I tried to use the PermissionsAndroid of RN with the "android.permission.USE_FULL_SCREEN_INTENT". It returns true even if the user has not enabled FSI in the FSI settings. It's probably just checking if this permission is part of my app's Android Manifest. Any pointers on how to check if the user has enabled this FSI permission using RN ?
ReemKrauss commented 4 months ago

@badhrin Thank for the answer. I'm using a launchActivity with the full screen and it seems to work fine. You can try and follow pierguinzani answer above. Feel free to DM me when ever

Irfanwani commented 2 weeks ago

@shawarmaz I had some issues with callkeep and have been working with notifee to solve this problem. In my case, I created a custom activity - CustomActivity.java in android/app/java/com/MyApp :

package com.myapp;

import com.facebook.react.ReactActivity;

public class CustomActivity extends ReactActivity {
  @Override
  protected String getMainComponentName() {
    return "custom";
  }
}

Next, I created a simple incoming call notification with notifee:

        title: '(21) 1234-1234',
        body: 'Incoming call',
        android: {
            channelId: 'call',
            category: AndroidCategory.CALL,
            visibility: AndroidVisibility.PUBLIC,
            importance: AndroidImportance.HIGH,
            smallIcon: 'myapp_icon',
            timestamp: Date.now(),
            showTimestamp: true,
            pressAction: {
                id: "default",
                launchActivity: 'com.myapp.CustomActivity',
            },
            actions: [{
                title: "Accept",
                pressAction: {
                    id: "accept",
                    launchActivity: 'default',
                }
            }, {
                title: 'Decline',
                pressAction: {
                    id: "reject",
                }
            }],
            fullScreenAction: {
                id: 'default',
                launchActivity: 'com.myapp.CustomActivity',
            },
        },

The fullScreenAction, which calls a customActivity, is an important part of this implementation.

And then, I needed to register this component in my index.js:

import { AppRegistry } from 'react-native';
import React from 'react';
import App from './src';
import IncomingCall from './src/screens/IncomingCall';

AppRegistry.registerComponent(appName,  App);

AppRegistry.registerComponent('custom', IncomingCall); //A custom component with two buttons to attend and decline the incoming call

I also configured my AndroidManifest.xml this way:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.myapp">

    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-feature android:name="android.hardware.audio.output" />
    <uses-feature android:name="android.hardware.microphone" />

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- Permission configs -->
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
    <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
    <uses-permission android:name="android.permission.READ_CALL_LOG" />
    <uses-permission android:name="android.permission.USE_SIP" />
    <!-- Xiaomi and similar devices overlay configs -->
    <uses-permission android:name="android.permission.ACTION_MANAGE_OVERLAY_PERMISSION" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
      </activity>
      <activity
        android:name="com.myapp.CustomActivity"
        android:showWhenLocked="true"
        android:turnScreenOn="true"
        android:launchMode="singleTop"
        android:showOnLockScreen="true"
      />
    </application>
</manifest>

This part will probably solve your problem because you will give permission to show only the custom component (Incoming Call) on the locked screen.

<activity
        android:name="com.myapp.CustomActivity"
        android:showWhenLocked="true"
        android:turnScreenOn="true"
        android:launchMode="singleTop"
        android:showOnLockScreen="true"
      />

I am not able to get a full screen notification, no matter what, i am just getting the normal popup notification. BTW, i want to show a incoming call screen. This requires a user to click on the notification and then the registered component is shown, but how to show it automatically in the case when the screen is locked, like whatsapp does?

Irfanwani commented 2 weeks ago

AFAICT android only displays it fullscreen when the phone is locked/closed

for me it is not working, no matter what, it always shows a normal notification, is there any thing more to do for it to work. Can anyone please provide a minimum working example of how to make full screen notifications work.