Network Handling and Offline Support
Overview
The SORI SDK processes audio recognition on-device. However, it requires an internet connection for receiving associated campaigns, loading related image resources, and webhook relay. When audio recognition succeeds but the network is unavailable during processing, the SDK provides several methods to handle this situation.
Features
- Automatic Network Detection: The SDK monitors network connectivity changes in real-time
- Request Queuing: Failed requests due to network issues are queued in memory
- Automatic Retry: Queued requests are automatically processed when network becomes available
- Configurable Behavior: You can customize how the SDK handles network failures
- Time-based Expiry: Requests older than 10 minutes are automatically discarded
Configuration
You can configure the network handling behavior using the SORIConfig
class:
import com.iplateia.sorisdk.SORIConfig
import com.iplateia.sorisdk.SORIAudioRecognizer
// Create a custom configuration
val config = SORIConfig(
enableDelayedSending = true, // Enable request queuing
maxPendingTimeMillis = 600000L, // 10 minutes (default)
autoRetryOnNetworkRecovery = true // Auto-retry when network returns
)
// Apply configuration to the recognizer
val sori = SORIAudioRecognizer(
"YOUR_SORI_API_KEY",
"YOUR_SORI_SECRET_KEY"
)
sori.setConfig(config)
import com.iplateia.sorisdk.SORIConfig;
import com.iplateia.sorisdk.SORIAudioRecognizer;
// Create a custom configuration
SORIConfig config = new SORIConfig(
true, // enableDelayedSending
600000L, // maxPendingTimeMillis (10 minutes)
true // autoRetryOnNetworkRecovery
);
// Apply configuration to the recognizer
SORIAudioRecognizer sori = new SORIAudioRecognizer(
"YOUR_SORI_API_KEY",
"YOUR_SORI_SECRET_KEY"
);
sori.setConfig(config);
Handling Network Events
The SDK provides callbacks to notify your app about network-related events through the ISORIListener
interface:
import com.iplateia.sorisdk.ISORIListener
import com.iplateia.sorisdk.SORICampaign
class MyListener : ISORIListener {
override fun onReady() {
// SDK is ready
}
override fun onStateChanged(state: String) {
// Service state changed
}
override fun onError(error: String) {
// An error occurred
}
override fun onCampaignFound(campaign: SORICampaign) {
// Campaign detected successfully
}
override fun onNetworkError(materialId: String, error: String) {
// Network error occurred while posting activity
Log.w("SORI", "Network error for material $materialId: $error")
// You can show a user-friendly message here
}
override fun onRequestQueued(materialId: String, queueCount: Int) {
// Request has been queued for later delivery
Log.i("SORI", "Request queued for material $materialId. Queue size: $queueCount")
// You can notify the user that the request will be sent later
}
}
// Set the listener
sori.setListener(context, MyListener())
import com.iplateia.sorisdk.ISORIListener;
import com.iplateia.sorisdk.SORICampaign;
import android.util.Log;
public class MyListener implements ISORIListener {
@Override
public void onReady() {
// SDK is ready
}
@Override
public void onStateChanged(String state) {
// Service state changed
}
@Override
public void onError(String error) {
// An error occurred
}
@Override
public void onCampaignFound(SORICampaign campaign) {
// Campaign detected successfully
}
@Override
public void onNetworkError(String materialId, String error) {
// Network error occurred while posting activity
Log.w("SORI", "Network error for material " + materialId + ": " + error);
// You can show a user-friendly message here
}
@Override
public void onRequestQueued(String materialId, int queueCount) {
// Request has been queued for later delivery
Log.i("SORI", "Request queued for material " + materialId +
". Queue size: " + queueCount);
// You can notify the user that the request will be sent later
}
}
// Set the listener
sori.setListener(context, new MyListener());
Configuration Options
SORIConfig
Parameters
Parameter | Type | Default | Description |
---|---|---|---|
enableDelayedSending | Boolean | true | Enable or disable request queuing when network is unavailable. When false , failed requests are immediately discarded. |
maxPendingTimeMillis | Long | 600000 (10 min) | Maximum time to keep pending requests before discarding them |
autoRetryOnNetworkRecovery | Boolean | true | Automatically retry queued requests when network becomes available. When false , queued requests remain in memory but require manual processing. Only effective when enableDelayedSending is true . |
Understanding the Configuration Options
The two boolean options serve different purposes:
enableDelayedSending
: Controls whether to save failed requests to a queue when network is unavailabletrue
: Save failed requests for later delivery when network is unavailablefalse
: Discard failed requests immediately when network is unavailable- Note: When network is available, requests are always sent immediately regardless of this setting
autoRetryOnNetworkRecovery
: Controls what happens when network is restoredtrue
: Automatically process queued requests when network returnsfalse
: Keep requests in queue but don't process automatically (requires manual intervention)- Note: This setting only takes effect when
enableDelayedSending
istrue
Behavior Summary
Network Status | enableDelayedSending | Behavior |
---|---|---|
Available | true or false | Request sent immediately |
Unavailable | true | Request queued for later delivery |
Unavailable | false | Request discarded |
Most applications will use both settings with the same value (true
for resilient behavior, false
for immediate failure handling).
Best Practices
1. User Feedback
Always provide feedback to users when network issues occur:
override fun onNetworkError(materialId: String, error: String) {
runOnUiThread {
Toast.makeText(
context,
"Network unavailable. Will retry when connection is restored.",
Toast.LENGTH_SHORT
).show()
}
}
2. Disable Queuing for Time-Sensitive Operations
If your use case requires immediate delivery, you can disable queuing:
val config = SORIConfig(
enableDelayedSending = false // Disable queuing
)
3. Monitor Queue Status
Keep track of pending requests to inform users:
override fun onRequestQueued(materialId: String, queueCount: Int) {
if (queueCount > 5) {
// Many requests are pending, consider notifying the user
showPendingRequestsNotification(queueCount)
}
}
How It Works
- Detection Phase: When audio is recognized, the SDK attempts to post the activity to the server
- Network Check: Before sending, the SDK checks network availability
- Queue on Failure: If network is unavailable, the request is added to an in-memory queue
- Network Monitoring: The SDK monitors network state changes via Android's
ConnectivityManager
- Automatic Retry: When network becomes available, queued requests are automatically processed
- Expiry Check: Requests older than the configured timeout are discarded
Permissions Required
The SDK requires the following permissions for network handling:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
These permissions are already included in the SDK's manifest and will be automatically merged into your app.
Troubleshooting
Requests Not Being Queued
Ensure that delayed sending is enabled:
val config = SORIConfig(enableDelayedSending = true)
sori.setConfig(config)
Queued Requests Not Being Sent
- Verify that
autoRetryOnNetworkRecovery
is enabled - Check that your app has the
ACCESS_NETWORK_STATE
permission - Ensure the requests haven't expired (older than 10 minutes by default)
Memory Considerations
The request queue is maintained in memory. If your app is killed by the system, queued requests will be lost. For critical operations, consider implementing your own persistent storage mechanism.
Example: Complete Implementation
Here's a complete example showing how to implement network handling:
class MainActivity : AppCompatActivity() {
private lateinit var sori: SORIAudioRecognizer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize SORI with custom configuration
initializeSori()
}
private fun initializeSori() {
// Create configuration
val config = SORIConfig(
enableDelayedSending = true,
maxPendingTimeMillis = 600000L, // 10 minutes
autoRetryOnNetworkRecovery = true
)
// Create recognizer
sori = SORIAudioRecognizer(
BuildConfig.SORI_API_KEY,
BuildConfig.SORI_SECRET_KEY
)
// Apply configuration
sori.setConfig(config)
// Set listener
sori.setListener(this, object : ISORIListener {
override fun onReady() {
Log.d("SORI", "SDK is ready")
}
override fun onStateChanged(state: String) {
Log.d("SORI", "State changed: $state")
}
override fun onError(error: String) {
Log.e("SORI", "Error: $error")
}
override fun onCampaignFound(campaign: SORICampaign) {
Log.i("SORI", "Campaign found: ${campaign.name}")
// Handle the campaign
handleCampaign(campaign)
}
override fun onNetworkError(materialId: String, error: String) {
Log.w("SORI", "Network error: $error")
// Show user-friendly message
showNetworkErrorMessage()
}
override fun onRequestQueued(materialId: String, queueCount: Int) {
Log.i("SORI", "Request queued. Total pending: $queueCount")
// Update UI to show pending status
updatePendingRequestsUI(queueCount)
}
})
// Start recognition
sori.startRecognition(this)
}
private fun handleCampaign(campaign: SORICampaign) {
// Your campaign handling logic
}
private fun showNetworkErrorMessage() {
runOnUiThread {
Toast.makeText(
this,
"Network unavailable. Campaign will be processed when connection is restored.",
Toast.LENGTH_LONG
).show()
}
}
private fun updatePendingRequestsUI(count: Int) {
runOnUiThread {
// Update your UI to show pending requests
val pendingIndicator = findViewById<TextView>(R.id.pendingIndicator)
if (count > 0) {
pendingIndicator.visibility = View.VISIBLE
pendingIndicator.text = "$count pending"
} else {
pendingIndicator.visibility = View.GONE
}
}
}
}
public class MainActivity extends AppCompatActivity {
private SORIAudioRecognizer sori;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initialize SORI with custom configuration
initializeSori();
}
private void initializeSori() {
// Create configuration
SORIConfig config = new SORIConfig(
true, // enableDelayedSending
600000L, // maxPendingTimeMillis (10 minutes)
true // autoRetryOnNetworkRecovery
);
// Create recognizer
sori = new SORIAudioRecognizer(
BuildConfig.SORI_API_KEY,
BuildConfig.SORI_SECRET_KEY
);
// Apply configuration
sori.setConfig(config);
// Set listener
sori.setListener(this, new ISORIListener() {
@Override
public void onReady() {
Log.d("SORI", "SDK is ready");
}
@Override
public void onStateChanged(String state) {
Log.d("SORI", "State changed: " + state);
}
@Override
public void onError(String error) {
Log.e("SORI", "Error: " + error);
}
@Override
public void onCampaignFound(SORICampaign campaign) {
Log.i("SORI", "Campaign found: " + campaign.getName());
// Handle the campaign
handleCampaign(campaign);
}
@Override
public void onNetworkError(String materialId, String error) {
Log.w("SORI", "Network error: " + error);
// Show user-friendly message
showNetworkErrorMessage();
}
@Override
public void onRequestQueued(String materialId, int queueCount) {
Log.i("SORI", "Request queued. Total pending: " + queueCount);
// Update UI to show pending status
updatePendingRequestsUI(queueCount);
}
});
// Start recognition
sori.startRecognition(this);
}
private void handleCampaign(SORICampaign campaign) {
// Your campaign handling logic
}
private void showNetworkErrorMessage() {
runOnUiThread(() -> {
Toast.makeText(
this,
"Network unavailable. Campaign will be processed when connection is restored.",
Toast.LENGTH_LONG
).show();
});
}
private void updatePendingRequestsUI(int count) {
runOnUiThread(() -> {
// Update your UI to show pending requests
TextView pendingIndicator = findViewById(R.id.pendingIndicator);
if (count > 0) {
pendingIndicator.setVisibility(View.VISIBLE);
pendingIndicator.setText(count + " pending");
} else {
pendingIndicator.setVisibility(View.GONE);
}
});
}
}