From 331e244a048c8aead1f58cc2e7e836893b0ddc55 Mon Sep 17 00:00:00 2001 From: Jochen Heizmann Date: Sat, 30 Mar 2019 17:13:46 +0100 Subject: [PATCH 1/7] added support for Andoird 8/9 for sharing text/html --- src/android/cc/fovea/openwith/Serializer.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/android/cc/fovea/openwith/Serializer.java b/src/android/cc/fovea/openwith/Serializer.java index 0bc6369..9d41bae 100644 --- a/src/android/cc/fovea/openwith/Serializer.java +++ b/src/android/cc/fovea/openwith/Serializer.java @@ -81,7 +81,15 @@ public static JSONArray itemsFromClipData( final int clipItemCount = clipData.getItemCount(); JSONObject[] items = new JSONObject[clipItemCount]; for (int i = 0; i < clipItemCount; i++) { - items[i] = toJSONObject(contentResolver, clipData.getItemAt(i).getUri()); + if (clipData.getItemAt(i).getUri() != null) { + items[i] = toJSONObject(contentResolver, clipData.getItemAt(i).getUri()); + } else if (clipData.getItemAt(i).getText() != null) { + items[i] = toJSONObject(contentResolver, clipData.getItemAt(i).getText().toString()); + } else if (clipData.getItemAt(i).getHtmlText() != null) { + items[i] = toJSONObject(contentResolver, clipData.getItemAt(i).getHtmlText()); + } else { + items[i] = toJSONObject(contentResolver, clipData.getItemAt(i).toString()); + } } return new JSONArray(items); } @@ -153,6 +161,27 @@ public static JSONObject toJSONObject( return json; } + /** Convert an String to JSON object. + * + * Object will include: + * "type" of data; + * "text" itself; + * "path" to the file, if applicable. + * "data" for the file. + */ + public static JSONObject toJSONObject( + final ContentResolver contentResolver, + final String text) + throws JSONException { + if (text == null) { + return null; + } + final JSONObject json = new JSONObject(); + json.put("type", "text/plain"); + json.put("text", text); + return json; + } + /** Return data contained at a given Uri as Base64. Defaults to null. */ public static String getDataFromURI( final ContentResolver contentResolver, From e4e886f5063defe77068c4601001cface5659c3a Mon Sep 17 00:00:00 2001 From: Jochen Heizmann Date: Sat, 30 Mar 2019 21:22:00 +0100 Subject: [PATCH 2/7] Added handling of different types to iOS --- hooks/iosAddTarget.js | 287 +++----------- hooks/iosCopyShareExtension.js | 110 ++---- hooks/utils.js | 132 +++++++ src/ios/OpenWithPlugin.m | 286 +++++--------- .../ShareExtension/ShareExtension-Info.plist | 24 -- .../ShareExtension.entitlements | 10 + src/ios/ShareExtension/ShareViewController.h | 3 +- src/ios/ShareExtension/ShareViewController.m | 351 ++++++++++-------- 8 files changed, 493 insertions(+), 710 deletions(-) create mode 100644 hooks/utils.js create mode 100644 src/ios/ShareExtension/ShareExtension.entitlements diff --git a/hooks/iosAddTarget.js b/hooks/iosAddTarget.js index eecb9da..dbcbd38 100755 --- a/hooks/iosAddTarget.js +++ b/hooks/iosAddTarget.js @@ -28,100 +28,39 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // +const fs = require('fs'); +const path = require('path'); + +const { + PLUGIN_ID, + iosFolder, + getPreferences, + findXCodeproject, + replacePreferencesInFile, + log, redError, +} = require('./utils') -const PLUGIN_ID = 'cc.fovea.cordova.openwith'; -const BUNDLE_SUFFIX = '.shareextension'; - -var fs = require('fs'); -var path = require('path'); - -function redError(message) { - return new Error('"' + PLUGIN_ID + '" \x1b[1m\x1b[31m' + message + '\x1b[0m'); -} - -function replacePreferencesInFile(filePath, preferences) { - var content = fs.readFileSync(filePath, 'utf8'); - for (var i = 0; i < preferences.length; i++) { - var pref = preferences[i]; - var regexp = new RegExp(pref.key, "g"); - content = content.replace(regexp, pref.value); - } - fs.writeFileSync(filePath, content); -} - -// Determine the full path to the app's xcode project file. -function findXCodeproject(context, callback) { - fs.readdir(iosFolder(context), function(err, data) { - var projectFolder; - var projectName; - // Find the project folder by looking for *.xcodeproj - if (data && data.length) { - data.forEach(function(folder) { - if (folder.match(/\.xcodeproj$/)) { - projectFolder = path.join(iosFolder(context), folder); - projectName = path.basename(folder, '.xcodeproj'); - } - }); - } - - if (!projectFolder || !projectName) { - throw redError('Could not find an .xcodeproj folder in: ' + iosFolder(context)); - } - - if (err) { - throw redError(err); - } - - callback(projectFolder, projectName); - }); -} - -// Determine the full path to the ios platform -function iosFolder(context) { - return context.opts.cordova.project - ? context.opts.cordova.project.root - : path.join(context.opts.projectRoot, 'platforms/ios/'); -} - -function getPreferenceValue(configXml, name) { - var value = configXml.match(new RegExp('name="' + name + '" value="(.*?)"', "i")); - if (value && value[1]) { - return value[1]; - } else { - return null; - } -} - -function getCordovaParameter(configXml, variableName) { - var variable; - var arg = process.argv.filter(function(arg) { - return arg.indexOf(variableName + '=') == 0; - }); - if (arg.length >= 1) { - variable = arg[0].split('=')[1]; - } else { - variable = getPreferenceValue(configXml, variableName); - } - return variable; -} - -// Get the bundle id from config.xml -// function getBundleId(context, configXml) { -// var elementTree = context.requireCordovaModule('elementtree'); -// var etree = elementTree.parse(configXml); -// return etree.getroot().get('id'); -// } +// Return the list of files in the share extension project, organized by type +const FILE_TYPES = { + '.h':'source', + '.m':'source', + '.plist':'config', + '.entitlements':'config', +}; function parsePbxProject(context, pbxProjectPath) { var xcode = context.requireCordovaModule('xcode'); - console.log(' Parsing existing project at location: ' + pbxProjectPath + '...'); + log(`Parsing existing project at location: ${pbxProjectPath}…`); + var pbxProject; + if (context.opts.cordova.project) { pbxProject = context.opts.cordova.project.parseProjectFile(context.opts.projectRoot).xcode; } else { pbxProject = xcode.project(pbxProjectPath); pbxProject.parseSync(); } + return pbxProject; } @@ -131,125 +70,50 @@ function forEachShareExtensionFile(context, callback) { // Ignore junk files like .DS_Store if (!/^\..*/.test(name)) { callback({ - name:name, - path:path.join(shareExtensionFolder, name), - extension:path.extname(name) + name: name, + path: path.join(shareExtensionFolder, name), + extension: path.extname(name) }); } }); } -function projectPlistPath(context, projectName) { - return path.join(iosFolder(context), projectName, projectName + '-Info.plist'); -} - -function projectPlistJson(context, projectName) { - var plist = require('plist'); - var path = projectPlistPath(context, projectName); - return plist.parse(fs.readFileSync(path, 'utf8')); -} - -function getPreferences(context, configXml, projectName) { - var plist = projectPlistJson(context, projectName); - var group = "group." + plist.CFBundleIdentifier + BUNDLE_SUFFIX; - if (getCordovaParameter(configXml, 'GROUP_IDENTIFIER') !== "") { - group = getCordovaParameter(configXml, 'IOS_GROUP_IDENTIFIER'); - } - return [{ - key: '__DISPLAY_NAME__', - value: projectName - }, { - key: '__BUNDLE_IDENTIFIER__', - value: plist.CFBundleIdentifier + BUNDLE_SUFFIX - } ,{ - key: '__GROUP_IDENTIFIER__', - value: group - }, { - key: '__BUNDLE_SHORT_VERSION_STRING__', - value: plist.CFBundleShortVersionString - }, { - key: '__BUNDLE_VERSION__', - value: plist.CFBundleVersion - }, { - key: '__URL_SCHEME__', - value: getCordovaParameter(configXml, 'IOS_URL_SCHEME') - }, { - key: '__UNIFORM_TYPE_IDENTIFIER__', - value: getCordovaParameter(configXml, 'IOS_UNIFORM_TYPE_IDENTIFIER') - }]; -} - -// Return the list of files in the share extension project, organized by type function getShareExtensionFiles(context) { - var files = {source:[],plist:[],resource:[]}; - var FILE_TYPES = { '.h':'source', '.m':'source', '.plist':'plist' }; + var files = { source: [], config: [], resource: [] }; + forEachShareExtensionFile(context, function(file) { var fileType = FILE_TYPES[file.extension] || 'resource'; files[fileType].push(file); }); - return files; -} - -function printShareExtensionFiles(files) { - console.log(' Found following files in your ShareExtension folder:'); - console.log(' Source files:'); - files.source.forEach(function(file) { - console.log(' - ', file.name); - }); - console.log(' Plist files:'); - files.plist.forEach(function(file) { - console.log(' - ', file.name); - }); - - console.log(' Resource files:'); - files.resource.forEach(function(file) { - console.log(' - ', file.name); - }); + return files; } -console.log('Adding target "' + PLUGIN_ID + '/ShareExtension" to XCode project'); - -module.exports = function (context) { +module.exports = function(context) { + log('Adding ShareExt target to XCode project') var Q = context.requireCordovaModule('q'); var deferral = new Q.defer(); - // if (context.opts.cordova.platforms.indexOf('ios') < 0) { - // log('You have to add the ios platform before adding this plugin!', 'error'); - // } - - var configXml = fs.readFileSync(path.join(context.opts.projectRoot, 'config.xml'), 'utf-8'); - if (configXml) { - configXml = configXml.substring(configXml.indexOf('<')); - } - findXCodeproject(context, function(projectFolder, projectName) { - - console.log(' - Folder containing your iOS project: ' + iosFolder(context)); + var preferences = getPreferences(context, projectName); var pbxProjectPath = path.join(projectFolder, 'project.pbxproj'); var pbxProject = parsePbxProject(context, pbxProjectPath); var files = getShareExtensionFiles(context); - // printShareExtensionFiles(files); - - var preferences = getPreferences(context, configXml, projectName); - files.plist.concat(files.source).forEach(function(file) { + files.config.concat(files.source).forEach(function(file) { replacePreferencesInFile(file.path, preferences); - // console.log(' Successfully updated ' + file.name); }); // Find if the project already contains the target and group var target = pbxProject.pbxTargetByName('ShareExt'); - if (target) { - console.log(' ShareExt target already exists.'); - } + if (target) { log('ShareExt target already exists') } if (!target) { // Add PBXNativeTarget to the project target = pbxProject.addTarget('ShareExt', 'app_extension', 'ShareExtension'); - + // Add a new PBXSourcesBuildPhase for our ShareViewController // (we can't add it to the existing one because an extension is kind of an extra app) pbxProject.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', target.uuid); @@ -261,9 +125,8 @@ module.exports = function (context) { // Create a separate PBXGroup for the shareExtensions files, name has to be unique and path must be in quotation marks var pbxGroupKey = pbxProject.findPBXGroupKey({name: 'ShareExtension'}); - if (pbxProject) { - console.log(' ShareExtension group already exists.'); - } + if (pbxProject) { log('ShareExtension group already exists') } + if (!pbxGroupKey) { pbxGroupKey = pbxProject.pbxCreateGroup('ShareExtension', 'ShareExtension'); @@ -273,7 +136,7 @@ module.exports = function (context) { } // Add files which are not part of any build phase (config) - files.plist.forEach(function (file) { + files.config.forEach(function (file) { pbxProject.addFile(file.name, pbxGroupKey); }); @@ -287,80 +150,26 @@ module.exports = function (context) { pbxProject.addResourceFile(file.name, {target: target.uuid}, pbxGroupKey); }); - //Add development team and provisioning profile - var PROVISIONING_PROFILE = getCordovaParameter(configXml, 'SHAREEXT_PROVISIONING_PROFILE'); - var DEVELOPMENT_TEAM = getCordovaParameter(configXml, 'SHAREEXT_DEVELOPMENT_TEAM'); - console.log('Adding team', DEVELOPMENT_TEAM, 'and provisoning profile', PROVISIONING_PROFILE); - if (PROVISIONING_PROFILE && DEVELOPMENT_TEAM) { - var configurations = pbxProject.pbxXCBuildConfigurationSection(); - for (var key in configurations) { - if (typeof configurations[key].buildSettings !== 'undefined') { - var buildSettingsObj = configurations[key].buildSettings; - if (typeof buildSettingsObj['PRODUCT_NAME'] !== 'undefined') { - var productName = buildSettingsObj['PRODUCT_NAME']; - if (productName.indexOf('ShareExt') >= 0) { - buildSettingsObj['PROVISIONING_PROFILE'] = PROVISIONING_PROFILE; - buildSettingsObj['DEVELOPMENT_TEAM'] = DEVELOPMENT_TEAM; - console.log('Added signing identities for extension!'); - } + // Add build settings for Swift support, bridging header and xcconfig files + var configurations = pbxProject.pbxXCBuildConfigurationSection(); + for (var key in configurations) { + if (typeof configurations[key].buildSettings !== 'undefined') { + var buildSettingsObj = configurations[key].buildSettings; + if (typeof buildSettingsObj['PRODUCT_NAME'] !== 'undefined') { + var productName = buildSettingsObj['PRODUCT_NAME']; + if (productName.indexOf('ShareExt') >= 0) { + buildSettingsObj['CODE_SIGN_ENTITLEMENTS'] = '"ShareExtension/ShareExtension.entitlements"'; } } } } - // Add a new PBXFrameworksBuildPhase for the Frameworks used by the Share Extension - // (NotificationCenter.framework, libCordova.a) - // var frameworksBuildPhase = pbxProject.addBuildPhase( - // [], - // 'PBXFrameworksBuildPhase', - // 'Frameworks', - // target.uuid - // ); - // if (frameworksBuildPhase) { - // log('Successfully added PBXFrameworksBuildPhase!', 'info'); - // } - - // Add the frameworks needed by our shareExtension, add them to the existing Frameworks PbxGroup and PBXFrameworksBuildPhase - // var frameworkFile1 = pbxProject.addFramework( - // 'NotificationCenter.framework', - // { target: target.uuid } - // ); - // var frameworkFile2 = pbxProject.addFramework('libCordova.a', { - // target: target.uuid, - // }); // seems to work because the first target is built before the second one - // if (frameworkFile1 && frameworkFile2) { - // log('Successfully added frameworks needed by the share extension!', 'info'); - // } - - // Add build settings for Swift support, bridging header and xcconfig files - // var configurations = pbxProject.pbxXCBuildConfigurationSection(); - // for (var key in configurations) { - // if (typeof configurations[key].buildSettings !== 'undefined') { - // var buildSettingsObj = configurations[key].buildSettings; - // if (typeof buildSettingsObj['PRODUCT_NAME'] !== 'undefined') { - // var productName = buildSettingsObj['PRODUCT_NAME']; - // if (productName.indexOf('ShareExtension') >= 0) { - // if (addXcconfig) { - // configurations[key].baseConfigurationReference = - // xcconfigReference + ' /* ' + xcconfigFileName + ' */'; - // log('Added xcconfig file reference to build settings!', 'info'); - // } - // if (addEntitlementsFile) { - // buildSettingsObj['CODE_SIGN_ENTITLEMENTS'] = '"' + 'ShareExtension' + '/' + entitlementsFileName + '"'; - // log('Added entitlements file reference to build settings!', 'info'); - // } - // } - // } - // } - // } - // Write the modified project back to disc - // console.log(' Writing the modified project back to disk...'); fs.writeFileSync(pbxProjectPath, pbxProject.writeSync()); - console.log('Added ShareExtension to XCode project'); + log('Successfully added ShareExt target to XCode project') deferral.resolve(); }); return deferral.promise; -}; +}; \ No newline at end of file diff --git a/hooks/iosCopyShareExtension.js b/hooks/iosCopyShareExtension.js index add83b8..bb09f7b 100644 --- a/hooks/iosCopyShareExtension.js +++ b/hooks/iosCopyShareExtension.js @@ -1,55 +1,15 @@ -// -// iosCopyShareExtension.js -// This hook runs for the iOS platform when the plugin or platform is added. -// -// Source: https://github.com/DavidStrausz/cordova-plugin-today-widget -// - -// -// The MIT License (MIT) -// -// Copyright (c) 2017 DavidStrausz -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -var fs = require('fs'); -var path = require('path'); -const PLUGIN_ID = "cc.fovea.cordova.openwith"; - -function redError(message) { - return new Error('"' + PLUGIN_ID + '" \x1b[1m\x1b[31m' + message + '\x1b[0m'); -} - -function getPreferenceValue (config, name) { - var value = config.match(new RegExp('name="' + name + '" value="(.*?)"', "i")); - if(value && value[1]) { - return value[1]; - } else { - return null; - } -} - -console.log('Copying "' + PLUGIN_ID + '/ShareExtension" to ios...'); - -// http://stackoverflow.com/a/26038979/5930772 -function copyFileSync(source, target) { +const fs = require('fs'); +const path = require('path'); + +const { + PLUGIN_ID, + getPreferences, + findXCodeproject, + replacePreferencesInFile, + log, redError, +} = require('./utils') + +function copyFileSync(source, target, preferences) { var targetFile = target; // If target is a directory a new file with the same name will be created @@ -60,9 +20,10 @@ function copyFileSync(source, target) { } fs.writeFileSync(targetFile, fs.readFileSync(source)); + replacePreferencesInFile(targetFile, preferences); } -function copyFolderRecursiveSync(source, target) { +function copyFolderRecursiveSync(source, target, preferences) { var files = []; // Check if folder needs to be created or integrated @@ -77,58 +38,33 @@ function copyFolderRecursiveSync(source, target) { files.forEach(function(file) { var curSource = path.join(source, file); if (fs.lstatSync(curSource).isDirectory()) { - copyFolderRecursiveSync(curSource, targetFolder); + copyFolderRecursiveSync(curSource, targetFolder, preferences); } else { - copyFileSync(curSource, targetFolder); + copyFileSync(curSource, targetFolder, preferences); } }); } } -// Determine the full path to the app's xcode project file. -function findXCodeproject(context, callback) { - var iosFolder = context.opts.cordova.project - ? context.opts.cordova.project.root - : path.join(context.opts.projectRoot, 'platforms/ios/'); - fs.readdir(iosFolder, function(err, data) { - var projectFolder; - var projectName; - // Find the project folder by looking for *.xcodeproj - if (data && data.length) { - data.forEach(function(folder) { - if (folder.match(/\.xcodeproj$/)) { - projectFolder = path.join(iosFolder, folder); - projectName = path.basename(folder, '.xcodeproj'); - } - }); - } - - if (!projectFolder || !projectName) { - throw redError('Could not find an .xcodeproj folder in: ' + iosFolder); - } - - if (err) { - throw redError(err); - } - - callback(projectFolder, projectName); - }); -} - module.exports = function(context) { + log('Copying ShareExtension files to iOS project') + var Q = context.requireCordovaModule('q'); var deferral = new Q.defer(); findXCodeproject(context, function(projectFolder, projectName) { + var preferences = getPreferences(context, projectName); var srcFolder = path.join(context.opts.projectRoot, 'plugins', PLUGIN_ID, 'src', 'ios', 'ShareExtension'); + var targetFolder = path.join(context.opts.projectRoot, 'platforms', 'ios'); + if (!fs.existsSync(srcFolder)) { throw redError('Missing extension project folder in ' + srcFolder + '.'); } - copyFolderRecursiveSync(srcFolder, path.join(context.opts.projectRoot, 'platforms', 'ios')); + copyFolderRecursiveSync(srcFolder, targetFolder, preferences); deferral.resolve(); }); return deferral.promise; -}; +}; \ No newline at end of file diff --git a/hooks/utils.js b/hooks/utils.js new file mode 100644 index 0000000..47c7638 --- /dev/null +++ b/hooks/utils.js @@ -0,0 +1,132 @@ +const fs = require('fs'); +const path = require('path'); + +const PLUGIN_ID = 'cc.fovea.cordova.openwith'; +const BUNDLE_SUFFIX = '.shareextension'; + +function getPreferences(context, projectName) { + var configXml = getConfigXml(context); + var plist = projectPlistJson(context, projectName); + + return [{ + key: '__DISPLAY_NAME__', + value: getCordovaParameter(configXml, 'DISPLAY_NAME') || projectName + }, { + key: '__BUNDLE_IDENTIFIER__', + value: getCordovaParameter(configXml, 'IOS_BUNDLE_IDENTIFIER') + BUNDLE_SUFFIX + }, { + key: '__BUNDLE_SHORT_VERSION_STRING__', + value: plist.CFBundleShortVersionString + }, { + key: '__BUNDLE_VERSION__', + value: plist.CFBundleVersion + }, { + key: '__URL_SCHEME__', + value: getCordovaParameter(configXml, 'IOS_URL_SCHEME') + }]; +} + +function getConfigXml(context) { + var configXml = fs.readFileSync(path.join(context.opts.projectRoot, 'config.xml'), 'utf-8'); + + if (configXml) { + configXml = configXml.substring(configXml.indexOf('<')); + } + + return configXml +} + +function projectPlistJson(context, projectName) { + var plist = require('plist'); + + var plistPath = path.join(iosFolder(context), projectName, projectName + '-Info.plist'); + return plist.parse(fs.readFileSync(plistPath, 'utf8')); +} + +function iosFolder(context) { + return context.opts.cordova.project + ? context.opts.cordova.project.root + : path.join(context.opts.projectRoot, 'platforms/ios/'); +} + +function getCordovaParameter(configXml, variableName) { + var variable; + var arg = process.argv.filter(function(arg) { + return arg.indexOf(variableName + '=') == 0; + }); + + if (arg.length >= 1) { + variable = arg[0].split('=')[1]; + } else { + variable = getPreferenceValue(configXml, variableName); + } + + return variable; +} + +function getPreferenceValue(configXml, name) { + var value = configXml.match(new RegExp('name="' + name + '" value="(.*?)"', "i")); + + if (value && value[1]) { + return value[1]; + } else { + return null; + } +} + +// Determine the full path to the app's xcode project file. +function findXCodeproject(context, callback) { + fs.readdir(iosFolder(context), function(err, data) { + var projectFolder; + var projectName; + + // Find the project folder by looking for *.xcodeproj + if (data && data.length) { + data.forEach(function(folder) { + if (folder.match(/\.xcodeproj$/)) { + projectFolder = path.join(iosFolder(context), folder); + projectName = path.basename(folder, '.xcodeproj'); + } + }); + } + + if (!projectFolder || !projectName) { + throw redError('Could not find an .xcodeproj folder in: ' + iosFolder(context)); + } + + if (err) { + throw redError(err); + } + + callback(projectFolder, projectName); + }); +} + +function replacePreferencesInFile(filePath, preferences) { + var content = fs.readFileSync(filePath, 'utf8'); + + for (var i = 0; i < preferences.length; i++) { + var pref = preferences[i]; + var regexp = new RegExp(pref.key, "g"); + content = content.replace(regexp, pref.value); + } + + fs.writeFileSync(filePath, content); +} + +function redError(message) { + return new Error('"' + PLUGIN_ID + '" \x1b[1m\x1b[31m' + message + '\x1b[0m'); +} + +function log(message) { + console.log(`[${PLUGIN_ID}] ${message}`); +} + +module.exports = { + PLUGIN_ID, + iosFolder, + getPreferences, + findXCodeproject, + replacePreferencesInFile, + log, redError, +} \ No newline at end of file diff --git a/src/ios/OpenWithPlugin.m b/src/ios/OpenWithPlugin.m index bd1ef93..a028f5c 100644 --- a/src/ios/OpenWithPlugin.m +++ b/src/ios/OpenWithPlugin.m @@ -2,50 +2,6 @@ #import "ShareViewController.h" #import -/* - * Add base64 export to NSData - */ -@interface NSData (Base64) -- (NSString*)convertToBase64; -@end - -@implementation NSData (Base64) -- (NSString*)convertToBase64 { - const uint8_t* input = (const uint8_t*)[self bytes]; - NSInteger length = [self length]; - - static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - - NSMutableData* data = [NSMutableData dataWithLength:((length + 2) / 3) * 4]; - uint8_t* output = (uint8_t*)data.mutableBytes; - - NSInteger i; - for (i=0; i < length; i += 3) { - NSInteger value = 0; - NSInteger j; - for (j = i; j < (i + 3); j++) { - value <<= 8; - - if (j < length) { - value |= (0xFF & input[j]); - } - } - - NSInteger theIndex = (i / 3) * 4; - output[theIndex + 0] = table[(value >> 18) & 0x3F]; - output[theIndex + 1] = table[(value >> 12) & 0x3F]; - output[theIndex + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '='; - output[theIndex + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '='; - } - - NSString *ret = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; -#if ARC_DISABLED - [ret autorelease]; -#endif - return ret; -} -@end - /* * Constants */ @@ -66,18 +22,16 @@ - (NSString*)convertToBase64 { */ @interface OpenWithPlugin : CDVPlugin { - NSString* _loggerCallback; - NSString* _handlerCallback; - NSUserDefaults *_userDefaults; - int _verbosityLevel; - NSString *_backURL; + NSString* _loggerCallback; + NSString* _handlerCallback; + NSUserDefaults *_userDefaults; + int _verbosityLevel; } @property (nonatomic,retain) NSString* loggerCallback; @property (nonatomic,retain) NSString* handlerCallback; @property (nonatomic) int verbosityLevel; @property (nonatomic,retain) NSUserDefaults *userDefaults; -@property (nonatomic,retain) NSString *backURL; @end /* @@ -90,7 +44,6 @@ @implementation OpenWithPlugin @synthesize handlerCallback = _handlerCallback; @synthesize verbosityLevel = _verbosityLevel; @synthesize userDefaults = _userDefaults; -@synthesize backURL = _backURL; // // Retrieve launchOptions @@ -111,192 +64,129 @@ + (void) load { } + (void) didFinishLaunching:(NSNotification*)notification { - launchOptions = notification.userInfo; + launchOptions = notification.userInfo; } - (void) log:(int)level message:(NSString*)message { - if (level >= self.verbosityLevel) { - NSLog(@"[OpenWithPlugin.m]%@", message); - if (self.loggerCallback != nil) { - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; - pluginResult.keepCallback = [NSNumber numberWithBool:YES]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.loggerCallback]; - } + if (level >= self.verbosityLevel) { + NSLog(@"[OpenWithPlugin.m]%@", message); + + if (self.loggerCallback != nil) { + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + pluginResult.keepCallback = [NSNumber numberWithBool:YES]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.loggerCallback]; } + } } + - (void) debug:(NSString*)message { [self log:VERBOSITY_DEBUG message:message]; } - (void) info:(NSString*)message { [self log:VERBOSITY_INFO message:message]; } - (void) warn:(NSString*)message { [self log:VERBOSITY_WARN message:message]; } - (void) error:(NSString*)message { [self log:VERBOSITY_ERROR message:message]; } - (void) pluginInitialize { - // You can listen to more app notifications, see: - // http://developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/UIApplication_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006728-CH3-DontLinkElementID_4 - - // NOTE: if you want to use these, make sure you uncomment the corresponding notification handler - - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPause) name:UIApplicationDidEnterBackgroundNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume) name:UIApplicationWillEnterForegroundNotification object:nil]; - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onOrientationWillChange) name:UIApplicationWillChangeStatusBarOrientationNotification object:nil]; - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onOrientationDidChange) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; - - // Added in 2.5.0 - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pageDidLoad:) name:CDVPageDidLoadNotification object:self.webView]; - //Added in 4.3.0 - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewWillAppear:) name:CDVViewWillAppearNotification object:nil]; - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewDidAppear:) name:CDVViewDidAppearNotification object:nil]; - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewWillDisappear:) name:CDVViewWillDisappearNotification object:nil]; - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewDidDisappear:) name:CDVViewDidDisappearNotification object:nil]; - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewWillLayoutSubviews:) name:CDVViewWillLayoutSubviewsNotification object:nil]; - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewDidLayoutSubviews:) name:CDVViewDidLayoutSubviewsNotification object:nil]; - // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewWillTransitionToSize:) name:CDVViewWillTransitionToSizeNotification object:nil]; - [self onReset]; - [self info:@"[pluginInitialize] OK"]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume) name:UIApplicationWillEnterForegroundNotification object:nil]; + + [self onReset]; + [self info:@"[pluginInitialize] OK"]; } - (void) onReset { - [self info:@"[onReset]"]; - self.userDefaults = [[NSUserDefaults alloc] initWithSuiteName:SHAREEXT_GROUP_IDENTIFIER]; - self.verbosityLevel = VERBOSITY_INFO; - self.loggerCallback = nil; - self.handlerCallback = nil; + [self info:@"[onReset]"]; + + self.userDefaults = [[NSUserDefaults alloc] initWithSuiteName:SHAREEXT_GROUP_IDENTIFIER]; + self.verbosityLevel = VERBOSITY_INFO; + self.loggerCallback = nil; + self.handlerCallback = nil; } - (void) onResume { - [self debug:@"[onResume]"]; - [self checkForFileToShare]; + [self debug:@"[onResume]"]; + [self checkForFileToShare]; } - (void) setVerbosity:(CDVInvokedUrlCommand*)command { - NSNumber *value = [command argumentAtIndex:0 - withDefault:[NSNumber numberWithInt: VERBOSITY_INFO] - andClass:[NSNumber class]]; - self.verbosityLevel = value.integerValue; - [self.userDefaults setInteger:self.verbosityLevel forKey:@"verbosityLevel"]; - [self.userDefaults synchronize]; - [self debug:[NSString stringWithFormat:@"[setVerbosity] %d", self.verbosityLevel]]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + NSNumber *value = [command argumentAtIndex:0 + withDefault:[NSNumber numberWithInt: VERBOSITY_INFO] + andClass:[NSNumber class]]; + + self.verbosityLevel = value.integerValue; + [self debug:[NSString stringWithFormat:@"[setVerbosity] %d", self.verbosityLevel]]; + + [self.userDefaults setInteger:self.verbosityLevel forKey:@"verbosityLevel"]; + [self.userDefaults synchronize]; + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } - (void) setLogger:(CDVInvokedUrlCommand*)command { - self.loggerCallback = command.callbackId; - [self debug:[NSString stringWithFormat:@"[setLogger] %@", self.loggerCallback]]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT]; - pluginResult.keepCallback = [NSNumber numberWithBool:YES]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + self.loggerCallback = command.callbackId; + [self debug:[NSString stringWithFormat:@"[setLogger] %@", self.loggerCallback]]; + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT]; + pluginResult.keepCallback = [NSNumber numberWithBool:YES]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } - (void) setHandler:(CDVInvokedUrlCommand*)command { - self.handlerCallback = command.callbackId; - [self debug:[NSString stringWithFormat:@"[setHandler] %@", self.handlerCallback]]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT]; - pluginResult.keepCallback = [NSNumber numberWithBool:YES]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; -} + self.handlerCallback = command.callbackId; + [self debug:[NSString stringWithFormat:@"[setHandler] %@", self.handlerCallback]]; -- (NSString *)mimeTypeFromUti: (NSString*)uti { - if (uti == nil) { - return nil; - } - CFStringRef cret = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)uti, kUTTagClassMIMEType); - NSString *ret = (__bridge_transfer NSString *)cret; - return ret == nil ? uti : ret; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT]; + pluginResult.keepCallback = [NSNumber numberWithBool:YES]; + + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } - (void) checkForFileToShare { - [self debug:@"[checkForFileToShare]"]; - if (self.handlerCallback == nil) { - [self debug:@"[checkForFileToShare] javascript not ready yet."]; - return; - } + [self debug:@"[checkForFileToShare]"]; - [self.userDefaults synchronize]; - NSObject *object = [self.userDefaults objectForKey:@"image"]; - if (object == nil) { - [self debug:@"[checkForFileToShare] Nothing to share"]; - return; - } + if (self.handlerCallback == nil) { + [self debug:@"[checkForFileToShare] javascript not ready yet."]; + return; + } - // Clean-up the object, assume it's been handled from now, prevent double processing - [self.userDefaults removeObjectForKey:@"image"]; + [self.userDefaults synchronize]; - // Extract sharing data, make sure that it is valid - if (![object isKindOfClass:[NSDictionary class]]) { - [self debug:@"[checkForFileToShare] Data object is invalid"]; - return; - } - NSDictionary *dict = (NSDictionary*)object; - NSData *data = dict[@"data"]; - NSString *text = dict[@"text"]; - NSString *name = dict[@"name"]; - self.backURL = dict[@"backURL"]; - NSString *type = [self mimeTypeFromUti:dict[@"uti"]]; - if (![data isKindOfClass:NSData.class] || ![text isKindOfClass:NSString.class]) { - [self debug:@"[checkForFileToShare] Data content is invalid"]; - return; - } - NSArray *utis = dict[@"utis"]; - if (utis == nil) { - utis = @[]; - } + NSObject *object = [self.userDefaults objectForKey:@"shared"]; + if (object == nil) { + [self debug:@"[checkForFileToShare] Nothing to share"]; + return; + } - // TODO: add the backURL to the shared intent, put it aside in the plugin - // TODO: implement cordova.openwith.exit(intent), will check if backURL is set - - // Send to javascript - [self debug:[NSString stringWithFormat: - @"[checkForFileToShare] Sharing text \"%@\" and a %d bytes image", - text, data.length]]; - - NSString *uri = [NSString stringWithFormat: @"shareextension://index=0,name=%@,type=%@", - name, type]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:@{ - @"action": @"SEND", - @"exit": @YES, - @"items": @[@{ - @"text" : text, - @"base64": [data convertToBase64], - @"type": type, - @"utis": utis, - @"uri": uri, - @"name": name - }] - }]; - pluginResult.keepCallback = [NSNumber numberWithBool:YES]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.handlerCallback]; + // Clean-up the object, assume it's been handled from now, prevent double processing + [self.userDefaults removeObjectForKey:@"shared"]; + + // Extract sharing data, make sure that it is valid + if (![object isKindOfClass:[NSDictionary class]]) { + [self debug:@"[checkForFileToShare] Data object is invalid"]; + return; + } + + NSDictionary *dict = (NSDictionary*)object; + NSString *text = dict[@"text"]; + NSArray *items = dict[@"items"]; + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:@{ + @"text": text, + @"items": items + }]; + + pluginResult.keepCallback = [NSNumber numberWithBool:YES]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.handlerCallback]; } // Initialize the plugin - (void) init:(CDVInvokedUrlCommand*)command { - [self debug:@"[init]"]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; - [self checkForFileToShare]; -} + [self debug:@"[init]"]; -// Load data from URL -- (void) load:(CDVInvokedUrlCommand*)command { - [self debug:@"[load]"]; - // Base64 data already loaded, so this shouldn't happen - // the function is defined just to prevent crashes from unexpected client behaviours. - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Load, it shouldn't have been!"]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; -} + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; -// Exit after sharing -- (void) exit:(CDVInvokedUrlCommand*)command { - [self debug:[NSString stringWithFormat:@"[exit] %@", self.backURL]]; - if (self.backURL != nil) { - UIApplication *app = [UIApplication sharedApplication]; - NSURL *url = [NSURL URLWithString:self.backURL]; - if ([app canOpenURL:url]) { - [app openURL:url]; - } - } - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + [self checkForFileToShare]; } -@end -// vim: ts=4:sw=4:et +@end \ No newline at end of file diff --git a/src/ios/ShareExtension/ShareExtension-Info.plist b/src/ios/ShareExtension/ShareExtension-Info.plist index ee828fe..9fe2634 100644 --- a/src/ios/ShareExtension/ShareExtension-Info.plist +++ b/src/ios/ShareExtension/ShareExtension-Info.plist @@ -20,29 +20,5 @@ __BUNDLE_SHORT_VERSION_STRING__ CFBundleVersion __BUNDLE_VERSION__ - NSExtension - - NSExtensionAttributes - - NSExtensionActivationRule - - - NSExtensionActivationSupportsImageWithMaxCount - 1 - - - - NSExtensionActivationUsesStrictMatching - 1 - NSExtensionActivationDictionaryVersion - 2 - - NSExtensionMainStoryboard - MainInterface - NSExtensionPointIdentifier - com.apple.share-services - diff --git a/src/ios/ShareExtension/ShareExtension.entitlements b/src/ios/ShareExtension/ShareExtension.entitlements new file mode 100644 index 0000000..3125df1 --- /dev/null +++ b/src/ios/ShareExtension/ShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.__BUNDLE_IDENTIFIER__ + + + \ No newline at end of file diff --git a/src/ios/ShareExtension/ShareViewController.h b/src/ios/ShareExtension/ShareViewController.h index f57416e..ae18069 100644 --- a/src/ios/ShareExtension/ShareViewController.h +++ b/src/ios/ShareExtension/ShareViewController.h @@ -25,6 +25,5 @@ // THE SOFTWARE. // -#define SHAREEXT_GROUP_IDENTIFIER @"__GROUP_IDENTIFIER__" +#define SHAREEXT_GROUP_IDENTIFIER @"group.__BUNDLE_IDENTIFIER__" #define SHAREEXT_URL_SCHEME @"__URL_SCHEME__" -#define SHAREEXT_UNIFORM_TYPE_IDENTIFIER @"__UNIFORM_TYPE_IDENTIFIER__" diff --git a/src/ios/ShareExtension/ShareViewController.m b/src/ios/ShareExtension/ShareViewController.m index 409a0f3..5b706ff 100644 --- a/src/ios/ShareExtension/ShareViewController.m +++ b/src/ios/ShareExtension/ShareViewController.m @@ -30,15 +30,16 @@ #import #import #import "ShareViewController.h" +#import -@interface ShareViewController : SLComposeServiceViewController { - int _verbosityLevel; - NSUserDefaults *_userDefaults; - NSString *_backURL; +@interface ShareViewController : SLComposeServiceViewController { + NSFileManager *_fileManager; + NSUserDefaults *_userDefaults; + int _verbosityLevel; } -@property (nonatomic) int verbosityLevel; +@property (nonatomic,retain) NSFileManager *fileManager; @property (nonatomic,retain) NSUserDefaults *userDefaults; -@property (nonatomic,retain) NSString *backURL; +@property (nonatomic) int verbosityLevel; @end /* @@ -52,189 +53,219 @@ @interface ShareViewController : SLComposeServiceViewController { @implementation ShareViewController -@synthesize verbosityLevel = _verbosityLevel; +@synthesize fileManager = _fileManager; @synthesize userDefaults = _userDefaults; -@synthesize backURL = _backURL; +@synthesize verbosityLevel = _verbosityLevel; - (void) log:(int)level message:(NSString*)message { - if (level >= self.verbosityLevel) { - NSLog(@"[ShareViewController.m]%@", message); - } + if (level >= self.verbosityLevel) { + NSLog(@"[ShareViewController.m]%@", message); + } } + - (void) debug:(NSString*)message { [self log:VERBOSITY_DEBUG message:message]; } - (void) info:(NSString*)message { [self log:VERBOSITY_INFO message:message]; } - (void) warn:(NSString*)message { [self log:VERBOSITY_WARN message:message]; } - (void) error:(NSString*)message { [self log:VERBOSITY_ERROR message:message]; } - (void) setup { - self.userDefaults = [[NSUserDefaults alloc] initWithSuiteName:SHAREEXT_GROUP_IDENTIFIER]; - self.verbosityLevel = [self.userDefaults integerForKey:@"verbosityLevel"]; - [self debug:@"[setup]"]; + [self debug:@"[setup]"]; + + self.fileManager = [NSFileManager defaultManager]; + self.userDefaults = [[NSUserDefaults alloc] initWithSuiteName:SHAREEXT_GROUP_IDENTIFIER]; + self.verbosityLevel = [self.userDefaults integerForKey:@"verbosityLevel"]; } - (BOOL) isContentValid { - return YES; + return YES; } - (void) openURL:(nonnull NSURL *)url { + SEL selector = NSSelectorFromString(@"openURL:options:completionHandler:"); - SEL selector = NSSelectorFromString(@"openURL:options:completionHandler:"); - - UIResponder* responder = self; - while ((responder = [responder nextResponder]) != nil) { - NSLog(@"responder = %@", responder); - if([responder respondsToSelector:selector] == true) { - NSMethodSignature *methodSignature = [responder methodSignatureForSelector:selector]; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; - - // Arguments - NSDictionary *options = [NSDictionary dictionary]; - void (^completion)(BOOL success) = ^void(BOOL success) { - NSLog(@"Completions block: %i", success); - }; - - [invocation setTarget: responder]; - [invocation setSelector: selector]; - [invocation setArgument: &url atIndex: 2]; - [invocation setArgument: &options atIndex:3]; - [invocation setArgument: &completion atIndex: 4]; - [invocation invoke]; - break; - } + UIResponder* responder = self; + while ((responder = [responder nextResponder]) != nil) { + + if([responder respondsToSelector:selector] == true) { + NSMethodSignature *methodSignature = [responder methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + + // Arguments + NSDictionary *options = [NSDictionary dictionary]; + void (^completion)(BOOL success) = ^void(BOOL success) {}; + + [invocation setTarget: responder]; + [invocation setSelector: selector]; + [invocation setArgument: &url atIndex: 2]; + [invocation setArgument: &options atIndex:3]; + [invocation setArgument: &completion atIndex: 4]; + [invocation invoke]; + break; } + } } -- (void) didSelectPost { - - [self setup]; - [self debug:@"[didSelectPost]"]; - - // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. - for (NSItemProvider* itemProvider in ((NSExtensionItem*)self.extensionContext.inputItems[0]).attachments) { - - if ([itemProvider hasItemConformingToTypeIdentifier:SHAREEXT_UNIFORM_TYPE_IDENTIFIER]) { - [self debug:[NSString stringWithFormat:@"item provider = %@", itemProvider]]; - - [itemProvider loadItemForTypeIdentifier:SHAREEXT_UNIFORM_TYPE_IDENTIFIER options:nil completionHandler: ^(id item, NSError *error) { - - NSData *data = [[NSData alloc] init]; - if([(NSObject*)item isKindOfClass:[NSURL class]]) { - data = [NSData dataWithContentsOfURL:(NSURL*)item]; - } - if([(NSObject*)item isKindOfClass:[UIImage class]]) { - data = UIImagePNGRepresentation((UIImage*)item); - } - - NSString *suggestedName = @""; - if ([itemProvider respondsToSelector:NSSelectorFromString(@"getSuggestedName")]) { - suggestedName = [itemProvider valueForKey:@"suggestedName"]; - } - - NSString *uti = @""; - NSArray *utis = [NSArray new]; - if ([itemProvider.registeredTypeIdentifiers count] > 0) { - uti = itemProvider.registeredTypeIdentifiers[0]; - utis = itemProvider.registeredTypeIdentifiers; - } - else { - uti = SHAREEXT_UNIFORM_TYPE_IDENTIFIER; - } - NSDictionary *dict = @{ - @"text": self.contentText, - @"backURL": self.backURL, - @"data" : data, - @"uti": uti, - @"utis": utis, - @"name": suggestedName - }; - [self.userDefaults setObject:dict forKey:@"image"]; - [self.userDefaults synchronize]; - - // Emit a URL that opens the cordova app - NSString *url = [NSString stringWithFormat:@"%@://image", SHAREEXT_URL_SCHEME]; - - // Not allowed: - // [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; - - // Crashes: - // [self.extensionContext openURL:[NSURL URLWithString:url] completionHandler:nil]; - - // From https://stackoverflow.com/a/25750229/2343390 - // Reported not to work since iOS 8.3 - // NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; - // [self.webView loadRequest:request]; - - [self openURL:[NSURL URLWithString:url]]; - - // Inform the host that we're done, so it un-blocks its UI. - [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; - }]; - - return; +- (void) viewDidAppear:(BOOL)animated { + [self.view endEditing:YES]; + + [self setup]; + [self debug:@"[viewDidAppear]"]; + + __block int remainingAttachments = ((NSExtensionItem*)self.extensionContext.inputItems[0]).attachments.count; + __block NSMutableArray *items = [[NSMutableArray alloc] init]; + __block NSDictionary *results = @{ + @"text" : self.contentText, + @"items": items, + }; + + NSString *lastDataType = @""; + + for (NSItemProvider* itemProvider in ((NSExtensionItem*)self.extensionContext.inputItems[0]).attachments) { + [self debug:[NSString stringWithFormat:@"item provider registered indentifiers = %@", itemProvider.registeredTypeIdentifiers]]; + + if ([itemProvider hasItemConformingToTypeIdentifier:@"public.image"]) { + [self debug:[NSString stringWithFormat:@"item provider = %@", itemProvider]]; + + if (([lastDataType length] > 0) && ![lastDataType isEqualToString:@"FILE"]) { + --remainingAttachments; + continue; + } + + lastDataType = [NSString stringWithFormat:@"FILE"]; + + [itemProvider loadItemForTypeIdentifier:@"public.image" options:nil completionHandler: ^(NSURL* item, NSError *error) { + NSString *fileUrl = [self saveFileToAppGroupFolder:item]; + NSString *suggestedName = item.lastPathComponent; + + NSString *uti = @"public.image"; + NSString *registeredType = nil; + + if ([itemProvider.registeredTypeIdentifiers count] > 0) { + registeredType = itemProvider.registeredTypeIdentifiers[0]; + } else { + registeredType = uti; } + + NSString *mimeType = [self mimeTypeFromUti:registeredType]; + NSDictionary *dict = @{ + @"text" : self.contentText, + @"fileUrl" : fileUrl, + @"uti" : uti, + @"utis" : itemProvider.registeredTypeIdentifiers, + @"name" : suggestedName, + @"type" : mimeType + }; + + [items addObject:dict]; + + --remainingAttachments; + if (remainingAttachments == 0) { + [self sendResults:results]; + } + }]; + } else if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"]) { + [self debug:[NSString stringWithFormat:@"item provider = %@", itemProvider]]; + + if ([lastDataType length] > 0 && ![lastDataType isEqualToString:@"URL"]) { + --remainingAttachments; + continue; + } + + lastDataType = [NSString stringWithFormat:@"URL"]; + + [itemProvider loadItemForTypeIdentifier:@"public.url" options:nil completionHandler: ^(NSURL* item, NSError *error) { + [self debug:[NSString stringWithFormat:@"public.url = %@", item]]; + + NSString *uti = @"public.url"; + NSDictionary *dict = @{ + @"data" : item.absoluteString, + @"uti": uti, + @"utis": itemProvider.registeredTypeIdentifiers, + @"name": @"", + @"type": [self mimeTypeFromUti:uti], + }; + + [items addObject:dict]; + + --remainingAttachments; + if (remainingAttachments == 0) { + [self sendResults:results]; + } + }]; + } else if ([itemProvider hasItemConformingToTypeIdentifier:@"public.text"]) { + [self debug:[NSString stringWithFormat:@"item provider = %@", itemProvider]]; + + if ([lastDataType length] > 0 && ![lastDataType isEqualToString:@"TEXT"]) { + --remainingAttachments; + continue; + } + + lastDataType = [NSString stringWithFormat:@"TEXT"]; + + [itemProvider loadItemForTypeIdentifier:@"public.text" options:nil completionHandler: ^(NSString* item, NSError *error) { + [self debug:[NSString stringWithFormat:@"public.text = %@", item]]; + + NSString *uti = @"public.text"; + NSDictionary *dict = @{ + @"text" : self.contentText, + @"data" : item, + @"uti": uti, + @"utis": itemProvider.registeredTypeIdentifiers, + @"name": @"", + @"type": @"text/plain", + }; + + [items addObject:dict]; + + --remainingAttachments; + if (remainingAttachments == 0) { + [self sendResults:results]; + } + }]; + } else { + --remainingAttachments; + if (remainingAttachments == 0) { + [self sendResults:results]; + } } + } +} + +- (void) sendResults: (NSDictionary*)results { + [self.userDefaults setObject:results forKey:@"shared"]; + [self.userDefaults synchronize]; + + // Emit a URL that opens the cordova app + NSString *url = [NSString stringWithFormat:@"%@://shared", SHAREEXT_URL_SCHEME]; + [self openURL:[NSURL URLWithString:url]]; - // Inform the host that we're done, so it un-blocks its UI. - [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; + // Shut down the extension + [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; } + - (void) didSelectPost { + [self debug:@"[didSelectPost]"]; + } + - (NSArray*) configurationItems { - // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. - return @[]; + // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. + return @[]; } -- (NSString*) backURLFromBundleID: (NSString*)bundleId { - if (bundleId == nil) return nil; - // App Store - com.apple.AppStore - if ([bundleId isEqualToString:@"com.apple.AppStore"]) return @"itms-apps://"; - // Calculator - com.apple.calculator - // Calendar - com.apple.mobilecal - // Camera - com.apple.camera - // Clock - com.apple.mobiletimer - // Compass - com.apple.compass - // Contacts - com.apple.MobileAddressBook - // FaceTime - com.apple.facetime - // Find Friends - com.apple.mobileme.fmf1 - // Find iPhone - com.apple.mobileme.fmip1 - // Game Center - com.apple.gamecenter - // Health - com.apple.Health - // iBooks - com.apple.iBooks - // iTunes Store - com.apple.MobileStore - // Mail - com.apple.mobilemail - message:// - if ([bundleId isEqualToString:@"com.apple.mobilemail"]) return @"message://"; - // Maps - com.apple.Maps - maps:// - if ([bundleId isEqualToString:@"com.apple.Maps"]) return @"maps://"; - // Messages - com.apple.MobileSMS - // Music - com.apple.Music - // News - com.apple.news - applenews:// - if ([bundleId isEqualToString:@"com.apple.news"]) return @"applenews://"; - // Notes - com.apple.mobilenotes - mobilenotes:// - if ([bundleId isEqualToString:@"com.apple.mobilenotes"]) return @"mobilenotes://"; - // Phone - com.apple.mobilephone - // Photos - com.apple.mobileslideshow - if ([bundleId isEqualToString:@"com.apple.mobileslideshow"]) return @"photos-redirect://"; - // Podcasts - com.apple.podcasts - // Reminders - com.apple.reminders - x-apple-reminder:// - if ([bundleId isEqualToString:@"com.apple.reminders"]) return @"x-apple-reminder://"; - // Safari - com.apple.mobilesafari - // Settings - com.apple.Preferences - // Stocks - com.apple.stocks - // Tips - com.apple.tips - // Videos - com.apple.videos - videos:// - if ([bundleId isEqualToString:@"com.apple.videos"]) return @"videos://"; - // Voice Memos - com.apple.VoiceMemos - voicememos:// - if ([bundleId isEqualToString:@"com.apple.VoiceMemos"]) return @"voicememos://"; - // Wallet - com.apple.Passbook - // Watch - com.apple.Bridge - // Weather - com.apple.weather - return @""; +- (NSString *) mimeTypeFromUti: (NSString*)uti { + if (uti == nil) { return nil; } + + CFStringRef cret = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)uti, kUTTagClassMIMEType); + NSString *ret = (__bridge_transfer NSString *)cret; + + return ret == nil ? uti : ret; } -// This is called at the point where the Post dialog is about to be shown. -// We use it to store the _hostBundleID -- (void) willMoveToParentViewController: (UIViewController*)parent { - NSString *hostBundleID = [parent valueForKey:(@"_hostBundleID")]; - self.backURL = [self backURLFromBundleID:hostBundleID]; +- (NSString *) saveFileToAppGroupFolder: (NSURL*)url { + NSURL *targetUrl = [[self.fileManager containerURLForSecurityApplicationGroupIdentifier:SHAREEXT_GROUP_IDENTIFIER] URLByAppendingPathComponent:url.lastPathComponent]; + [self.fileManager copyItemAtURL:url toURL:targetUrl error:nil]; + + return targetUrl.absoluteString; } -@end +@end \ No newline at end of file From 7bc5b9ab34080affbbe0f807bbf6588122decb86 Mon Sep 17 00:00:00 2001 From: Jochen Heizmann Date: Sat, 30 Mar 2019 21:41:26 +0100 Subject: [PATCH 3/7] Readme updated --- README.md | 81 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index acb3e5e..f663b3f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,16 @@ The plugin's API mostly follows Android's terminology. Below is a short introduction to how the technology works on Android and iOS. +### Note + +Right now the plugin doesnt work with Cordova new than 8.1.0. (Edit: Is this still true?) + +If you have cordova installed globally you also need to install the xcode package globally: + +``` +npm install -g xcode@2.0.0 +``` + #### Android On Android, the app defines, in its __AndroidManifest.xml__ file, the **mime type** of file types it can handle. Wildcard are accepted, so `image/*` can be used to accept all images regardless of the sub-type. The app also defines the type of actions accepted for this file types. By default, only the [SEND](https://developer.android.com/reference/android/content/Intent.html#ACTION_SEND) event is declared by the plugin. Other events that can be of interest are `SEND_MULTIPLE` and `VIEW`. @@ -49,15 +59,63 @@ Once the data is in place in the Shared User-Preferences Container, the Share Ex On the Cordova App side, the plugin checks listens for app start or resume events. When this happens, it looks into the Shared User-Preferences Container for any content to share and report it to the javascript application. +You need to config the ios app through your config.xml file, so the Entitlements and plist settings are generated correctly through cordova. For example, to do this, add in the ios platform section: + +``` + + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationUsesStrictMatching + 0 + NSExtensionActivationDictionaryVersion + 2 + NSExtensionActivationSupportsAttachmentsWithMaxCount + 1 + NSExtensionActivationSupportsFileWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsWebPageWithMaxCount + 1 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + + + group.imw.com.xxx.yyy.shareextension + + + + + group.imw.com.xxx.yyy.shareextension + + +``` + ## Installation Here's the promised one liner: ``` -cordova plugin add cc.fovea.cordova.openwith \ - --variable ANDROID_MIME_TYPE="image/*" \ - --variable IOS_URL_SCHEME=ccfoveaopenwithdemo \ - --variable IOS_UNIFORM_TYPE_IDENTIFIER=public.image +cordova plugin add https://github.com/JochenHeizmann/cordova-plugin-openwith.git \ + --variable ANDROID_MIME_TYPE="text/plain" \ + --variable IOS_URL_SCHEME=xxx \ + --variable IOS_UNIFORM_TYPE_IDENTIFIER=public.plain-text \ + --variable IOS_BUNDLE_IDENTIFIER=com.xxx.yyy ``` | variable | example | notes | @@ -73,20 +131,7 @@ It shouldn't be too hard. But just in case, I [posted a screencast of it](https: ### iOS Setup -After having installed the plugin, with the ios platform in place, 1 operation needs to be done manually: setup the App Group on both the Cordova App and the Share Extension. - - 1. open the **xcodeproject** for your application - 1. select the root element of your **project navigator** (the left-side pane) - 1. select the **target** of your application - 1. select **capabilities** - 1. scroll down to **App Groups** - 1. make sure it's **ON** - 1. create and activate an **App Group** called: `group..shareextension` - 1. repeat the previous five steps for the **ShareExtension target**. - -You might also have to select a Team for both the App and Share Extension targets, make sure to select the same. - -Build, XCode might complain about a few things to setup that it will fix for you (creation entitlements files, etc). +After having installed the plugin, with the ios platform in place, 1 operation needs to be done manually: Register the APP Group in you developer portal, the app group is called **App Group** called: `group..shareextension` ### Advanced installation options From 95515cb23cfa685e14fb710a256688fab5e77de9 Mon Sep 17 00:00:00 2001 From: Jochen Heizmann Date: Sat, 30 Mar 2019 21:57:47 +0100 Subject: [PATCH 4/7] fixed file permissions/extended attributes --- hooks/iosAddTarget.js | 0 hooks/utils.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 hooks/iosAddTarget.js diff --git a/hooks/iosAddTarget.js b/hooks/iosAddTarget.js old mode 100755 new mode 100644 diff --git a/hooks/utils.js b/hooks/utils.js index 47c7638..7f9007d 100644 --- a/hooks/utils.js +++ b/hooks/utils.js @@ -129,4 +129,4 @@ module.exports = { findXCodeproject, replacePreferencesInFile, log, redError, -} \ No newline at end of file +} From cf8e50335982d791a1b04daf263a1082c2d306a5 Mon Sep 17 00:00:00 2001 From: Jochen Heizmann Date: Sat, 30 Mar 2019 22:01:32 +0100 Subject: [PATCH 5/7] missing file added --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b40476d..8d298de 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "hooks/iosRemoveTarget.js", "hooks/iosCopyShareExtension.js", "hooks/npmInstall.js", + "hooks/utils.js", "install-pmd", "plugin.xml", "LICENSE", From f707db3d8b57803dde516e0ab8d4bfb7fd6ae1e6 Mon Sep 17 00:00:00 2001 From: Jochen Heizmann Date: Sat, 30 Mar 2019 22:40:59 +0100 Subject: [PATCH 6/7] fixed hook --- plugin.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.xml b/plugin.xml index 96ffbd9..00e49f8 100644 --- a/plugin.xml +++ b/plugin.xml @@ -75,8 +75,8 @@ SOFTWARE. - - + + From a5c241559b0e3e30f529a2cb0a83ab6f40074b84 Mon Sep 17 00:00:00 2001 From: Jochen Heizmann Date: Sat, 30 Mar 2019 22:52:51 +0100 Subject: [PATCH 7/7] plugin meta data updated --- package.json | 6 +++--- plugin.xml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8d298de..faa753f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/j3k0/cordova-plugin-openwith.git" + "url": "https://github.com/JochenHeizmann/cordova-plugin-openwith.git" }, "keywords": [ "ecosystem:cordova", @@ -36,9 +36,9 @@ "author": "Jean-Christophe Hoelt ", "license": "MIT", "bugs": { - "url": "https://github.com/j3k0/cordova-plugin-openwith/issues" + "url": "https://github.com/JochenHeizmann/cordova-plugin-openwith.git/issues" }, - "homepage": "https://github.com/j3k0/cordova-plugin-openwith", + "homepage": "cordova-plugin-openwith", "# Dependencies required by the hooks": "", "dependencies": { "file-system": "^2.2.2", diff --git a/plugin.xml b/plugin.xml index 00e49f8..af950d4 100644 --- a/plugin.xml +++ b/plugin.xml @@ -33,8 +33,8 @@ SOFTWARE. - https://github.com/j3k0/cordova-plugin-openwith.git - https://github.com/j3k0/cordova-plugin-openwith/issues + https://github.com/JochenHeizmann/cordova-plugin-openwith.git + https://github.com/JochenHeizmann/cordova-plugin-openwith.git/issues MIT cordova,phonegap,openwith,ios,android