@capgo/cordova-updater 8.1.1
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/LICENSE +373 -0
- package/README.md +82 -0
- package/dist/esm/definitions.d.ts +2684 -0
- package/dist/esm/definitions.js +109 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/exec.d.ts +4 -0
- package/dist/esm/exec.js +25 -0
- package/dist/esm/exec.js.map +1 -0
- package/dist/esm/history.d.ts +1 -0
- package/dist/esm/history.js +283 -0
- package/dist/esm/history.js.map +1 -0
- package/dist/esm/index.d.ts +57 -0
- package/dist/esm/index.js +68 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +57 -0
- package/dist/esm/plugin.js +15 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/plugin.cjs.js +217 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +222 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +96 -0
- package/plugin.xml +207 -0
- package/src/android/CordovaUpdaterPlugin.java +5662 -0
- package/src/android/UpdaterPathHandler.java +99 -0
- package/src/android/app/capgo/cordova/updater/AndroidAppExitReporter.java +92 -0
- package/src/android/app/capgo/cordova/updater/AppLifecycleObserver.java +115 -0
- package/src/android/app/capgo/cordova/updater/BundleInfo.java +246 -0
- package/src/android/app/capgo/cordova/updater/BundleStatus.java +45 -0
- package/src/android/app/capgo/cordova/updater/Callback.java +13 -0
- package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +3404 -0
- package/src/android/app/capgo/cordova/updater/CordovaUpdaterConfig.java +56 -0
- package/src/android/app/capgo/cordova/updater/CryptoCipher.java +462 -0
- package/src/android/app/capgo/cordova/updater/DataManager.java +44 -0
- package/src/android/app/capgo/cordova/updater/DelayCondition.java +56 -0
- package/src/android/app/capgo/cordova/updater/DelayUntilNext.java +14 -0
- package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +307 -0
- package/src/android/app/capgo/cordova/updater/DeviceIdHelper.java +230 -0
- package/src/android/app/capgo/cordova/updater/DownloadService.java +1219 -0
- package/src/android/app/capgo/cordova/updater/DownloadWorkerManager.java +260 -0
- package/src/android/app/capgo/cordova/updater/InternalUtils.java +54 -0
- package/src/android/app/capgo/cordova/updater/Logger.java +345 -0
- package/src/android/app/capgo/cordova/updater/ShakeDetector.java +72 -0
- package/src/android/app/capgo/cordova/updater/ShakeMenu.java +39 -0
- package/src/android/app/capgo/cordova/updater/ThreeFingerPinchDetector.java +323 -0
- package/src/android/app/capgo/cordova/updater/compat/JSArray.java +9 -0
- package/src/android/app/capgo/cordova/updater/compat/JSObject.java +83 -0
- package/src/android/app/capgo/cordova/updater/compat/PluginCall.java +165 -0
- package/src/android/capgo-cordova-updater.gradle +19 -0
- package/src/android/capgo-updater.xml +3 -0
- package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +3039 -0
- package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +177 -0
- package/src/android/test/app/capgo/cordova/updater/NativeContractTest.java +105 -0
- package/src/ios/AES.swift +216 -0
- package/src/ios/AppHealthTracker.swift +82 -0
- package/src/ios/BundleInfo.swift +185 -0
- package/src/ios/BundleStatus.swift +127 -0
- package/src/ios/CapgoRawRsa.swift +36 -0
- package/src/ios/CapgoUpdater.swift +3544 -0
- package/src/ios/CordovaCompat.swift +11 -0
- package/src/ios/CordovaPluginCall.swift +190 -0
- package/src/ios/CordovaUpdaterCommands.swift +270 -0
- package/src/ios/CordovaUpdaterPlugin.swift +4706 -0
- package/src/ios/CryptoCipher.swift +326 -0
- package/src/ios/DelayCondition.swift +74 -0
- package/src/ios/DelayUntilNext.swift +30 -0
- package/src/ios/DelayUpdateUtils.swift +257 -0
- package/src/ios/DeviceIdHelper.swift +120 -0
- package/src/ios/Info.plist +28 -0
- package/src/ios/InternalUtils.swift +471 -0
- package/src/ios/Logger.swift +283 -0
- package/src/ios/RSA.swift +96 -0
- package/src/ios/ShakeMenu.swift +52 -0
- package/src/ios/UpdaterSchemeHandler.swift +81 -0
- package/src/ios/UserDefaultsExtension.swift +46 -0
- package/src/ios/WebViewStatsReporter.swift +303 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
package app.capgo.cordova.updater;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.os.Build;
|
|
5
|
+
import androidx.work.BackoffPolicy;
|
|
6
|
+
import androidx.work.Configuration;
|
|
7
|
+
import androidx.work.Constraints;
|
|
8
|
+
import androidx.work.Data;
|
|
9
|
+
import androidx.work.ExistingWorkPolicy;
|
|
10
|
+
import androidx.work.NetworkType;
|
|
11
|
+
import androidx.work.OneTimeWorkRequest;
|
|
12
|
+
import androidx.work.OutOfQuotaPolicy;
|
|
13
|
+
import androidx.work.WorkInfo;
|
|
14
|
+
import androidx.work.WorkManager;
|
|
15
|
+
import androidx.work.WorkRequest;
|
|
16
|
+
import java.util.HashSet;
|
|
17
|
+
import java.util.List;
|
|
18
|
+
import java.util.Set;
|
|
19
|
+
import java.util.concurrent.ExecutorService;
|
|
20
|
+
import java.util.concurrent.Executors;
|
|
21
|
+
import java.util.concurrent.Future;
|
|
22
|
+
import java.util.concurrent.TimeUnit;
|
|
23
|
+
import java.util.concurrent.TimeoutException;
|
|
24
|
+
|
|
25
|
+
public class DownloadWorkerManager {
|
|
26
|
+
|
|
27
|
+
private static Logger logger;
|
|
28
|
+
|
|
29
|
+
public static void setLogger(Logger loggerInstance) {
|
|
30
|
+
logger = loggerInstance;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private static volatile boolean isInitialized = false;
|
|
34
|
+
private static final ExecutorService cancelExecutor = Executors.newSingleThreadExecutor();
|
|
35
|
+
|
|
36
|
+
private static synchronized void initializeIfNeeded(Context context) {
|
|
37
|
+
if (!isInitialized) {
|
|
38
|
+
try {
|
|
39
|
+
Configuration config = new Configuration.Builder().setMinimumLoggingLevel(android.util.Log.INFO).build();
|
|
40
|
+
WorkManager.initialize(context, config);
|
|
41
|
+
isInitialized = true;
|
|
42
|
+
} catch (IllegalStateException e) {
|
|
43
|
+
// WorkManager was already initialized, ignore
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static boolean isVersionDownloading(Context context, String version) {
|
|
49
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
50
|
+
try {
|
|
51
|
+
return WorkManager.getInstance(context)
|
|
52
|
+
.getWorkInfosByTag(version)
|
|
53
|
+
.get()
|
|
54
|
+
.stream()
|
|
55
|
+
.anyMatch((workInfo) -> !workInfo.getState().isFinished());
|
|
56
|
+
} catch (Exception e) {
|
|
57
|
+
logger.error("Error checking download status: " + e.getMessage());
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static void enqueueDownload(
|
|
63
|
+
Context context,
|
|
64
|
+
String url,
|
|
65
|
+
String id,
|
|
66
|
+
String documentsDir,
|
|
67
|
+
String dest,
|
|
68
|
+
String version,
|
|
69
|
+
String sessionKey,
|
|
70
|
+
String checksum,
|
|
71
|
+
String publicKey,
|
|
72
|
+
boolean isManifest,
|
|
73
|
+
boolean isEmulator,
|
|
74
|
+
String appId,
|
|
75
|
+
String pluginVersion,
|
|
76
|
+
boolean isProd,
|
|
77
|
+
String installSource,
|
|
78
|
+
String statsUrl,
|
|
79
|
+
String deviceId,
|
|
80
|
+
String versionBuild,
|
|
81
|
+
String versionCode,
|
|
82
|
+
String versionOs,
|
|
83
|
+
String customId,
|
|
84
|
+
String defaultChannel
|
|
85
|
+
) {
|
|
86
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
87
|
+
|
|
88
|
+
// Use unique work name for this bundle to prevent duplicates
|
|
89
|
+
String uniqueWorkName = "bundle_" + id + "_" + version;
|
|
90
|
+
|
|
91
|
+
// Create input data
|
|
92
|
+
Data inputData = new Data.Builder()
|
|
93
|
+
.putString(DownloadService.URL, url)
|
|
94
|
+
.putString(DownloadService.ID, id)
|
|
95
|
+
.putString(DownloadService.DOCDIR, documentsDir)
|
|
96
|
+
.putString(DownloadService.FILEDEST, dest)
|
|
97
|
+
.putString(DownloadService.VERSION, version)
|
|
98
|
+
.putString(DownloadService.SESSIONKEY, sessionKey)
|
|
99
|
+
.putString(DownloadService.CHECKSUM, checksum)
|
|
100
|
+
.putBoolean(DownloadService.IS_MANIFEST, isManifest)
|
|
101
|
+
.putString(DownloadService.PUBLIC_KEY, publicKey)
|
|
102
|
+
.putString(DownloadService.APP_ID, appId)
|
|
103
|
+
.putString(DownloadService.pluginVersion, pluginVersion)
|
|
104
|
+
.putString(DownloadService.INSTALL_SOURCE, installSource)
|
|
105
|
+
.putString(DownloadService.STATS_URL, statsUrl)
|
|
106
|
+
.putString(DownloadService.DEVICE_ID, deviceId)
|
|
107
|
+
.putString(DownloadService.VERSION_BUILD, versionBuild)
|
|
108
|
+
.putString(DownloadService.VERSION_CODE, versionCode)
|
|
109
|
+
.putString(DownloadService.VERSION_OS, versionOs)
|
|
110
|
+
.putString(DownloadService.CUSTOM_ID, customId)
|
|
111
|
+
.putString(DownloadService.DEFAULT_CHANNEL, defaultChannel)
|
|
112
|
+
.putBoolean(DownloadService.IS_PROD, isProd)
|
|
113
|
+
.putBoolean(DownloadService.IS_EMULATOR, isEmulator)
|
|
114
|
+
.build();
|
|
115
|
+
|
|
116
|
+
// Create network constraints - be more lenient on emulators
|
|
117
|
+
Constraints.Builder constraintsBuilder = new Constraints.Builder();
|
|
118
|
+
if (isEmulator) {
|
|
119
|
+
logger.info("Emulator detected - using lenient network constraints");
|
|
120
|
+
// On emulators, use NOT_REQUIRED to avoid background network issues
|
|
121
|
+
constraintsBuilder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
|
|
122
|
+
} else {
|
|
123
|
+
constraintsBuilder.setRequiredNetworkType(NetworkType.CONNECTED);
|
|
124
|
+
}
|
|
125
|
+
Constraints constraints = constraintsBuilder.build();
|
|
126
|
+
|
|
127
|
+
// Create work request with tags for tracking
|
|
128
|
+
OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(DownloadService.class)
|
|
129
|
+
.setConstraints(constraints)
|
|
130
|
+
.setInputData(inputData)
|
|
131
|
+
.addTag(id)
|
|
132
|
+
.addTag(version)
|
|
133
|
+
.addTag("capacitor_updater_download");
|
|
134
|
+
// Android 12+ expedited jobs skip the WorkManager delay without a
|
|
135
|
+
// foreground service. Older APIs require getForegroundInfo().
|
|
136
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
137
|
+
workRequestBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// More aggressive retry policy for emulators
|
|
141
|
+
if (isEmulator) {
|
|
142
|
+
workRequestBuilder.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS);
|
|
143
|
+
} else {
|
|
144
|
+
workRequestBuilder.setBackoffCriteria(BackoffPolicy.LINEAR, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
OneTimeWorkRequest workRequest = workRequestBuilder.build();
|
|
148
|
+
|
|
149
|
+
// Use beginUniqueWork to prevent duplicate downloads
|
|
150
|
+
WorkManager.getInstance(context)
|
|
151
|
+
.beginUniqueWork(
|
|
152
|
+
uniqueWorkName,
|
|
153
|
+
ExistingWorkPolicy.KEEP, // Don't start if already running
|
|
154
|
+
workRequest
|
|
155
|
+
)
|
|
156
|
+
.enqueue();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public static void cancelVersionDownload(Context context, String version) {
|
|
160
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
161
|
+
cancelExecutor.execute(() -> cancelVersionDownloadInternal(context, version, false));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public static boolean cancelVersionDownloadAndAwait(Context context, String version) {
|
|
165
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
166
|
+
Future<?> future = cancelExecutor.submit(() -> cancelVersionDownloadInternal(context, version, true));
|
|
167
|
+
try {
|
|
168
|
+
future.get(10, TimeUnit.SECONDS);
|
|
169
|
+
return true;
|
|
170
|
+
} catch (TimeoutException e) {
|
|
171
|
+
future.cancel(true);
|
|
172
|
+
logger.error("Timed out awaiting version download cancel");
|
|
173
|
+
return false;
|
|
174
|
+
} catch (Exception e) {
|
|
175
|
+
logger.error("Error awaiting version download cancel: " + e.getMessage());
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private static Set<String> collectManifestIdsForVersion(WorkManager workManager, String version) {
|
|
181
|
+
Set<String> downloadIds = new HashSet<>();
|
|
182
|
+
try {
|
|
183
|
+
List<WorkInfo> workInfos = workManager.getWorkInfosByTag(version).get(5, TimeUnit.SECONDS);
|
|
184
|
+
for (WorkInfo workInfo : workInfos) {
|
|
185
|
+
for (String tag : workInfo.getTags()) {
|
|
186
|
+
if (!"capacitor_updater_download".equals(tag) && !version.equals(tag)) {
|
|
187
|
+
downloadIds.add(tag);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch (TimeoutException e) {
|
|
192
|
+
logger.error("Timed out collecting manifest ids before version cancel");
|
|
193
|
+
} catch (InterruptedException e) {
|
|
194
|
+
Thread.currentThread().interrupt();
|
|
195
|
+
throw new IllegalStateException("Interrupted while collecting manifest ids before version cancel", e);
|
|
196
|
+
} catch (Exception e) {
|
|
197
|
+
logger.error("Error collecting manifest ids before version cancel: " + e.getMessage());
|
|
198
|
+
}
|
|
199
|
+
return downloadIds;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private static void clearManifestIds(Set<String> downloadIds) {
|
|
203
|
+
for (String downloadId : downloadIds) {
|
|
204
|
+
DataManager.getInstance().clearManifest(downloadId);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private static void cancelVersionDownloadInternal(Context context, String version, boolean awaitFinished) {
|
|
209
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
213
|
+
Set<String> downloadIds = collectManifestIdsForVersion(workManager, version);
|
|
214
|
+
workManager.cancelAllWorkByTag(version);
|
|
215
|
+
clearManifestIds(downloadIds);
|
|
216
|
+
if (awaitFinished) {
|
|
217
|
+
awaitVersionWorkFinished(workManager, version);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static void awaitVersionWorkFinished(WorkManager workManager, String version) {
|
|
222
|
+
for (int i = 0; i < 100; i++) {
|
|
223
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
224
|
+
throw new IllegalStateException("Interrupted while waiting for version download cancel");
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
boolean anyActive = workManager
|
|
228
|
+
.getWorkInfosByTag(version)
|
|
229
|
+
.get()
|
|
230
|
+
.stream()
|
|
231
|
+
.anyMatch((workInfo) -> !workInfo.getState().isFinished());
|
|
232
|
+
if (!anyActive) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
Thread.sleep(100);
|
|
236
|
+
} catch (InterruptedException e) {
|
|
237
|
+
Thread.currentThread().interrupt();
|
|
238
|
+
throw new IllegalStateException("Interrupted while waiting for download cancel", e);
|
|
239
|
+
} catch (Exception e) {
|
|
240
|
+
throw new IllegalStateException("Error waiting for download cancel: " + e.getMessage(), e);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
throw new IllegalStateException("Timed out waiting for version download cancel: " + version);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public static void cancelBundleDownload(Context context, String id, String version) {
|
|
247
|
+
String uniqueWorkName = "bundle_" + id + "_" + version;
|
|
248
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
249
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
250
|
+
workManager.cancelUniqueWork(uniqueWorkName);
|
|
251
|
+
DataManager.getInstance().clearManifest(id);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public static void cancelAllDownloads(Context context) {
|
|
255
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
256
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
257
|
+
workManager.cancelAllWorkByTag("capacitor_updater_download");
|
|
258
|
+
DataManager.getInstance().clearAllManifests();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package app.capgo.cordova.updater;
|
|
2
|
+
|
|
3
|
+
import android.content.pm.PackageInfo;
|
|
4
|
+
import android.content.pm.PackageManager;
|
|
5
|
+
import android.os.Build;
|
|
6
|
+
import app.capgo.cordova.updater.compat.JSObject;
|
|
7
|
+
import org.json.JSONObject;
|
|
8
|
+
import java.util.Map;
|
|
9
|
+
|
|
10
|
+
public class InternalUtils {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Converts a Map to JSONObject for proper bridge serialization.
|
|
14
|
+
*/
|
|
15
|
+
public static JSObject mapToJSObject(Map<String, Object> map) {
|
|
16
|
+
JSObject jsObject = new JSObject();
|
|
17
|
+
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
|
18
|
+
jsObject.put(entry.getKey(), entry.getValue());
|
|
19
|
+
}
|
|
20
|
+
return jsObject;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static JSONObject mapToJSONObject(Map<String, Object> map) {
|
|
24
|
+
JSONObject jsObject = new JSONObject();
|
|
25
|
+
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
|
26
|
+
try { jsObject.put(entry.getKey(), entry.getValue()); } catch (org.json.JSONException ignored) {}
|
|
27
|
+
}
|
|
28
|
+
return jsObject;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public static String getPackageName(PackageManager pm, String packageName) {
|
|
32
|
+
try {
|
|
33
|
+
PackageInfo pInfo = getPackageInfoInternal(pm, packageName);
|
|
34
|
+
return (pInfo != null) ? pInfo.packageName : null;
|
|
35
|
+
} catch (PackageManager.NameNotFoundException e) {
|
|
36
|
+
// Exception is handled internally, and null is returned to indicate the package name could not be retrieved
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private static PackageInfo getPackageInfoInternal(PackageManager pm, String packageName) throws PackageManager.NameNotFoundException {
|
|
42
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
43
|
+
return pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0));
|
|
44
|
+
} else {
|
|
45
|
+
return getPackageInfoLegacy(pm, packageName, (int) (long) 0);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@SuppressWarnings("deprecation")
|
|
50
|
+
private static PackageInfo getPackageInfoLegacy(PackageManager pm, String packageName, int flags)
|
|
51
|
+
throws PackageManager.NameNotFoundException {
|
|
52
|
+
return pm.getPackageInfo(packageName, flags);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
package app.capgo.cordova.updater;
|
|
2
|
+
|
|
3
|
+
import android.util.ArrayMap;
|
|
4
|
+
import android.util.Log;
|
|
5
|
+
import androidx.annotation.NonNull;
|
|
6
|
+
import androidx.annotation.Nullable;
|
|
7
|
+
import java.util.ArrayList;
|
|
8
|
+
import java.util.Arrays;
|
|
9
|
+
import java.util.Date;
|
|
10
|
+
import java.util.Locale;
|
|
11
|
+
import java.util.Map;
|
|
12
|
+
import org.jetbrains.annotations.Contract;
|
|
13
|
+
|
|
14
|
+
public class Logger {
|
|
15
|
+
|
|
16
|
+
public enum LogLevel {
|
|
17
|
+
silent,
|
|
18
|
+
error,
|
|
19
|
+
warn,
|
|
20
|
+
info,
|
|
21
|
+
debug
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public static class Options {
|
|
25
|
+
|
|
26
|
+
LogLevel level;
|
|
27
|
+
Map<String, String> labels;
|
|
28
|
+
boolean useSystemLog;
|
|
29
|
+
|
|
30
|
+
Options() {
|
|
31
|
+
level = LogLevel.info;
|
|
32
|
+
labels = null;
|
|
33
|
+
useSystemLog = true; // Default to true for backward compatibility
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Options(@NonNull Options other) {
|
|
37
|
+
level = other.level;
|
|
38
|
+
labels = other.labels;
|
|
39
|
+
useSystemLog = other.useSystemLog;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Options(LogLevel level) {
|
|
43
|
+
this.level = level;
|
|
44
|
+
this.labels = null;
|
|
45
|
+
useSystemLog = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Options(Map<String, String> labels) {
|
|
49
|
+
this.level = LogLevel.info;
|
|
50
|
+
this.labels = labels;
|
|
51
|
+
useSystemLog = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Options(LogLevel level, Map<String, String> labels) {
|
|
55
|
+
this.level = level;
|
|
56
|
+
this.labels = labels;
|
|
57
|
+
useSystemLog = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Options(LogLevel level, Map<String, String> labels, boolean useSystemLog) {
|
|
61
|
+
this.level = level;
|
|
62
|
+
this.labels = labels;
|
|
63
|
+
this.useSystemLog = useSystemLog;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Options(boolean useSystemLog) {
|
|
67
|
+
this.level = LogLevel.info;
|
|
68
|
+
this.labels = null;
|
|
69
|
+
this.useSystemLog = useSystemLog;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public LogLevel level;
|
|
74
|
+
private final Map<LogLevel, String> labels = new ArrayMap<>();
|
|
75
|
+
private String tag;
|
|
76
|
+
private final ArrayMap<String, Long> timers = new ArrayMap<>();
|
|
77
|
+
private final String kDefaultTimerLabel = "default";
|
|
78
|
+
private boolean useSystemLog;
|
|
79
|
+
|
|
80
|
+
public Logger(String tag) {
|
|
81
|
+
super();
|
|
82
|
+
init(tag, new Options());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public Logger(String tag, Options options) {
|
|
86
|
+
super();
|
|
87
|
+
init(tag, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private void init(String tag, @NonNull Options options) {
|
|
91
|
+
this.level = options.level;
|
|
92
|
+
this.useSystemLog = options.useSystemLog;
|
|
93
|
+
this.labels.putAll(
|
|
94
|
+
Map.of(LogLevel.silent, "", LogLevel.error, "🔴", LogLevel.warn, "🟠", LogLevel.info, "🟢", LogLevel.debug, "\uD83D\uDD0E")
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (options.labels != null) {
|
|
98
|
+
setLabels(options.labels);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.tag = tag;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Nullable
|
|
105
|
+
public Object getLevelWithName(String name) {
|
|
106
|
+
try {
|
|
107
|
+
return LogLevel.valueOf(name);
|
|
108
|
+
} catch (IllegalArgumentException ex) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public String getLevelName() {
|
|
114
|
+
return level.name();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public void setLevelName(String name) {
|
|
118
|
+
try {
|
|
119
|
+
level = LogLevel.valueOf(name);
|
|
120
|
+
} catch (IllegalArgumentException e) {
|
|
121
|
+
// Ignore it
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public Map<String, String> getLabels() {
|
|
126
|
+
ArrayMap<String, String> result = new ArrayMap<>();
|
|
127
|
+
|
|
128
|
+
for (Map.Entry<LogLevel, String> entry : labels.entrySet()) {
|
|
129
|
+
result.put(entry.getKey().name(), entry.getValue());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public void setLabels(@NonNull Map<String, String> labels) {
|
|
136
|
+
for (Map.Entry<String, String> entry : labels.entrySet()) {
|
|
137
|
+
Object level = getLevelWithName(entry.getKey());
|
|
138
|
+
|
|
139
|
+
if (level != null) {
|
|
140
|
+
this.labels.put((LogLevel) level, entry.getValue());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public String getTag() {
|
|
146
|
+
return tag;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public void setTag(@NonNull String tag) {
|
|
150
|
+
if (!tag.isEmpty()) {
|
|
151
|
+
this.tag = tag;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public void error(String message) {
|
|
156
|
+
logWithTagAtLevel(LogLevel.error, "", tag, message);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public void warn(String message) {
|
|
160
|
+
logWithTagAtLevel(LogLevel.warn, "", tag, message);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public void info(String message) {
|
|
164
|
+
logWithTagAtLevel(LogLevel.info, "", tag, message);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public void log(String message) {
|
|
168
|
+
info(message);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public void debug(String message) {
|
|
172
|
+
logWithTagAtLevel(LogLevel.debug, "", tag, message);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public void logAtLevel(LogLevel level, String message) {
|
|
176
|
+
logWithTagAtLevel(level, "", tag, message);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public void logAtLevel(String level, String message) {
|
|
180
|
+
logWithTagAtLevel(resolveLevelName(level), "", tag, message);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public void logWithTagAtLevel(LogLevel level, String tag, String message) {
|
|
184
|
+
logWithTagAtLevel(level, "", tag, message);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public void logWithTagAtLevel(String level, String tag, String message) {
|
|
188
|
+
logWithTagAtLevel(resolveLevelName(level), "", tag, message);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private LogLevel resolveLevelName(String name) {
|
|
192
|
+
Object level = getLevelWithName(name);
|
|
193
|
+
|
|
194
|
+
if (level != null) {
|
|
195
|
+
return (LogLevel) level;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return LogLevel.info;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public void logWithTagAtLevel(LogLevel level, String label, String tag, String message) {
|
|
202
|
+
if (this.level.compareTo(level) < 0) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (label.isEmpty()) {
|
|
207
|
+
label = labels.get(level);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// If the label is ASCII, surround it with []
|
|
211
|
+
String format;
|
|
212
|
+
|
|
213
|
+
if (label != null) {
|
|
214
|
+
format = label.charAt(0) <= 127 ? "[%s]: %s" : "%s %s";
|
|
215
|
+
} else {
|
|
216
|
+
label = "";
|
|
217
|
+
format = "%s%s";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
String formattedMessage = String.format(format, label, message);
|
|
221
|
+
|
|
222
|
+
if (tag.isEmpty()) {
|
|
223
|
+
tag = this.tag;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Log to Android system log if enabled
|
|
227
|
+
if (useSystemLog) {
|
|
228
|
+
switch (level) {
|
|
229
|
+
case error:
|
|
230
|
+
Log.e(tag, formattedMessage);
|
|
231
|
+
break;
|
|
232
|
+
case warn:
|
|
233
|
+
Log.w(tag, formattedMessage);
|
|
234
|
+
break;
|
|
235
|
+
case info:
|
|
236
|
+
Log.i(tag, formattedMessage);
|
|
237
|
+
break;
|
|
238
|
+
case debug:
|
|
239
|
+
Log.d(tag, formattedMessage);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
public void dir(Object value) {
|
|
246
|
+
String message;
|
|
247
|
+
|
|
248
|
+
if (value != null) {
|
|
249
|
+
if (value.getClass().isArray()) {
|
|
250
|
+
Object[] arr = (Object[]) value;
|
|
251
|
+
message = Arrays.deepToString(arr);
|
|
252
|
+
} else {
|
|
253
|
+
try {
|
|
254
|
+
message = value.toString();
|
|
255
|
+
} catch (Exception ex) {
|
|
256
|
+
message = String.format("<%s>", ex.getMessage());
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
message = "null";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
info(message);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
public void time() {
|
|
267
|
+
time(kDefaultTimerLabel);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public void time(String label) {
|
|
271
|
+
timers.put(label, new Date().getTime());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public void timeLog() {
|
|
275
|
+
timeLog(kDefaultTimerLabel);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public void timeLog(String label) {
|
|
279
|
+
label = resolveTimerLabel(label);
|
|
280
|
+
|
|
281
|
+
if (timers.containsKey(label)) {
|
|
282
|
+
Long start = timers.get(label);
|
|
283
|
+
|
|
284
|
+
// This will always be true
|
|
285
|
+
if (start != null) {
|
|
286
|
+
long now = new Date().getTime();
|
|
287
|
+
long diff = now - start;
|
|
288
|
+
info(String.format(Locale.getDefault(), "%s: %s", label, formatMilliseconds(diff)));
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
warn(String.format("timer '%s' does not exist", label));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public void timeEnd() {
|
|
296
|
+
timeEnd(kDefaultTimerLabel);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public void timeEnd(String label) {
|
|
300
|
+
label = resolveTimerLabel(label);
|
|
301
|
+
timeLog(label);
|
|
302
|
+
timers.remove(label);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@Contract(pure = true)
|
|
306
|
+
private String resolveTimerLabel(@NonNull String label) {
|
|
307
|
+
return label.isEmpty() ? kDefaultTimerLabel : label;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@NonNull
|
|
311
|
+
private String formatMilliseconds(long milliseconds) {
|
|
312
|
+
long seconds = Math.floorDiv(milliseconds, 1000);
|
|
313
|
+
long minutes = Math.floorDiv(seconds, 60);
|
|
314
|
+
long hours = Math.floorDiv(minutes, 60);
|
|
315
|
+
long millis = milliseconds % 1000;
|
|
316
|
+
|
|
317
|
+
if (seconds < 1) {
|
|
318
|
+
return String.format(Locale.getDefault(), "%dms", milliseconds);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (minutes < 1) {
|
|
322
|
+
return String.format(Locale.getDefault(), "%d.%ds", seconds, millis);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
seconds = seconds % 60;
|
|
326
|
+
|
|
327
|
+
if (hours < 1) {
|
|
328
|
+
return String.format(Locale.getDefault(), "%d:%02d.%03d (min:sec.ms)", minutes, seconds, millis);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
minutes = minutes % 60;
|
|
332
|
+
return String.format(Locale.getDefault(), "%d:%02d:%02d (hr:min:sec)", hours, minutes, seconds);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public void trace() {
|
|
336
|
+
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
|
337
|
+
ArrayList<String> stack = new ArrayList<>();
|
|
338
|
+
|
|
339
|
+
for (StackTraceElement element : stackTrace) {
|
|
340
|
+
stack.add(element.toString());
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
info("trace\n" + String.join("\n", stack));
|
|
344
|
+
}
|
|
345
|
+
}
|