cordova-plugin-crosswire-sword 0.2.9 → 0.2.12

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.
Files changed (42) hide show
  1. package/RELEASENOTES.md +19 -0
  2. package/libs/android/arm64-v8a/libsword.so +0 -0
  3. package/libs/android/armeabi-v7a/libsword.so +0 -0
  4. package/libs/android/x86/libsword.so +0 -0
  5. package/libs/android/x86_64/libsword.so +0 -0
  6. package/libs/ios/SWORD.xcframework/Info.plist +4 -4
  7. package/libs/ios/SWORD.xcframework/ios-arm64/libSWORD.a +2 -2
  8. package/libs/ios/SWORD.xcframework/ios-arm64-simulator/libSWORD.a +2 -2
  9. package/package.json +1 -1
  10. package/plugin.xml +1 -3
  11. package/src/android/SWMgr.java +23 -0
  12. package/src/android/SWORD.java +93 -4
  13. package/src/android/SafStorage.java +66 -2
  14. package/src/ios/SWORD.swift +623 -69
  15. package/www/SWORD.js +49 -5
  16. package/libs/ios/curl.xcframework/Info.plist +0 -47
  17. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/curl.h +0 -3373
  18. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/curlver.h +0 -77
  19. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/easy.h +0 -123
  20. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/header.h +0 -74
  21. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/mprintf.h +0 -85
  22. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/multi.h +0 -560
  23. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/options.h +0 -70
  24. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/stdcheaders.h +0 -35
  25. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/system.h +0 -399
  26. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/typecheck-gcc.h +0 -964
  27. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/urlapi.h +0 -156
  28. package/libs/ios/curl.xcframework/ios-arm64/Headers/curl/websockets.h +0 -98
  29. package/libs/ios/curl.xcframework/ios-arm64/libcurl-all.a +0 -0
  30. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/curl.h +0 -3373
  31. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/curlver.h +0 -77
  32. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/easy.h +0 -123
  33. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/header.h +0 -74
  34. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/mprintf.h +0 -85
  35. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/multi.h +0 -560
  36. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/options.h +0 -70
  37. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/stdcheaders.h +0 -35
  38. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/system.h +0 -399
  39. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/typecheck-gcc.h +0 -964
  40. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/urlapi.h +0 -156
  41. package/libs/ios/curl.xcframework/ios-arm64-simulator/Headers/curl/websockets.h +0 -98
  42. package/libs/ios/curl.xcframework/ios-arm64-simulator/libcurl-all.a +0 -0
package/RELEASENOTES.md CHANGED
@@ -20,6 +20,25 @@
20
20
  -->
21
21
  # Release Notes
22
22
 
23
+ ### 0.2.12 (Oct 5, 2026)
24
+ * BibleSync on a network that loses packets, as Wi-Fi does: each verse carries a sequence number and is sent three times, which a receiver shows once; a peer is kept through half an hour of silence, not one minute; a newcomer is answered at once, twice; a beacon every 10 seconds (biblesync 2.2.0 with pull requests 19 and 20)
25
+ * startBibleSync takes options { repeatCount, repeatDelayMS, peerLifetimeS }; the defaults are 2, 500 and 1800
26
+ * getBibleSyncStatus says whether a running session is still alive on the same network interface (off, alive, disabled, moved), so an app resumed from sleep or the background restarts BibleSync only when it must
27
+ * The page hears BibleSync's interface, its peers and its errors on both platforms ({ cmd: info | peer | error }); a beacon that cannot be sent while the phone sleeps no longer ends the session
28
+ * iOS: BibleSync is offered (features.bibleSync), the app carrying Apple's multicast entitlement; every received message had been dropped
29
+
30
+ ### 0.2.11 (Oct 1, 2026)
31
+ * Study data (bookmarks and, in time, notes) in a folder of the user's choosing, named sword-studydata so several devices and SWORD apps can share it (doc/bookmarks-format.md in Bishop): a picked folder holding bookmarks/, or itself named sword-studydata, is used as it is, otherwise sword-studydata/ is made inside it
32
+ * iOS: the study-data folder may be in iCloud Drive or any app's folder; where a provider grants files but no folder (Google Drive), the bookmark files are picked in place (SWMgr_chooseDataFiles) or Bishop's own are saved there and opened once (SWMgr_exportDataFiles); a provider's file is rewritten in place so it keeps its identity
33
+ * Android: a cloud provider's stale record of a replaced file is refreshed before the retry by name
34
+ * initSWORD answers `features` ({ dataStorage, dataFiles, sharedStorage, bibleSync }) so the page can offer what the platform has; SWORD.js keeps it as SWORD.features
35
+ * iOS: HTTPUtils_makeRequest takes the method codes SWORD.js defines (METHOD_GET is 0) and no longer crashes on a bad URL; a user agent naming the app, device and engine; sharing a verse through the system share sheet; passive FTP and verified peers set in InstallMgr.conf
36
+
37
+ ### 0.2.10 (Sep 26, 2026)
38
+ * iOS: HTTP(S) through Apple's NSURLSession; no curl or OpenSSL in the app, so App Store Connect's export-compliance questions do not arise (only Apple's encryption)
39
+ * Android: a cloud folder (Google Drive) whose provider is still loading is waited for instead of being taken as empty, a listing that fails is reported to the page, and a file another device replaced is found again by name; the app no longer creates a second personal.conf beside the real one
40
+ * FTP without curl (iOS): EPSV first and PASV's advertised address ignored, as curl does, so sources such as STEP answer; failures are reported as with curl, so file-by-file installs proceed on sources without packages
41
+
23
42
  ### 0.2.9 (Sep 25, 2026)
24
43
  * BibleSync works: biblesync 2.2.0 with the Android and Apple interface discovery sent upstream; Android holds a Wi-Fi multicast lock while it runs (CHANGE_WIFI_MULTICAST_STATE); every speaker in the room is heard, not only the first
25
44
  * Android: a sword:// or bible:// link the app is opened with reaches the page as handleOpenURL, exactly as cordova-ios does, on a cold start and a warm one
Binary file
Binary file
Binary file
@@ -8,7 +8,7 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>libSWORD.a</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64-simulator</string>
11
+ <string>ios-arm64</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>libSWORD.a</string>
14
14
  <key>SupportedArchitectures</key>
@@ -17,14 +17,12 @@
17
17
  </array>
18
18
  <key>SupportedPlatform</key>
19
19
  <string>ios</string>
20
- <key>SupportedPlatformVariant</key>
21
- <string>simulator</string>
22
20
  </dict>
23
21
  <dict>
24
22
  <key>BinaryPath</key>
25
23
  <string>libSWORD.a</string>
26
24
  <key>LibraryIdentifier</key>
27
- <string>ios-arm64</string>
25
+ <string>ios-arm64-simulator</string>
28
26
  <key>LibraryPath</key>
29
27
  <string>libSWORD.a</string>
30
28
  <key>SupportedArchitectures</key>
@@ -33,6 +31,8 @@
33
31
  </array>
34
32
  <key>SupportedPlatform</key>
35
33
  <string>ios</string>
34
+ <key>SupportedPlatformVariant</key>
35
+ <string>simulator</string>
36
36
  </dict>
37
37
  </array>
38
38
  <key>CFBundlePackageType</key>
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/libs/ios/SWORD.xcframework/ios-arm64/libSWORD.a
3
- size: 37291856 bytes
4
- sha256: 7409692a95d76c57900b0b8744ec40929a86d3b8ab9e1484ed44a9d2f6ef5a88
3
+ size: 37330824 bytes
4
+ sha256: 6d136d967767f7d43435f027eb9b8c07480da1c56e9002a541d75eb0fb23662f
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/libs/ios/SWORD.xcframework/ios-arm64-simulator/libSWORD.a
3
- size: 37334896 bytes
4
- sha256: 8feff5c7f71809535934d6d4f72c040eb1c8064fdfa042450dac079420e203d9
3
+ size: 37373760 bytes
4
+ sha256: 72a444c94c56c42f8b07cc847e09a99364a1fabe63d8343088393c6ec3e89ba5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cordova-plugin-crosswire-sword",
3
- "version": "0.2.9",
3
+ "version": "0.2.12",
4
4
  "description": "The SWORD Project Plugin",
5
5
  "files": [
6
6
  "plugin.xml",
package/plugin.xml CHANGED
@@ -4,7 +4,7 @@
4
4
  xmlns:rim="http://www.blackberry.com/ns/widgets"
5
5
  xmlns:android="http://schemas.android.com/apk/res/android"
6
6
  id="cordova-plugin-crosswire-sword"
7
- version="0.2.9">
7
+ version="0.2.12">
8
8
  <name>cordova-plugin-crosswire-sword</name>
9
9
  <description>The SWORD Project Plugin</description>
10
10
  <license>GNU 2.0</license>
@@ -137,13 +137,11 @@
137
137
  <header-file src="src/ios/SWORD-Bridging-Header.h" type="BridgingHeader" />
138
138
  <source-file src="src/ios/SWORD.swift" />
139
139
  <framework src="libs/ios/SWORD.xcframework" custom="true" embed="false" />
140
- <framework src="libs/ios/curl.xcframework" custom="true" embed="false" />
141
140
  <framework src="libc++.tbd" />
142
141
  <framework src="libz.tbd" />
143
142
  <framework src="libicucore.tbd" />
144
143
  <framework src="Security.framework" />
145
144
  <framework src="CoreFoundation.framework" />
146
- <framework src="SystemConfiguration.framework" />
147
145
 
148
146
  <!-- <source-file src="lib/ios/libSWORD.a" framework="true" /> -->
149
147
  </platform>
@@ -164,9 +164,32 @@ public class SWMgr {
164
164
  public static interface BibleSyncListener {
165
165
  public void navReceived(String osisRef);
166
166
  public void chatReceived(String user, String message);
167
+ /** the interface address BibleSync is on */
168
+ public void infoReceived(String iface);
169
+ /**
170
+ * @param state 'A' a peer announced itself, 'S' it is a speaker we now listen to, 'D' a speaker went quiet
171
+ * @param who user @ address ('D': the uuid)
172
+ * @param what app version; device; uuid ('D': empty)
173
+ */
174
+ public void peerReceived(char state, String who, String what);
175
+ /** the library could not send, or stopped itself: what it said, and its detail */
176
+ public void errorReceived(String message, String detail);
167
177
  }
168
178
  public native void startBibleSync(String appName, String userName, String passphrase, BibleSyncListener listener);
179
+ /**
180
+ * For the next startBibleSync; a negative value leaves that one as it is.
181
+ * @param repeatCount how many more times each verse is sent (nobody resends multicast); default 2
182
+ * @param repeatDelayMS at least this long between those; default 500
183
+ * @param peerLifetimeS a peer not heard from for this many seconds is given up; default 1800
184
+ */
185
+ public native void setBibleSyncOptions(int repeatCount, int repeatDelayMS, int peerLifetimeS);
169
186
  public native void stopBibleSync();
187
+ /**
188
+ * @return "off" (not running), "alive <address>",
189
+ * "disabled <address>" (the library shut itself down) or
190
+ * "moved <address> <address now>" (the network changed under it)
191
+ */
192
+ public native String getBibleSyncStatus();
170
193
  public native void sendBibleSyncMessage(String osisRef);
171
194
  }
172
195
 
@@ -40,6 +40,7 @@ import org.json.JSONException;
40
40
  import org.json.JSONObject;
41
41
 
42
42
  import org.crosswire.android.sword.SWMgr;
43
+ import org.crosswire.android.sword.SafStorage;
43
44
  import org.crosswire.android.sword.AndroidMgr;
44
45
  import org.crosswire.android.sword.SWModule;
45
46
  import org.crosswire.android.sword.InstallMgr;
@@ -52,6 +53,8 @@ import android.net.wifi.WifiManager;
52
53
  import android.app.Activity;
53
54
 
54
55
  public class SWORD extends CordovaPlugin {
56
+ // the app-neutral study-data folder (see SWMgr_getDataPath)
57
+ public static final String STUDY_DATA_FOLDER = "sword-studydata";
55
58
  // held while BibleSync runs: without it most Wi-Fi drivers filter out the multicast group
56
59
  private WifiManager.MulticastLock multicastLock = null;
57
60
  public static final String TAG = "SWORD";
@@ -228,6 +231,15 @@ public class SWORD extends CordovaPlugin {
228
231
  if (action.equals("initSWORD")) {
229
232
  JSONObject r = new JSONObject();
230
233
  r.put("version", mgr.version());
234
+ // what the page may offer on Android (SWORD.features): a shared
235
+ // module library and a study-data folder, both picked as SAF trees;
236
+ // no picking of single files (a tree grant covers any provider)
237
+ JSONObject features = new JSONObject();
238
+ features.put("sharedStorage", true);
239
+ features.put("dataStorage", true);
240
+ features.put("dataFiles", false);
241
+ features.put("bibleSync", true);
242
+ r.put("features", features);
231
243
  callbackContext.success(r);
232
244
  }
233
245
  else if (action.equals("echo")) {
@@ -540,6 +552,8 @@ Log.d(TAG, "SWModule_getRenderChapter");
540
552
  final String appName = args.getString(0);
541
553
  final String userName = args.getString(1);
542
554
  final String passphrase = args.getString(2);
555
+ // repeatCount, repeatDelayMS, peerLifetimeS: -1 (or absent) keeps the default
556
+ mgr.setBibleSyncOptions(args.optInt(3, -1), args.optInt(4, -1), args.optInt(5, -1));
543
557
 
544
558
  if (multicastLock == null) {
545
559
  WifiManager wifi = (WifiManager) cordova.getActivity().getApplicationContext().getSystemService(Context.WIFI_SERVICE);
@@ -574,6 +588,42 @@ Log.d(TAG, "SWModule_getRenderChapter");
574
588
  }
575
589
  catch (Exception e) { e.printStackTrace(); }
576
590
  }
591
+ public void infoReceived(String iface) {
592
+ try {
593
+ JSONObject m = new JSONObject();
594
+ m.put("cmd", "info");
595
+ m.put("interface", iface);
596
+ PluginResult result = new PluginResult(PluginResult.Status.OK, m);
597
+ result.setKeepCallback(true);
598
+ bibleSyncListener.sendPluginResult(result);
599
+ }
600
+ catch (Exception e) { e.printStackTrace(); }
601
+ }
602
+ public void peerReceived(char state, String who, String what) {
603
+ try {
604
+ JSONObject m = new JSONObject();
605
+ m.put("cmd", "peer");
606
+ m.put("state", state == 'A' ? "announce" : (state == 'S' ? "speaker" : "gone"));
607
+ m.put("who", who);
608
+ m.put("what", what);
609
+ PluginResult result = new PluginResult(PluginResult.Status.OK, m);
610
+ result.setKeepCallback(true);
611
+ bibleSyncListener.sendPluginResult(result);
612
+ }
613
+ catch (Exception e) { e.printStackTrace(); }
614
+ }
615
+ public void errorReceived(String message, String detail) {
616
+ try {
617
+ JSONObject m = new JSONObject();
618
+ m.put("cmd", "error");
619
+ m.put("message", message);
620
+ m.put("detail", detail);
621
+ PluginResult result = new PluginResult(PluginResult.Status.OK, m);
622
+ result.setKeepCallback(true);
623
+ bibleSyncListener.sendPluginResult(result);
624
+ }
625
+ catch (Exception e) { e.printStackTrace(); }
626
+ }
577
627
  });
578
628
  }
579
629
  });
@@ -587,6 +637,16 @@ Log.d(TAG, "SWModule_getRenderChapter");
587
637
  if (multicastLock != null && multicastLock.isHeld()) multicastLock.release();
588
638
  callbackContext.success();
589
639
  }
640
+ else if (action.equals("SWMgr_getBibleSyncStatus")) {
641
+ // "alive 10.0.0.5" -> { state, alive, interface, current }
642
+ String[] s = mgr.getBibleSyncStatus().split(" ");
643
+ JSONObject r = new JSONObject();
644
+ r.put("state", s[0]);
645
+ r.put("alive", s[0].equals("alive"));
646
+ r.put("interface", s.length > 1 ? s[1] : "");
647
+ r.put("current", s.length > 2 ? s[2] : (s.length > 1 ? s[1] : ""));
648
+ callbackContext.success(r);
649
+ }
590
650
  else if (action.equals("SWMgr_setJavascript")) {
591
651
  boolean val = args.getBoolean(0);
592
652
  mgr.setJavascript(val);
@@ -635,9 +695,19 @@ Log.d(TAG, "SWModule_getRenderChapter");
635
695
  else callbackContext.error("couldn't write: " + args.getString(0));
636
696
  }
637
697
  else if (action.equals("SWMgr_listDir")) {
638
- JSONArray r = new JSONArray();
639
- for (String s : mgr.listDir(args.getString(0))) r.put(s);
640
- callbackContext.success(r);
698
+ // off the WebView thread: a cloud folder's provider may take
699
+ // seconds to finish loading, and the listing waits for it
700
+ final String path = args.getString(0);
701
+ cordova.getThreadPool().execute(new Runnable() { public void run() {
702
+ JSONArray r = new JSONArray();
703
+ for (String s : mgr.listDir(path)) r.put(s);
704
+ // a shared or data folder whose provider failed or never finished
705
+ // loading answers an empty listing natively; the page must know
706
+ SWMgr.SharedStorage ds = mgr.getDataStorage(), ss = mgr.getSharedStorage();
707
+ boolean failed = (ds instanceof SafStorage && ((SafStorage) ds).lastListFailed) || (ss instanceof SafStorage && ((SafStorage) ss).lastListFailed);
708
+ if (failed) callbackContext.error("couldn't list: " + path);
709
+ else callbackContext.success(r);
710
+ }});
641
711
  }
642
712
  else if (action.equals("SWMgr_removeFile")) {
643
713
  if (mgr.removeFile(args.getString(0))) callbackContext.success();
@@ -685,7 +755,26 @@ Log.d(TAG, "SWModule_getRenderChapter");
685
755
  callbackContext.success(label);
686
756
  }
687
757
  else if (action.equals("SWMgr_getDataPath")) {
688
- callbackContext.success(mgr.getDataPath());
758
+ // study data lives in an app-neutral folder named sword-studydata
759
+ // (bookmarks/ inside it, later notes and the like), so several
760
+ // devices and several SWORD apps can share one. Decided by what
761
+ // the picked folder holds, not by its name (a provider can report
762
+ // a name from before a rename for a while): a folder which already
763
+ // has bookmarks/ at its root IS the study-data folder, and so is one
764
+ // named sword-studydata (just created to be it, so still empty);
765
+ // otherwise sword-studydata/ inside it, existing or made on the
766
+ // first write. The provider is asked, so off the WebView thread.
767
+ cordova.getThreadPool().execute(new Runnable() { public void run() {
768
+ String path = mgr.getDataPath();
769
+ SWMgr.SharedStorage ds = mgr.getDataStorage();
770
+ if (ds instanceof SafStorage && path.startsWith("/sword-data")) {
771
+ SafStorage saf = (SafStorage) ds;
772
+ SWMgr.FileInfo root = saf.stat("bookmarks");
773
+ boolean isIt = (root != null && root.isDirectory) || STUDY_DATA_FOLDER.equals(saf.rootName());
774
+ if (!isIt) path += STUDY_DATA_FOLDER + "/";
775
+ }
776
+ callbackContext.success(path);
777
+ }});
689
778
  }
690
779
  else if (action.equals("SWMgr_getDataStorage")) {
691
780
  // the separately chosen study-data folder, or "" if study data lives beside the library
@@ -33,6 +33,7 @@ import android.content.ContentResolver;
33
33
  import android.content.Context;
34
34
  import android.database.Cursor;
35
35
  import android.net.Uri;
36
+ import android.os.Bundle;
36
37
  import android.os.ParcelFileDescriptor;
37
38
  import android.provider.DocumentsContract;
38
39
  import android.provider.DocumentsContract.Document;
@@ -74,6 +75,8 @@ public class SafStorage implements SWMgr.SharedStorage {
74
75
  private final Map<Integer, Staged> staged = new HashMap<Integer, Staged>();
75
76
  // relative path -> document id, learned from listings and creations
76
77
  private final Map<String, String> docIds = new HashMap<String, String>();
78
+ // how long a first listing of a cloud folder may keep loading before we take what there is
79
+ private static final long LIST_WAIT_MILLIS = 15000, LIST_POLL_MILLIS = 250;
77
80
 
78
81
  public SafStorage(Context context, Uri treeUri) {
79
82
  this.stagingDir = new java.io.File(context.getCacheDir(), "saf-staging");
@@ -86,6 +89,12 @@ public class SafStorage implements SWMgr.SharedStorage {
86
89
 
87
90
  public Uri getTreeUri() { return treeUri; }
88
91
 
92
+ /** the picked folder's own name as the provider reports it, or null */
93
+ public String rootName() {
94
+ SWMgr.FileInfo info = queryOne(docUri(treeDocId));
95
+ return (info == null || info.name == null || info.name.length() == 0) ? null : info.name;
96
+ }
97
+
89
98
  /** true if we still hold a grant for the tree and it is reachable */
90
99
  public boolean isAvailable() {
91
100
  return stat("") != null;
@@ -159,7 +168,30 @@ public class SafStorage implements SWMgr.SharedStorage {
159
168
  String prefix = (dirPath.length() == 0) ? "" : dirPath + "/";
160
169
  Cursor c = null;
161
170
  try {
162
- c = resolver.query(childrenUri, COLUMNS, null, null, null);
171
+ // a cloud provider (Google Drive) answers the first query on a folder
172
+ // it has not fetched yet with a partial, often empty, cursor flagged
173
+ // EXTRA_LOADING: taking that as the folder's contents made Bishop
174
+ // think a bookmarks file was absent and create a second one. Ask
175
+ // again until the provider says it is done, within reason.
176
+ long deadline = System.currentTimeMillis() + LIST_WAIT_MILLIS;
177
+ while (true) {
178
+ c = resolver.query(childrenUri, COLUMNS, null, null, null);
179
+ if (c == null) break;
180
+ Bundle extras = c.getExtras();
181
+ String error = (extras == null) ? null : extras.getString(DocumentsContract.EXTRA_ERROR);
182
+ if (error != null) {
183
+ Log.w(TAG, "list failed for " + dirPath + ": provider says " + error);
184
+ return null;
185
+ }
186
+ boolean loading = extras != null && extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
187
+ if (!loading) break;
188
+ if (System.currentTimeMillis() > deadline) {
189
+ Log.w(TAG, "list of " + dirPath + " still loading after " + LIST_WAIT_MILLIS + " ms: not trusting it");
190
+ return null;
191
+ }
192
+ c.close(); c = null;
193
+ try { Thread.sleep(LIST_POLL_MILLIS); } catch (InterruptedException e) { break; }
194
+ }
163
195
  if (c != null) {
164
196
  while (c.moveToNext()) {
165
197
  SWMgr.FileInfo info = fromCursor(c);
@@ -191,6 +223,32 @@ public class SafStorage implements SWMgr.SharedStorage {
191
223
 
192
224
  @Override
193
225
  public int open(String path, String mode, boolean create) {
226
+ int fd = openResolved(path, mode, create);
227
+ if (fd >= 0) return fd;
228
+ // the id we had may be stale: another device can replace a file
229
+ // under the same name (a cloud provider gives it a new id); forget
230
+ // what we knew and look the name up once more
231
+ String known;
232
+ synchronized (this) { known = docIds.get(path); }
233
+ if (known == null) return fd;
234
+ forget(path);
235
+ // ask the provider to refresh what it knows about the file and its
236
+ // folder (a cloud provider's local record can lag a change made on
237
+ // the web or on another device) before the name is looked up again
238
+ try {
239
+ resolver.refresh(docUri(known), null, null);
240
+ String parentId = resolve(parentOf(path));
241
+ if (parentId != null) resolver.refresh(DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentId), null, null);
242
+ }
243
+ catch (Exception e) {
244
+ Log.i(TAG, "refresh not honoured for " + path + ": " + e);
245
+ }
246
+ forget(path);
247
+ Log.i(TAG, "retrying " + path + " by name after a failed open");
248
+ return openResolved(path, mode, create);
249
+ }
250
+
251
+ private int openResolved(String path, String mode, boolean create) {
194
252
  String id = resolve(path);
195
253
  if (id == null) {
196
254
  if (!create) return -1;
@@ -301,12 +359,18 @@ public class SafStorage implements SWMgr.SharedStorage {
301
359
  return info;
302
360
  }
303
361
 
362
+ /** set when the last list() could not be trusted (the provider failed or
363
+ * never finished loading): native code sees only an empty listing, and
364
+ * the app must not take that for an empty folder */
365
+ public volatile boolean lastListFailed = false;
366
+
304
367
  @Override
305
368
  public SWMgr.FileInfo[] list(String path) {
369
+ lastListFailed = false;
306
370
  String id = resolve(path);
307
371
  if (id == null) return null;
308
372
  List<SWMgr.FileInfo> entries = listChildren(path, id);
309
- if (entries == null) return null;
373
+ if (entries == null) { lastListFailed = true; return null; }
310
374
  return entries.toArray(new SWMgr.FileInfo[entries.size()]);
311
375
  }
312
376