Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Handling Notification Deep Links with Cold Start Resilience in Android

Tech Oct 16 10

Handling Notification Deep Links with Cold Start Resilience in Android

When implementing push notification handlers in Android, a common challenge involves routing users to specific content screens regardless of whether the application process is currently active. The complexity arises from initialization dependencies: target atcivities often require application-level setup (user authentication, configuration loading, database initialization) that only occurs during normal app startup.

Consider an e-commerce scenario where a promotional notification must open a product page. If the user taps while the app runs, navigation should proceed immediately. However, if the process terminated, the app must complete its bootstrap sequence before displaying the product, ensuring that checkout functionality dependent on session data functions correctly.

Architectural Approach

The solution requires distinguishing between two process states and handling navigation accordingly:

Active Process State: The application process exists in memory, though the activity stack might be empty (user pressed back to exit). Directly launching the target activity creates an orphaned task stack where the back button exits to the home screen rather than the main interface.

Incative Process State: No application process exists. Launching requires starting from the entry point (Splash/Launch activity), initializing dependencies, then navigating to the destination.

The implementation uses a routing broadcast receiver to intercept notification taps, detect process viability, and construct appropriate intent sequences.

Component Structure

The implementation comprises these components executing across separate processes:

  1. MessageService: Background service monitoring push channels in an isolated process (:remote)
  2. NotificationPublisher: Broadcast receiver constructing the notification UI
  3. NavigationRouter: Broadcast receiver handling tap gestures and route determination
  4. LaunchActivity: Entry point managing initialization and splash display
  5. HomeActivity: Primary interface serving as the navigation hub
  6. ProductActivity: Destination displaying item details

Service Implementation

Configure the remote service in AndroidManifest.xml:

<service 
    android:name=".MessageService"
    android:process=":remote" />

The service schedules periodic notification simulations using AlarmManager:

public class MessageService extends Service {
    private static final String TAG = "RemoteService";

    @Override
    public void onCreate() {
        super.onCreate();
        AlarmManager scheduler = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        
        Intent trigger = new Intent(this, NotificationPublisher.class);
        PendingIntent operation = PendingIntent.getBroadcast(
            this, 
            0, 
            trigger, 
            PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
        );
        
        scheduler.setExact(
            AlarmManager.ELAPSED_REALTIME_WAKEUP,
            SystemClock.elapsedRealtime() + 5000,
            operation
        );
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

Notification Construction

The publisher builds the notification with a routing pending intent:

public class NotificationPublisher extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Intent routeAction = new Intent(context, NavigationRouter.class);
        PendingIntent tapAction = PendingIntent.getBroadcast(
            context,
            (int) System.currentTimeMillis(),
            routeAction,
            PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
        );

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "promotions")
            .setContentTitle("Special Offer")
            .setContentText("Check out today's deals")
            .setSmallIcon(R.drawable.ic_notification)
            .setAutoCancel(true)
            .setContentIntent(tapAction);

        NotificationManager manager = (NotificationManager) 
            context.getSystemService(Context.NOTIFICATION_SERVICE);
        manager.notify(1, builder.build());
    }
}

Route Resolution Logic

The router determines process state and constructs the appropriate launch sequence:

public class NavigationRouter extends BroadcastReceiver {
    private static final String TARGET_PACKAGE = "com.example.notificationrouter";
    private static final String EXTRA_PAYLOAD = "navigation_payload";

    @Override
    public void onReceive(Context context, Intent intent) {
        boolean isRunning = ProcessChecker.isApplicationRunning(context, TARGET_PACKAGE);
        
        if (isRunning) {
            // Process exists: Ensure MainActivity exists in stack before ProductActivity
            Intent mainIntent = new Intent(context, HomeActivity.class);
            mainIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            
            Intent productIntent = new Intent(context, ProductActivity.class);
            productIntent.putExtra("product_id", "SKU-8842");
            productIntent.putExtra("product_name", "Wireless Headphones");
            productIntent.putExtra("discount", "30%");
            
            // Launch array preserves back-stack navigation
            context.startActivities(new Intent[]{mainIntent, productIntent});
        } else {
            // Process dead: Launch via entry point with payload
            Intent launcher = context.getPackageManager()
                .getLaunchIntentForPackage(TARGET_PACKAGE);
            
            launcher.setFlags(
                Intent.FLAG_ACTIVITY_NEW_TASK | 
                Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED |
                Intent.FLAG_ACTIVITY_CLEAR_TOP
            );
            
            Bundle payload = new Bundle();
            payload.putString("product_id", "SKU-8842");
            payload.putString("product_name", "Wireless Headphones");
            payload.putString("discount", "30%");
            payload.putBoolean("from_notification", true);
            
            launcher.putExtra(EXTRA_PAYLOAD, payload);
            context.startActivity(launcher);
        }
    }
}

Process State Detection

Utility class for determining application viability:

public class ProcessChecker {
    public static boolean isApplicationRunning(Context context, String packageName) {
        ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<RunningAppProcessInfo> processes = manager.getRunningAppProcesses();
        
        if (processes == null) return false;
        
        for (RunningAppProcessInfo process : processes) {
            if (process.processName.equals(packageName) && 
                process.importance != RunningAppProcessInfo.IMPORTANCE_EMPTY) {
                return true;
            }
        }
        return false;
    }
}

Activity Chain Handling

LaunchActivity manages the cold start parameter propagation:

public class LaunchActivity extends AppCompatActivity {
    private static final String EXTRA_PAYLOAD = "navigation_payload";
    private Handler uiHandler = new Handler(Looper.getMainLooper());

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_launch);
        
        // Simulate initialization (3 seconds)
        uiHandler.postDelayed(() -> {
            Intent nextStep = new Intent(this, HomeActivity.class);
            
            // Forward notification payload if present
            Bundle notificationData = getIntent().getBundleExtra(EXTRA_PAYLOAD);
            if (notificationData != null) {
                nextStep.putExtra(EXTRA_PAYLOAD, notificationData);
            }
            
            startActivity(nextStep);
            finish();
        }, 3000);
    }
}

HomeActivity checks for pending navigation requests after initialization:

public class HomeActivity extends AppCompatActivity {
    private static final String EXTRA_PAYLOAD = "navigation_payload";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        
        // Check for deep link request from notification
        Bundle routeData = getIntent().getBundleExtra(EXTRA_PAYLOAD);
        if (routeData != null && routeData.getBoolean("from_notification", false)) {
            navigateToProduct(routeData);
        }
    }
    
    private void navigateToProduct(Bundle data) {
        Intent detailView = new Intent(this, ProductActivity.class);
        detailView.putExtras(data);
        detailView.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        startActivity(detailView);
    }
}

ProductActivity displays the final destination:

public class ProductActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_product);
        
        String productName = getIntent().getStringExtra("product_name");
        String discount = getIntent().getStringExtra("discount");
        String sku = getIntent().getStringExtra("product_id");
        
        TextView titleView = findViewById(R.id.product_title);
        TextView discountView = findViewById(R.id.discount_badge);
        
        titleView.setText(productName);
        discountView.setText(discount);
    }
}

Key Considerations

When implementing this pattern, ensure that HomeActivity uses launchMode="singleTask" or handle intent flags appropriately to prevent duplicate instances in the stack. The intent array approach for active processes ensures proper back navigation, while the bundle chaining through LaunchActivity guarantees initialization completion during cold starts.

For production environments, consider using WorkManager for reliable notification scheduling instead of AlarmManager, and implement proper notification channels for Android O and above.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.