Skip to content

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:

kotlin
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)
java
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:

kotlin
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())
java
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

ParameterTypeDefaultDescription
enableDelayedSendingBooleantrueEnable or disable request queuing when network is unavailable. When false, failed requests are immediately discarded.
maxPendingTimeMillisLong600000 (10 min)Maximum time to keep pending requests before discarding them
autoRetryOnNetworkRecoveryBooleantrueAutomatically 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 unavailable

    • true: Save failed requests for later delivery when network is unavailable
    • false: 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 restored

    • true: Automatically process queued requests when network returns
    • false: Keep requests in queue but don't process automatically (requires manual intervention)
    • Note: This setting only takes effect when enableDelayedSending is true

Behavior Summary

Network StatusenableDelayedSendingBehavior
Availabletrue or falseRequest sent immediately
UnavailabletrueRequest queued for later delivery
UnavailablefalseRequest 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:

kotlin
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:

kotlin
val config = SORIConfig(
    enableDelayedSending = false  // Disable queuing
)

3. Monitor Queue Status

Keep track of pending requests to inform users:

kotlin
override fun onRequestQueued(materialId: String, queueCount: Int) {
    if (queueCount > 5) {
        // Many requests are pending, consider notifying the user
        showPendingRequestsNotification(queueCount)
    }
}

How It Works

  1. Detection Phase: When audio is recognized, the SDK attempts to post the activity to the server
  2. Network Check: Before sending, the SDK checks network availability
  3. Queue on Failure: If network is unavailable, the request is added to an in-memory queue
  4. Network Monitoring: The SDK monitors network state changes via Android's ConnectivityManager
  5. Automatic Retry: When network becomes available, queued requests are automatically processed
  6. Expiry Check: Requests older than the configured timeout are discarded

Permissions Required

The SDK requires the following permissions for network handling:

xml
<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:

kotlin
val config = SORIConfig(enableDelayedSending = true)
sori.setConfig(config)

Queued Requests Not Being Sent

  1. Verify that autoRetryOnNetworkRecovery is enabled
  2. Check that your app has the ACCESS_NETWORK_STATE permission
  3. 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:

kotlin
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
            }
        }
    }
}
java
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);
            }
        });
    }
}