From 925c231db482141066fafdfe73c95dbb43b6394c Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Wed, 15 Mar 2023 09:26:11 +0100 Subject: [PATCH] intent error on android solved --- .../xcshareddata/xcschemes/App.xcscheme | 78 +++++++ .../xcshareddata/xcschemes/upload.xcscheme | 101 +++++++++ .../Base.lproj/MainInterface.storyboard | 24 +++ ios/App/upload/Info.plist | 18 ++ ios/App/upload/ShareViewController.swift | 192 ++++++++++++++++++ src/app/_helper/intent-init.factory.ts | 15 ++ src/app/_helper/utils.ts | 43 ++++ src/app/d4s-intent.service.spec.ts | 16 ++ src/app/d4s-intent.service.ts | 61 ++++++ src/app/event.service.spec.ts | 16 ++ src/app/event.service.ts | 13 ++ src/app/model/actions/open-file.ts | 99 +++++++++ src/app/model/actions/upload-file.ts | 57 ++++++ src/app/uploader-info.service.spec.ts | 16 ++ src/app/uploader-info.service.ts | 38 ++++ 15 files changed, 787 insertions(+) create mode 100644 ios/App/App.xcworkspace/xcshareddata/xcschemes/App.xcscheme create mode 100644 ios/App/App.xcworkspace/xcshareddata/xcschemes/upload.xcscheme create mode 100644 ios/App/upload/Base.lproj/MainInterface.storyboard create mode 100644 ios/App/upload/Info.plist create mode 100644 ios/App/upload/ShareViewController.swift create mode 100644 src/app/_helper/intent-init.factory.ts create mode 100644 src/app/_helper/utils.ts create mode 100644 src/app/d4s-intent.service.spec.ts create mode 100644 src/app/d4s-intent.service.ts create mode 100644 src/app/event.service.spec.ts create mode 100644 src/app/event.service.ts create mode 100644 src/app/model/actions/open-file.ts create mode 100644 src/app/model/actions/upload-file.ts create mode 100644 src/app/uploader-info.service.spec.ts create mode 100644 src/app/uploader-info.service.ts diff --git a/ios/App/App.xcworkspace/xcshareddata/xcschemes/App.xcscheme b/ios/App/App.xcworkspace/xcshareddata/xcschemes/App.xcscheme new file mode 100644 index 0000000..d708c4b --- /dev/null +++ b/ios/App/App.xcworkspace/xcshareddata/xcschemes/App.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App.xcworkspace/xcshareddata/xcschemes/upload.xcscheme b/ios/App/App.xcworkspace/xcshareddata/xcschemes/upload.xcscheme new file mode 100644 index 0000000..08986b0 --- /dev/null +++ b/ios/App/App.xcworkspace/xcshareddata/xcschemes/upload.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/upload/Base.lproj/MainInterface.storyboard b/ios/App/upload/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000..286a508 --- /dev/null +++ b/ios/App/upload/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/upload/Info.plist b/ios/App/upload/Info.plist new file mode 100644 index 0000000..4b1f7e7 --- /dev/null +++ b/ios/App/upload/Info.plist @@ -0,0 +1,18 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + TRUEPREDICATE + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + diff --git a/ios/App/upload/ShareViewController.swift b/ios/App/upload/ShareViewController.swift new file mode 100644 index 0000000..7c1d68b --- /dev/null +++ b/ios/App/upload/ShareViewController.swift @@ -0,0 +1,192 @@ +// +// ShareViewController.swift +// upload +// +// Created by Lucio Lelii on 10/03/23. +// + +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? +} + +class ShareViewController: UIViewController { + private var shareItems: [ShareItem] = [] + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + print("did appear called"); + 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) ?? "") + ] + }.flatMap({ $0 }) + var urlComps = URLComponents(string: "d4sworkspace://share-input")! + urlComps.queryItems = queryItems + var opened = openURL(urlComps.url!) + print("sending data %s",opened); + } + + fileprivate func createSharedFileUrl(_ url: URL?) -> String { + let fileManager = FileManager.default + print("share url: " + url!.absoluteString) + let copyFileUrl = + fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.gcube.workspace")! + .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.gcube.workspace")! + .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" + } + 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 + 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 + default: + print("Unexpected image data:", type(of: data)) + } + return shareItem + } + + @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 + } +} diff --git a/src/app/_helper/intent-init.factory.ts b/src/app/_helper/intent-init.factory.ts new file mode 100644 index 0000000..5aa4c46 --- /dev/null +++ b/src/app/_helper/intent-init.factory.ts @@ -0,0 +1,15 @@ +import { ShareExtension } from "capacitor-share-extension"; +import { D4sIntentService } from "../d4s-intent.service"; + +export function initializeIntent(d4sIntent: D4sIntentService) { + + return () => { + window.addEventListener('sendIntentReceived', () => { + console.log("event fired"); + d4sIntent.checkIntent(); + }); + d4sIntent.checkIntent(); + } + +} + diff --git a/src/app/_helper/utils.ts b/src/app/_helper/utils.ts new file mode 100644 index 0000000..c31b073 --- /dev/null +++ b/src/app/_helper/utils.ts @@ -0,0 +1,43 @@ +import { AlertController } from "@ionic/angular"; + +/** + * Format bytes as human-readable text. + * + * @param bytes Number of bytes. + * @param si True to use metric (SI) units, aka powers of 1000. False to use + * binary (IEC), aka powers of 1024. + * @param dp Number of decimal places to display. + * + * @return Formatted string. + */ +export function humanFileSize(bytes:number, si=true, dp=1) { + const thresh = si ? 1000 : 1024; + + if (Math.abs(bytes) < thresh) { + return bytes + ' B'; + } + + const units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + const r = 10**dp; + + do { + bytes /= thresh; + ++u; + } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); + + + return bytes.toFixed(dp) + ' ' + units[u]; + } + + + export async function presentConnectionAlert(err: string, alertCtrl: AlertController){ + const alert = await alertCtrl.create({ + header: 'Connection ERROR', + message: err, + buttons: ['OK'], + }); + await alert.present(); + } \ No newline at end of file diff --git a/src/app/d4s-intent.service.spec.ts b/src/app/d4s-intent.service.spec.ts new file mode 100644 index 0000000..9bd3c23 --- /dev/null +++ b/src/app/d4s-intent.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { D4sIntentService } from './d4s-intent.service'; + +describe('D4sIntentService', () => { + let service: D4sIntentService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(D4sIntentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/d4s-intent.service.ts b/src/app/d4s-intent.service.ts new file mode 100644 index 0000000..c26bc71 --- /dev/null +++ b/src/app/d4s-intent.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import { ModalController, ModalOptions, ToastController } from '@ionic/angular'; +import { ShareExtension } from 'capacitor-share-extension'; +import { EventService } from './event.service'; +import { UploadFile } from './model/actions/upload-file'; +import { StoragehubService } from './storagehub.service'; +import { UploaderInfoService } from './uploader-info.service'; + +@Injectable({ + providedIn: 'root' +}) +export class D4sIntentService { + + constructor(private modalCtrl: ModalController, + private toastController: ToastController, + private storagehub: StoragehubService, + private uploaderInfo: UploaderInfoService, + private event: EventService) { } + + + 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.payload[0])); + var modalOptions: ModalOptions = new UploadFile().getModalOptions(this.storagehub, this.uploaderInfo, result.payload[0], + (id : string) =>{ + this.event.ReloadEvent.emit(id); + ShareExtension.finish(); + }, (text: string) => this.presentToast(text)); + let modal = await this.modalCtrl.create(modalOptions); + await modal.present(); + } else { + console.log("nothing received"); + } + } catch (err) { + console.log(err); + } + } + + async presentToast(text: string) { + const toast = await this.toastController.create({ + message: text, + duration: 1500 + }); + await toast.present(); + } +} diff --git a/src/app/event.service.spec.ts b/src/app/event.service.spec.ts new file mode 100644 index 0000000..df6b168 --- /dev/null +++ b/src/app/event.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { EventService } from './event.service'; + +describe('EventService', () => { + let service: EventService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EventService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/event.service.ts b/src/app/event.service.ts new file mode 100644 index 0000000..a32e5bd --- /dev/null +++ b/src/app/event.service.ts @@ -0,0 +1,13 @@ +import { EventEmitter, Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class EventService { + + ReloadEvent = new EventEmitter(); + + constructor() { } + + +} diff --git a/src/app/model/actions/open-file.ts b/src/app/model/actions/open-file.ts new file mode 100644 index 0000000..fb70f6c --- /dev/null +++ b/src/app/model/actions/open-file.ts @@ -0,0 +1,99 @@ +import { AlertController, LoadingController } from "@ionic/angular"; +import { WSItem } from "../ws-item"; +import { StoragehubService } from "src/app/storagehub.service"; +import { Directory, Filesystem } from "@capacitor/filesystem"; +import { FileOpener } from "@awesome-cordova-plugins/file-opener/ngx"; +import { App } from '@capacitor/app'; +import { isPlatform } from '@ionic/angular'; +import { presentConnectionAlert } from "src/app/_helper/utils"; + + +export class OpenFile { + + pluginListener: any; + + item: WSItem; + + constructor(item: WSItem) { + this.item = item; + if (isPlatform("android")) { + App.addListener('resume', () => { + Filesystem.deleteFile( + { + path: item.getTitle(), + directory: Directory.Documents + }).then(() => console.log("file deleted")); + this.pluginListener?.remove(); + }).then((handler) => this.pluginListener = handler); + } + } + + async open(storageHub: StoragehubService, fileOpener: FileOpener, loadingCtrl: LoadingController, alertCtrl: AlertController) { + const size = this.item.item.content?.size; + if (!size || size / 1000000 > 20) { + this.presentAlert(alertCtrl); + return; + } + + this.showLoading(loadingCtrl); + storageHub.downloadFile(this.item.item.id).then(obs => obs.subscribe({ + next: async blob => { + const base64 = await this.blobToBase64(blob) as string; + const fileWrited = await Filesystem.writeFile({ + path: this.item.getTitle(), + data: base64, + directory: Directory.Documents + }); + + const mimeType = this.item.item.content?.mimeType; + fileOpener.open(fileWrited.uri, mimeType ? mimeType : "application/octet-stream").then(() => { + loadingCtrl.dismiss(); + if (isPlatform("ios")) + Filesystem.deleteFile( + { + path: this.item.getTitle(), + directory: Directory.Documents + }); + + }) + + }, + error: (err) => { + loadingCtrl.dismiss(); + this.pluginListener?.remove(); + presentConnectionAlert(err, alertCtrl); + } + })) + } + + + async presentAlert(alertCtrl: AlertController) { + const alert = await alertCtrl.create({ + header: 'Alert', + message: 'File too big to be opeend here!', + buttons: ['OK'], + }); + + await alert.present(); + } + + async showLoading(loadingCtrl: LoadingController) { + const loading = await loadingCtrl.create({ + message: 'Loading...', + duration: 20000, + spinner: 'circles', + }); + + loading.present(); + return loading; + } + + + blobToBase64(blob: Blob) { + return new Promise((resolve, _) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(blob); + }); + } +} \ No newline at end of file diff --git a/src/app/model/actions/upload-file.ts b/src/app/model/actions/upload-file.ts new file mode 100644 index 0000000..a527cd4 --- /dev/null +++ b/src/app/model/actions/upload-file.ts @@ -0,0 +1,57 @@ +import { ModalOptions } from "@ionic/angular"; +import { StoragehubService } from "src/app/storagehub.service"; +import { WsViewerComponent } from "src/app/ws-viewer/ws-viewer.component"; +import { WSItem } from "../ws-item"; +import { Filesystem } from "@capacitor/filesystem"; +import { UploaderInfoService } from "src/app/uploader-info.service"; + +export class UploadFile { + + + + getModalOptions(storagehub: StoragehubService, uploaderInfo: UploaderInfoService, data: any, reload: Function, notify: Function): ModalOptions { + return { + component: WsViewerComponent, + componentProps: { + finishLabel: "Upload here", + title: "Select destination folder", + notClickableIds: [], + notSelectableIds: [], + onSelected: (destinationItem: WSItem) => { + const itemId = destinationItem.item.id; + uploaderInfo.uploadStarted(itemId, data.title); + this.actionHandler(destinationItem, data, storagehub).then( obs => obs.subscribe({ + next: () => {}, + error: err => uploaderInfo.uploadFinished(itemId, data.title), + complete: () => { + notify(`uploaded file ${data.title}`); + uploaderInfo.uploadFinished(itemId, data.title), + reload(itemId); + }, + + } + )); + } + } + }; + } + + + async actionHandler(destinationItem: WSItem, result: any, storagehub: StoragehubService) { + const pathDecodedWebPath = decodeURIComponent(result.webPath); + + const file = await fetch(pathDecodedWebPath) + .then(res => res.blob()) // Gets the response and returns it as a blob + .then(blob => { return new File([blob], result.title)}); + + console.log("before uploading file "); + return storagehub.uploadFile(destinationItem.item.id, result.title, file); + } + + getName(): string { + return "UploadFile"; + } + getActionType(): string | undefined { + return undefined; + } +} \ No newline at end of file diff --git a/src/app/uploader-info.service.spec.ts b/src/app/uploader-info.service.spec.ts new file mode 100644 index 0000000..9a32a38 --- /dev/null +++ b/src/app/uploader-info.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UploaderInfoService } from './uploader-info.service'; + +describe('UploaderInfoService', () => { + let service: UploaderInfoService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UploaderInfoService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/uploader-info.service.ts b/src/app/uploader-info.service.ts new file mode 100644 index 0000000..0dd2086 --- /dev/null +++ b/src/app/uploader-info.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class UploaderInfoService { + + myMap = new Map(); + + constructor() { + } + + uploadStarted(itemId: string, fileName: string) { + var values = this.myMap.get(itemId); + if (!values) + values = [fileName]; + else + values.push(fileName); + this.myMap.set(itemId, values); + } + + uploadFinished(itemId: string , fileName: string) { + var values = this.myMap.get(itemId); + var newValues: string[] = []; + values?.filter(val => val!= fileName).forEach(val => newValues?.push(val)) + if (newValues) + if (newValues.length>0) + this.myMap.set(itemId, newValues); + else this.myMap.delete(itemId); + } + + getUnderUpload(itemId: string): string[]{ + var values = this.myMap.get(itemId); + //console.log(`get upload called with array ${values}`); + return values? values : []; + } + +}