CH-J SM Mobilespráva serverů přes SSH
Menu
Vydané zdroje

CH-J SM Mobile

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J / SM Mobile 175 souborů
sm-mobile-20261016T185235Z-8e93ecc1b59a Přidáno: · Sestaveno:
AppDelegate.swift Přidáno: Datum není evidováno 13,0 KB · 332 řádků
Kód Stáhnout soubor
1import Flutter
2import QuickLook
3import UIKit
4import UniformTypeIdentifiers
6@main
7@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
8 override func application(
9 _ application: UIApplication,
10 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
11 ) -> Bool {
12 return super.application(application, didFinishLaunchingWithOptions: launchOptions)
13 }
15 func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
16 GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
17 if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "SMDocuments") {
18 Documents.shared.attach(registrar.messenger())
19 }
20 }
23/// Files on the device through the system document picker (Files app and
24/// file providers). Access exists only for what the user picks: documents
25/// are kept as security-scoped bookmarks and read/written with
26/// NSFileCoordinator, inside the iOS sandbox.
27final class Documents: NSObject, UIDocumentPickerDelegate, QLPreviewControllerDataSource,
28 QLPreviewControllerDelegate
30 static let shared = Documents()
31 private var channel: FlutterMethodChannel?
32 private var pending: (([URL]?) -> Void)?
33 private var incoming: [[String: Any?]] = []
34 private var previewURL: URL?
35 private var previewResult: FlutterResult?
36 private let queue = DispatchQueue(label: "eu.chj.sm_mobile.documents")
38 func attach(_ messenger: FlutterBinaryMessenger) {
39 let channel = FlutterMethodChannel(name: "eu.chj.sm_mobile/documents", binaryMessenger: messenger)
40 channel.setMethodCallHandler { [weak self] call, result in self?.handle(call, result) }
41 self.channel = channel
42 }
44 // MARK: identifiers
46 /// "b:<bookmark>" for a picked item; "b:<folder bookmark>\n<relative path>"
47 /// for an item inside a picked folder.
48 private func id(bookmarkOf url: URL) throws -> String {
49 let scoped = url.startAccessingSecurityScopedResource()
50 defer { if scoped { url.stopAccessingSecurityScopedResource() } }
51 let data = try url.bookmarkData(options: [], includingResourceValuesForKeys: nil, relativeTo: nil)
52 return "b:" + data.base64EncodedString()
53 }
55 private func resolve(_ id: String) throws -> (url: URL, scope: URL) {
56 let parts = id.split(separator: "\n", maxSplits: 1).map(String.init)
57 guard let first = parts.first, first.hasPrefix("b:"),
58 let data = Data(base64Encoded: String(first.dropFirst(2)))
59 else { throw DocError.code("notFound") }
60 var stale = false
61 let scope: URL
62 do {
63 scope = try URL(resolvingBookmarkData: data, options: [], relativeTo: nil, bookmarkDataIsStale: &stale)
64 } catch {
65 throw DocError.code("stale")
66 }
67 if parts.count == 2 {
68 let relative = parts[1]
69 if relative.split(separator: "/").contains("..") { throw DocError.code("denied") }
70 return (scope.appendingPathComponent(relative), scope)
71 }
72 return (scope, scope)
73 }
75 enum DocError: Error { case code(String) }
77 private func access<T>(_ id: String, _ work: (URL) throws -> T) throws -> T {
78 let (url, scope) = try resolve(id)
79 let scoped = scope.startAccessingSecurityScopedResource()
80 defer { if scoped { scope.stopAccessingSecurityScopedResource() } }
81 return try work(url)
82 }
84 private func describe(_ url: URL, id: String, location: String? = nil) -> [String: Any?] {
85 let keys: Set<URLResourceKey> = [
86 .nameKey, .fileSizeKey, .contentModificationDateKey, .isDirectoryKey, .isWritableKey,
87 .contentTypeKey,
88 ]
89 let v = try? url.resourceValues(forKeys: keys)
90 let directory = v?.isDirectory ?? false
91 return [
92 "id": id,
93 "name": v?.name ?? url.lastPathComponent,
94 "size": v?.fileSize,
95 "modified": v?.contentModificationDate.map { Int($0.timeIntervalSince1970 * 1000) },
96 "directory": directory,
97 "writable": !directory && (v?.isWritable ?? FileManager.default.isWritableFile(atPath: url.path)),
98 "mime": v?.contentType?.preferredMIMEType,
99 "location": location ?? url.deletingLastPathComponent().lastPathComponent,
100 ]
101 }
103 private func coordinatedRead(_ url: URL) throws -> Data {
104 var error: NSError?
105 var result: Result<Data, Error> = .failure(DocError.code("io"))
106 NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &error) { u in
107 result = Result { try Data(contentsOf: u) }
108 }
109 if let error { throw error }
110 return try result.get()
111 }
113 private func coordinatedWrite(_ url: URL, _ data: Data) throws {
114 var error: NSError?
115 var result: Result<Void, Error> = .success(())
116 NSFileCoordinator().coordinate(writingItemAt: url, options: .forReplacing, error: &error) { u in
117 result = Result { try data.write(to: u, options: .atomic) }
118 }
119 if let error { throw error }
120 try result.get()
121 }
123 // MARK: presentation
125 private var top: UIViewController? {
126 let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
127 var vc = scenes.flatMap { $0.windows }.first { $0.isKeyWindow }?.rootViewController
128 while let presented = vc?.presentedViewController { vc = presented }
129 return vc
130 }
132 private func present(_ picker: UIDocumentPickerViewController, _ done: @escaping ([URL]?) -> Void) -> Bool {
133 guard pending == nil, let top else { return false }
134 pending = done
135 picker.delegate = self
136 top.present(picker, animated: true)
137 return true
138 }
140 func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
141 let done = pending
142 pending = nil
143 done?(urls)
144 }
146 func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
147 let done = pending
148 pending = nil
149 done?(nil)
150 }
152 func numberOfPreviewItems(in controller: QLPreviewController) -> Int { previewURL == nil ? 0 : 1 }
153 func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
154 previewURL! as QLPreviewItem
155 }
156 func previewControllerDidDismiss(_ controller: QLPreviewController) {
157 previewResult?(true)
158 previewResult = nil
159 previewURL = nil
160 }
162 // MARK: calls
164 private func fail(_ result: @escaping FlutterResult, _ error: Error) {
165 let code: String
166 switch error {
167 case DocError.code(let c): code = c
168 case let e as NSError where e.domain == NSCocoaErrorDomain:
169 switch e.code {
170 case NSFileWriteNoPermissionError, NSFileReadNoPermissionError: code = "denied"
171 case NSFileWriteOutOfSpaceError: code = "noSpace"
172 case NSFileNoSuchFileError, NSFileReadNoSuchFileError: code = "notFound"
173 case NSFileWriteVolumeReadOnlyError: code = "readOnly"
174 default: code = "io"
175 }
176 default: code = "io"
177 }
178 DispatchQueue.main.async { result(FlutterError(code: code, message: "\(error)", details: nil)) }
179 }
181 private func background(_ result: @escaping FlutterResult, _ work: @escaping () throws -> Any?) {
182 queue.async {
183 do {
184 let value = try work()
185 DispatchQueue.main.async { result(value) }
186 } catch {
187 self.fail(result, error)
188 }
189 }
190 }
192 private func handle(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
193 let args = call.arguments as? [String: Any] ?? [:]
194 switch call.method {
195 case "pick":
196 let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: false)
197 picker.allowsMultipleSelection = args["multiple"] as? Bool ?? false
198 let shown = present(picker) { urls in
199 guard let urls else { return result([]) }
200 self.background(result) {
201 try urls.map { url in
202 let id = try self.id(bookmarkOf: url)
203 return try self.access(id) { self.describe($0, id: id) }
204 }
205 }
206 }
207 if !shown { result(FlutterError(code: "busy", message: nil, details: nil)) }
208 case "pickFolder":
209 let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder], asCopy: false)
210 let shown = present(picker) { urls in
211 guard let url = urls?.first else { return result(nil) }
212 self.background(result) {
213 let id = try self.id(bookmarkOf: url)
214 return try self.access(id) { self.describe($0, id: id) }
215 }
216 }
217 if !shown { result(FlutterError(code: "busy", message: nil, details: nil)) }
218 case "list":
219 let folder = args["id"] as! String
220 background(result) {
221 try self.access(folder) { url in
222 let (_, scope) = try self.resolve(folder)
223 let base = folder.split(separator: "\n", maxSplits: 1).first.map(String.init)!
224 let items = try FileManager.default.contentsOfDirectory(
225 at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
226 return items.map { item -> [String: Any?] in
227 let relative = String(item.standardizedFileURL.path.dropFirst(scope.standardizedFileURL.path.count))
228 .trimmingCharacters(in: CharacterSet(charactersIn: "/"))
229 return self.describe(item, id: base + "\n" + relative, location: url.lastPathComponent)
230 }
231 }
232 }
233 case "stat":
234 let id = args["id"] as! String
235 background(result) { try self.access(id) { self.describe($0, id: id) } }
236 case "read":
237 let id = args["id"] as! String
238 background(result) { try self.access(id) { FlutterStandardTypedData(bytes: try self.coordinatedRead($0)) } }
239 case "write":
240 let id = args["id"] as! String
241 let bytes = (args["bytes"] as! FlutterStandardTypedData).data
242 background(result) {
243 try self.access(id) { url in
244 if !FileManager.default.isWritableFile(atPath: url.path) { throw DocError.code("readOnly") }
245 try self.coordinatedWrite(url, bytes)
246 return self.describe(url, id: id)
247 }
248 }
249 case "create":
250 let folder = args["folder"] as! String
251 let name = args["name"] as! String
252 let bytes = (args["bytes"] as! FlutterStandardTypedData).data
253 background(result) {
254 if name.contains("/") || name == ".." { throw DocError.code("denied") }
255 return try self.access(folder) { url in
256 let target = url.appendingPathComponent(name)
257 if FileManager.default.fileExists(atPath: target.path) { throw DocError.code("exists") }
258 try self.coordinatedWrite(target, bytes)
259 let base = folder.split(separator: "\n", maxSplits: 1).map(String.init)
260 let relative = (base.count == 2 ? base[1] + "/" : "") + name
261 return self.describe(target, id: base[0] + "\n" + relative)
262 }
263 }
264 case "saveAs":
265 let name = args["name"] as! String
266 let bytes = (args["bytes"] as! FlutterStandardTypedData).data
267 let dir = FileManager.default.temporaryDirectory.appendingPathComponent("sm-transfer-export-\(UUID().uuidString)")
268 do {
269 try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
270 let temp = dir.appendingPathComponent((name as NSString).lastPathComponent)
271 try bytes.write(to: temp, options: .atomic)
272 let picker = UIDocumentPickerViewController(forExporting: [temp], asCopy: true)
273 let shown = present(picker) { urls in
274 try? FileManager.default.removeItem(at: dir)
275 guard let url = urls?.first else { return result(nil) }
276 self.background(result) {
277 if let id = try? self.id(bookmarkOf: url) {
278 return try self.access(id) { self.describe($0, id: id) }
279 }
280 // Saved, but iOS gave no lasting access to the new file.
281 var info = self.describe(url, id: "")
282 info["writable"] = false
283 return info
284 }
285 }
286 if !shown {
287 try? FileManager.default.removeItem(at: dir)
288 result(FlutterError(code: "busy", message: nil, details: nil))
289 }
290 } catch {
291 fail(result, error)
292 }
293 case "preview":
294 let url = URL(fileURLWithPath: args["path"] as! String)
295 guard url.standardizedFileURL.path.hasPrefix(FileManager.default.temporaryDirectory.standardizedFileURL.path),
296 QLPreviewController.canPreview(url as QLPreviewItem), let top
297 else { return result(false) }
298 previewURL = url
299 previewResult = result
300 let ql = QLPreviewController()
301 ql.dataSource = self
302 ql.delegate = self
303 top.present(ql, animated: true)
304 case "takeIncoming":
305 result(incoming)
306 incoming.removeAll()
307 case "forget":
308 result(nil)
309 default:
310 result(FlutterMethodNotImplemented)
311 }
312 }
314 /// Files opened in SM Mobile from other apps ("Open in", Files).
315 func receive(_ urls: [URL]) {
316 queue.async {
317 let docs: [[String: Any?]] = urls.compactMap { url in
318 guard let id = try? self.id(bookmarkOf: url) else { return nil }
319 return try? self.access(id) { self.describe($0, id: id) }
320 }
321 DispatchQueue.main.async {
322 guard !docs.isEmpty else { return }
323 if let channel = self.channel {
324 channel.invokeMethod("incoming", arguments: self.incoming + docs)
325 self.incoming.removeAll()
326 } else {
327 self.incoming.append(contentsOf: docs)
328 }
329 }
330 }
331 }
SHA-2566b9c72191ccf5175b2993f8c283bc8102e47b7a20a81c8d7fea2cf3f2b0978ce

CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

SHA-256 archivu46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Soubory