@capgo/capacitor-updater 5.50.2 → 5.51.15
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/CapgoCapacitorUpdater.podspec +1 -1
- package/Package.swift +3 -2
- package/README.md +53 -48
- package/android/build.gradle +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +29 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +452 -139
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1354 -412
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +102 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +23 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +540 -224
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +103 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +131 -145
- package/dist/docs.json +32 -8
- package/dist/esm/definitions.d.ts +41 -17
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +124 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +9 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +787 -92
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1014 -268
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +49 -31
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +44 -20
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +28 -0
- package/package.json +12 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package ee.forgr.capacitor_updater;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
+
import android.os.Build;
|
|
4
5
|
import androidx.work.BackoffPolicy;
|
|
5
6
|
import androidx.work.Configuration;
|
|
6
7
|
import androidx.work.Constraints;
|
|
@@ -8,9 +9,18 @@ import androidx.work.Data;
|
|
|
8
9
|
import androidx.work.ExistingWorkPolicy;
|
|
9
10
|
import androidx.work.NetworkType;
|
|
10
11
|
import androidx.work.OneTimeWorkRequest;
|
|
12
|
+
import androidx.work.OutOfQuotaPolicy;
|
|
13
|
+
import androidx.work.WorkInfo;
|
|
11
14
|
import androidx.work.WorkManager;
|
|
12
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;
|
|
13
22
|
import java.util.concurrent.TimeUnit;
|
|
23
|
+
import java.util.concurrent.TimeoutException;
|
|
14
24
|
|
|
15
25
|
public class DownloadWorkerManager {
|
|
16
26
|
|
|
@@ -21,6 +31,7 @@ public class DownloadWorkerManager {
|
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
private static volatile boolean isInitialized = false;
|
|
34
|
+
private static final ExecutorService cancelExecutor = Executors.newSingleThreadExecutor();
|
|
24
35
|
|
|
25
36
|
private static synchronized void initializeIfNeeded(Context context) {
|
|
26
37
|
if (!isInitialized) {
|
|
@@ -120,6 +131,11 @@ public class DownloadWorkerManager {
|
|
|
120
131
|
.addTag(id)
|
|
121
132
|
.addTag(version)
|
|
122
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
|
+
}
|
|
123
139
|
|
|
124
140
|
// More aggressive retry policy for emulators
|
|
125
141
|
if (isEmulator) {
|
|
@@ -142,17 +158,101 @@ public class DownloadWorkerManager {
|
|
|
142
158
|
|
|
143
159
|
public static void cancelVersionDownload(Context context, String version) {
|
|
144
160
|
initializeIfNeeded(context.getApplicationContext());
|
|
145
|
-
|
|
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();
|
|
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 (InterruptedException e) {
|
|
192
|
+
Thread.currentThread().interrupt();
|
|
193
|
+
throw new IllegalStateException("Interrupted while collecting manifest ids before version cancel", e);
|
|
194
|
+
} catch (Exception e) {
|
|
195
|
+
logger.error("Error collecting manifest ids before version cancel: " + e.getMessage());
|
|
196
|
+
}
|
|
197
|
+
return downloadIds;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private static void clearManifestIds(Set<String> downloadIds) {
|
|
201
|
+
for (String downloadId : downloadIds) {
|
|
202
|
+
DataManager.getInstance().clearManifest(downloadId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private static void cancelVersionDownloadInternal(Context context, String version, boolean awaitFinished) {
|
|
207
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
211
|
+
Set<String> downloadIds = collectManifestIdsForVersion(workManager, version);
|
|
212
|
+
workManager.cancelAllWorkByTag(version);
|
|
213
|
+
clearManifestIds(downloadIds);
|
|
214
|
+
if (awaitFinished) {
|
|
215
|
+
awaitVersionWorkFinished(workManager, version);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private static void awaitVersionWorkFinished(WorkManager workManager, String version) {
|
|
220
|
+
for (int i = 0; i < 100; i++) {
|
|
221
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
222
|
+
throw new IllegalStateException("Interrupted while waiting for version download cancel");
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
boolean anyActive = workManager
|
|
226
|
+
.getWorkInfosByTag(version)
|
|
227
|
+
.get()
|
|
228
|
+
.stream()
|
|
229
|
+
.anyMatch((workInfo) -> !workInfo.getState().isFinished());
|
|
230
|
+
if (!anyActive) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
Thread.sleep(100);
|
|
234
|
+
} catch (InterruptedException e) {
|
|
235
|
+
Thread.currentThread().interrupt();
|
|
236
|
+
throw new IllegalStateException("Interrupted while waiting for download cancel", e);
|
|
237
|
+
} catch (Exception e) {
|
|
238
|
+
throw new IllegalStateException("Error waiting for download cancel: " + e.getMessage(), e);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
throw new IllegalStateException("Timed out waiting for version download cancel: " + version);
|
|
146
242
|
}
|
|
147
243
|
|
|
148
244
|
public static void cancelBundleDownload(Context context, String id, String version) {
|
|
149
245
|
String uniqueWorkName = "bundle_" + id + "_" + version;
|
|
150
246
|
initializeIfNeeded(context.getApplicationContext());
|
|
151
|
-
WorkManager.getInstance(context)
|
|
247
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
248
|
+
workManager.cancelUniqueWork(uniqueWorkName);
|
|
249
|
+
DataManager.getInstance().clearManifest(id);
|
|
152
250
|
}
|
|
153
251
|
|
|
154
252
|
public static void cancelAllDownloads(Context context) {
|
|
155
253
|
initializeIfNeeded(context.getApplicationContext());
|
|
156
|
-
WorkManager.getInstance(context)
|
|
254
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
255
|
+
workManager.cancelAllWorkByTag("capacitor_updater_download");
|
|
256
|
+
DataManager.getInstance().clearAllManifests();
|
|
157
257
|
}
|
|
158
258
|
}
|
|
@@ -22,7 +22,7 @@ public class InternalUtils {
|
|
|
22
22
|
public static String getPackageName(PackageManager pm, String packageName) {
|
|
23
23
|
try {
|
|
24
24
|
PackageInfo pInfo = getPackageInfoInternal(pm, packageName);
|
|
25
|
-
return
|
|
25
|
+
return pInfo != null ? pInfo.packageName : null;
|
|
26
26
|
} catch (PackageManager.NameNotFoundException e) {
|
|
27
27
|
// Exception is handled internally, and null is returned to indicate the package name could not be retrieved
|
|
28
28
|
return null;
|
|
@@ -13,6 +13,7 @@ import android.hardware.SensorManager;
|
|
|
13
13
|
import android.text.Editable;
|
|
14
14
|
import android.text.TextWatcher;
|
|
15
15
|
import android.widget.ArrayAdapter;
|
|
16
|
+
import android.widget.Button;
|
|
16
17
|
import android.widget.EditText;
|
|
17
18
|
import android.widget.LinearLayout;
|
|
18
19
|
import android.widget.ListView;
|
|
@@ -117,65 +118,8 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
117
118
|
isShowing = false;
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
120
|
-
if (Boolean.TRUE.equals(plugin.shakeChannelSelectorEnabled)) {
|
|
121
|
-
showCombinedPreviewMenu();
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
String appName = activity.getPackageManager().getApplicationLabel(activity.getApplicationInfo()).toString();
|
|
125
|
-
String title = "Preview " + appName + " Menu";
|
|
126
|
-
String message = "Reload, switch, or leave the current preview.";
|
|
127
|
-
List<String> actions = new ArrayList<>();
|
|
128
|
-
actions.add("Reload preview");
|
|
129
|
-
if (plugin.previewMenuPreviews().length() > 0) {
|
|
130
|
-
actions.add("Switch preview");
|
|
131
|
-
}
|
|
132
|
-
actions.add("Leave test app");
|
|
133
|
-
final boolean[] openingNestedSelector = { false };
|
|
134
|
-
final boolean[] previewActionRunning = { false };
|
|
135
121
|
|
|
136
|
-
|
|
137
|
-
builder.setTitle(title);
|
|
138
|
-
builder.setMessage(message);
|
|
139
|
-
builder.setItems(actions.toArray(new String[0]), (dialogInterface, which) -> {
|
|
140
|
-
AlertDialog dialog = (AlertDialog) dialogInterface;
|
|
141
|
-
String action = actions.get(which);
|
|
142
|
-
if ("Reload preview".equals(action)) {
|
|
143
|
-
previewActionRunning[0] = true;
|
|
144
|
-
logger.info("Reloading webview");
|
|
145
|
-
runPreviewMenuAction(dialog, "Could not reload the test app.", "Error reloading test app: ", () ->
|
|
146
|
-
plugin.reloadPreviewSessionFromShakeMenu()
|
|
147
|
-
);
|
|
148
|
-
} else if ("Switch preview".equals(action)) {
|
|
149
|
-
openingNestedSelector[0] = true;
|
|
150
|
-
dialog.dismiss();
|
|
151
|
-
showPreviewSelector();
|
|
152
|
-
} else {
|
|
153
|
-
previewActionRunning[0] = true;
|
|
154
|
-
runPreviewMenuAction(dialog, "Could not leave the test app.", "Error leaving test app: ", () ->
|
|
155
|
-
plugin.leavePreviewSessionFromShakeMenu()
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Cancel button
|
|
161
|
-
builder.setNegativeButton(
|
|
162
|
-
"Close menu",
|
|
163
|
-
new DialogInterface.OnClickListener() {
|
|
164
|
-
public void onClick(DialogInterface dialog, int id) {
|
|
165
|
-
logger.info("Shake menu cancelled");
|
|
166
|
-
dialog.dismiss();
|
|
167
|
-
isShowing = false;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
AlertDialog dialog = builder.create();
|
|
173
|
-
dialog.setOnDismissListener((dialogInterface) -> {
|
|
174
|
-
if (!openingNestedSelector[0] && !previewActionRunning[0]) {
|
|
175
|
-
isShowing = false;
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
dialog.show();
|
|
122
|
+
showPreviewActionsMenu(Boolean.TRUE.equals(plugin.shakeChannelSelectorEnabled));
|
|
179
123
|
} catch (Exception e) {
|
|
180
124
|
logger.error("Error showing shake menu: " + e.getMessage());
|
|
181
125
|
isShowing = false;
|
|
@@ -183,65 +127,95 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
183
127
|
});
|
|
184
128
|
}
|
|
185
129
|
|
|
186
|
-
private void
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
130
|
+
private void showPreviewActionsMenu(boolean includeChannelSelector) {
|
|
131
|
+
String appName = activity.getPackageManager().getApplicationLabel(activity.getApplicationInfo()).toString();
|
|
132
|
+
String title = "Preview " + appName + " Menu";
|
|
133
|
+
String message = "Reload, switch, or leave the current preview.";
|
|
134
|
+
final boolean[] openingNestedSelector = { false };
|
|
135
|
+
final boolean[] previewActionRunning = { false };
|
|
136
|
+
final AlertDialog[] dialogRef = { null };
|
|
137
|
+
List<Button> buttons = new ArrayList<>();
|
|
138
|
+
|
|
139
|
+
LinearLayout layout = new LinearLayout(activity);
|
|
140
|
+
layout.setOrientation(LinearLayout.VERTICAL);
|
|
141
|
+
int horizontalPadding = dpToPx(16);
|
|
142
|
+
int verticalPadding = dpToPx(8);
|
|
143
|
+
layout.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
|
144
|
+
|
|
145
|
+
addPreviewMenuButton(layout, buttons, "Reload preview", () -> {
|
|
146
|
+
AlertDialog dialog = dialogRef[0];
|
|
147
|
+
previewActionRunning[0] = true;
|
|
148
|
+
setPreviewMenuButtonsEnabled(buttons, false);
|
|
149
|
+
logger.info("Reloading webview");
|
|
150
|
+
runPreviewMenuAction(dialog, "Could not reload the test app.", "Error reloading test app: ", () ->
|
|
151
|
+
plugin.reloadPreviewSessionFromShakeMenu()
|
|
152
|
+
);
|
|
153
|
+
});
|
|
200
154
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
AlertDialog dialog = (AlertDialog) dialogInterface;
|
|
206
|
-
String action = actions.get(which);
|
|
207
|
-
if ("Reload preview".equals(action)) {
|
|
208
|
-
previewActionRunning[0] = true;
|
|
209
|
-
logger.info("Reloading webview");
|
|
210
|
-
runPreviewMenuAction(dialog, "Could not reload the test app.", "Error reloading test app: ", () ->
|
|
211
|
-
plugin.reloadPreviewSessionFromShakeMenu()
|
|
212
|
-
);
|
|
213
|
-
} else if ("Leave test app".equals(action)) {
|
|
214
|
-
previewActionRunning[0] = true;
|
|
215
|
-
runPreviewMenuAction(dialog, "Could not leave the test app.", "Error leaving test app: ", () ->
|
|
216
|
-
plugin.leavePreviewSessionFromShakeMenu()
|
|
217
|
-
);
|
|
218
|
-
} else if ("Switch preview".equals(action)) {
|
|
219
|
-
openingNestedSelector[0] = true;
|
|
220
|
-
dialog.dismiss();
|
|
221
|
-
showPreviewSelector();
|
|
222
|
-
} else {
|
|
223
|
-
openingNestedSelector[0] = true;
|
|
224
|
-
dialog.dismiss();
|
|
225
|
-
showChannelSelector();
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
builder.setNegativeButton("Close menu", (dialog, id) -> {
|
|
229
|
-
logger.info("Shake menu cancelled");
|
|
155
|
+
if (plugin.previewMenuPreviews().length() > 0) {
|
|
156
|
+
addPreviewMenuButton(layout, buttons, "Switch preview", () -> {
|
|
157
|
+
AlertDialog dialog = dialogRef[0];
|
|
158
|
+
openingNestedSelector[0] = true;
|
|
230
159
|
dialog.dismiss();
|
|
231
|
-
|
|
160
|
+
showPreviewSelector();
|
|
232
161
|
});
|
|
162
|
+
}
|
|
233
163
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
164
|
+
if (includeChannelSelector) {
|
|
165
|
+
addPreviewMenuButton(layout, buttons, "Switch channel", () -> {
|
|
166
|
+
AlertDialog dialog = dialogRef[0];
|
|
167
|
+
openingNestedSelector[0] = true;
|
|
168
|
+
dialog.dismiss();
|
|
169
|
+
showChannelSelector();
|
|
239
170
|
});
|
|
240
|
-
dialog.show();
|
|
241
|
-
} catch (Exception e) {
|
|
242
|
-
logger.error("Error showing combined shake menu: " + e.getMessage());
|
|
243
|
-
isShowing = false;
|
|
244
171
|
}
|
|
172
|
+
|
|
173
|
+
addPreviewMenuButton(layout, buttons, "Leave test app", () -> {
|
|
174
|
+
AlertDialog dialog = dialogRef[0];
|
|
175
|
+
previewActionRunning[0] = true;
|
|
176
|
+
setPreviewMenuButtonsEnabled(buttons, false);
|
|
177
|
+
runPreviewMenuAction(dialog, "Could not leave the test app.", "Error leaving test app: ", () ->
|
|
178
|
+
plugin.leavePreviewSessionFromShakeMenu()
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
addPreviewMenuButton(layout, buttons, "Close menu", () -> {
|
|
183
|
+
AlertDialog dialog = dialogRef[0];
|
|
184
|
+
if (dialog != null) {
|
|
185
|
+
logger.info("Shake menu cancelled");
|
|
186
|
+
dialog.dismiss();
|
|
187
|
+
isShowing = false;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
192
|
+
builder.setTitle(title);
|
|
193
|
+
builder.setMessage(message);
|
|
194
|
+
builder.setView(layout);
|
|
195
|
+
|
|
196
|
+
AlertDialog dialog = builder.create();
|
|
197
|
+
dialogRef[0] = dialog;
|
|
198
|
+
dialog.setOnDismissListener((dialogInterface) -> {
|
|
199
|
+
if (!openingNestedSelector[0] && !previewActionRunning[0]) {
|
|
200
|
+
isShowing = false;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
dialog.show();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private void addPreviewMenuButton(LinearLayout layout, List<Button> buttons, String title, Runnable action) {
|
|
207
|
+
Button button = new Button(activity);
|
|
208
|
+
button.setAllCaps(false);
|
|
209
|
+
button.setText(title);
|
|
210
|
+
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
211
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
212
|
+
LinearLayout.LayoutParams.WRAP_CONTENT
|
|
213
|
+
);
|
|
214
|
+
params.setMargins(0, 0, 0, dpToPx(8));
|
|
215
|
+
button.setLayoutParams(params);
|
|
216
|
+
button.setOnClickListener((view) -> action.run());
|
|
217
|
+
buttons.add(button);
|
|
218
|
+
layout.addView(button);
|
|
245
219
|
}
|
|
246
220
|
|
|
247
221
|
private void runPreviewMenuAction(AlertDialog dialog, String failureMessage, String errorPrefix, PreviewMenuAction action) {
|
|
@@ -259,13 +233,17 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
259
233
|
isShowing = false;
|
|
260
234
|
});
|
|
261
235
|
}
|
|
262
|
-
})
|
|
236
|
+
})
|
|
237
|
+
.start();
|
|
263
238
|
}
|
|
264
239
|
|
|
265
|
-
private void setPreviewMenuButtonsEnabled(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
240
|
+
private void setPreviewMenuButtonsEnabled(List<Button> buttons, boolean enabled) {
|
|
241
|
+
for (Button button : buttons) {
|
|
242
|
+
if (!enabled && "Close menu".equals(button.getText().toString())) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
button.setEnabled(enabled);
|
|
246
|
+
}
|
|
269
247
|
}
|
|
270
248
|
|
|
271
249
|
private void showPreviewSelector() {
|
|
@@ -299,7 +277,7 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
299
277
|
String name = preview.optString("name", "");
|
|
300
278
|
JSObject bundle = preview.getJSObject("bundle");
|
|
301
279
|
String version = bundle == null ? "" : bundle.optString("version", "");
|
|
302
|
-
String label = !name.isEmpty() ? name :
|
|
280
|
+
String label = !name.isEmpty() ? name : !version.isEmpty() ? version : preview.optString("id", "Preview");
|
|
303
281
|
if (preview.optBoolean("isActive", false)) {
|
|
304
282
|
label += " (current)";
|
|
305
283
|
}
|
|
@@ -405,7 +383,8 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
405
383
|
} finally {
|
|
406
384
|
isShowing = false;
|
|
407
385
|
}
|
|
408
|
-
})
|
|
386
|
+
})
|
|
387
|
+
.start();
|
|
409
388
|
}
|
|
410
389
|
|
|
411
390
|
private void showConfiguredDefaultMenu() {
|
|
@@ -592,7 +571,8 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
592
571
|
presentChannelPicker(channels);
|
|
593
572
|
});
|
|
594
573
|
});
|
|
595
|
-
})
|
|
574
|
+
})
|
|
575
|
+
.start();
|
|
596
576
|
} catch (Exception e) {
|
|
597
577
|
logger.error("Error showing channel selector: " + e.getMessage());
|
|
598
578
|
isShowing = false;
|
|
@@ -785,14 +765,13 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
785
765
|
String latestKind = getString(latestRes, "kind");
|
|
786
766
|
String latestMessage = getString(latestRes, "message");
|
|
787
767
|
|
|
788
|
-
String detail =
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
: "server did not provide a message";
|
|
768
|
+
String detail = latestMessage != null && !latestMessage.isEmpty()
|
|
769
|
+
? latestMessage
|
|
770
|
+
: latestError != null && !latestError.isEmpty()
|
|
771
|
+
? latestError
|
|
772
|
+
: latestKind != null && !latestKind.isEmpty()
|
|
773
|
+
? latestKind
|
|
774
|
+
: "server did not provide a message";
|
|
796
775
|
|
|
797
776
|
// Handle update errors first (before "no new version" check)
|
|
798
777
|
if (
|
|
@@ -819,8 +798,20 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
819
798
|
|
|
820
799
|
String latestUrl = getString(latestRes, "url");
|
|
821
800
|
|
|
822
|
-
|
|
823
|
-
|
|
801
|
+
Object manifestObj = latestRes.get("manifest");
|
|
802
|
+
JSONArray manifestArray = null;
|
|
803
|
+
if (manifestObj instanceof JSONArray) {
|
|
804
|
+
manifestArray = (JSONArray) manifestObj;
|
|
805
|
+
} else if (manifestObj instanceof List) {
|
|
806
|
+
manifestArray = new JSONArray((List<?>) manifestObj);
|
|
807
|
+
}
|
|
808
|
+
final boolean hasManifest = manifestArray != null && manifestArray.length() > 0;
|
|
809
|
+
|
|
810
|
+
// Check if there's an actual update available. A manifest-only
|
|
811
|
+
// response legitimately has no URL (the files come from the
|
|
812
|
+
// manifest, not a zip), so only report "already on latest" when
|
|
813
|
+
// the URL is empty AND there is no manifest to download from.
|
|
814
|
+
if ("up_to_date".equals(latestKind) || ((latestUrl == null || latestUrl.isEmpty()) && !hasManifest)) {
|
|
824
815
|
activity.runOnUiThread(() -> {
|
|
825
816
|
progressDialog.dismiss();
|
|
826
817
|
showSuccess("Channel set to " + channelName + ". Already on latest version.");
|
|
@@ -843,25 +834,19 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
843
834
|
|
|
844
835
|
String sessionKey = getString(latestRes, "sessionKey");
|
|
845
836
|
String checksum = getString(latestRes, "checksum");
|
|
846
|
-
|
|
837
|
+
|
|
838
|
+
// A manifest-only response has no zip URL; downloadManifest
|
|
839
|
+
// tolerates the placeholder URL the plugin already uses.
|
|
840
|
+
final String downloadUrl = latestUrl == null || latestUrl.isEmpty()
|
|
841
|
+
? "https://404.capgo.app/no.zip"
|
|
842
|
+
: latestUrl;
|
|
847
843
|
|
|
848
844
|
// Download the update
|
|
849
845
|
try {
|
|
850
846
|
BundleInfo bundle;
|
|
851
|
-
if (
|
|
852
|
-
JSONArray manifestArray = null;
|
|
853
|
-
if (manifestObj instanceof JSONArray) {
|
|
854
|
-
manifestArray = (JSONArray) manifestObj;
|
|
855
|
-
} else if (manifestObj instanceof List) {
|
|
856
|
-
manifestArray = new JSONArray((List<?>) manifestObj);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
if (manifestArray == null) {
|
|
860
|
-
throw new IllegalArgumentException("Invalid manifest format");
|
|
861
|
-
}
|
|
862
|
-
|
|
847
|
+
if (hasManifest) {
|
|
863
848
|
bundle = updater.downloadManifest(
|
|
864
|
-
|
|
849
|
+
downloadUrl,
|
|
865
850
|
versionForUi,
|
|
866
851
|
sessionKey != null ? sessionKey : "",
|
|
867
852
|
checksum != null ? checksum : "",
|
|
@@ -869,7 +854,7 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
869
854
|
);
|
|
870
855
|
} else {
|
|
871
856
|
bundle = updater.download(
|
|
872
|
-
|
|
857
|
+
downloadUrl,
|
|
873
858
|
versionForUi,
|
|
874
859
|
sessionKey != null ? sessionKey : "",
|
|
875
860
|
checksum != null ? checksum : ""
|
|
@@ -911,7 +896,8 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
911
896
|
});
|
|
912
897
|
}
|
|
913
898
|
);
|
|
914
|
-
})
|
|
899
|
+
})
|
|
900
|
+
.start();
|
|
915
901
|
} catch (Exception e) {
|
|
916
902
|
logger.error("Error selecting channel: " + e.getMessage());
|
|
917
903
|
isShowing = false;
|