@capgo/capacitor-updater 4.0.0-alpha.18 → 4.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +32 -24
- package/ios/Plugin/CapacitorUpdater.swift +27 -22
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +24 -22
- package/package.json +1 -1
|
@@ -46,7 +46,7 @@ public class CapacitorUpdater {
|
|
|
46
46
|
private static final String bundleDirectory = "versions";
|
|
47
47
|
|
|
48
48
|
public static final String TAG = "Capacitor-updater";
|
|
49
|
-
public static final String pluginVersion = "4.0.0-alpha.
|
|
49
|
+
public static final String pluginVersion = "4.0.0-alpha.20";
|
|
50
50
|
|
|
51
51
|
public SharedPreferences.Editor editor;
|
|
52
52
|
public SharedPreferences prefs;
|
|
@@ -307,7 +307,7 @@ public class CapacitorUpdater {
|
|
|
307
307
|
public void reset(final boolean internal) {
|
|
308
308
|
this.setCurrentBundle(new File("public"));
|
|
309
309
|
this.setFallbackVersion(null);
|
|
310
|
-
this.
|
|
310
|
+
this.setNext(null);
|
|
311
311
|
if(!internal) {
|
|
312
312
|
this.sendStats("reset", this.getCurrentBundle().getVersionName());
|
|
313
313
|
}
|
|
@@ -511,7 +511,7 @@ public class CapacitorUpdater {
|
|
|
511
511
|
}
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
-
public boolean
|
|
514
|
+
public boolean setNext(final String next) {
|
|
515
515
|
if (next == null) {
|
|
516
516
|
this.editor.remove(NEXT_VERSION);
|
|
517
517
|
} else {
|
|
@@ -236,7 +236,7 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi
|
|
|
236
236
|
|
|
237
237
|
try {
|
|
238
238
|
Log.i(CapacitorUpdater.TAG, "Setting next active id " + id);
|
|
239
|
-
if (!this.implementation.
|
|
239
|
+
if (!this.implementation.setNext(id)) {
|
|
240
240
|
Log.e(CapacitorUpdater.TAG, "Set next id failed. Bundle " + id + " does not exist.");
|
|
241
241
|
call.reject("Set next id failed. Bundle " + id + " does not exist.");
|
|
242
242
|
} else {
|
|
@@ -409,54 +409,61 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi
|
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
-
private boolean _cancelDelay() {
|
|
412
|
+
private boolean _cancelDelay(String source) {
|
|
413
413
|
try {
|
|
414
414
|
this.editor.remove(DELAY_UPDATE);
|
|
415
415
|
this.editor.remove(DELAY_UPDATE_VAL);
|
|
416
416
|
this.editor.commit();
|
|
417
|
-
Log.i(CapacitorUpdater.TAG, "delay
|
|
417
|
+
Log.i(CapacitorUpdater.TAG, "delay canceled from " + source);
|
|
418
|
+
return true;
|
|
418
419
|
}
|
|
419
420
|
catch(final Exception e) {
|
|
420
421
|
Log.e(CapacitorUpdater.TAG, "Failed to cancel update delay", e);
|
|
421
|
-
|
|
422
|
+
return false;
|
|
422
423
|
}
|
|
423
424
|
}
|
|
424
425
|
|
|
425
426
|
@PluginMethod
|
|
426
427
|
public void cancelDelay(final PluginCall call) {
|
|
427
|
-
this._cancelDelay()
|
|
428
|
-
|
|
428
|
+
if(this._cancelDelay("JS")) {
|
|
429
|
+
call.resolve();
|
|
430
|
+
} else {
|
|
431
|
+
call.reject("Failed to cancel delay");
|
|
432
|
+
}
|
|
429
433
|
}
|
|
430
434
|
|
|
431
435
|
private void _checkCancelDelay(Boolean killed) {
|
|
432
436
|
final String delayUpdate = this.prefs.getString(DELAY_UPDATE, "");
|
|
433
437
|
if ("".equals(delayUpdate)) {
|
|
434
438
|
if ("background".equals(delayUpdate) && !killed) {
|
|
435
|
-
this._cancelDelay();
|
|
439
|
+
this._cancelDelay("background check");
|
|
436
440
|
} else if ("kill".equals(delayUpdate) && killed) {
|
|
437
|
-
this._cancelDelay();
|
|
441
|
+
this._cancelDelay("kill check");
|
|
438
442
|
}
|
|
439
443
|
final String delayVal = this.prefs.getString(DELAY_UPDATE_VAL, "");
|
|
440
|
-
if (delayVal
|
|
441
|
-
this._cancelDelay();
|
|
444
|
+
if ("".equals(delayVal)) {
|
|
445
|
+
this._cancelDelay("delayVal absent");
|
|
442
446
|
} else if ("date".equals(delayUpdate)) {
|
|
443
447
|
try {
|
|
444
448
|
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
|
445
449
|
Date date = sdf.parse(delayVal);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
this._cancelDelay();
|
|
450
|
+
if (date.compareTo(new Date()) > 0) {
|
|
451
|
+
this._cancelDelay("date expired");
|
|
449
452
|
}
|
|
450
453
|
}
|
|
451
454
|
catch(final Exception e) {
|
|
452
|
-
|
|
453
|
-
this._cancelDelay();
|
|
455
|
+
this._cancelDelay("date parsing issue");
|
|
454
456
|
}
|
|
455
457
|
|
|
456
458
|
} else if ("nativeVersion".equals(delayUpdate)) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
this.
|
|
459
|
+
try {
|
|
460
|
+
final Version versionLimit = new Version(delayVal);
|
|
461
|
+
if (this.currentVersionNative.isAtLeast(versionLimit)) {
|
|
462
|
+
this._cancelDelay("nativeVersion above limit");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch(final Exception e) {
|
|
466
|
+
this._cancelDelay("nativeVersion parsing issue");
|
|
460
467
|
}
|
|
461
468
|
}
|
|
462
469
|
}
|
|
@@ -490,6 +497,8 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi
|
|
|
490
497
|
}
|
|
491
498
|
}
|
|
492
499
|
|
|
500
|
+
|
|
501
|
+
|
|
493
502
|
@Override // appMovedToForeground
|
|
494
503
|
public void onActivityStarted(@NonNull final Activity activity) {
|
|
495
504
|
if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled()) {
|
|
@@ -522,7 +531,7 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi
|
|
|
522
531
|
}
|
|
523
532
|
if(latest.isDownloaded()){
|
|
524
533
|
Log.e(CapacitorUpdater.TAG, "Latest bundle already exists and download is NOT required. Update will occur next time app moves to background.");
|
|
525
|
-
CapacitorUpdaterPlugin.this.implementation.
|
|
534
|
+
CapacitorUpdaterPlugin.this.implementation.setNext(latest.getId());
|
|
526
535
|
return;
|
|
527
536
|
}
|
|
528
537
|
}
|
|
@@ -537,7 +546,7 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi
|
|
|
537
546
|
final String url = (String) res.get("url");
|
|
538
547
|
final BundleInfo next = CapacitorUpdaterPlugin.this.implementation.download(url, latestVersionName);
|
|
539
548
|
|
|
540
|
-
CapacitorUpdaterPlugin.this.implementation.
|
|
549
|
+
CapacitorUpdaterPlugin.this.implementation.setNext(next.getId());
|
|
541
550
|
} catch (final Exception e) {
|
|
542
551
|
Log.e(CapacitorUpdater.TAG, "error downloading file", e);
|
|
543
552
|
}
|
|
@@ -561,13 +570,12 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi
|
|
|
561
570
|
public void onActivityStopped(@NonNull final Activity activity) {
|
|
562
571
|
Log.i(CapacitorUpdater.TAG, "Checking for pending update");
|
|
563
572
|
try {
|
|
564
|
-
final
|
|
573
|
+
final String delayUpdate = this.prefs.getString(DELAY_UPDATE, "");
|
|
565
574
|
this._checkCancelDelay(false);
|
|
566
|
-
if (delayUpdate) {
|
|
575
|
+
if ("".equals(delayUpdate)) {
|
|
567
576
|
Log.i(CapacitorUpdater.TAG, "Update delayed to next backgrounding");
|
|
568
577
|
return;
|
|
569
578
|
}
|
|
570
|
-
|
|
571
579
|
final BundleInfo fallback = this.implementation.getFallbackVersion();
|
|
572
580
|
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
573
581
|
final BundleInfo next = this.implementation.getNextVersion();
|
|
@@ -582,7 +590,7 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi
|
|
|
582
590
|
Log.d(CapacitorUpdater.TAG, "Next bundle is: " + next.getVersionName());
|
|
583
591
|
if (this.implementation.set(next) && this._reload()) {
|
|
584
592
|
Log.i(CapacitorUpdater.TAG, "Updated to bundle: " + next.getVersionName());
|
|
585
|
-
this.implementation.
|
|
593
|
+
this.implementation.setNext(null);
|
|
586
594
|
} else {
|
|
587
595
|
Log.e(CapacitorUpdater.TAG, "Update to bundle: " + next.getVersionName() + " Failed!");
|
|
588
596
|
}
|
|
@@ -148,7 +148,7 @@ extension CustomError: LocalizedError {
|
|
|
148
148
|
|
|
149
149
|
public let TAG = "✨ Capacitor-updater:";
|
|
150
150
|
public let CAP_SERVER_PATH = "serverBasePath"
|
|
151
|
-
public let pluginVersion = "4.0.0-alpha.
|
|
151
|
+
public let pluginVersion = "4.0.0-alpha.20"
|
|
152
152
|
public var statsUrl = ""
|
|
153
153
|
public var appId = ""
|
|
154
154
|
public var deviceID = UIDevice.current.identifierForVendor?.uuidString ?? ""
|
|
@@ -259,8 +259,8 @@ extension CustomError: LocalizedError {
|
|
|
259
259
|
|
|
260
260
|
private func setCurrentBundle(bundle: String) {
|
|
261
261
|
UserDefaults.standard.set(bundle, forKey: self.CAP_SERVER_PATH)
|
|
262
|
-
print("\(self.TAG) Current bundle set to: \(bundle)")
|
|
263
262
|
UserDefaults.standard.synchronize()
|
|
263
|
+
print("\(self.TAG) Current bundle set to: \(bundle == "" ? BundleInfo.ID_BUILTIN : bundle)")
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
public func download(url: URL, version: String) throws -> BundleInfo {
|
|
@@ -372,9 +372,12 @@ extension CustomError: LocalizedError {
|
|
|
372
372
|
|
|
373
373
|
public func set(id: String) -> Bool {
|
|
374
374
|
let newBundle: BundleInfo = self.getBundleInfo(id: id)
|
|
375
|
+
if(newBundle.isBuiltin()) {
|
|
376
|
+
self.reset()
|
|
377
|
+
return true
|
|
378
|
+
}
|
|
375
379
|
if (bundleExists(id: id)) {
|
|
376
|
-
|
|
377
|
-
self.setCurrentBundle(bundle: String(url.path.suffix(10)))
|
|
380
|
+
self.setCurrentBundle(bundle: id)
|
|
378
381
|
self.setBundleStatus(id: id, status: BundleStatus.PENDING)
|
|
379
382
|
sendStats(action: "set", versionName: newBundle.getVersionName())
|
|
380
383
|
return true
|
|
@@ -398,8 +401,7 @@ extension CustomError: LocalizedError {
|
|
|
398
401
|
public func reset(isInternal: Bool) {
|
|
399
402
|
self.setCurrentBundle(bundle: "")
|
|
400
403
|
self.setFallbackVersion(fallback: Optional<BundleInfo>.none)
|
|
401
|
-
let _ = self.
|
|
402
|
-
UserDefaults.standard.synchronize()
|
|
404
|
+
let _ = self.setNext(next: Optional<String>.none)
|
|
403
405
|
if(!isInternal) {
|
|
404
406
|
sendStats(action: "reset", versionName: self.getCurrentBundle().getVersionName())
|
|
405
407
|
}
|
|
@@ -441,7 +443,7 @@ extension CustomError: LocalizedError {
|
|
|
441
443
|
}
|
|
442
444
|
do {
|
|
443
445
|
let result: BundleInfo = try UserDefaults.standard.getObj(forKey: "\(id)\(self.INFO_SUFFIX)", castTo: BundleInfo.self)
|
|
444
|
-
print("\(self.TAG) Returning info bundle [\(id)]", result.toString())
|
|
446
|
+
// print("\(self.TAG) Returning info bundle [\(id)]", result.toString())
|
|
445
447
|
return result
|
|
446
448
|
} catch {
|
|
447
449
|
print("\(self.TAG) Failed to parse info for bundle [\(id)]", error.localizedDescription)
|
|
@@ -495,25 +497,22 @@ extension CustomError: LocalizedError {
|
|
|
495
497
|
self.saveBundleInfo(id: id, bundle: info.setStatus(status: status.localizedString))
|
|
496
498
|
}
|
|
497
499
|
|
|
498
|
-
private func getCurrentBundleVersion() -> String {
|
|
499
|
-
if(self.isUsingBuiltin()) {
|
|
500
|
-
return BundleInfo.ID_BUILTIN
|
|
501
|
-
} else {
|
|
502
|
-
let path: String = self.getCurrentBundleId()
|
|
503
|
-
return path.lastPathComponent
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
500
|
public func getCurrentBundle() -> BundleInfo {
|
|
508
501
|
return self.getBundleInfo(id: self.getCurrentBundleId());
|
|
509
502
|
}
|
|
510
503
|
|
|
511
504
|
public func getCurrentBundleId() -> String {
|
|
512
|
-
|
|
505
|
+
guard let bundleID = UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH) else {
|
|
506
|
+
return BundleInfo.ID_BUILTIN
|
|
507
|
+
}
|
|
508
|
+
if (bundleID == "") {
|
|
509
|
+
return BundleInfo.ID_BUILTIN
|
|
510
|
+
}
|
|
511
|
+
return bundleID
|
|
513
512
|
}
|
|
514
513
|
|
|
515
514
|
public func isUsingBuiltin() -> Bool {
|
|
516
|
-
return self.
|
|
515
|
+
return (UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH) ?? "") == self.DEFAULT_FOLDER
|
|
517
516
|
}
|
|
518
517
|
|
|
519
518
|
public func getFallbackVersion() -> BundleInfo {
|
|
@@ -523,6 +522,7 @@ extension CustomError: LocalizedError {
|
|
|
523
522
|
|
|
524
523
|
private func setFallbackVersion(fallback: BundleInfo?) {
|
|
525
524
|
UserDefaults.standard.set(fallback == nil ? BundleInfo.ID_BUILTIN : fallback!.getId(), forKey: self.FALLBACK_VERSION)
|
|
525
|
+
UserDefaults.standard.synchronize()
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
public func getNextVersion() -> BundleInfo? {
|
|
@@ -534,19 +534,24 @@ extension CustomError: LocalizedError {
|
|
|
534
534
|
}
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
-
public func
|
|
537
|
+
public func setNext(next: String?) -> Bool {
|
|
538
538
|
guard let nextId = next else {
|
|
539
539
|
UserDefaults.standard.removeObject(forKey: self.NEXT_VERSION)
|
|
540
540
|
UserDefaults.standard.synchronize()
|
|
541
|
-
return
|
|
541
|
+
return false
|
|
542
542
|
}
|
|
543
|
-
let
|
|
543
|
+
let newBundle: BundleInfo = self.getBundleInfo(id: nextId)
|
|
544
|
+
if(newBundle.isBuiltin()) {
|
|
545
|
+
self.reset()
|
|
546
|
+
return true
|
|
547
|
+
}
|
|
548
|
+
let bundle: URL = self.getBundleDirectory(id: nextId)
|
|
544
549
|
if (!bundle.exist) {
|
|
545
550
|
return false
|
|
546
551
|
}
|
|
547
552
|
UserDefaults.standard.set(next, forKey: self.NEXT_VERSION)
|
|
548
|
-
self.setBundleStatus(id: next!, status: BundleStatus.PENDING)
|
|
549
553
|
UserDefaults.standard.synchronize()
|
|
554
|
+
self.setBundleStatus(id: next!, status: BundleStatus.PENDING)
|
|
550
555
|
return true
|
|
551
556
|
}
|
|
552
557
|
}
|
|
@@ -140,7 +140,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
140
140
|
return
|
|
141
141
|
}
|
|
142
142
|
print("\(self.implementation.TAG) Setting next active id \(id)")
|
|
143
|
-
if (!self.implementation.
|
|
143
|
+
if (!self.implementation.setNext(next: id)) {
|
|
144
144
|
print("\(self.implementation.TAG) Set next version failed. id \(id) does not exist.")
|
|
145
145
|
call.reject("Set next version failed. id \(id) does not exist.")
|
|
146
146
|
} else {
|
|
@@ -250,19 +250,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
250
250
|
UserDefaults.standard.set(kind, forKey: DELAY_UPDATE)
|
|
251
251
|
UserDefaults.standard.set(val, forKey: DELAY_UPDATE_VAL)
|
|
252
252
|
UserDefaults.standard.synchronize()
|
|
253
|
-
print("\(self.implementation.TAG) Delay update saved.")
|
|
253
|
+
print("\(self.implementation.TAG) Delay update saved.", kind, val)
|
|
254
254
|
call.resolve()
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
private func _cancelDelay() -> Void {
|
|
258
|
-
print("\(self.implementation.TAG) delay Canceled")
|
|
257
|
+
private func _cancelDelay(source: String) -> Void {
|
|
258
|
+
print("\(self.implementation.TAG) delay Canceled from \(source)")
|
|
259
259
|
UserDefaults.standard.removeObject(forKey: DELAY_UPDATE)
|
|
260
260
|
UserDefaults.standard.removeObject(forKey: DELAY_UPDATE_VAL)
|
|
261
261
|
UserDefaults.standard.synchronize()
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
@objc func cancelDelay(_ call: CAPPluginCall) {
|
|
265
|
-
self._cancelDelay()
|
|
265
|
+
self._cancelDelay(source: "JS")
|
|
266
266
|
call.resolve()
|
|
267
267
|
}
|
|
268
268
|
|
|
@@ -270,31 +270,33 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
270
270
|
let delayUpdate = UserDefaults.standard.string(forKey: DELAY_UPDATE)
|
|
271
271
|
if (delayUpdate != nil) {
|
|
272
272
|
if (delayUpdate == "background" && !killed) {
|
|
273
|
-
self._cancelDelay()
|
|
273
|
+
self._cancelDelay(source: "background check")
|
|
274
274
|
} else if (delayUpdate == "kill" && killed) {
|
|
275
|
-
self._cancelDelay()
|
|
275
|
+
self._cancelDelay(source: "kill check")
|
|
276
276
|
}
|
|
277
|
-
let delayVal = UserDefaults.standard.string(forKey: DELAY_UPDATE_VAL)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
277
|
+
guard let delayVal = UserDefaults.standard.string(forKey: DELAY_UPDATE_VAL) else {
|
|
278
|
+
self._cancelDelay(source: "delayVal absent")
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
if (delayUpdate == "date") {
|
|
281
282
|
let dateFormatter = ISO8601DateFormatter()
|
|
282
|
-
let
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
283
|
+
guard let ExpireDate = dateFormatter.date(from: delayVal) else {
|
|
284
|
+
self._cancelDelay(source: "date parsing issue")
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
if (ExpireDate < Date()) {
|
|
288
|
+
self._cancelDelay(source: "date expired")
|
|
286
289
|
}
|
|
287
290
|
} else if (delayUpdate == "nativeVersion") {
|
|
288
291
|
do {
|
|
289
|
-
let versionLimit = try Version(delayVal
|
|
292
|
+
let versionLimit = try Version(delayVal)
|
|
290
293
|
if (self.currentVersionNative >= versionLimit) {
|
|
291
|
-
self._cancelDelay()
|
|
294
|
+
self._cancelDelay(source: "nativeVersion above limit")
|
|
292
295
|
}
|
|
293
296
|
} catch {
|
|
294
|
-
self._cancelDelay()
|
|
297
|
+
self._cancelDelay(source: "nativeVersion parsing issue")
|
|
295
298
|
}
|
|
296
299
|
}
|
|
297
|
-
UserDefaults.standard.synchronize()
|
|
298
300
|
}
|
|
299
301
|
}
|
|
300
302
|
|
|
@@ -370,7 +372,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
370
372
|
}
|
|
371
373
|
if(latest!.isDownloaded()){
|
|
372
374
|
print("\(self.implementation.TAG) Latest version already exists and download is NOT required. Update will occur next time app moves to background.")
|
|
373
|
-
let _ = self.implementation.
|
|
375
|
+
let _ = self.implementation.setNext(next: latest!.getId())
|
|
374
376
|
return
|
|
375
377
|
}
|
|
376
378
|
}
|
|
@@ -379,7 +381,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
379
381
|
print("\(self.implementation.TAG) New bundle: \(latestVersionName!) found. Current is: \(current.getVersionName()). Update will occur next time app moves to background.")
|
|
380
382
|
let next = try self.implementation.download(url: downloadUrl, version: latestVersionName!)
|
|
381
383
|
|
|
382
|
-
let _ = self.implementation.
|
|
384
|
+
let _ = self.implementation.setNext(next: next.getId())
|
|
383
385
|
} catch {
|
|
384
386
|
print("\(self.implementation.TAG) Error downloading file", error.localizedDescription)
|
|
385
387
|
}
|
|
@@ -412,7 +414,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
412
414
|
print("\(self.implementation.TAG) Next bundle is: \(next!.toString())")
|
|
413
415
|
if (self.implementation.set(bundle: next!) && self._reload()) {
|
|
414
416
|
print("\(self.implementation.TAG) Updated to bundle: \(next!)")
|
|
415
|
-
let _ = self.implementation.
|
|
417
|
+
let _ = self.implementation.setNext(next: Optional<String>.none)
|
|
416
418
|
} else {
|
|
417
419
|
print("\(self.implementation.TAG) Updated to bundle: \(next!) Failed!")
|
|
418
420
|
}
|