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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## Unreleased
## 8.24.0-alpha.2

### Features

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
android.useAndroidX=true

# Release information
versionName=8.23.0
versionName=8.24.0-alpha.2

# Override the SDK name on native crashes on Android
sentryAndroidSdkName=sentry.native.android
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
public final class io/sentry/android/distribution/DistributionIntegration : io/sentry/IDistributionApi, io/sentry/Integration {
public fun <init> (Landroid/content/Context;)V
public fun checkForUpdate (Lio/sentry/IDistributionApi$UpdateCallback;)V
public fun checkForUpdate ()Ljava/util/concurrent/Future;
public fun checkForUpdateBlocking ()Lio/sentry/UpdateStatus;
public fun downloadUpdate (Lio/sentry/UpdateInfo;)V
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import io.sentry.UpdateInfo
import io.sentry.UpdateStatus
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.util.concurrent.Future
import org.jetbrains.annotations.ApiStatus

/**
Expand Down Expand Up @@ -84,14 +85,12 @@ public class DistributionIntegration(context: Context) : Integration, IDistribut
}

/**
* Check for available updates asynchronously using a callback.
* Check for available updates asynchronously.
*
* @param onResult Callback that will be called with the UpdateStatus result
* @return Future that will resolve to an UpdateStatus result
*/
public override fun checkForUpdate(onResult: IDistributionApi.UpdateCallback) {
// TODO implement this in a async way
val result = checkForUpdateBlocking()
onResult.onResult(result)
public override fun checkForUpdate(): Future<UpdateStatus> {
return sentryOptions.executorService.submit<UpdateStatus> { checkForUpdateBlocking() }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import timber.log.Timber;

public class MainActivity extends AppCompatActivity {
Expand Down Expand Up @@ -315,35 +316,51 @@ public void run() {
binding.checkForUpdate.setOnClickListener(
view -> {
Toast.makeText(this, "Checking for updates...", Toast.LENGTH_SHORT).show();
Sentry.distribution()
.checkForUpdate(
result -> {
runOnUiThread(
() -> {
String message;
if (result instanceof UpdateStatus.NewRelease) {
UpdateStatus.NewRelease newRelease = (UpdateStatus.NewRelease) result;
message =
"Update available: "
+ newRelease.getInfo().getBuildVersion()
+ " (Build "
+ newRelease.getInfo().getBuildNumber()
+ ")\nDownload URL: "
+ newRelease.getInfo().getDownloadUrl();
} else if (result instanceof UpdateStatus.UpToDate) {
message = "App is up to date!";
} else if (result instanceof UpdateStatus.NoNetwork) {
UpdateStatus.NoNetwork noNetwork = (UpdateStatus.NoNetwork) result;
message = "No network connection: " + noNetwork.getMessage();
} else if (result instanceof UpdateStatus.UpdateError) {
UpdateStatus.UpdateError error = (UpdateStatus.UpdateError) result;
message = "Error checking for updates: " + error.getMessage();
} else {
message = "Unknown status";
}
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
});
});
Future<UpdateStatus> future = Sentry.distribution().checkForUpdate();
// In production, convert this to use your preferred async library (RxJava, Coroutines,
// etc.)
// This sample uses raw threads and Future.get() for simplicity
// Process result on background thread, then update UI
new Thread(
() -> {
try {
UpdateStatus result = future.get();
runOnUiThread(
() -> {
String message;
if (result instanceof UpdateStatus.NewRelease) {
UpdateStatus.NewRelease newRelease = (UpdateStatus.NewRelease) result;
message =
"Update available: "
+ newRelease.getInfo().getBuildVersion()
+ " (Build "
+ newRelease.getInfo().getBuildNumber()
+ ")\nDownload URL: "
+ newRelease.getInfo().getDownloadUrl();
} else if (result instanceof UpdateStatus.UpToDate) {
message = "App is up to date!";
} else if (result instanceof UpdateStatus.NoNetwork) {
UpdateStatus.NoNetwork noNetwork = (UpdateStatus.NoNetwork) result;
message = "No network connection: " + noNetwork.getMessage();
} else if (result instanceof UpdateStatus.UpdateError) {
UpdateStatus.UpdateError error = (UpdateStatus.UpdateError) result;
message = "Error checking for updates: " + error.getMessage();
} else {
message = "Unknown status";
}
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
});
} catch (Exception e) {
runOnUiThread(
() ->
Toast.makeText(
this,
"Error checking for updates: " + e.getMessage(),
Toast.LENGTH_LONG)
.show());
}
})
.start();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unnecessary Thread Creation in Update Check

The checkForUpdate sample unnecessarily creates a new Thread to block on the Future result. This goes against the goal of using a shared thread pool and avoiding unbounded thread creation, as checkForUpdate already runs on a background thread. It leads to inefficient resource use.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no, we can’t call future.get() on the main thread since it will be blocking so we spawn a thread to wait for the result. Does that seem right?
I feel like this sample is overly complicated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Java async quagmire always makes me so sad.

This is much better now it's only in the sample but still ideally we would not encourage users to create a thread for each call.

I think most users will already have some kind of background worker setup which they could use convert the Future into something usable (kotlinx-coroutines-guava, androidx.concurrent, their own worker pool, etc). Assuming we don't already have such a library in the sample I would suggest something like:

class FuturePoller<T> {
    private static final Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper());

    public static <T> void futureToRunnable(
            @NonNull final Future<T> future,
            @NonNull final Runnable onCompleteRunnable) {
        MAIN_THREAD_HANDLER.post(() -> {
            pollAndExecute(future, onCompleteRunnable, 50, 1000);
        });
    }

    private static <T> void pollAndExecute(
            @NonNull final Future<T> future,
            @NonNull final Runnable onCompleteRunnable,
            final long currentDelayMs,
            final long maxDelayMs) {
        if (future.isDone()) {
            T result = future.get(); 
            onCompleteRunnable.run(); 
        } else {
            long nextDelay = Math.min(currentDelayMs * 2, maxDelayMs);
            MAIN_THREAD_HANDLER.postDelayed(() -> {
                pollAndExecute(future, onCompleteRunnable, nextDelay, maxDelayMs);
            }, currentDelayMs);
        }
    }
}

e.g. Something that continually reposts the task to a handler with an increasing backoff until the future is resolvable and once it's resolvable calls the callback.

Alternatively we can do what we have now (or even just make a blocking call / do .get() on the UI thread) - and leave a big comment about the user needs to convert this to be async with their favorite library.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feel free to pull any dependencies we need for the sample btw

Copy link
Contributor Author

@runningcode runningcode Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romtsn I think adding dependencies is the way to go. This module is java only but I don’t think any Android developers are java only so I think we should add some kotlin and coroutines here.
But that would be a much bigger change since this entire file would have to be switched to kotlin in order to use coroutines.
@chromy I appreciate the sample code you wrote but I don’t think it would help anyone to have that since it will not be relevant to all users who have their own async libs so it will just cause extra confusion. I will just add a comment as you suggest to use your own asynchronous code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make it easier we could have a separate screen which polls for updates (or maybe even a foreground service?) which could be in kotlin from the get go

});

binding.openCameraActivity.setOnClickListener(
Expand Down
8 changes: 2 additions & 6 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -776,15 +776,11 @@ public abstract interface class io/sentry/IContinuousProfiler {
}

public abstract interface class io/sentry/IDistributionApi {
public abstract fun checkForUpdate (Lio/sentry/IDistributionApi$UpdateCallback;)V
public abstract fun checkForUpdate ()Ljava/util/concurrent/Future;
public abstract fun checkForUpdateBlocking ()Lio/sentry/UpdateStatus;
public abstract fun downloadUpdate (Lio/sentry/UpdateInfo;)V
}

public abstract interface class io/sentry/IDistributionApi$UpdateCallback {
public abstract fun onResult (Lio/sentry/UpdateStatus;)V
}

public abstract interface class io/sentry/IEnvelopeReader {
public abstract fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope;
}
Expand Down Expand Up @@ -1493,7 +1489,7 @@ public final class io/sentry/NoOpContinuousProfiler : io/sentry/IContinuousProfi
}

public final class io/sentry/NoOpDistributionApi : io/sentry/IDistributionApi {
public fun checkForUpdate (Lio/sentry/IDistributionApi$UpdateCallback;)V
public fun checkForUpdate ()Ljava/util/concurrent/Future;
public fun checkForUpdateBlocking ()Lio/sentry/UpdateStatus;
public fun downloadUpdate (Lio/sentry/UpdateInfo;)V
public static fun getInstance ()Lio/sentry/NoOpDistributionApi;
Expand Down
17 changes: 7 additions & 10 deletions sentry/src/main/java/io/sentry/IDistributionApi.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry;

import java.util.concurrent.Future;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

Expand All @@ -14,20 +15,21 @@ public interface IDistributionApi {

/**
* Check for available updates synchronously (blocking call). This method will block the calling
* thread while making the network request. Consider using checkForUpdate with callback for
* non-blocking behavior.
* thread while making the network request. Consider using checkForUpdate for non-blocking
* behavior.
*
* @return UpdateStatus indicating if an update is available, up to date, or error
*/
@NotNull
UpdateStatus checkForUpdateBlocking();

/**
* Check for available updates asynchronously using a callback.
* Check for available updates asynchronously.
*
* @param onResult Callback that will be called with the UpdateStatus result
* @return Future that will resolve to an UpdateStatus result
*/
void checkForUpdate(@NotNull UpdateCallback onResult);
@NotNull
Future<UpdateStatus> checkForUpdate();

/**
* Download and install the provided update by opening the download URL in the default browser or
Expand All @@ -36,9 +38,4 @@ public interface IDistributionApi {
* @param info Information about the update to download
*/
void downloadUpdate(@NotNull UpdateInfo info);

/** Callback interface for receiving async update check results. */
interface UpdateCallback {
void onResult(@NotNull UpdateStatus status);
}
}
46 changes: 44 additions & 2 deletions sentry/src/main/java/io/sentry/NoOpDistributionApi.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.sentry;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

Expand All @@ -21,12 +25,50 @@ public static NoOpDistributionApi getInstance() {
}

@Override
public void checkForUpdate(@NotNull UpdateCallback onResult) {
// No-op implementation - do nothing
public @NotNull Future<UpdateStatus> checkForUpdate() {
return new CompletedFuture<>(UpdateStatus.UpToDate.getInstance());
}

@Override
public void downloadUpdate(@NotNull UpdateInfo info) {
// No-op implementation - do nothing
}

/**
* A Future implementation that is already completed with a result. This is used instead of
* CompletableFuture.completedFuture() to maintain compatibility with Android API 21+.
*/
private static final class CompletedFuture<T> implements Future<T> {
private final T result;

CompletedFuture(T result) {
this.result = result;
}

@Override
public boolean cancel(final boolean mayInterruptIfRunning) {
return false;
}

@Override
public boolean isCancelled() {
return false;
}

@Override
public boolean isDone() {
return true;
}

@Override
public T get() throws ExecutionException {
return result;
}

@Override
public T get(final long timeout, final @NotNull TimeUnit unit)
throws ExecutionException, TimeoutException {
return result;
}
}
}
Loading