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;
}
}
}
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>
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!
Related
We are using the Facebook Unity SDK 15.1.0 in Unity 2019.4.40f1. This SDK has some serious bugs. One of them is that it won't add the required frameworks to the Unity-iPhone target. The project will build, but immediately crash on startup.
The frameworks are there, but in a Pod:
I can add them manually in the General => Frameworks, Libraries, and Embedded Content section of the target:
Everything works fine then.
However, doing this after every build is quite tedious, so I would like to automate this task via a post build script. I am scratching my head about this, since I cannot find good samples online that actually work.
So my question is: How do you add a .xcframework buried in Facebook SDK's pods so it correctly shows up in the target?
Here is how i would do it. This may not be 100% correct but good enough to modify it to make it work for you. Basically you need to use PBXProject.AddFrameworkToProject to add frameworks.
#if UNITY_IOS
[PostProcessBuild(1)]
public static void ChangeXcodePlist(BuildTarget buildTarget, string pathToBuiltProject) {
if (buildTarget == BuildTarget.iOS) {
// get pbx project path
var projPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
if (File.Exists(projPath))
{
var proj = new PBXProject();
proj.ReadFromString(File.ReadAllText(projPath));
string mainTargetGuid = null, testTargetGuid = null, frameworkTargetGuid = null;
#if UNITY_2019_4_OR_NEWER // APIs are different for getting main unity targets changes based on versions
mainTargetGuid = proj.GetUnityMainTargetGuid();
frameworkTargetGuid = proj.GetUnityFrameworkTargetGuid();
#else
mainTargetGuid =
proj.TargetGuidByName(PBXProject.GetUnityTargetName());
testTargetGuid =
proj.TargetGuidByName(PBXProject.GetUnityTestTargetName());
frameworkTargetGuid = proj.TargetGuidByName("UnityFramework");
#endif
// add your frameworks here
if (!String.IsNullOrEmpty(mainTargetGuid))
{
Debug.Log("Adding targets to mainTargetGuid")
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKCoreKit.xcframework", false);
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKGamingServicesKit.xcframework", false);
}
// add to test target aswell if exists
if (!String.IsNullOrEmpty(testTargetGuid))
{
Debug.Log("Adding targets to testTargetGuid")
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKCoreKit.xcframework", false);
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKGamingServicesKit.xcframework", false);
}
if (!String.IsNullOrEmpty(frameworkTargetGuid))
{
Debug.Log("Adding targets to frameworkTargetGuid")
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKCoreKit.xcframework", false);
proj.AddFrameworkToProject(mainTargetGuid, "FBSDKGamingServicesKit.xcframework", false);
}
proj.WriteToFile(projPath);
}
}
}
#endif
You can also use PBXProject.ContainsFramework before you include the frameworks.
The code in the previous answer is not working. Freimworks are not added like that.
Correct code for adding facebook's frameworks (working):
[PostProcessBuild(1000)]
public static void OnPostprocessBuild(BuildTarget buildTarget, string pathToBuiltProject) {
if (buildTarget != BuildTarget.iOS) return;
string projectPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
PBXProject project = new PBXProject();
project.ReadFromFile(projectPath);
string mainTargetGuid = project.GetUnityMainTargetGuid();
List<string> frameworks = new List<string> {
"FBSDKCoreKit",
"FBSDKGamingServicesKit"
};
foreach (string framework in frameworks) {
string frameworkName = framework + ".xcframework";
var src = Path.Combine("Pods", framework, "XCFrameworks", frameworkName);
var frameworkPath = project.AddFile(src, src);
project.AddFileToBuild(mainTargetGuid, frameworkPath);
project.AddFileToEmbedFrameworks(mainTargetGuid, frameworkPath);
}
// Write.
project.WriteToFile(projectPath);
}
Build environment:
Macbook M1
vscode(1.69.0) as well as vs2022 (17.3)
Steps to reproduce:
create new Maui app
add nuget package "Microsoft.Extensions.Http" Version="6.0.0" to project
Modify MauiProgram.cs:
builder.Services.AddHttpClient<EndPointAHttpClient>(client =>
{
var EndPointA = "https://www.montemagno.com/";
client.BaseAddress = new Uri(EndPointA);
});
public class EndPointAHttpClient
{
public EndPointAHttpClient(HttpClient client)
{
Client = client;
}
public HttpClient Client { get; }
}
Publish:
dotnet publish <project.csproj> -f:net6.0-ios -c:Release /p:ServerAddress=<xxx.xxx.xxx.xxx> /p:ServerUser=user /p:TcpPort=58181 /p:ServerPassword=pwd -p:AotAssemblies=false
Install on iphone using Transporter/TestFlight
CRASHES WHEN OPENING THE APP
Please let me know:
1. Is there any demo code that works
2. Kindly provide advise on how I can use HttpClient in a .net Maui app
Use the code found here. https://github.com/dotnet/maui-samples/tree/main/6.0/WebServices/TodoREST/TodoREST/Services
Grab the RestService, IRestService, HttpsClientHandlerService and IHttpsClientHandlerService.
Get the Contstants file as well.
https://github.com/dotnet/maui-samples/blob/main/6.0/WebServices/TodoREST/TodoREST/Constants.cs
Makes sure you add your Url to the HttpsClientHandlerService like so. I was getting a System.Net.WebException: Error: TrustFailure. The only way I was able to catch what was happening was using Sentry.io. I guessed that this might be the problem.
public bool IsSafeUrl(NSUrlSessionHandler sender, string url, Security.SecTrust trust)
{
if (url.StartsWith("https://localhost") || url.StartsWith("https://yourservice.azurewebsites.net"))
return true;
return false;
}
Then change this line.
var handler = new NSUrlSessionHandler
{
TrustOverrideForUrl = IsSafeUrl
};
I am targeting Android API 30. My app was storing log file and taking database backup in location "/storage/emulated/0/SpecialDir/". Now I am facing access denied issue while my app was workinng fine previously.
I got an overview about scoped storage and came to know that we have some managed locaitons where we can store our data accordingly. i.e Audio, Video, Images, and Download
My question is What is the solution for existing apps that was previously saving files on "/storage/emulated/0/SpecialDir/".
Can anyone please guide me what should i do.
string dir = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.ToString(), "LogFolder");
if (Directory.Exists(dir))
{
return Path.Combine(dir, "MyLogFile.txt");
}
try
{
string newDirectory = Directory.CreateDirectory(dir).FullName;
path = Path.Combine(newDirectory, "MyLogFile.txt");
System.IO.File.WriteAllText(path, "This is some testing log.");
}
catch (Exception ex)
{
string msg = ex.Message;
}
The above code is used to make 'LogFolder' if not exist and 'MyLogFile' as well. What changes do i needed to make it compatiable to Android 10. Thankyou
In Android 10, Google has introduced a new feature for external Storage. Its name is Scoped Storage. Google officially translates it as partitioned Storage, or Scoped Storage.The intent is to limit what programs can do with public directories in external storage. Partitioned storage has no effect on either the internal storage private directory or the external storage private directory.In short, in Android 10, there is no change to private directory reads and writes, and you can still use the File set without any permissions. For reading and writing to public directories, you must use the API provided by MediaStore or the SAF (storage access framework), which means you can no longer use the File set to manipulate public directories at will.
If you set targetSdkVersion above 29,you could try to add below codes into your AndroidManifest.Then you could access the File as before.
<manifest ... >
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
Update (you could try this for public external storage ):
var path = Android.OS.Environment.GetExternalStoragePublicDirectory("LogFolder").AbsolutePath;
Java.IO.File file = new Java.IO.File(path);
if (!file.Exists())
{
file.Mkdirs();
}
try
{
FileWriter fw = new FileWriter(path + Java.IO.File.Separator + "MyLogFile.txt");
fw.Write("This is some testing log.");
fw.Close();
}
catch (Exception ex)
{
string msg = ex.Message;
}
Update for Android 11:
add MANAGE_EXTERNAL_STORAGE permission in your AndroidManifest.
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
in your activity:
if (Environment.IsExternalStorageManager)
{
var path = Android.OS.Environment.GetExternalStoragePublicDirectory("LogFolder").AbsolutePath;
Java.IO.File file = new Java.IO.File(path);
if (!file.Exists())
{
file.Mkdirs();
}
try
{
FileWriter fw = new FileWriter(path + Java.IO.File.Separator + "MyLogFile.txt");
fw.Write("This is some testing log.");
fw.Close();
}
catch (Exception ex)
{
string msg = ex.Message;
}
}
else
{
StartActivityForResult(new Intent(Settings.ActionManageAllFilesAccessPermission), 0);
}
I am trying to get push notifications to work on iOS, but I can not get access to the device token!
My Unity version is 5.4.1f1.
I have enabled the Push Notifications capability in XCode and all the certificates are setup correctly:
In my script in the start method I call this:
UnityEngine.iOS.NotificationServices.RegisterForNotifications
(UnityEngine.iOS.NotificationType.Alert | UnityEngine.iOS.NotificationType.Badge
| UnityEngine.iOS.NotificationType.Sound, true);
Then from the update method I call this method:
private bool RegisterTokenWithPlayfab( System.Action successCallback,
System.Action<PlayFabError> errorCallback )
{
byte[] token = UnityEngine.iOS.NotificationServices.deviceToken;
if(token != null)
{
// Registration on backend
}
else
{
string errorDescription = UnityEngine.iOS.NotificationServices.registrationError;
Debug.Log( "Push Notifications Registration failed with: " + errorDescription );
return false;
}
}
The token keeps being empty, so the else-branch is entered every call. Also, the registrationError keeps being empty.
Can someone point me in the right direction on this? What else can I try or how can I get more infos on what is going wrong??
Try this one
Go to your application target. Choose Capabilities and ensure that ‘Push Notifications’ is enabled there.
You need to check deviceToken in a Coroutine or Update.
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using NotificationServices = UnityEngine.iOS.NotificationServices;
using NotificationType = UnityEngine.iOS.NotificationType;
public class NotificationRegistrationExample : MonoBehaviour
{
bool tokenSent;
void Start()
{
tokenSent = false;
NotificationServices.RegisterForNotifications(
NotificationType.Alert |
NotificationType.Badge |
NotificationType.Sound, true);
}
void Update()
{
if (!tokenSent)
{
byte[] token = NotificationServices.deviceToken;
if (token != null)
{
// send token to a provider
string token = System.BitConverter.ToString(token).Replace('-', '%');
Debug.Log(token)
tokenSent = true;
}
}
}
}
The implementation is horrible, but we don't have callbacks from Unity side so we need to keep listening that variable value.
Check the documentation:
https://docs.unity3d.com/ScriptReference/iOS.NotificationServices.RegisterForNotifications.html
Also seems to need internet. I guess there is an Apple service going on there.
Yes, registration error is empty even when user deniy permissions.
What i did is to use UniRX and set an Observable fron a Coroutine with a time out so it dont keep forever asking for it.
If you accepted and you do not received the token might be the internet conection. And i guess you are teying this on a real device.
I want use function 'dlopen()' to invoke a dynamic library on iOS platform, is the function 'dlopen()' private API?
I've had success using dlopen on iOS for years. In my use case, I use dlopen to load public system frameworks on demand instead of having them loaded on app launch. Works great!
[EDIT] - as of iOS 8, extensions and shared frameworks are prohibited from using dlopen, however the application itself can still use dlopen (and is now documented as being supported for not only Apple frameworks, but custom frameworks too). See the Deploying a Containing App to Older Versions of iOS section in this Apple doc: https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensibilityPG.pdf
[EDIT] - contrived example
#import <dlfcn.h>
void printApplicationState()
{
Class UIApplicationClass = NSClassFromString(#"UIApplication");
if (Nil == UIApplicationClass) {
void *handle = dlopen("System/Library/Frameworks/UIKit.framework/UIKit", RTLD_NOW);
if (handle) {
UIApplicationClass = NSClassFromString(#"UIApplication");
assert(UIApplicationClass != Nil);
NSInteger applicationState = [UIApplicationClass applicationState];
printf("app state: %ti\n", applicationState);
if (0 != dlclose(handle)) {
printf("dlclose failed! %s\n", dlerror());
}
} else {
printf("dlopen failed! %s\n", dlerror());
}
} else {
printf("app state: %ti\n", [UIApplicationClass applicationState]);
}
}