iospush-notificationnotificationsxamarin.ioslocalytics

Xamarin Notification Service Extension issue


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.

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;
        }

    }
}

Solution

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