Skip to content
Open
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
20 changes: 10 additions & 10 deletions __tests__/unit/services/modelManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ describe('ModelManager', () => {
.mockResolvedValueOnce(true); // mmProjExists (no mmproj)

mockedBackgroundDownloadService.startDownload.mockResolvedValue({
downloadId: 42,
downloadId: '42',
fileName: 'bg-model.gguf',
modelId: 'test/model',
status: 'pending',
Expand All @@ -486,7 +486,7 @@ describe('ModelManager', () => {
const result = await modelManager.downloadModelBackground('test/model', file);

expect(mockedBackgroundDownloadService.startDownload).toHaveBeenCalled();
expect(result.downloadId).toBe(42);
expect(result.downloadId).toBe('42');
});

it('sets up progress listener during start and complete/error via watchDownload', async () => {
Expand All @@ -498,7 +498,7 @@ describe('ModelManager', () => {
.mockResolvedValueOnce(true);

mockedBackgroundDownloadService.startDownload.mockResolvedValue({
downloadId: 42,
downloadId: '42',
fileName: 'bg-model.gguf',
modelId: 'test/model',
status: 'pending',
Expand All @@ -510,9 +510,9 @@ describe('ModelManager', () => {
const info = await modelManager.downloadModelBackground('test/model', file);
modelManager.watchDownload(info.downloadId, jest.fn(), jest.fn());

expect(mockedBackgroundDownloadService.onProgress).toHaveBeenCalledWith(42, expect.any(Function));
expect(mockedBackgroundDownloadService.onComplete).toHaveBeenCalledWith(42, expect.any(Function));
expect(mockedBackgroundDownloadService.onError).toHaveBeenCalledWith(42, expect.any(Function));
expect(mockedBackgroundDownloadService.onProgress).toHaveBeenCalledWith('42', expect.any(Function));
expect(mockedBackgroundDownloadService.onComplete).toHaveBeenCalledWith('42', expect.any(Function));
expect(mockedBackgroundDownloadService.onError).toHaveBeenCalledWith('42', expect.any(Function));
});

it('calls metadata callback with download info', async () => {
Expand All @@ -524,7 +524,7 @@ describe('ModelManager', () => {
.mockResolvedValueOnce(true);

mockedBackgroundDownloadService.startDownload.mockResolvedValue({
downloadId: 42,
downloadId: '42',
fileName: 'bg-model.gguf',
modelId: 'test/model',
status: 'pending',
Expand All @@ -538,7 +538,7 @@ describe('ModelManager', () => {

await modelManager.downloadModelBackground('test/model', file);

expect(metadataCallback).toHaveBeenCalledWith(42, expect.objectContaining({
expect(metadataCallback).toHaveBeenCalledWith('42', expect.objectContaining({
modelId: 'test/model',
fileName: 'bg-model.gguf',
}));
Expand All @@ -562,7 +562,7 @@ describe('ModelManager', () => {

mockedBackgroundDownloadService.startDownload
.mockResolvedValueOnce({
downloadId: 42,
downloadId: '42',
fileName: 'vision.gguf',
modelId: 'test/model',
status: 'pending',
Expand Down Expand Up @@ -1674,7 +1674,7 @@ describe('ModelManager', () => {

mockedBackgroundDownloadService.startDownload
.mockResolvedValueOnce({
downloadId: 42,
downloadId: '42',
fileName: 'bg-vision.gguf',
modelId: 'test/model',
status: 'pending',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import android.os.Environment
import android.util.Log
import android.os.PowerManager
import android.provider.Settings
import androidx.lifecycle.Observer
Expand Down Expand Up @@ -162,7 +163,7 @@ class DownloadManagerModule(reactContext: ReactApplicationContext) :
if (download != null) {
downloadDao.updateStatus(downloadId, DownloadStatus.CANCELLED, DownloadReason.USER_CANCELLED)
val file = File(download.destination)
if (file.exists()) file.delete()
if (file.exists() && !file.delete()) Log.w("DownloadManagerModule", "Failed to delete cancelled download file: ${file.path}")
}
}
WorkerDownload.cancel(reactApplicationContext, downloadId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ class WorkerDownload(
val current = downloadDao.getDownload(downloadId) ?: download
return if (current.status == DownloadStatus.CANCELLED) {
val partialFile = File(current.destination)
if (partialFile.exists()) partialFile.delete()
if (partialFile.exists() && !partialFile.delete()) Log.w(TAG, "Failed to delete partial file on cancel: ${partialFile.path}")
Result.failure()
} else {
// System stopped the worker — retry silently, no JS state change.
Expand Down
6 changes: 5 additions & 1 deletion fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,11 @@ platform :android do
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
release_status: "draft" # human confirms rollout in Play Console
# A track PROMOTION reads track_promote_release_status, NOT release_status — the latter only
# applies to a fresh upload. Without this it defaulted to "completed", so the promotion went
# straight to review/100% rollout instead of parking as a draft (device 2026-07-16, 0.0.103).
release_status: "draft",
track_promote_release_status: "draft" # human confirms rollout in Play Console
)
UI.success("Promoted internal build (versionCode #{version_code}) to Play production (draft - confirm rollout in console)")
end
Expand Down
5 changes: 3 additions & 2 deletions src/screens/DownloadManagerScreen/useVoiceDownloadItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ async function deleteItem(item: DownloadItem): Promise<void> {
// Models screen keep showing the deleted model as present/active.
await useWhisperStore.getState().deleteModelById(item.modelId);
} else {
const pending = callHook<Promise<void>>(HOOKS.downloadsDeleteVoiceModel, item.modelId);
if (pending) await pending.catch(() => {});
// callHook returns undefined when the hook isn't registered (free build); await the promise
// only when it exists. `?.` keeps a Promise out of a boolean condition (SonarJS).
await callHook<Promise<void>>(HOOKS.downloadsDeleteVoiceModel, item.modelId)?.catch(() => {});
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/services/activeDownloadPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
subscribed = true;
useDownloadStore.subscribe((state) => {
const active = serializeActiveDownloads(state.downloads);
const signature = active.map((e) => `${e.modelKey}:${e.status}`).sort().join('|');
const signature = active.map((e) => `${e.modelKey}:${e.status}`).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('|');

Check warning on line 67 in src/services/activeDownloadPersistence.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=off-grid-ai_off-grid-ai-mobile&issues=AZ9nn4dgV73YrftjxpEf&open=AZ9nn4dgV73YrftjxpEf&pullRequest=560
if (signature === lastSignature) return;
lastSignature = signature;
saveActiveDownloads(active).catch(() => { /* saveActiveDownloads already logs; never throws */ });
Expand Down
5 changes: 3 additions & 2 deletions src/services/modelPreloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ async function preloadText(): Promise<void> {
async function preloadTts(): Promise<void> {
// Pro implements the audio.preload hook (fits-gated + registers the engine);
// no-op in free builds.
const pending = callHook<Promise<void>>(HOOKS.audioPreload);
if (pending) await pending;
// callHook returns undefined in free builds (no audio.preload hook); awaiting undefined is a
// no-op, so this needs no Promise-in-condition check (SonarJS).
await callHook<Promise<void>>(HOOKS.audioPreload);
}

async function preloadStt(): Promise<void> {
Expand Down
Loading