21

I have an issue with Notification Service Extension. I have followed step by step documentation https://developer.xamarin.com/guides/ios/platform_features/introduction-to-ios10/user-notifications/enhanced-user-notifications/#Working_with_Service_Extensions

To implement, I have done in that way.

  • Added Notification Service Extension with same prefix of my app (adding a suffix, ex: APP: com.testapp.main - EXT: com.testapp.main.notificationextension)
  • Created APPID identifier com.testapp.main.notificationextension into Member Center of Apple
  • Created certificate and provisioning profile to send push notification for APP ID com.testapp.main.notificationextension
  • Imported into Xcode and Xamarin certificate and provisioning
  • Build my app with reference to Notification Extension reference.
  • Created archive to upload to TestFlight
  • Signed app with its Distribution Certificate and Provisioning Profile
  • Signed extension with its Distribution Certificate and Provisioning Profile
  • Uploaded to TestFlight
  • Download and allowed push notification for my app
  • Sent rich push notification with Localytics Dashboard for messaging - Device receive push notification but not pass for NotificationService.cs code of Notification Service Extension!

This is my NotificationService code:

using System;
using Foundation;
using UserNotifications;

namespace NotificationServiceExtension
{
    [Register("NotificationService")]
    public class NotificationService : UNNotificationServiceExtension
    {
        Action<UNNotificationContent> ContentHandler { get; set; }
        UNMutableNotificationContent BestAttemptContent { get; set; }
        const string ATTACHMENT_IMAGE_KEY = "ll_attachment_url";
        const string ATTACHMENT_TYPE_KEY = "ll_attachment_type";
        const string ATTACHMENT_FILE_NAME = "-localytics-rich-push-attachment.";

        protected NotificationService(IntPtr handle) : base(handle)
        {
            // Note: this .ctor should not contain any initialization logic.
        }

        public override void DidReceiveNotificationRequest(UNNotificationRequest request, Action<UNNotificationContent> contentHandler)
        {
            System.Diagnostics.Debug.WriteLine("Notification Service DidReceiveNotificationRequest");
            ContentHandler = contentHandler;
            BestAttemptContent = (UNMutableNotificationContent)request.Content.MutableCopy();
            if (BestAttemptContent != null)
            {
                string imageURL = null;
                string imageType = null;
                if (BestAttemptContent.UserInfo.ContainsKey(new NSString(ATTACHMENT_IMAGE_KEY)))
                {
                    imageURL = BestAttemptContent.UserInfo.ValueForKey(new NSString(ATTACHMENT_IMAGE_KEY)).ToString();
                }
                if (BestAttemptContent.UserInfo.ContainsKey(new NSString(ATTACHMENT_TYPE_KEY)))
                {
                    imageType = BestAttemptContent.UserInfo.ValueForKey(new NSString(ATTACHMENT_TYPE_KEY)).ToString();
                }

                if (imageURL == null || imageType == null)
                {
                    ContentHandler(BestAttemptContent);
                    return;
                }
                var url = NSUrl.FromString(imageURL);
                var task = NSUrlSession.SharedSession.CreateDownloadTask(url, (tempFile, response, error) =>
                {
                    if (error != null)
                    {
                        ContentHandler(BestAttemptContent);
                        return;
                    }
                    if (tempFile == null)
                    {
                        ContentHandler(BestAttemptContent);
                        return;
                    }
                    var cache = NSSearchPath.GetDirectories(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomain.User, true);
                    var cachesFolder = cache[0];
                    var guid = NSProcessInfo.ProcessInfo.GloballyUniqueString;
                    var fileName = guid + ATTACHMENT_FILE_NAME + imageType;
                    var cacheFile = cachesFolder + fileName;
                    var attachmentURL = NSUrl.CreateFileUrl(cacheFile, false, null);
                    NSError err = null;
                    NSFileManager.DefaultManager.Move(tempFile, attachmentURL, out err);
                    if (err != null)
                    {
                        ContentHandler(BestAttemptContent);
                        return;
                    }
                    UNNotificationAttachmentOptions options = null;
                    var attachment = UNNotificationAttachment.FromIdentifier("localytics-rich-push-attachment", attachmentURL, options, out err);
                    if (attachment != null)
                    {
                        BestAttemptContent.Attachments = new UNNotificationAttachment[] { attachment };
                    }
                    ContentHandler(BestAttemptContent);
                    return;
                });
                task.Resume();
            }
            else {
                ContentHandler(BestAttemptContent);
            }
        }

        public override void TimeWillExpire()
        {
            // Called just before the extension will be terminated by the system.
            // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
            ContentHandler(BestAttemptContent);
            return;
        }

    }
}
Luigi Saggese
  • 5,299
  • 3
  • 43
  • 94
  • So what is the issue? – Demitrian Jan 02 '17 at 14:18
  • Device receive push notification but not pass for NotificationService.cs code of Notification Service Extension! – Luigi Saggese Jan 02 '17 at 14:51
  • 1
    What do you mean by "does not pass"? Do you mean that `DidReceiveNotificationRequest` isn't executed? – Demitrian Jan 02 '17 at 15:10
  • Exact. I have also tried to change only title of push inside DidReceiveNotificationRequest but it's not called. – Luigi Saggese Jan 02 '17 at 15:11
  • Which version of iOS are you running? Extensions are not available until iOS 10. Also, I assume that you included the extension as a new project in the solution where your application resides? – Demitrian Jan 02 '17 at 15:18
  • Yes is a new project (created as Notification Service Extension) with Deployment Target 10.0. Inside project of my app I have added reference to project extension (inside .csproj there is also automatic setted to true xml tag true, then is recognized as extension). My app Deployment Target is starting from 8.1. I have installed app on a device with iOS 10.2. – Luigi Saggese Jan 02 '17 at 15:25
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/132119/discussion-between-luigi-saggese-and-demitrian). – Luigi Saggese Jan 02 '17 at 15:26
  • Can you share an example of the message you are sending? I am hoping to confirm that it contains `mutable-content: 1`. Also, check the device logs when debugging this and look for any obvious errors that might be related. – therealjohn Jan 10 '17 at 13:09
  • We sent rich push via Localytics Dashboard http://docs.localytics.com/dev/ios.html#rich-push-ios – Luigi Saggese Jan 10 '17 at 13:56

2 Answers2

5

You are doing everything correctly, this is an issue raised by a few other xamarin developers. From what I can tell, as soon as you run the NSURLSession to download something, even if it's super super small, you go above the memory limit allowed for this type of extension. This is most probably very specific to xamarin. Here is the link to the bugzilla. https://bugzilla.xamarin.com/show_bug.cgi?id=43985

The work-around/hack I found is far from ideal. I rewrote this app extension in xcode in objective-c (you could use swift too I suppose). It is a fairly small (1 class) extension. Then built it in xcode using the same code signature certificate / provisioning profile and then found the .appex file in the xcode's output.

From then, you can take the "cheap way", and swap this .appex file in your .ipa folder manually just before resigning and submitting the app. If that's good enough for you, you can stop here.

Or you can automate this process, to to so, place the appex file in the csproj's extension and set the build-action as "content". Then in this csproj's file (you'll need to edit directly) you can add something like this. (In this case, the file is called Notifications.appex and is placed in a folder called NativeExtension)

<Target Name="BeforeCodeSign">
    <ItemGroup>
        <NativeExtensionDirectory Include="NativeExtension\Debug\**\*.*" />
    </ItemGroup>

    <!-- cleanup the application extension built with Xamarin (too heavy in memory)-->
    <RemoveDir SessionId="$(BuildSessionId)"
               Directories="bin\iPhone\Debug\Notifications.appex"/>

    <!-- copy the native one, built in obj-c -->
    <Copy
            SessionId="$(BuildSessionId)"
            SourceFiles="@(NativeExtensionDirectory)"
            DestinationFolder="bin\iPhone\Debug\Notifications.appex"
            SkipUnchangedFiles="true"
            OverwriteReadOnlyFiles="true"
            Retries="3"
            RetryDelayMilliseconds="300"/>
</Target>

This gives you the general idea, but obviously if you want to support ad-hoc distribution signature, iOS app-store distribution signature you will need to add a bit more code into this (and possibly add in the csproj a native appex file for each different signature), I would suggest putting such xml code in separate ".targets" file and use conditional calltargets in the csproj. Like this:

<Target Name="BeforeCodeSign">
    <CallTarget Targets="ImportExtension_Debug" Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' " />
    <CallTarget Targets="ImportExtension" Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' " />
 </Target>
matfillion
  • 867
  • 6
  • 10
  • Archive for Publish my app -> Signing and distribute with same certificate used to sign Obj extension. Now I have unzipped ipa and replaced appex. Compressed and zipped as ipa. Now i have used this tool to resign application (https://github.com/maciekish/iReSign). Maybe there are issue to resign extension because i have this error installing app [_validateSignatureAndCopyInfoForURL:withOptions:error:]: 147: Failed to verify code signature of .../extracted/Payload/JRUITouch.app/PlugIns/NotificationServiceExtension.appex : 0xe8008001]. Do you know which command execute to resign app? – Luigi Saggese Apr 27 '17 at 16:48
  • If you are using iResign, it will only sign the whole ipa, but you need to sign the inner appex before that, and then use iresign to sign your full ipa package. Here is an example of the command you can run before rezipping the ipa package. codesign -f -s "name of certificate authority" -i "com.company.app.notifications" --entitlements "../ExtensionEntitlements.plist" "Payload/MyApp.app/PlugIns/Notifications.appex" – matfillion Apr 27 '17 at 18:12
  • I have this issue when i try to install on device after resigning Apr 28 09:41:16 iPhone installd(MobileSystemServices)[3541] : 0x16dfef000 +[MICodeSigningVerifier _validateSignatureAndCopyInfoForURL:withOptions:error:]: 147: Failed to verify code signature of /private/var/installd/Library/Caches/com.apple.mobile.installd.staging/temp.QBb4HH/extracted/Payload/JRUITouch.app/PlugIns/NotificationServiceExtension.appex : 0xe8008001 (An unknown error has occurred.) – Luigi Saggese Apr 28 '17 at 07:42
  • 1
    Sorry It's my issue based on name of extension compiled with Xcode. With same name works perfectly – Luigi Saggese Apr 28 '17 at 12:20
  • @LuigiSaggese Can you elaborate on your last comment to that we can avoid this? It sounds as if there was something about the name others might run into. – Philipp Sumi Jul 13 '17 at 12:36
  • @PhilippSumi extension name must be the same compiled between Xamarin and Xcode – Luigi Saggese Jul 13 '17 at 14:14
1

If anyone else comes here, the code by original poster works for me and the bug mention is now marked as fixed. If I have one tip, do not try to do this on Windows. You will be in for a whole world of pain and will get nowhere (actually, it did work for me, once!). Also expect Visual Studio on Mac to crash, a lot, if you try to debug!

Si-N
  • 1,495
  • 1
  • 13
  • 27
  • what do you suggest then? Cant i intercept the push notification in the extension service in Xamarin on Visual Studio on Windows? – FreedomOfSpeech Sep 06 '18 at 18:07