Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: '20.x'
node-version: '24.x'
- name: Cache dependencies
id: yarn-cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
**/node_modules
Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ on:
jobs:
build-and-publish:
runs-on: ubuntu-latest
# Must match the Environment on npm's trusted publisher for this package
environment: prod
# Prevent the app's own release commit from re-triggering this workflow
if: github.actor != 'stallion-release-bot[bot]'
permissions: write-all
# id-token: write mints the OIDC token npm's trusted publisher verifies
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
Expand Down Expand Up @@ -42,11 +49,13 @@ jobs:
- name: Prep release
run: yarn prepare-release

# Publishes to npm via trusted publishing, so no NPM_TOKEN is needed.
# npm binds the trusted publisher to this workflow's file name: renaming
# ci.yml means updating the publisher on npmjs.com in the same change.
- name: Release package
run: yarn semantic-release
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GIT_AUTHOR_EMAIL: ${{ secrets.GH_DEPLOY_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GH_DEPLOY_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GH_DEPLOY_EMAIL }}
Expand Down
106 changes: 64 additions & 42 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
package="com.stallion">
<application>
<activity
android:name="com.stallion.utils.StallionErrorActivity">
android:name="com.stallion.utils.StallionErrorActivity"
android:exported="false"
android:noHistory="true">
</activity>
<activity
android:name="com.stallion.utils.ProcessPhoenix"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ public class StallionApiConstants {
// Buffer Size
public static final int DOWNLOAD_BUFFER_SIZE = 1024 * 256;

// Network timeouts (aligned with iOS URLSession defaults)
public static final int CONNECT_TIMEOUT_MS = 30_000;
public static final int READ_TIMEOUT_MS = 60_000;

// File and Folder Names
public static final String ZIP_FILE_NAME = "build.zip";
public static final String UNZIP_FOLDER_NAME = "/build";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public static JSONObject post(String urlString, String requestBodyString) {
URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setConnectTimeout(StallionApiConstants.CONNECT_TIMEOUT_MS);
urlConnection.setReadTimeout(StallionApiConstants.READ_TIMEOUT_MS);
urlConnection.setRequestProperty("Content-Type", "application/json");

String appToken = stallionConfig.getAppToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public static void downloadBundle(

} catch (Exception e) {
Log.e(TAG, "Error in downloadBundle: " + e.getMessage(), e);
stallionDownloadCallback.onReject(
StallionApiConstants.DOWNLOAD_ERROR_PREFIX,
e.getMessage() != null ? e.getMessage() : "Unknown download error"
);
}
});
}
Expand Down Expand Up @@ -159,8 +163,8 @@ private static void downloadFile(
return;
}
} catch (IOException e) {
callback.onReject(StallionApiConstants.DOWNLOAD_ERROR_PREFIX, "IOException occurred: ");
throw e;
String message = e.getMessage() != null ? e.getMessage() : "Unknown IO error";
throw new IOException("IOException occurred: " + message, e);
} finally {
connection.disconnect();
}
Expand All @@ -176,6 +180,8 @@ private static HttpURLConnection setupConnection(
URL url = new URL(downloadUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(StallionApiConstants.CONNECT_TIMEOUT_MS);
connection.setReadTimeout(StallionApiConstants.READ_TIMEOUT_MS);

if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ public static void applyPatch(String baseBundlePath, String diffPath, boolean is
// Create a temporary directory for the patched bundle
File tempPatchedDir = new File(diffDir.getParent(), diffDir.getName() + "_patched_temp");
try {
// Copy base bundle to temporary location
// Clear any leftover temp dir, then copy base bundle
if (tempPatchedDir.exists()) {
StallionFileManager.deleteFileOrFolderSilently(tempPatchedDir);
}
StallionFileManager.copyDirectory(baseBundleDir, tempPatchedDir);

// Read and parse manifest.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static void downloadStageBundle(ReadableMap bundleInfo, Promise promise)
long alreadyDownloaded = StallionDownloadCacheManager.getDownloadCache(config, receivedDownloadUrl, downloadPath);

emitDownloadStartedStage(receivedHash, alreadyDownloaded > 0);
final long downloadStartMs = System.currentTimeMillis();

StallionFileDownloader.downloadBundle(
receivedDownloadUrl,
Expand All @@ -43,10 +44,11 @@ public void onReject(String prefix, String error) {

@Override
public void onSuccess(String successPayload) {
long downloadTimeMs = System.currentTimeMillis() - downloadStartMs;
stallionStateManager.stallionMeta.setCurrentStageSlot(StallionMetaConstants.SlotStates.NEW_SLOT);
stallionStateManager.stallionMeta.setStageTempHash(receivedHash);
stallionStateManager.syncStallionMeta();
emitDownloadSuccessStage(receivedHash);
emitDownloadSuccessStage(receivedHash, downloadTimeMs);
StallionDownloadCacheManager.deleteDownloadCache(downloadPath);
promise.resolve(successPayload);
}
Expand All @@ -60,10 +62,13 @@ public void onProgress(double downloadFraction) {
}
}

private static void emitDownloadSuccessStage(String releaseHash) {
private static void emitDownloadSuccessStage(String releaseHash, long downloadTimeMs) {
JSONObject successPayload = new JSONObject();
try {
successPayload.put("releaseHash", releaseHash);
JSONObject meta = new JSONObject();
meta.put("downloadTime", downloadTimeMs);
successPayload.put("meta", meta.toString());
} catch (Exception ignored) { }
StallionEventManager.getInstance().sendEvent(
StallionEventConstants.NativeStageEventTypes.DOWNLOAD_COMPLETE_STAGE.toString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ public static void downloadNewRelease(String newReleaseHash, String newReleaseUr
long alreadyDownloaded = StallionDownloadCacheManager.getDownloadCache(config, downloadUrl, downloadPath);

emitDownloadStarted(newReleaseHash, alreadyDownloaded > 0, bundleDiffIdForEvents);
final long downloadStartMs = System.currentTimeMillis();

StallionFileDownloader.downloadBundle(
downloadUrl,
Expand All @@ -185,6 +186,7 @@ public void onReject(String prefix, String error) {

@Override
public void onSuccess(String successPayload) {
long downloadTimeMs = System.currentTimeMillis() - downloadStartMs;
isDownloadInProgress.set(false);
StallionDownloadCacheManager.deleteDownloadCache(downloadPath);

Expand Down Expand Up @@ -224,14 +226,14 @@ public void onSuccess(String successPayload) {
}
}

stateManager.stallionMeta.setCurrentProdSlot(StallionMetaConstants.SlotStates.NEW_SLOT);
stateManager.stallionMeta.setProdTempHash(newReleaseHash);
String currentProdNewHash = stateManager.stallionMeta.getProdNewHash();
if(currentProdNewHash != null && !currentProdNewHash.isEmpty()) {
StallionSlotManager.stabilizeProd();
}
stateManager.syncStallionMeta();
emitDownloadSuccess(newReleaseHash, bundleDiffIdForEvents);
stateManager.stallionMeta.setCurrentProdSlot(StallionMetaConstants.SlotStates.NEW_SLOT);
stateManager.stallionMeta.setProdTempHash(newReleaseHash);
String currentProdNewHash = stateManager.stallionMeta.getProdNewHash();
if(currentProdNewHash != null && !currentProdNewHash.isEmpty()) {
StallionSlotManager.stabilizeProd();
}
stateManager.syncStallionMeta();
emitDownloadSuccess(newReleaseHash, bundleDiffIdForEvents, downloadTimeMs);
}

@Override
Expand Down Expand Up @@ -282,13 +284,16 @@ private static void emitDownloadError(String releaseHash, String error) {
);
}

private static void emitDownloadSuccess(String releaseHash, String bundleDiffId) {
private static void emitDownloadSuccess(String releaseHash, String bundleDiffId, long downloadTimeMs) {
JSONObject successPayload = new JSONObject();
try {
successPayload.put("releaseHash", releaseHash);
if (bundleDiffId != null && !bundleDiffId.isEmpty()) {
successPayload.put("diffId", bundleDiffId);
}
JSONObject meta = new JSONObject();
meta.put("downloadTime", downloadTimeMs);
successPayload.put("meta", meta.toString());
} catch (Exception ignored) { }
StallionEventManager.getInstance().sendEvent(
NativeProdEventTypes.DOWNLOAD_COMPLETE_PROD.toString(),
Expand Down
56 changes: 32 additions & 24 deletions android/src/main/java/com/stallion/storage/StallionConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.provider.Settings;

import org.json.JSONException;
import org.json.JSONObject;
Expand Down Expand Up @@ -32,43 +31,37 @@ public StallionConfig(Context context, SharedPreferences sharedPreferences) {
Resources res = context.getResources();
String parentPackageName= context.getPackageName();

int stallionProjectIdRes = res.getIdentifier(
this.projectId = resolveConfigString(
context,
res,
parentPackageName,
StallionConfigConstants.STALLION_PROJECT_ID_IDENTIFIER,
"string",
parentPackageName
StallionConfigConstants.STALLION_PROJECT_ID_FALLBACK_IDENTIFIER
);
this.projectId = stallionProjectIdRes != 0 ?context.getString(stallionProjectIdRes) : "";

int stallionAppTokenRes = res.getIdentifier(
this.appToken = resolveConfigString(
context,
res,
parentPackageName,
StallionConfigConstants.STALLION_APP_TOKEN_IDENTIFIER,
"string",
parentPackageName
StallionConfigConstants.STALLION_APP_TOKEN_FALLBACK_IDENTIFIER
);
this.appToken = stallionAppTokenRes != 0 ? context.getString(stallionAppTokenRes) : "";

int stallionPublicKeyRes = res.getIdentifier(
this.publicSigningKey = resolveConfigString(
context,
res,
parentPackageName,
StallionConfigConstants.STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER,
"string",
parentPackageName
StallionConfigConstants.STALLION_PUBLIC_SIGNING_KEY_FALLBACK_IDENTIFIER
);
this.publicSigningKey = stallionPublicKeyRes != 0 ? context.getString(stallionPublicKeyRes) : "";

// get or generate UID
// get or generate UID (random install-scoped UUID)
String cachedUniqueId = sharedPreferences.getString(
StallionConfigConstants.UNIQUE_ID_IDENTIFIER,
""
);
if(!cachedUniqueId.isEmpty()) {
this.uid = cachedUniqueId;
} else {
try {
this.uid = Settings.Secure.getString(
context.getContentResolver(),
Settings.Secure.ANDROID_ID
);
} catch (Exception ignored) {
this.uid = UUID.randomUUID().toString();
}
this.uid = UUID.randomUUID().toString();
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(StallionConfigConstants.UNIQUE_ID_IDENTIFIER, this.uid);
editor.apply();
Expand Down Expand Up @@ -106,6 +99,21 @@ public void setLastUnverifiedHash(String newUnverifiedHash) {
editor.apply();
}

private static String resolveConfigString(
Context context,
Resources res,
String packageName,
String... names
) {
for (String name : names) {
int id = res.getIdentifier(name, "string", packageName);
if (id != 0) {
return context.getString(id);
}
}
return "";
}

private String fetchAppVersion(Context context) {
try {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ public class StallionConfigConstants {
public static final String PLATFORM = "android";

public static final String STALLION_PROJECT_ID_IDENTIFIER = "StallionProjectId";
public static final String STALLION_PROJECT_ID_FALLBACK_IDENTIFIER = "stallion_project_id";
public static final String STALLION_APP_TOKEN_IDENTIFIER = "StallionAppToken";
public static final String STALLION_APP_TOKEN_FALLBACK_IDENTIFIER = "stallion_app_token";
public static final String STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER = "StallionPublicSigningKey";
public static final String STALLION_PUBLIC_SIGNING_KEY_FALLBACK_IDENTIFIER = "stallion_public_signing_key";
public static final String UNIQUE_ID_IDENTIFIER = "stallionDeviceId";
public static final String API_KEY_IDENTIFIER = "x-sdk-access-token";
public static final String LAST_DOWNLOADING_URL_IDENTIFIER = "StallionLastDownloadingUrl";
Expand Down
19 changes: 17 additions & 2 deletions android/src/main/java/com/stallion/utils/ProcessPhoenix.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,29 @@ private static Intent getRestartIntent(Context context) {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

Process.killProcess(getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // Kill original main process

int mainPid = getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1);
ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
if (intents == null || intents.isEmpty()) {
relaunchDefaultAndFinish();
return;
}

Process.killProcess(mainPid); // Kill original main process

startActivities(intents.toArray(new Intent[intents.size()]));
finish();
Runtime.getRuntime().exit(0); // Kill kill kill!
}

private void relaunchDefaultAndFinish() {
Intent fallback = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (fallback != null) {
fallback.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
startActivity(fallback);
}
finish();
}

/**
* Checks if the current process is a temporary Phoenix Process.
* This can be used to avoid initialisation of unused resources or to prevent running code that
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
package com.stallion.utils;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

import com.stallion.R;

import java.util.Objects;

public class StallionErrorActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String receivedStackTrace = Objects.requireNonNull(getIntent().getExtras()).getString("stack_trace_string");
Bundle extras = getIntent() != null ? getIntent().getExtras() : null;
if (extras == null) {
Intent launch = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (launch != null) {
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(launch);
}
finish();
return;
}
String receivedStackTrace = extras.getString("stack_trace_string");
String stackTraceString = receivedStackTrace != null ? receivedStackTrace : "null";
setContentView(R.layout.stallion_default_error_activity);
TextView stackTraceView = findViewById(R.id.seb_stack_trace_text_view);
Expand All @@ -25,4 +34,3 @@ private void continueExceptionFlow() {
StallionExceptionHandler.continueExceptionFlow();
}
}

Loading
Loading