capacitor-shared-extension
v2.0.7
Published
A Capacitor Plugin that provides native capabilities to retrieve media files sent via iOS Share Extension and Android Send Intent events
Downloads
2
Readme
@calvinckho/capacitor-share-extension
This Capacitor Plugin provides native capabilities to retrieve media files sent via iOS Share Extension and Android Send Intent events
Compatibility to Capacitor Versions
Installation
Github Branch
npm i git+ssh://[email protected]/calvinckho/capacitor-share-extension#[branch-name]]
NPM
npm i capacitor-share-extension@[version number]
Usage
Capacitor 2:
import 'capacitor-share-extension';
const { ShareExtension } = Plugins;
Capacitor 3 & 4:
import { ShareExtension } from 'capacitor-share-extension';
// run this as part of the app launch
if (this.platform.is('cordova') && Capacitor.isPluginAvailable('ShareExtension')) {
window.addEventListener('sendIntentReceived', () => {
this.checkIntent();
});
this.checkIntent();
}
async checkIntent() {
try {
const result: any = await ShareExtension.checkSendIntentReceived();
/* sample result::
{ payload: [
{
"type":"image%2Fjpg",
"description":"",
"title":"IMG_0002.JPG",
// url contains a full, platform-specific file URL that can be read later using the Filsystem API.
"url":"file%3A%2F%2F%2FUsers%2Fcalvinho%2FLibrary%2FDeveloper%2FCoreSimulator%2FDevices%2FE4C13502-3A0B-4DF4-98ED-9F31DDF03672%2Fdata%2FContainers%2FShared%2FAppGroup%2FF41DC1F5-54D7-4EC5-9785-5248BAE06588%2FIMG_0002.JPG",
// webPath returns a path that can be used to set the src attribute of an image for efficient loading and rendering.
"webPath":"capacitor%3A%2F%2Flocalhost%2F_capacitor_file_%2FUsers%2Fcalvinho%2FLibrary%2FDeveloper%2FCoreSimulator%2FDevices%2FE4C13502-3A0B-4DF4-98ED-9F31DDF03672%2Fdata%2FContainers%2FShared%2FAppGroup%2FF41DC1F5-54D7-4EC5-9785-5248BAE06588%2FIMG_0002.JPG",
}]
}
*/
if (result && result.payload && result.payload.length) {
console.log('Intent received: ', JSON.stringify(result));
}
} catch (err) {
console.log(err);
}
}
// in Android, call finish when done processing the Intent
await ShareExtension.finish()
// iOS keychain methods
try {
// load an authentication token
const token = 'token XXYYZZ';
// use the extension to save the auth token to iOS Keychain
await ShareExtension.saveDataToKeychain({ key: 'token', data: token });
} catch (err) {
console.log(err);
}
// when user is about to log out, remove the token from iOS Keychain
try {
await ShareExtension.clearKeychainData( { key: 'token' });
} catch (err) {
console.log(err);
}
Android
Configure AndroidManifest.xml to allow file types to be received by your main app. See here for a list of available mimeTypes.
<activity
android:name="com.myown.app.MainActivity"
android:label="@string/app_name"
android:exported="true"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
<data android:mimeType="image/*" />
<data android:mimeType="application/pdf" />
<data android:mimeType="application/msword" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
<data android:mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" />
</intent-filter>
</activity>
Update your Android main app's /android/app/src/main/java/.../MainActivity.java with these code:
package com.myown.app;
import com.getcapacitor.BridgeActivity;
+import android.content.Intent;
+import android.webkit.ValueCallback;
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
}
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ String action = intent.getAction();
+ String type = intent.getType();
+ if ((Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) && type != null) {
+ bridge.getActivity().setIntent(intent);
+ bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", new ValueCallback<String>() {
+ @Override
+ public void onReceiveValue(String s) {
+ }
+ });
+ }
+ }
}
On Android, after having processed the Send Intent in your app, it is a good practice to end the Intent using the finish() method. Not doing so can lead to app state issues (because you have two instances running) or trigger the same intent again if your app reloads from idle mode.
ShareExtension.finish();
iOS
Create a new "Share Extension" in Xcode (Creating an App extension)
Set the activation rules in the extension's Info.plist, so that your app will be displayed as an option in the share view. To add more types, see here.
...
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<string>SUBQUERY (
extensionItems,
$extensionItem,
SUBQUERY (
$extensionItem.attachments,
$attachment,
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.spreadsheet" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.presentation" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.openxmlformats.wordprocessingml.document" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg-2000" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.compuserve.gif"
).@count == $extensionItem.attachments.@count
).@count == 1
</string>
</dict>
...
Overwrite ShareViewController.swift with this code. In Target - [This Extension's Name] - Build Settings, set the iOS Deployment Target to iOS 13 or higher, as the syntax in the following code is not compatible with older iOS version.
import UIKit
import Social
import MobileCoreServices
import Foundation.NSURLSession
class ShareItem {
public var title: String?
public var type: String?
public var url: String?
public var webPath: String?
public var qrStrings: String?
}
class ShareViewController: UIViewController {
private var shareItems: [ShareItem] = []
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
override public func viewDidLoad() {
super.viewDidLoad()
shareItems.removeAll()
let extensionItem = extensionContext?.inputItems[0] as! NSExtensionItem
Task {
try await withThrowingTaskGroup(
of: ShareItem.self,
body: { taskGroup in
for attachment in extensionItem.attachments! {
if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
taskGroup.addTask {
return try await self.handleTypeUrl(attachment)
}
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
taskGroup.addTask {
return try await self.handleTypeText(attachment)
}
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
taskGroup.addTask {
return try await self.handleTypeMovie(attachment)
}
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
taskGroup.addTask {
return try await self.handleTypeImage(attachment)
}
}
}
for try await item in taskGroup {
self.shareItems.append(item)
}
})
self.sendData()
}
}
private func sendData() {
let queryItems = shareItems.map {
[
URLQueryItem(
name: "title",
value: $0.title?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
URLQueryItem(name: "description", value: ""),
URLQueryItem(
name: "type",
value: $0.type?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
URLQueryItem(
name: "url",
value: $0.url?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
URLQueryItem(
name: "webPath",
value: $0.webPath?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
URLQueryItem(
name: "qrStrings",
value: $0.qrStrings?.description.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "")
]
}.flatMap({ $0 })
var urlComps = URLComponents(string: "restvo://;")!
urlComps.queryItems = queryItems
openURL(urlComps.url!)
}
fileprivate func createSharedFileUrl(_ url: URL?) -> String {
let fileManager = FileManager.default
print("share url: " + url!.absoluteString)
let copyFileUrl =
fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.restvo.test")!
.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + url!
.lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
try? Data(contentsOf: url!).write(to: URL(string: copyFileUrl)!)
return copyFileUrl
}
func saveScreenshot(_ image: UIImage) -> String {
let fileManager = FileManager.default
let copyFileUrl =
fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.restvo.test")!
.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
+ "/screenshot.png"
do {
try image.pngData()?.write(to: URL(string: copyFileUrl)!)
return copyFileUrl
} catch {
print(error.localizedDescription)
return ""
}
}
fileprivate func handleTypeUrl(_ attachment: NSItemProvider)
async throws -> ShareItem
{
let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil)
let url = results as! URL?
let shareItem: ShareItem = ShareItem()
if url!.isFileURL {
shareItem.title = url!.lastPathComponent
shareItem.type = "application/" + url!.pathExtension.lowercased()
shareItem.url = createSharedFileUrl(url)
shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
} else {
shareItem.title = url!.absoluteString
shareItem.url = url!.absoluteString
shareItem.webPath = url!.absoluteString
shareItem.type = "text/plain"
do {
let _coding = try await attachment.loadPreviewImage();
if let image = _coding as? UIImage {
self.processQR(image: image, shareItem: shareItem)
}
}
catch {
}
}
return shareItem
}
fileprivate func handleTypeText(_ attachment: NSItemProvider)
async throws -> ShareItem
{
let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
let shareItem: ShareItem = ShareItem()
let text = results as! String
shareItem.title = text
shareItem.type = "text/plain"
return shareItem
}
fileprivate func handleTypeMovie(_ attachment: NSItemProvider)
async throws -> ShareItem
{
let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeMovie as String, options: nil)
let shareItem: ShareItem = ShareItem()
let url = results as! URL?
shareItem.title = url!.lastPathComponent
shareItem.type = "video/" + url!.pathExtension.lowercased()
shareItem.url = createSharedFileUrl(url)
shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
return shareItem
}
fileprivate func handleTypeImage(_ attachment: NSItemProvider)
async throws -> ShareItem
{
let data = try await attachment.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil)
let shareItem: ShareItem = ShareItem()
switch data {
case let image as UIImage:
shareItem.title = "screenshot"
shareItem.type = "image/png"
shareItem.url = self.saveScreenshot(image)
shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
self.processQR(image: image, shareItem: shareItem)
case let url as URL:
shareItem.title = url.lastPathComponent
shareItem.type = "image/" + url.pathExtension.lowercased()
shareItem.url = self.createSharedFileUrl(url)
shareItem.webPath = "capacitor://localhost/_capacitor_file_" + URL(string: shareItem.url ?? "")!.path
if let ciImage = CIImage(contentsOf: url) {
self.processQR(image: ciImage, shareItem: shareItem)
}
default:
print("Unexpected image data:", type(of: data))
}
return shareItem
}
private func processQR( image: UIImage, shareItem: ShareItem){
let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [ CIDetectorAccuracy : CIDetectorAccuracyHigh ])
if let _cgImage = image.cgImage {
self.processQR(image: CIImage(cgImage: _cgImage), shareItem: shareItem);
}
}
private func processQR( image: CIImage, shareItem: ShareItem){
let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [ CIDetectorAccuracy : CIDetectorAccuracyHigh ])
let features = detector?.features(in: image)
if let _features = features as? [CIQRCodeFeature] {
var _qrStrings: [String]=[]
if (!_features.isEmpty){
for _feature in _features {
if let _messageString = _feature.messageString {
_qrStrings.append("\""+_messageString+"\"");
}
}
}
if (_qrStrings.count>0){
shareItem.qrStrings = "["+_qrStrings.joined(separator: ",")+"]";
}
};
}
@objc func openURL(_ url: URL) -> Bool {
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
return application.perform(#selector(openURL(_:)), with: url) != nil
}
responder = responder?.next
}
return false
}
}
The share extension is like a little standalone program. The extension receives the media, and issues an openURL call. In order for your main app to respond to the openURL call, you have to define a URL scheme (Register Your URL Scheme). The code above calls a URL scheme named "restvo://", so just replace it with your scheme. To allow sharing of files between the extension and your main app, you need to create an app group which is checked for both your extension and main app. Search and replace "group.com.restvo.test" with your app group's name.
Finally, in your main app's AppDelegate.swift, override the following function. This is the function that is activated when the main app is opened by URL.
import UIKit
import Capacitor
// ...
import CapacitorShareExtension
// ...
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
let store = ShareStore.store
// ...
private func parseAndPopulateJSONString(shareItem: inout JSObject, name: String, items: [URLQueryItem], index: Int){
if let _value = items[index].value {
if !_value.isEmpty {
shareItem[name]=_value.removingPercentEncoding;
}
}
}
private func parseAndPopulateJSONArray(shareItem: inout JSObject, name: String, items: [URLQueryItem], index: Int){
if let _value = items[index].value {
if !_value.isEmpty {
if let _percentage_removed = _value.removingPercentEncoding {
let data = Data(_percentage_removed.utf8);
do {
if let _json_array = try JSONSerialization.jsonObject(with: data, options: []) as? [String] {
shareItem[name] = _json_array;
}
} catch {
print("ERROR: parsing json: \(error)");
}
}
}
}
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
var success = true
if CAPBridge.handleOpenUrl(url, options) {
success = ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let params = components.queryItems else {
return false
}
let titles = params.filter { $0.name == "title" }
let descriptions = params.filter { $0.name == "description" }
let types = params.filter { $0.name == "type" }
let urls = params.filter { $0.name == "url" }
let webPaths = params.filter { $0.name == "webPath" }
let qrStrings = params.filter { $0.name == "qrStrings" }
store.shareItems.removeAll()
if (titles.count > 0){
for index in 0...titles.count-1 {
var shareItem: JSObject = JSObject()
self.parseAndPopulateJSONString(shareItem: &shareItem, name: "title", items: titles, index: index);
self.parseAndPopulateJSONString(shareItem: &shareItem, name: "description", items: descriptions, index: index);
self.parseAndPopulateJSONString(shareItem: &shareItem, name: "type", items: types, index: index);
self.parseAndPopulateJSONString(shareItem: &shareItem, name: "url", items: urls, index: index);
self.parseAndPopulateJSONString(shareItem: &shareItem, name: "webPath", items: webPaths, index: index);
self.parseAndPopulateJSONArray(shareItem: &shareItem, name: "qrStrings", items: qrStrings, index: index);
store.shareItems.append(shareItem)
}
}
store.processed = false
let nc = NotificationCenter.default
nc.post(name: Notification.Name("triggerSendIntent"), object: nil )
return success
}
// ...
}