Skip to content

Commit

Permalink
iOS: add infrastructure to natively send email
Browse files Browse the repository at this point in the history
This will allow us to send attachments, just like we do on Android.

Signed-off-by: Dirk Hohndel <[email protected]>
  • Loading branch information
dirkhh committed Aug 30, 2022
1 parent 0af410d commit 8164ca5
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Subsurface-mobile.pro
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,12 @@ ios {
images.files = icons/subsurface-mobile-icon.png
QMAKE_BUNDLE_DATA += app_launch_images images

OBJECTIVE_SOURCES += ios/ios-share.mm
HEADERS += ios/ios-share.h
Q_ENABLE_BITCODE.name = ENABLE_BITCODE
Q_ENABLE_BITCODE.value = NO
QMAKE_MAC_XCODE_SETTINGS += Q_ENABLE_BITCODE

LIBS += ../install-root/ios/lib/libdivecomputer.a \
../install-root/ios/lib/libgit2.a \
../install-root/ios/lib/libzip.a \
Expand All @@ -417,6 +423,8 @@ ios {
-lsqlite3 \
-lxml2

LIBS += -framework MessageUI

INCLUDEPATH += ../install-root/ios/include/ \
../install-root/lib/libzip/include \
../install-root/ios/include/libxstl \
Expand Down
18 changes: 18 additions & 0 deletions ios/ios-share.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0
#ifndef IOSSHARE_H
#define IOSSHARE_H

// onlt Qt headers and data structures allowed here
#include <QString>

class IosShare {
public:
IosShare();
~IosShare();
void supportEmail(const QString &firstPath, const QString &secondPath);
void shareViaEmail(const QString &subject, const QString &recipient, const QString &body, const QString &firstPath, const QString &secondPath);
private:
void *self;
};

#endif /* IOSSHARE_H */
114 changes: 114 additions & 0 deletions ios/ios-share.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: GPL-2.0
//
// this code was inspired by the discussions in
// https://forum.qt.io/topic/88297/native-objective-c-calls-from-cpp-qt-ios-email-call

// this include file only has the C++/Qt headers that can be used from C++
#include "ios-share.h"

// these are the required ObjC++ headers
#import <Foundation/Foundation.h>
#import <Foundation/NSString.h>
#import <UIKit/UIKit.h>
#import <MessageUI/MessageUI.h>

// declare an ObjC++ class that will interact with the mail controller
// that second member that is called when the mail app is finished is critical for this to work
@interface IosShareObject : UIViewController <MFMailComposeViewControllerDelegate>
{
}
- (void)shareViaEmail:(const QString &) subject :(const QString &) recipient :(const QString &) body :(const QString &) firstPath :(const QString &) secondPath;
- (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(nullable NSError *)error;
@end

@implementation IosShareObject
// first, inside the implementation of the ObjC++ class, implement the Qt class
IosShare::IosShare() : self(NULL) {
// call init to ensure that the ObjC++ object is instantiated, which in return
// apparently sets up the Controller
self = [ [IosShareObject alloc] init];
}

IosShare::~IosShare() {
[(id)self dealloc];
}

// simplified method that fills subject, recipient, and body for support emails
void IosShare::supportEmail(const QString &firstPath, const QString &secondPath) {
QString subject("Subsurface-mobile support request");
QString recipient("[email protected]");
QString body("Please describe your issue here and keep the attached logs.\n\n\n\n");
shareViaEmail(subject, recipient, body, firstPath, secondPath);
}

void IosShare::shareViaEmail(const QString &subject, const QString &recipient, const QString &body, const QString &firstPath, const QString &secondPath) {
// ObjC++ syntax to call the shareViaEmail method of that class - so this is
// where we transition from Qt/C++ code to ObjC++ code that can interact
// directly with iOS
[(id)self shareViaEmail:subject:recipient:body:firstPath:secondPath];
}

// the rest is the ObjC++ implementation
- (instancetype)init {
// this is just boiler plate that I really don't understand
// it appears to make sure that the ViewController infrastructure is initialized?
return super.init;
}

- (void)shareViaEmail:(const QString &) subjectQS :(const QString &) recipientQS :(const QString &) bodyQS :(const QString &) firstPathQS :(const QString &) secondPathQS {
// since we are mixing Qt and ObjC++ data structures, let's allocate copies
// of our Qt strings and convert recipients into an array
NSString *firstPath = [[NSString alloc] initWithUTF8String:firstPathQS.toUtf8().data()];
NSString *secondPath = [[NSString alloc] initWithUTF8String:secondPathQS.toUtf8().data()];
NSString *subject = [[NSString alloc] initWithUTF8String:subjectQS.toUtf8().data()];
NSString *recipient = [[NSString alloc] initWithUTF8String:recipientQS.toUtf8().data()];
NSString *body = [[NSString alloc] initWithUTF8String:bodyQS.toUtf8().data()];
NSArray *recipents = [NSArray arrayWithObject:recipient];
// create the mail controller and connect it with the object
MFMailComposeViewController *mc = [[MFMailComposeViewController alloc] init];
mc.mailComposeDelegate = self;
[mc setSubject:subject];
[mc setMessageBody:body isHTML:NO];
[mc setToRecipients:recipents];
// set up up to two attachments - only if we have a path and the file isn't empty (iOS throws up if you have an empty attachment)
if (!firstPathQS.isEmpty()) {
NSData *myData = [NSData dataWithContentsOfFile: firstPath];
if (myData != nil)
[mc addAttachmentData:myData mimeType:@"text/plain" fileName:[firstPath lastPathComponent]];
}
if (!secondPathQS.isEmpty()) {
//NSString *path = [[NSBundle mainBundle] pathForResource:@"log2" ofType:@"txt"];
NSData *myData = [NSData dataWithContentsOfFile: secondPath];
if (myData != nil)
[mc addAttachmentData:myData mimeType:@"text/plain" fileName:[secondPath lastPathComponent]];
}
// more black magic; get a view controller that is connected to our application window
UIViewController * topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController){
topController = topController.presentedViewController;
}
// finally, show the controller - the code returns right away, which is why we need the 'didFinishWithResult' method below
[topController presentViewController:mc animated:YES completion:NULL];
}

// I would have kinda liked to inform the caller that sending mail failed, but I can't figure
// out how to get that information back to the Qt code calling us. Oh well. At least we log the results.
// But the critically important part is that we dismiss the view controller.
- (void) mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(nullable NSError *)error {
switch (result) {
case MFMailComposeResultCancelled:
NSLog(@"Mail cancelled");
break;
case MFMailComposeResultSaved:
NSLog(@"Mail saved");break;
case MFMailComposeResultSent:
NSLog(@"Mail sent");break;
case MFMailComposeResultFailed:
NSLog(@"Mail sent failure: %@", [error localizedDescription]);
break;
default:
break;
}
[controller dismissViewControllerAnimated:YES completion:NULL];
}
@end

0 comments on commit 8164ca5

Please sign in to comment.