@capgo/capacitor-launch-navigator 8.0.20 → 8.0.22
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/README.md +200 -15
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/app/capgo/plugin/launch_navigator/LaunchNavigator.java +847 -71
- package/android/src/main/java/app/capgo/plugin/launch_navigator/LaunchNavigatorPlugin.java +62 -2
- package/dist/docs.json +358 -0
- package/dist/esm/definitions.d.ts +180 -1
- package/dist/esm/definitions.js +12 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +8 -2
- package/dist/esm/web.js +332 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +344 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +344 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LaunchNavigatorPlugin/LaunchNavigator.swift +228 -19
- package/ios/Sources/LaunchNavigatorPlugin/LaunchNavigatorIcons.swift +447 -0
- package/ios/Sources/LaunchNavigatorPlugin/LaunchNavigatorPlugin.swift +47 -2
- package/ios/Tests/LaunchNavigatorPluginTests/LaunchNavigatorPluginTests.swift +16 -6
- package/package.json +1 -1
|
@@ -4,48 +4,131 @@ import android.content.Context;
|
|
|
4
4
|
import android.content.Intent;
|
|
5
5
|
import android.content.pm.PackageManager;
|
|
6
6
|
import android.net.Uri;
|
|
7
|
+
import com.getcapacitor.Bridge;
|
|
8
|
+
import com.getcapacitor.FileUtils;
|
|
7
9
|
import com.getcapacitor.JSArray;
|
|
8
10
|
import com.getcapacitor.JSObject;
|
|
9
|
-
import java.
|
|
11
|
+
import java.io.ByteArrayOutputStream;
|
|
12
|
+
import java.io.File;
|
|
13
|
+
import java.io.FileInputStream;
|
|
14
|
+
import java.io.FileOutputStream;
|
|
15
|
+
import java.io.IOException;
|
|
16
|
+
import java.io.InputStream;
|
|
17
|
+
import java.net.HttpURLConnection;
|
|
18
|
+
import java.net.URL;
|
|
19
|
+
import java.nio.charset.StandardCharsets;
|
|
20
|
+
import java.security.MessageDigest;
|
|
21
|
+
import java.security.NoSuchAlgorithmException;
|
|
22
|
+
import java.util.LinkedHashMap;
|
|
10
23
|
import java.util.Locale;
|
|
11
24
|
import java.util.Map;
|
|
25
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
26
|
+
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
27
|
+
import java.util.regex.Matcher;
|
|
28
|
+
import java.util.regex.Pattern;
|
|
29
|
+
import org.json.JSONArray;
|
|
30
|
+
import org.json.JSONObject;
|
|
12
31
|
|
|
13
32
|
public class LaunchNavigator {
|
|
14
33
|
|
|
15
|
-
private
|
|
34
|
+
private static final long DEFAULT_ICON_CACHE_MAX_AGE_MS = 24L * 60L * 60L * 1000L;
|
|
35
|
+
private static final int CONNECT_TIMEOUT_MS = 8000;
|
|
36
|
+
private static final int READ_TIMEOUT_MS = 8000;
|
|
37
|
+
private static final int MAX_ICON_BYTES = 2 * 1024 * 1024;
|
|
38
|
+
private static final int MAX_HTML_BYTES = 512 * 1024;
|
|
39
|
+
private static final String ICON_CACHE_DIR = "launch_navigator_icons";
|
|
40
|
+
private static final Pattern ICON_LINK_PATTERN = Pattern.compile(
|
|
41
|
+
"<link\\s+[^>]*rel=[\"'][^\"']*(?:apple-touch-icon|icon)[^\"']*[\"'][^>]*>",
|
|
42
|
+
Pattern.CASE_INSENSITIVE
|
|
43
|
+
);
|
|
44
|
+
private static final Pattern HREF_PATTERN = Pattern.compile("\\shref=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE);
|
|
45
|
+
|
|
46
|
+
private final Context context;
|
|
47
|
+
private final Bridge bridge;
|
|
48
|
+
private final Map<String, Object> iconCacheLocks = new ConcurrentHashMap<>();
|
|
49
|
+
private final ReentrantReadWriteLock iconCacheDirectoryLock = new ReentrantReadWriteLock();
|
|
16
50
|
private Map<String, AppInfo> navigationApps;
|
|
17
51
|
|
|
18
52
|
private static class AppInfo {
|
|
19
53
|
|
|
20
54
|
String name;
|
|
21
|
-
String
|
|
55
|
+
String[] packageNames;
|
|
56
|
+
String url;
|
|
57
|
+
|
|
58
|
+
AppInfo(String name, String url, String... packageNames) {
|
|
59
|
+
this.name = name;
|
|
60
|
+
this.url = url;
|
|
61
|
+
this.packageNames = packageNames;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private static class IconProvider {
|
|
66
|
+
|
|
67
|
+
String app;
|
|
68
|
+
String name;
|
|
69
|
+
String url;
|
|
70
|
+
String iconUrl;
|
|
22
71
|
|
|
23
|
-
|
|
72
|
+
IconProvider(String app, String name, String url, String iconUrl) {
|
|
73
|
+
this.app = app;
|
|
24
74
|
this.name = name;
|
|
25
|
-
this.
|
|
75
|
+
this.url = url;
|
|
76
|
+
this.iconUrl = iconUrl;
|
|
26
77
|
}
|
|
27
78
|
}
|
|
28
79
|
|
|
29
|
-
|
|
80
|
+
private static class CachedIcon {
|
|
81
|
+
|
|
82
|
+
File file;
|
|
83
|
+
JSONObject metadata;
|
|
84
|
+
|
|
85
|
+
CachedIcon(File file, JSONObject metadata) {
|
|
86
|
+
this.file = file;
|
|
87
|
+
this.metadata = metadata;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static class DownloadedIcon {
|
|
92
|
+
|
|
93
|
+
byte[] data;
|
|
94
|
+
String sourceUrl;
|
|
95
|
+
String mimeType;
|
|
96
|
+
|
|
97
|
+
DownloadedIcon(byte[] data, String sourceUrl, String mimeType) {
|
|
98
|
+
this.data = data;
|
|
99
|
+
this.sourceUrl = sourceUrl;
|
|
100
|
+
this.mimeType = mimeType;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public LaunchNavigator(Context context, Bridge bridge) {
|
|
30
105
|
this.context = context;
|
|
106
|
+
this.bridge = bridge;
|
|
31
107
|
initializeApps();
|
|
32
108
|
}
|
|
33
109
|
|
|
34
110
|
private void initializeApps() {
|
|
35
|
-
navigationApps = new
|
|
36
|
-
navigationApps.put("google_maps", new AppInfo("Google Maps", "com.google.android.apps.maps"));
|
|
37
|
-
navigationApps.put("waze", new AppInfo("Waze", "com.waze"));
|
|
38
|
-
navigationApps.put("citymapper", new AppInfo("Citymapper", "com.citymapper.app.release"));
|
|
39
|
-
navigationApps.put("uber", new AppInfo("Uber", "com.ubercab"));
|
|
40
|
-
navigationApps.put("yandex", new AppInfo("Yandex Navigator", "ru.yandex.yandexnavi"));
|
|
41
|
-
navigationApps.put("sygic", new AppInfo("Sygic", "com.sygic.aura"));
|
|
42
|
-
navigationApps.put("here", new AppInfo("HERE Maps", "com.here.app.maps"));
|
|
43
|
-
navigationApps.put("moovit", new AppInfo("Moovit", "com.tranzmate"));
|
|
44
|
-
navigationApps.put("lyft", new AppInfo("Lyft", "me.lyft.android"));
|
|
45
|
-
navigationApps.put("mapsme", new AppInfo("MAPS.ME", "com.mapswithme.maps.pro"));
|
|
46
|
-
navigationApps.put("
|
|
47
|
-
navigationApps.put("
|
|
48
|
-
navigationApps.put("
|
|
111
|
+
navigationApps = new LinkedHashMap<>();
|
|
112
|
+
navigationApps.put("google_maps", new AppInfo("Google Maps", "https://www.google.com/maps", "com.google.android.apps.maps"));
|
|
113
|
+
navigationApps.put("waze", new AppInfo("Waze", "https://www.waze.com", "com.waze"));
|
|
114
|
+
navigationApps.put("citymapper", new AppInfo("Citymapper", "https://citymapper.com", "com.citymapper.app.release"));
|
|
115
|
+
navigationApps.put("uber", new AppInfo("Uber", "https://www.uber.com", "com.ubercab"));
|
|
116
|
+
navigationApps.put("yandex", new AppInfo("Yandex Navigator", "https://yandex.com/maps", "ru.yandex.yandexnavi"));
|
|
117
|
+
navigationApps.put("sygic", new AppInfo("Sygic", "https://www.sygic.com/gps-navigation", "com.sygic.aura"));
|
|
118
|
+
navigationApps.put("here", new AppInfo("HERE Maps", "https://wego.here.com", "com.here.app.maps"));
|
|
119
|
+
navigationApps.put("moovit", new AppInfo("Moovit", "https://moovitapp.com", "com.tranzmate"));
|
|
120
|
+
navigationApps.put("lyft", new AppInfo("Lyft", "https://www.lyft.com", "me.lyft.android"));
|
|
121
|
+
navigationApps.put("mapsme", new AppInfo("MAPS.ME", "https://maps.me", "com.mapswithme.maps.pro"));
|
|
122
|
+
navigationApps.put("tomtom", new AppInfo("TomTom GO", "https://www.tomtom.com", "com.tomtom.gplay.navapp"));
|
|
123
|
+
navigationApps.put("guru_maps", new AppInfo("Guru Maps", "https://gurumaps.app", "com.bodunov.galileo", "com.bodunov.GalileoPro"));
|
|
124
|
+
navigationApps.put("organic_maps", new AppInfo("Organic Maps", "https://organicmaps.app", "app.organicmaps"));
|
|
125
|
+
navigationApps.put("yandex_maps", new AppInfo("Yandex Maps", "https://yandex.com/maps", "ru.yandex.yandexmaps"));
|
|
126
|
+
navigationApps.put("mapy", new AppInfo("Mapy.com", "https://mapy.com", "cz.seznam.mapy"));
|
|
127
|
+
navigationApps.put("2gis", new AppInfo("2GIS", "https://2gis.com", "ru.dublgis.dgismobile"));
|
|
128
|
+
navigationApps.put("cabify", new AppInfo("Cabify", "https://cabify.com", "com.cabify.rider"));
|
|
129
|
+
navigationApps.put("baidu", new AppInfo("Baidu Maps", "https://map.baidu.com", "com.baidu.BaiduMap"));
|
|
130
|
+
navigationApps.put("gaode", new AppInfo("Gaode Maps", "https://www.amap.com", "com.autonavi.minimap"));
|
|
131
|
+
navigationApps.put("tesla", new AppInfo("Tesla", "https://www.tesla.com", "com.teslamotors.tesla"));
|
|
49
132
|
}
|
|
50
133
|
|
|
51
134
|
public boolean navigate(
|
|
@@ -59,53 +142,9 @@ public class LaunchNavigator {
|
|
|
59
142
|
String transportMode
|
|
60
143
|
) {
|
|
61
144
|
try {
|
|
62
|
-
Intent intent;
|
|
63
|
-
|
|
64
|
-
switch (app) {
|
|
65
|
-
case "google_maps":
|
|
66
|
-
intent = createGoogleMapsIntent(lat, lon, startLat, startLon, transportMode);
|
|
67
|
-
break;
|
|
68
|
-
case "waze":
|
|
69
|
-
intent = createWazeIntent(lat, lon);
|
|
70
|
-
break;
|
|
71
|
-
case "citymapper":
|
|
72
|
-
intent = createCitymapperIntent(lat, lon, startLat, startLon);
|
|
73
|
-
break;
|
|
74
|
-
case "uber":
|
|
75
|
-
intent = createUberIntent(lat, lon, startLat, startLon);
|
|
76
|
-
break;
|
|
77
|
-
case "yandex":
|
|
78
|
-
intent = createYandexIntent(lat, lon, startLat, startLon);
|
|
79
|
-
break;
|
|
80
|
-
case "sygic":
|
|
81
|
-
intent = createSygicIntent(lat, lon);
|
|
82
|
-
break;
|
|
83
|
-
case "here":
|
|
84
|
-
intent = createHereIntent(lat, lon, startLat, startLon);
|
|
85
|
-
break;
|
|
86
|
-
case "moovit":
|
|
87
|
-
intent = createMoovitIntent(lat, lon, startLat, startLon);
|
|
88
|
-
break;
|
|
89
|
-
case "lyft":
|
|
90
|
-
intent = createLyftIntent(lat, lon);
|
|
91
|
-
break;
|
|
92
|
-
case "mapsme":
|
|
93
|
-
intent = createMapsMeIntent(lat, lon);
|
|
94
|
-
break;
|
|
95
|
-
case "cabify":
|
|
96
|
-
intent = createCabifyIntent(lat, lon, startLat, startLon);
|
|
97
|
-
break;
|
|
98
|
-
case "baidu":
|
|
99
|
-
intent = createBaiduIntent(lat, lon, startLat, startLon);
|
|
100
|
-
break;
|
|
101
|
-
case "gaode":
|
|
102
|
-
intent = createGaodeIntent(lat, lon, startLat, startLon);
|
|
103
|
-
break;
|
|
104
|
-
default:
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
145
|
+
Intent intent = createNavigationIntent(app, lat, lon, startLat, startLon, destinationName, transportMode);
|
|
107
146
|
|
|
108
|
-
if (intent != null) {
|
|
147
|
+
if (intent != null && canResolveIntent(intent)) {
|
|
109
148
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
110
149
|
context.startActivity(intent);
|
|
111
150
|
return true;
|
|
@@ -118,6 +157,61 @@ public class LaunchNavigator {
|
|
|
118
157
|
}
|
|
119
158
|
}
|
|
120
159
|
|
|
160
|
+
private Intent createNavigationIntent(
|
|
161
|
+
String app,
|
|
162
|
+
double lat,
|
|
163
|
+
double lon,
|
|
164
|
+
Double startLat,
|
|
165
|
+
Double startLon,
|
|
166
|
+
String destinationName,
|
|
167
|
+
String transportMode
|
|
168
|
+
) {
|
|
169
|
+
switch (app) {
|
|
170
|
+
case "google_maps":
|
|
171
|
+
return createGoogleMapsIntent(lat, lon, startLat, startLon, transportMode);
|
|
172
|
+
case "waze":
|
|
173
|
+
return createWazeIntent(lat, lon);
|
|
174
|
+
case "citymapper":
|
|
175
|
+
return createCitymapperIntent(lat, lon, startLat, startLon);
|
|
176
|
+
case "uber":
|
|
177
|
+
return createUberIntent(lat, lon, startLat, startLon);
|
|
178
|
+
case "yandex":
|
|
179
|
+
return createYandexIntent(lat, lon, startLat, startLon);
|
|
180
|
+
case "sygic":
|
|
181
|
+
return createSygicIntent(lat, lon);
|
|
182
|
+
case "here":
|
|
183
|
+
return createHereIntent(lat, lon, startLat, startLon);
|
|
184
|
+
case "moovit":
|
|
185
|
+
return createMoovitIntent(lat, lon, startLat, startLon);
|
|
186
|
+
case "lyft":
|
|
187
|
+
return createLyftIntent(lat, lon);
|
|
188
|
+
case "mapsme":
|
|
189
|
+
return createMapsMeIntent(lat, lon);
|
|
190
|
+
case "tomtom":
|
|
191
|
+
return createTomTomIntent(lat, lon);
|
|
192
|
+
case "guru_maps":
|
|
193
|
+
return createGuruMapsIntent(lat, lon, startLat, startLon, transportMode);
|
|
194
|
+
case "organic_maps":
|
|
195
|
+
return createOrganicMapsIntent(lat, lon, startLat, startLon, destinationName, transportMode);
|
|
196
|
+
case "yandex_maps":
|
|
197
|
+
return createYandexMapsIntent(lat, lon, startLat, startLon);
|
|
198
|
+
case "mapy":
|
|
199
|
+
return createMapyIntent(lat, lon, transportMode);
|
|
200
|
+
case "2gis":
|
|
201
|
+
return create2GisIntent(lat, lon, startLat, startLon, transportMode);
|
|
202
|
+
case "cabify":
|
|
203
|
+
return createCabifyIntent(lat, lon, startLat, startLon);
|
|
204
|
+
case "baidu":
|
|
205
|
+
return createBaiduIntent(lat, lon, startLat, startLon);
|
|
206
|
+
case "gaode":
|
|
207
|
+
return createGaodeIntent(lat, lon, startLat, startLon);
|
|
208
|
+
case "tesla":
|
|
209
|
+
return createTeslaIntent(lat, lon, destinationName);
|
|
210
|
+
default:
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
121
215
|
private Intent createGoogleMapsIntent(double lat, double lon, Double startLat, Double startLon, String transportMode) {
|
|
122
216
|
String uri;
|
|
123
217
|
if (startLat != null && startLon != null) {
|
|
@@ -230,6 +324,129 @@ public class LaunchNavigator {
|
|
|
230
324
|
return new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
|
231
325
|
}
|
|
232
326
|
|
|
327
|
+
private Intent createTomTomIntent(double lat, double lon) {
|
|
328
|
+
String uri = String.format(Locale.US, "tomtomgo://x-callback-url/navigate?destination=%f,%f", lat, lon);
|
|
329
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
|
330
|
+
intent.setPackage("com.tomtom.gplay.navapp");
|
|
331
|
+
return intent;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private Intent createGuruMapsIntent(double lat, double lon, Double startLat, Double startLon, String transportMode) {
|
|
335
|
+
String uri = String.format(
|
|
336
|
+
Locale.US,
|
|
337
|
+
"guru://nav?finish=%f,%f&mode=%s&start_navigation=true",
|
|
338
|
+
lat,
|
|
339
|
+
lon,
|
|
340
|
+
getGuruMapsMode(transportMode)
|
|
341
|
+
);
|
|
342
|
+
if (startLat != null && startLon != null) {
|
|
343
|
+
uri += String.format(Locale.US, "&start=%f,%f", startLat, startLon);
|
|
344
|
+
}
|
|
345
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
|
346
|
+
String packageName = getFirstInstalledPackage("guru_maps");
|
|
347
|
+
if (packageName != null) {
|
|
348
|
+
intent.setPackage(packageName);
|
|
349
|
+
}
|
|
350
|
+
return intent;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private String getGuruMapsMode(String transportMode) {
|
|
354
|
+
switch (transportMode) {
|
|
355
|
+
case "walking":
|
|
356
|
+
return "pedestrian";
|
|
357
|
+
case "bicycling":
|
|
358
|
+
return "bicycle";
|
|
359
|
+
case "driving":
|
|
360
|
+
case "transit":
|
|
361
|
+
default:
|
|
362
|
+
return "auto";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private Intent createOrganicMapsIntent(
|
|
367
|
+
double lat,
|
|
368
|
+
double lon,
|
|
369
|
+
Double startLat,
|
|
370
|
+
Double startLon,
|
|
371
|
+
String destinationName,
|
|
372
|
+
String transportMode
|
|
373
|
+
) {
|
|
374
|
+
String origin = "currentLocation";
|
|
375
|
+
if (startLat != null && startLon != null) {
|
|
376
|
+
origin = String.format(Locale.US, "%f,%f", startLat, startLon);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
String uri = String.format(
|
|
380
|
+
Locale.US,
|
|
381
|
+
"om://v2/nav?origin=%s&destination=%f,%f&mode=%s",
|
|
382
|
+
Uri.encode(origin),
|
|
383
|
+
lat,
|
|
384
|
+
lon,
|
|
385
|
+
getOrganicMapsMode(transportMode)
|
|
386
|
+
);
|
|
387
|
+
if (destinationName != null && !destinationName.isEmpty()) {
|
|
388
|
+
uri += "&destination_name=" + Uri.encode(destinationName);
|
|
389
|
+
}
|
|
390
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
|
391
|
+
intent.setPackage("app.organicmaps");
|
|
392
|
+
return intent;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private String getOrganicMapsMode(String transportMode) {
|
|
396
|
+
switch (transportMode) {
|
|
397
|
+
case "walking":
|
|
398
|
+
return "pedestrian";
|
|
399
|
+
case "bicycling":
|
|
400
|
+
return "bicycle";
|
|
401
|
+
case "transit":
|
|
402
|
+
return "transit";
|
|
403
|
+
case "driving":
|
|
404
|
+
default:
|
|
405
|
+
return "drive";
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private Intent createYandexMapsIntent(double lat, double lon, Double startLat, Double startLon) {
|
|
410
|
+
String uri = String.format(Locale.US, "yandexmaps://build_route_on_map/?lat_to=%f&lon_to=%f", lat, lon);
|
|
411
|
+
if (startLat != null && startLon != null) {
|
|
412
|
+
uri += String.format(Locale.US, "&lat_from=%f&lon_from=%f", startLat, startLon);
|
|
413
|
+
}
|
|
414
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
|
415
|
+
intent.setPackage("ru.yandex.yandexmaps");
|
|
416
|
+
return intent;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private Intent createMapyIntent(double lat, double lon, String transportMode) {
|
|
420
|
+
String uri = String.format(Locale.US, "google.navigation:q=%f,%f&mode=%s", lat, lon, getGoogleMapsMode(transportMode));
|
|
421
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
|
422
|
+
intent.setPackage("cz.seznam.mapy");
|
|
423
|
+
return intent;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private Intent create2GisIntent(double lat, double lon, Double startLat, Double startLon, String transportMode) {
|
|
427
|
+
String uri = String.format(Locale.US, "dgis://2gis.ru/routeSearch/rsType/%s", get2GisMode(transportMode));
|
|
428
|
+
if (startLat != null && startLon != null) {
|
|
429
|
+
uri += String.format(Locale.US, "/from/%f,%f", startLon, startLat);
|
|
430
|
+
}
|
|
431
|
+
uri += String.format(Locale.US, "/to/%f,%f", lon, lat);
|
|
432
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
|
433
|
+
intent.setPackage("ru.dublgis.dgismobile");
|
|
434
|
+
return intent;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private String get2GisMode(String transportMode) {
|
|
438
|
+
switch (transportMode) {
|
|
439
|
+
case "walking":
|
|
440
|
+
return "pedestrian";
|
|
441
|
+
case "transit":
|
|
442
|
+
return "ctx";
|
|
443
|
+
case "driving":
|
|
444
|
+
case "bicycling":
|
|
445
|
+
default:
|
|
446
|
+
return "car";
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
233
450
|
private Intent createCabifyIntent(double lat, double lon, Double startLat, Double startLon) {
|
|
234
451
|
String uri;
|
|
235
452
|
if (startLat != null && startLon != null) {
|
|
@@ -267,18 +484,577 @@ public class LaunchNavigator {
|
|
|
267
484
|
return intent;
|
|
268
485
|
}
|
|
269
486
|
|
|
487
|
+
private Intent createTeslaIntent(double lat, double lon, String destinationName) {
|
|
488
|
+
Intent intent = new Intent(Intent.ACTION_SEND);
|
|
489
|
+
intent.setType("text/plain");
|
|
490
|
+
intent.putExtra(Intent.EXTRA_TEXT, createTeslaShareText(lat, lon, destinationName));
|
|
491
|
+
intent.setPackage("com.teslamotors.tesla");
|
|
492
|
+
return intent;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
static String createTeslaShareText(double lat, double lon, String destinationName) {
|
|
496
|
+
return createTeslaShareLabel(destinationName) + "\n\n" + createGoogleMapsPositionUrl(lat, lon);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
static String createGoogleMapsPositionUrl(double lat, double lon) {
|
|
500
|
+
return String.format(Locale.US, "https://maps.google.com/?q=%.6f,%.6f", lat, lon);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private static String createTeslaShareLabel(String destinationName) {
|
|
504
|
+
if (destinationName == null) {
|
|
505
|
+
return "Dropped pin";
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
String normalizedName = destinationName.trim().replaceAll("[\\r\\n]+", " ");
|
|
509
|
+
return normalizedName.isEmpty() ? "Dropped pin" : normalizedName;
|
|
510
|
+
}
|
|
511
|
+
|
|
270
512
|
public boolean isAppAvailable(String app) {
|
|
513
|
+
Intent intent = createNavigationIntent(app, 0, 0, null, null, null, "driving");
|
|
514
|
+
return intent != null && canResolveIntent(intent);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private boolean canResolveIntent(Intent intent) {
|
|
518
|
+
PackageManager pm = context.getPackageManager();
|
|
519
|
+
return intent.resolveActivity(pm) != null || !pm.queryIntentActivities(intent, 0).isEmpty();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private String getFirstInstalledPackage(String app) {
|
|
271
523
|
AppInfo appInfo = navigationApps.get(app);
|
|
272
524
|
if (appInfo == null) {
|
|
273
|
-
return
|
|
525
|
+
return null;
|
|
274
526
|
}
|
|
275
527
|
|
|
276
528
|
PackageManager pm = context.getPackageManager();
|
|
529
|
+
for (String packageName : appInfo.packageNames) {
|
|
530
|
+
try {
|
|
531
|
+
pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);
|
|
532
|
+
return packageName;
|
|
533
|
+
} catch (PackageManager.NameNotFoundException e) {
|
|
534
|
+
// Try the next package for apps that ship free and pro variants.
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
public JSObject getAppIcons(JSObject options) {
|
|
541
|
+
return getAppIcons(options, false);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
public JSObject refreshAppIcons(JSObject options) {
|
|
545
|
+
return getAppIcons(options, true);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private JSObject getAppIcons(JSObject options, boolean forceRefreshOverride) {
|
|
549
|
+
JSObject safeOptions = options == null ? new JSObject() : options;
|
|
550
|
+
long maxAgeMs = getMaxAgeMs(safeOptions);
|
|
551
|
+
boolean forceRefresh = forceRefreshOverride || safeOptions.optBoolean("forceRefresh", false);
|
|
552
|
+
JSArray icons = new JSArray();
|
|
553
|
+
JSArray failures = new JSArray();
|
|
554
|
+
|
|
555
|
+
for (IconProvider provider : resolveIconProviders(safeOptions)) {
|
|
556
|
+
try {
|
|
557
|
+
icons.put(resolveProviderIcon(provider, maxAgeMs, forceRefresh));
|
|
558
|
+
} catch (Exception e) {
|
|
559
|
+
failures.put(createIconFailure(provider, e));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
JSObject ret = new JSObject();
|
|
564
|
+
ret.put("icons", icons);
|
|
565
|
+
ret.put("failures", failures);
|
|
566
|
+
return ret;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
public JSObject clearIconCache(JSObject options) {
|
|
570
|
+
JSObject safeOptions = options == null ? new JSObject() : options;
|
|
571
|
+
JSONArray apps = safeOptions.optJSONArray("apps");
|
|
572
|
+
int cleared = 0;
|
|
573
|
+
|
|
574
|
+
if (apps != null && apps.length() > 0) {
|
|
575
|
+
for (int i = 0; i < apps.length(); i++) {
|
|
576
|
+
String app = apps.optString(i, "");
|
|
577
|
+
if (!app.isEmpty()) {
|
|
578
|
+
cleared += clearIconCacheForApp(app);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
cleared = clearAllIconCache();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
JSObject ret = new JSObject();
|
|
586
|
+
ret.put("cleared", cleared);
|
|
587
|
+
return ret;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private JSObject resolveProviderIcon(IconProvider provider, long maxAgeMs, boolean forceRefresh) throws Exception {
|
|
591
|
+
iconCacheDirectoryLock.readLock().lock();
|
|
277
592
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
593
|
+
synchronized (iconCacheLock(provider.app)) {
|
|
594
|
+
return resolveProviderIconLocked(provider, maxAgeMs, forceRefresh);
|
|
595
|
+
}
|
|
596
|
+
} finally {
|
|
597
|
+
iconCacheDirectoryLock.readLock().unlock();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private JSObject resolveProviderIconLocked(IconProvider provider, long maxAgeMs, boolean forceRefresh) throws Exception {
|
|
602
|
+
CachedIcon cachedIcon = readCachedIcon(provider.app);
|
|
603
|
+
long now = System.currentTimeMillis();
|
|
604
|
+
|
|
605
|
+
if (cachedIcon != null && !forceRefresh && now - cachedIcon.metadata.optLong("fetchedAt", 0) < maxAgeMs) {
|
|
606
|
+
return createIconObject(cachedIcon, true, false);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
DownloadedIcon downloadedIcon = downloadIcon(provider);
|
|
611
|
+
File iconFile = writeIcon(provider.app, downloadedIcon);
|
|
612
|
+
JSONObject metadata = new JSONObject();
|
|
613
|
+
metadata.put("app", provider.app);
|
|
614
|
+
metadata.put("name", provider.name);
|
|
615
|
+
metadata.put("sourceUrl", downloadedIcon.sourceUrl);
|
|
616
|
+
metadata.put("mimeType", downloadedIcon.mimeType);
|
|
617
|
+
metadata.put("fetchedAt", now);
|
|
618
|
+
metadata.put("fileName", iconFile.getName());
|
|
619
|
+
writeString(metadataFile(provider.app), metadata.toString());
|
|
620
|
+
deleteCachedFilesExcept(provider.app, iconFile.getName(), metadataFile(provider.app).getName());
|
|
621
|
+
return createIconObject(new CachedIcon(iconFile, metadata), false, false);
|
|
622
|
+
} catch (Exception e) {
|
|
623
|
+
if (cachedIcon != null) {
|
|
624
|
+
return createIconObject(cachedIcon, true, true);
|
|
625
|
+
}
|
|
626
|
+
throw e;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private int clearIconCacheForApp(String app) {
|
|
631
|
+
iconCacheDirectoryLock.readLock().lock();
|
|
632
|
+
try {
|
|
633
|
+
synchronized (iconCacheLock(app)) {
|
|
634
|
+
return deleteCachedFiles(app);
|
|
635
|
+
}
|
|
636
|
+
} finally {
|
|
637
|
+
iconCacheDirectoryLock.readLock().unlock();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private int clearAllIconCache() {
|
|
642
|
+
iconCacheDirectoryLock.writeLock().lock();
|
|
643
|
+
try {
|
|
644
|
+
File cacheDirectory = ensureIconCacheDirectory();
|
|
645
|
+
File[] files = cacheDirectory.listFiles();
|
|
646
|
+
int cleared = 0;
|
|
647
|
+
if (files != null) {
|
|
648
|
+
for (File file : files) {
|
|
649
|
+
if (file.isFile() && file.delete()) {
|
|
650
|
+
cleared++;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
iconCacheLocks.clear();
|
|
655
|
+
return cleared;
|
|
656
|
+
} finally {
|
|
657
|
+
iconCacheDirectoryLock.writeLock().unlock();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private JSObject createIconObject(CachedIcon cachedIcon, boolean fromCache, boolean stale) {
|
|
662
|
+
JSObject icon = new JSObject();
|
|
663
|
+
icon.put("app", cachedIcon.metadata.optString("app"));
|
|
664
|
+
String name = cachedIcon.metadata.optString("name", null);
|
|
665
|
+
if (name != null && !name.isEmpty()) {
|
|
666
|
+
icon.put("name", name);
|
|
667
|
+
}
|
|
668
|
+
icon.put("localUrl", portablePath(cachedIcon.file));
|
|
669
|
+
icon.put("sourceUrl", cachedIcon.metadata.optString("sourceUrl"));
|
|
670
|
+
String mimeType = cachedIcon.metadata.optString("mimeType", null);
|
|
671
|
+
if (mimeType != null && !mimeType.isEmpty()) {
|
|
672
|
+
icon.put("mimeType", mimeType);
|
|
673
|
+
}
|
|
674
|
+
icon.put("fetchedAt", cachedIcon.metadata.optLong("fetchedAt", 0));
|
|
675
|
+
icon.put("fromCache", fromCache);
|
|
676
|
+
icon.put("stale", stale);
|
|
677
|
+
return icon;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private JSObject createIconFailure(IconProvider provider, Exception error) {
|
|
681
|
+
JSObject failure = new JSObject();
|
|
682
|
+
failure.put("app", provider.app);
|
|
683
|
+
if (provider.name != null && !provider.name.isEmpty()) {
|
|
684
|
+
failure.put("name", provider.name);
|
|
685
|
+
}
|
|
686
|
+
String sourceUrl = provider.iconUrl != null ? provider.iconUrl : provider.url;
|
|
687
|
+
if (sourceUrl != null && !sourceUrl.isEmpty()) {
|
|
688
|
+
failure.put("sourceUrl", sourceUrl);
|
|
689
|
+
}
|
|
690
|
+
failure.put("message", error.getMessage() == null ? error.toString() : error.getMessage());
|
|
691
|
+
return failure;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private DownloadedIcon downloadIcon(IconProvider provider) throws IOException {
|
|
695
|
+
String sourceUrl;
|
|
696
|
+
if (provider.iconUrl != null && !provider.iconUrl.isEmpty()) {
|
|
697
|
+
sourceUrl = resolveUrl(provider.iconUrl, provider.url);
|
|
698
|
+
} else {
|
|
699
|
+
if (provider.url == null || provider.url.isEmpty()) {
|
|
700
|
+
throw new IOException("Provider url or iconUrl is required");
|
|
701
|
+
}
|
|
702
|
+
sourceUrl = discoverIconUrl(provider.url);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
HttpURLConnection connection = openConnection(sourceUrl);
|
|
706
|
+
try {
|
|
707
|
+
int status = connection.getResponseCode();
|
|
708
|
+
if (status < 200 || status >= 300) {
|
|
709
|
+
throw new IOException("Icon request failed with status " + status);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
String mimeType = normalizeMimeType(connection.getContentType());
|
|
713
|
+
byte[] data = readLimitedBytes(connection.getInputStream(), MAX_ICON_BYTES);
|
|
714
|
+
if (!isSupportedImageResponse(mimeType, sourceUrl)) {
|
|
715
|
+
throw new IOException("Icon response is not an image");
|
|
716
|
+
}
|
|
717
|
+
return new DownloadedIcon(data, connection.getURL().toString(), mimeType);
|
|
718
|
+
} finally {
|
|
719
|
+
connection.disconnect();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private String discoverIconUrl(String providerUrl) throws IOException {
|
|
724
|
+
HttpURLConnection connection = openConnection(providerUrl);
|
|
725
|
+
try {
|
|
726
|
+
int status = connection.getResponseCode();
|
|
727
|
+
if (status < 200 || status >= 300) {
|
|
728
|
+
throw new IOException("Provider page request failed with status " + status);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
String html = new String(readLimitedBytes(connection.getInputStream(), MAX_HTML_BYTES), StandardCharsets.UTF_8);
|
|
732
|
+
Matcher linkMatcher = ICON_LINK_PATTERN.matcher(html);
|
|
733
|
+
if (linkMatcher.find()) {
|
|
734
|
+
Matcher hrefMatcher = HREF_PATTERN.matcher(linkMatcher.group());
|
|
735
|
+
if (hrefMatcher.find()) {
|
|
736
|
+
return resolveUrl(hrefMatcher.group(1), connection.getURL().toString());
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return resolveUrl("/favicon.ico", connection.getURL().toString());
|
|
741
|
+
} finally {
|
|
742
|
+
connection.disconnect();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private HttpURLConnection openConnection(String url) throws IOException {
|
|
747
|
+
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
|
748
|
+
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
|
749
|
+
connection.setReadTimeout(READ_TIMEOUT_MS);
|
|
750
|
+
connection.setInstanceFollowRedirects(true);
|
|
751
|
+
connection.setRequestProperty("User-Agent", "CapgoLaunchNavigator/8");
|
|
752
|
+
return connection;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private String resolveUrl(String url, String baseUrl) throws IOException {
|
|
756
|
+
if (baseUrl != null && !baseUrl.isEmpty()) {
|
|
757
|
+
return new URL(new URL(baseUrl), url).toString();
|
|
758
|
+
}
|
|
759
|
+
return new URL(url).toString();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private byte[] readLimitedBytes(InputStream inputStream, int maxBytes) throws IOException {
|
|
763
|
+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
764
|
+
byte[] buffer = new byte[8192];
|
|
765
|
+
int total = 0;
|
|
766
|
+
int read;
|
|
767
|
+
|
|
768
|
+
while ((read = inputStream.read(buffer)) != -1) {
|
|
769
|
+
total += read;
|
|
770
|
+
if (total > maxBytes) {
|
|
771
|
+
throw new IOException("Response is too large");
|
|
772
|
+
}
|
|
773
|
+
outputStream.write(buffer, 0, read);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return outputStream.toByteArray();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
private boolean isSupportedImageResponse(String mimeType, String sourceUrl) {
|
|
780
|
+
String path = pathForExtension(sourceUrl);
|
|
781
|
+
if (mimeType == null || mimeType.isEmpty()) {
|
|
782
|
+
return hasKnownImageExtension(path);
|
|
783
|
+
}
|
|
784
|
+
return mimeType.startsWith("image/") || (mimeType.equals("application/octet-stream") && hasKnownImageExtension(path));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private boolean hasKnownImageExtension(String path) {
|
|
788
|
+
String lowerPath = path.toLowerCase(Locale.US);
|
|
789
|
+
return (
|
|
790
|
+
lowerPath.endsWith(".png") ||
|
|
791
|
+
lowerPath.endsWith(".jpg") ||
|
|
792
|
+
lowerPath.endsWith(".jpeg") ||
|
|
793
|
+
lowerPath.endsWith(".gif") ||
|
|
794
|
+
lowerPath.endsWith(".webp") ||
|
|
795
|
+
lowerPath.endsWith(".svg") ||
|
|
796
|
+
lowerPath.endsWith(".ico")
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private File writeIcon(String app, DownloadedIcon downloadedIcon) throws IOException {
|
|
801
|
+
File cacheDirectory = ensureIconCacheDirectory();
|
|
802
|
+
File iconFile = new File(cacheDirectory, cacheKey(app) + guessExtension(downloadedIcon.mimeType, downloadedIcon.sourceUrl));
|
|
803
|
+
File tempFile = new File(cacheDirectory, iconFile.getName() + ".tmp");
|
|
804
|
+
File backupFile = new File(cacheDirectory, iconFile.getName() + ".bak");
|
|
805
|
+
if (tempFile.exists()) {
|
|
806
|
+
tempFile.delete();
|
|
807
|
+
}
|
|
808
|
+
if (backupFile.exists()) {
|
|
809
|
+
backupFile.delete();
|
|
810
|
+
}
|
|
811
|
+
try (FileOutputStream outputStream = new FileOutputStream(tempFile)) {
|
|
812
|
+
outputStream.write(downloadedIcon.data);
|
|
813
|
+
}
|
|
814
|
+
if (iconFile.exists() && !iconFile.renameTo(backupFile)) {
|
|
815
|
+
tempFile.delete();
|
|
816
|
+
throw new IOException("Could not replace cached icon");
|
|
817
|
+
}
|
|
818
|
+
if (!tempFile.renameTo(iconFile)) {
|
|
819
|
+
tempFile.delete();
|
|
820
|
+
if (backupFile.exists()) {
|
|
821
|
+
backupFile.renameTo(iconFile);
|
|
822
|
+
}
|
|
823
|
+
throw new IOException("Could not store cached icon");
|
|
824
|
+
}
|
|
825
|
+
if (backupFile.exists()) {
|
|
826
|
+
backupFile.delete();
|
|
827
|
+
}
|
|
828
|
+
return iconFile;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private CachedIcon readCachedIcon(String app) {
|
|
832
|
+
File metadata = metadataFile(app);
|
|
833
|
+
if (!metadata.exists()) {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
JSONObject metadataObject = new JSONObject(readString(metadata));
|
|
839
|
+
String fileName = metadataObject.optString("fileName", "");
|
|
840
|
+
if (fileName.isEmpty()) {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
File iconFile = new File(ensureIconCacheDirectory(), fileName);
|
|
844
|
+
if (!iconFile.exists()) {
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
return new CachedIcon(iconFile, metadataObject);
|
|
848
|
+
} catch (Exception e) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private String readString(File file) throws IOException {
|
|
854
|
+
try (FileInputStream inputStream = new FileInputStream(file)) {
|
|
855
|
+
return new String(readLimitedBytes(inputStream, MAX_HTML_BYTES), StandardCharsets.UTF_8);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private void writeString(File file, String value) throws IOException {
|
|
860
|
+
try (FileOutputStream outputStream = new FileOutputStream(file)) {
|
|
861
|
+
outputStream.write(value.getBytes(StandardCharsets.UTF_8));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private File ensureIconCacheDirectory() {
|
|
866
|
+
File directory = new File(context.getCacheDir(), ICON_CACHE_DIR);
|
|
867
|
+
if (!directory.exists()) {
|
|
868
|
+
directory.mkdirs();
|
|
869
|
+
}
|
|
870
|
+
return directory;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private File metadataFile(String app) {
|
|
874
|
+
return new File(ensureIconCacheDirectory(), cacheKey(app) + ".json");
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private Object iconCacheLock(String app) {
|
|
878
|
+
return iconCacheLocks.computeIfAbsent(cacheKey(app), (key) -> new Object());
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
private int deleteCachedFiles(String app) {
|
|
882
|
+
File cacheDirectory = ensureIconCacheDirectory();
|
|
883
|
+
String prefix = cacheKey(app) + ".";
|
|
884
|
+
int deleted = 0;
|
|
885
|
+
File[] files = cacheDirectory.listFiles();
|
|
886
|
+
if (files == null) {
|
|
887
|
+
return 0;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
for (File file : files) {
|
|
891
|
+
if (file.isFile() && file.getName().startsWith(prefix) && file.delete()) {
|
|
892
|
+
deleted++;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return deleted;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private int deleteCachedFilesExcept(String app, String iconFileName, String metadataFileName) {
|
|
899
|
+
File cacheDirectory = ensureIconCacheDirectory();
|
|
900
|
+
String prefix = cacheKey(app) + ".";
|
|
901
|
+
int deleted = 0;
|
|
902
|
+
File[] files = cacheDirectory.listFiles();
|
|
903
|
+
if (files == null) {
|
|
904
|
+
return 0;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
for (File file : files) {
|
|
908
|
+
String fileName = file.getName();
|
|
909
|
+
if (
|
|
910
|
+
file.isFile() &&
|
|
911
|
+
fileName.startsWith(prefix) &&
|
|
912
|
+
!fileName.equals(iconFileName) &&
|
|
913
|
+
!fileName.equals(metadataFileName) &&
|
|
914
|
+
file.delete()
|
|
915
|
+
) {
|
|
916
|
+
deleted++;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return deleted;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private String portablePath(File file) {
|
|
923
|
+
String host = bridge == null ? null : bridge.getLocalUrl();
|
|
924
|
+
if (host == null || host.isEmpty()) {
|
|
925
|
+
return Uri.fromFile(file).toString();
|
|
926
|
+
}
|
|
927
|
+
return FileUtils.getPortablePath(context, host, Uri.fromFile(file));
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
private long getMaxAgeMs(JSObject options) {
|
|
931
|
+
if (!options.has("maxAgeMs")) {
|
|
932
|
+
return DEFAULT_ICON_CACHE_MAX_AGE_MS;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
double maxAgeMs = options.optDouble("maxAgeMs", DEFAULT_ICON_CACHE_MAX_AGE_MS);
|
|
936
|
+
if (Double.isNaN(maxAgeMs) || maxAgeMs < 0) {
|
|
937
|
+
return DEFAULT_ICON_CACHE_MAX_AGE_MS;
|
|
938
|
+
}
|
|
939
|
+
return (long) maxAgeMs;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
private IconProvider[] resolveIconProviders(JSObject options) {
|
|
943
|
+
Map<String, IconProvider> providers = new LinkedHashMap<>();
|
|
944
|
+
|
|
945
|
+
for (Map.Entry<String, AppInfo> entry : navigationApps.entrySet()) {
|
|
946
|
+
providers.put(entry.getKey(), new IconProvider(entry.getKey(), entry.getValue().name, entry.getValue().url, null));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
JSONArray customProviders = options.optJSONArray("providers");
|
|
950
|
+
if (customProviders != null) {
|
|
951
|
+
for (int i = 0; i < customProviders.length(); i++) {
|
|
952
|
+
JSONObject providerObject = customProviders.optJSONObject(i);
|
|
953
|
+
if (providerObject == null) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
String app = providerObject.optString("app", "");
|
|
958
|
+
if (app.isEmpty()) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
IconProvider existing = providers.get(app);
|
|
963
|
+
String name = providerObject.optString("name", existing == null ? null : existing.name);
|
|
964
|
+
String url = providerObject.optString("url", existing == null ? null : existing.url);
|
|
965
|
+
String iconUrl = providerObject.optString("iconUrl", existing == null ? null : existing.iconUrl);
|
|
966
|
+
providers.put(app, new IconProvider(app, name, url, iconUrl));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
JSONArray apps = options.optJSONArray("apps");
|
|
971
|
+
if (apps != null && apps.length() > 0) {
|
|
972
|
+
IconProvider[] selected = new IconProvider[apps.length()];
|
|
973
|
+
for (int i = 0; i < apps.length(); i++) {
|
|
974
|
+
String app = apps.optString(i, "");
|
|
975
|
+
IconProvider provider = providers.get(app);
|
|
976
|
+
selected[i] = provider == null ? new IconProvider(app, null, null, null) : provider;
|
|
977
|
+
}
|
|
978
|
+
return selected;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return providers.values().toArray(new IconProvider[0]);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private String normalizeMimeType(String mimeType) {
|
|
985
|
+
if (mimeType == null) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
return mimeType.split(";")[0].trim().toLowerCase(Locale.US);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
private String guessExtension(String mimeType, String sourceUrl) {
|
|
992
|
+
if ("image/jpeg".equals(mimeType)) {
|
|
993
|
+
return ".jpg";
|
|
994
|
+
} else if ("image/png".equals(mimeType)) {
|
|
995
|
+
return ".png";
|
|
996
|
+
} else if ("image/gif".equals(mimeType)) {
|
|
997
|
+
return ".gif";
|
|
998
|
+
} else if ("image/webp".equals(mimeType)) {
|
|
999
|
+
return ".webp";
|
|
1000
|
+
} else if ("image/svg+xml".equals(mimeType)) {
|
|
1001
|
+
return ".svg";
|
|
1002
|
+
} else if ("image/x-icon".equals(mimeType) || "image/vnd.microsoft.icon".equals(mimeType)) {
|
|
1003
|
+
return ".ico";
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
String lowerPath = pathForExtension(sourceUrl).toLowerCase(Locale.US);
|
|
1007
|
+
if (lowerPath.endsWith(".png")) {
|
|
1008
|
+
return ".png";
|
|
1009
|
+
} else if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) {
|
|
1010
|
+
return ".jpg";
|
|
1011
|
+
} else if (lowerPath.endsWith(".gif")) {
|
|
1012
|
+
return ".gif";
|
|
1013
|
+
} else if (lowerPath.endsWith(".webp")) {
|
|
1014
|
+
return ".webp";
|
|
1015
|
+
} else if (lowerPath.endsWith(".svg")) {
|
|
1016
|
+
return ".svg";
|
|
1017
|
+
} else if (lowerPath.endsWith(".ico")) {
|
|
1018
|
+
return ".ico";
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return ".img";
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private String pathForExtension(String sourceUrl) {
|
|
1025
|
+
try {
|
|
1026
|
+
return new URL(sourceUrl).getPath();
|
|
1027
|
+
} catch (Exception e) {
|
|
1028
|
+
int queryIndex = sourceUrl.indexOf('?');
|
|
1029
|
+
int fragmentIndex = sourceUrl.indexOf('#');
|
|
1030
|
+
int endIndex = sourceUrl.length();
|
|
1031
|
+
if (queryIndex >= 0) {
|
|
1032
|
+
endIndex = Math.min(endIndex, queryIndex);
|
|
1033
|
+
}
|
|
1034
|
+
if (fragmentIndex >= 0) {
|
|
1035
|
+
endIndex = Math.min(endIndex, fragmentIndex);
|
|
1036
|
+
}
|
|
1037
|
+
return sourceUrl.substring(0, endIndex);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private String cacheKey(String app) {
|
|
1042
|
+
String safeApp = app.replaceAll("[^A-Za-z0-9._-]", "_");
|
|
1043
|
+
String hash = hashed(app);
|
|
1044
|
+
return safeApp + "_" + hash.substring(0, Math.min(8, hash.length()));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
private String hashed(String input) {
|
|
1048
|
+
try {
|
|
1049
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
1050
|
+
byte[] encoded = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
|
1051
|
+
StringBuilder sb = new StringBuilder();
|
|
1052
|
+
for (byte b : encoded) {
|
|
1053
|
+
sb.append(String.format(Locale.US, "%02x", b));
|
|
1054
|
+
}
|
|
1055
|
+
return sb.toString();
|
|
1056
|
+
} catch (NoSuchAlgorithmException e) {
|
|
1057
|
+
return String.valueOf(input.hashCode());
|
|
282
1058
|
}
|
|
283
1059
|
}
|
|
284
1060
|
|