Skip to content

New in-app type - PIP SDK-5617#969

Open
piyush-kukadiya wants to merge 77 commits intotask/release/8.1.0from
feat/pip/SDK-5617
Open

New in-app type - PIP SDK-5617#969
piyush-kukadiya wants to merge 77 commits intotask/release/8.1.0from
feat/pip/SDK-5617

Conversation

@piyush-kukadiya
Copy link
Copy Markdown
Contributor

@piyush-kukadiya piyush-kukadiya commented Mar 26, 2026

Summary by CodeRabbit

  • New Features

    • Picture-in-Picture (PIP) in-app notifications: draggable, repositionable compact windows with full‑screen expansion.
    • Media support for video, GIF, and images with automatic fallback handling.
    • Interactive controls: play/pause, mute, expand/collapse, close, and action/deeplink buttons; auto‑hide overlay behavior.
    • Configurable positioning (9 anchors), sizing, margins, animation styles, and control visibility.
  • Tests

    • Added unit tests covering PIP configuration parsing and callback forwarding.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • \b(master|main|develop)(_\w*)?

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7e9ac601-df92-4cc0-86c5-856fd11b7763

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds Picture-in-Picture (PIP) in-app notifications: new PIP types, config factory, UI (compact/expanded), media renderers (video/image/GIF), session/manager lifecycle, animations, drag/positioning, and InAppController integration plus tests and resources.

Changes

Cohort / File(s) Summary
Core InApp & Type
clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.kt, clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppType.kt
Added pip in-app type; CTInAppNotification stores/restores pipConfigJson and validates PIP requires media.
Controller Integration
clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.kt
Dispatch added for CTInAppTypePIP: initializes PIPManager, builds PIPConfig, shows/dismisses PIP; new private init helper.
PIP Config & Factory
clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/PIPConfigFactory.kt, clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPConfig.kt, clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPAnimationConfig.kt
New immutable PIPConfig + builder and PIPAnimationConfig; PIPConfigFactory parses JSON, validates inputs, maps animation/interpolator and media type.
PIP SDK Surface & Manager
clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPManager.kt, clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/session/PIPSession.kt, clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/lifecycle/PIPLifecycleObserver.kt
New PIPManager singleton for thread-safe show/dismiss/isVisible, lifecycle and rotation handling; PIPSession holds runtime state; lifecycle observer triggers stop behavior.
Callbacks & Bridge
clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPCallbacks.kt, clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/PIPInAppCallbacksBridge.kt
PIPCallbacks interface and PIPInAppCallbacksBridge bridge events to existing InAppListener.
Media Rendering & Player
.../pipsdk/internal/renderer/MediaRenderer.kt, .../VideoRenderer.kt, .../ImageRenderer.kt, .../GifRenderer.kt, .../PIPVideoPlayerWrapper.kt, .../RendererStateListener.kt
Implemented renderer abstraction and concrete renderers for video/image/GIF, ExoPlayer wrapper with rotation/first-frame handling, and renderer state callbacks.
Views & UI
.../internal/view/PIPRootContainer.kt, .../PIPCompactView.kt, .../PIPExpandedView.kt, .../PIPMediaView.kt, .../PIPControlsOverlay.kt, .../PIPIcons.kt, .../PIPDimens.kt
Added root container managing compact/expanded UI, shared media view, controls overlay, icons, and dimens constant.
Interaction, Layout & Animation
.../internal/engine/PIPAnimator.kt, .../PIPDragHandler.kt, .../PIPPositionResolver.kt, .../internal/engine/Extensions.kt
Animation utilities (in/out/snap/expand/collapse), touch drag handling with snap behavior, anchor resolver for 9-grid positions, and dp→px helper.
Renderer Helpers
.../internal/renderer/FallbackImageLoader.kt
Fallback image loader with cache-first fetch and UI posting logic.
PIP SDK Types
clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPMediaType.kt, .../PIPPosition.kt, .../PIPAnimation.kt
Added enums for media types, positions, and animation types.
In-App Response (debug)
clevertap-core/src/main/java/com/clevertap/android/sdk/response/InAppResponse.java
Debug-only injection of a test PIP notification into server response when flag enabled.
Video Player Layout Simplification
clevertap-core/src/main/java/com/clevertap/android/sdk/video/inapps/ExoplayerHandle.kt, .../Media3Handle.kt
Replaced device-dependent sizing with MATCH_PARENT for player view layout; removed helper sizing functions.
Resources
clevertap-core/src/main/res/drawable/ct_ic_close_pip.xml, ct_ic_collapse.xml, ct_ic_deeplink.xml, ct_ic_expand.xml, ct_ic_pause.xml, ct_ic_play.xml, ct_ic_volume_off_tint.xml, ct_ic_volume_on_tint.xml
Added vector drawables used by PIP controls.
Tests & Sample
clevertap-core/src/test/.../PIPConfigFactoryTest.kt, .../PIPInAppCallbacksBridgeTest.kt, sample/.../HomeScreenViewModel.kt
Unit tests for config factory and callbacks bridge; sample app AB variants logging commented out.

Sequence Diagram(s)

sequenceDiagram
    participant Activity
    participant InAppController
    participant PIPConfigFactory
    participant PIPManager
    participant PIPSession
    participant PIPRootContainer
    participant PIPMediaView
    participant MediaRenderer

    Activity->>InAppController: showInApp(notification of type pip)
    InAppController->>PIPConfigFactory: create(CTInAppNotification)
    PIPConfigFactory-->>InAppController: PIPConfig or null
    InAppController->>PIPManager: init(resourceProvider)
    InAppController->>PIPManager: show(activity, PIPConfig)
    PIPManager->>PIPSession: new PIPSession(config)
    PIPManager->>PIPRootContainer: bindSession(PIPSession)
    PIPRootContainer->>PIPMediaView: initialize(config, session)
    PIPMediaView->>MediaRenderer: attach(container, config, session)
    MediaRenderer-->>PIPMediaView: ready
    PIPRootContainer->>PIPRootContainer: animateIn compact view (PIPAnimator)
Loading
sequenceDiagram
    participant User
    participant PIPCompactView
    participant PIPDragHandler
    participant PIPPositionResolver
    participant PIPAnimator
    participant PIPSession

    User->>PIPCompactView: touch DOWN
    PIPCompactView->>PIPDragHandler: onInterceptDown()
    User->>PIPCompactView: MOVE...
    PIPCompactView->>PIPDragHandler: shouldIntercept()
    PIPDragHandler-->>PIPCompactView: start drag (threshold exceeded)
    User->>PIPCompactView: UP
    PIPDragHandler->>PIPPositionResolver: resolveAnchors(...)
    PIPPositionResolver-->>PIPDragHandler: anchors
    PIPDragHandler->>PIPPositionResolver: nearestPosition(...)
    PIPPositionResolver-->>PIPDragHandler: PIPPosition
    PIPDragHandler->>PIPAnimator: animateSnap(targetX,targetY)
    PIPAnimator-->>PIPDragHandler: onComplete
    PIPDragHandler->>PIPSession: currentPosition = resolved
Loading
sequenceDiagram
    participant User
    participant PIPCompactView
    participant PIPRootContainer
    participant PIPMediaView
    participant MediaRenderer
    participant PIPAnimator

    User->>PIPCompactView: tap expand
    PIPCompactView->>PIPRootContainer: expandToFull()
    PIPRootContainer->>PIPMediaView: onContainerChanged()
    PIPMediaView->>MediaRenderer: rebindSurface(...)
    PIPRootContainer->>PIPAnimator: animateExpand(...)
    PIPAnimator-->>PIPRootContainer: expand complete
    User->>PIPRootContainer: back press
    PIPRootContainer->>PIPAnimator: animateCollapse(...)
    PIPAnimator-->>PIPRootContainer: collapse complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • piyush-kukadiya
  • Anush-Shand
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introduction of a new PIP (Picture-in-Picture) in-app notification type. While brief, it directly reflects the primary purpose of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pip/SDK-5617

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@francispereira
Copy link
Copy Markdown

francispereira commented Mar 26, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@piyush-kukadiya
Copy link
Copy Markdown
Contributor Author

@coderabbitai update PR description

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the current PR details
gh pr view 969 --repo CleverTap/clevertap-android-sdk --json title,body,files,commits

Length of output: 44102


@piyush-kukadiya: I've updated the PR description with a comprehensive overview of all the changes. Here's what's now included:

  • Summary & Motivation — What this PR does and why.
  • Architecture Overview — A table-based breakdown of every new component across public API, internal engine, renderers, views, and session management.
  • Core Integration — How CTInAppType, CTInAppNotification, InAppController, PIPConfigFactory, and PIPInAppCallbacksBridge tie in.
  • Key Features — 9-position drag-snap, expand/collapse, rotation state preservation, edge-to-edge insets, all in-app action types, video-specific handling, fallback-to-image, and configurable controls.
  • Bug Fixes — A table covering all 13 fixes (lifecycle callback leak, first-frame race, ViewTreeObserver leak, renderer async guards, ExoPlayer error listener, scrim timeout, stale rotation state, etc.).
  • RefactoringFallbackImageLoader, PIPSession class conversion, RendererStateListener, effectiveAnimConfig(), FallbackLoadRequest, PIPIcons, and magic-number constants.
  • Tests — Coverage summary for PIPConfigFactoryTest and PIPInAppCallbacksBridgeTest.
  • Vector Drawables — List of all 8 new drawables added.

🏁 Script executed:

#!/bin/bash
gh pr edit 969 --repo CleverTap/clevertap-android-sdk --body "## Summary

Introduces a new **Picture-in-Picture (PIP)** In-App notification type (\`CTInAppTypePIP\`) that supports Video (HLS/MP4), animated GIFs, and static images in a draggable, lifecycle-aware floating window.

---

## Motivation

Enables marketers to deliver rich, persistent media In-App messages that float over the host application as a compact PIP widget. Users can expand the widget to full-screen, interact via deep links, and continue navigating the app while the media plays.

---

## New PIP SDK — Architecture Overview

### Public API
| Component | Role |
|---|---|
| \`PIPManager\` | Singleton entry point — \`show()\`, \`dismiss()\`, lifecycle hooks |
| \`PIPConfig\` / \`PIPConfig.Builder\` | Immutable configuration (media, position, animation, controls) |
| \`PIPCallbacks\` | Listener interface for show / close / action / expand / collapse events |
| \`PIPAnimation\` | Enum: \`DISSOLVE\`, \`SLIDE\`, \`INSTANT\` |
| \`PIPPosition\` | 9-position enum (top/center/bottom × left/center/right) |
| \`PIPMediaType\` | \`VIDEO\`, \`GIF\`, \`IMAGE\` |

### Internal Engine
| Component | Role |
|---|---|
| \`PIPAnimator\` | Entry/exit animations (dissolve, slide with easing, instant) |
| \`PIPDragHandler\` | Touch intercept + snap-to-9-positions drag (configurable) |
| \`PIPPositionResolver\` | Maps position enum + margins to window coordinates |
| \`PIPLifecycleObserver\` | Activity lifecycle callbacks for auto-reattach/dismiss |

### Renderers
| Component | Role |
|---|---|
| \`ImageRenderer\` | Loads static bitmap with cache-first strategy |
| \`GifRenderer\` | Renders animated GIFs via \`GifImageView\`; replaces instance on reparent to avoid race conditions |
| \`VideoRenderer\` | ExoPlayer/Media3 HLS+MP4 playback; reports state via \`RendererStateListener\` |
| \`PIPVideoPlayerWrapper\` | Self-contained ExoPlayer management (surface attach/detach, mute, seek preservation) |
| \`FallbackImageLoader\` | Shared utility for loading fallback images when primary media fails |

### Views
| Component | Role |
|---|---|
| \`PIPRootContainer\` | Window-attached root; owns animation, drag, insets, and expand/collapse orchestration |
| \`PIPCompactView\` | Draggable floating widget with close, mute, expand, and deep-link controls |
| \`PIPExpandedView\` | Full-screen overlay with close, mute, play/pause, collapse, and deep-link controls |
| \`PIPMediaView\` | Hosts the active renderer; handles surface rebind on rotation |
| \`PIPControlsOverlay\` | Fade-in/out controls layer with touch intercept when hidden |
| \`PIPIcons\` | Centralised stateful icon mapping (play/pause, mute/unmute) |

### Session
| Component | Role |
|---|---|
| \`PIPSession\` | Mutable runtime state (playback position, mute, expanded flag, player reference) |

---

## Integration with CleverTap Core

- **\`CTInAppType\`** — Added \`CTInAppTypePIP\` variant.
- **\`CTInAppNotification\`** — Exposed PIP-specific fields from the in-app JSON payload.
- **\`InAppController\`** — Handles display and dismissal of the PIP type via \`PIPManager\`.
- **\`PIPConfigFactory\`** — Maps CT In-App JSON (media, position, animation, controls, buttons array) to \`PIPConfig\`.
- **\`PIPInAppCallbacksBridge\`** — Bridges \`PIPCallbacks\` to \`InAppListener\` (onShow, onClose, onAction with \`CTInAppAction\` dispatch; expand/collapse/playback/error are log-only).
- **\`InAppResponse\`** — Added debug injection flag to facilitate PIP testing.
- **\`ExoplayerHandle\` / \`Media3Handle\`** — Simplified to support full-container layout for PIP video.

---

## Key Features

- **Draggable compact window** snapping to 9 screen positions with configurable margins.
- **Expand/collapse transitions** with smooth animations between compact and full-screen modes.
- **State preservation** across Activity rotation: playback position, mute state, and expanded mode all survive \`onDestroy\`/\`onCreate\`.
- **Edge-to-edge insets** — compact view respects system bars and display cutouts; expanded controls avoid cutouts while keeping media edge-to-edge.
- **All in-app action types** supported via the standard \`buttons\` array (\`OPEN_URL\`, \`KEY_VALUES\`, \`CUSTOM_CODE\`, \`CLOSE\`).
- **Video-specific handling** — Instant animation override for entry/exit (SurfaceView ignores parent alpha); muted autoplay; \`Player.REPEAT_MODE_ONE\`.
- **Fallback to static image** when video playback fails; video controls are hidden automatically.
- **Drop-shadow elevation** on the compact floating window for visual depth.
- **Controls visibility** — touch events are blocked (not just invisible) when the controls overlay is hidden.
- **Rich animation config** — duration, easing curve, and move-in direction configurable from server JSON.
- **Configurable controls** — drag, play/pause, mute, and expand/collapse buttons can each be individually disabled from JSON.

---

## Bug Fixes

| Fix | Description |
|---|---|
| Memory leak — \`ActivityLifecycleCallbacks\` | Unregistered on PIP dismiss; re-registered on next \`show()\` |
| Race condition — first-frame callback | Synchronized with a shared lock; prevents permanent black scrim after rotation |
| ViewTreeObserver listener leak | Stored and removed in \`detach()\` as a safety net |
| Renderer async callbacks post-release | \`@Volatile released\` flag guards every \`view.post\` callback |
| ExoPlayer error listener leak | Stored and properly removed in \`release()\` |
| Video scrim stuck after rotation error | 3-second \`postDelayed\` safety net removes the scrim even if no first frame fires |
| Stale rotation state | Cleared at the start of \`showInternal()\` to prevent spurious reattach |
| \`createSurface\` re-entrance | \`check()\` guard prevents double-call without releasing the first \`PlayerView\` |
| Fallback image lost on rotation | \`VideoRenderer\` releases failed player in \`loadFallbackAsImage()\`; \`PIPMediaView\` detects null wrapper and reloads fallback |
| GIF crash on expand/collapse | Fresh \`GifImageView\` instance created after reparenting |
| Expanded state lost on rotation | \`isExpanded\` flag stored in \`PIPSession\` and restored on reattach |
| Height overflow in landscape | Clamped to 60% of container height (\`MAX_HEIGHT_PERCENT\`) |
| Border/corner rendering on SurfaceView | Removed entirely — SurfaceView renders in a separate hardware layer and ignores \`clipToOutline\` |
| Invisible controls still clickable | \`onInterceptTouchEvent\` blocks all touches when controls are hidden |

---

## Refactoring

- Extracted \`FallbackImageLoader\` to eliminate triplicated fallback-loading code across renderers.
- \`PIPSession\` changed from \`data class\` to regular class — removed unsafe auto-generated \`copy()\`/\`equals()\`/\`hashCode()\` for mutable fields.
- \`VideoRenderer\` decoupled from \`PIPSession\` via new \`RendererStateListener\` interface.
- \`effectiveAnimConfig()\` helper extracted in \`PIPRootContainer\` to DRY the video dissolve override.
- \`FallbackLoadRequest\` data class replaces 9 separate parameters across all three renderer callers.
- \`PIPIcons\` centralises stateful icon-to-drawable mapping shared between compact and expanded views.
- Magic numbers extracted to named constants in \`PIPAnimator\`, \`PIPControlsOverlay\`, \`PIPCompactView\`, and \`PIPExpandedView\`.

---

## Tests

- **\`PIPConfigFactoryTest\`** (23 tests) — null/invalid input handling, media type mapping, all 9 position strings, animation variants, config field extraction (margins, width, aspect ratio, controls), and builder validation failure.
- **\`PIPInAppCallbacksBridgeTest\`** (9 tests) — \`onShow\`/\`onClose\`/\`onRedirect\` forwarding, \`onRedirect\` action creation, and log-only callback verification.

---

## Vector Drawables Added

\`ct_ic_close_pip\`, \`ct_ic_collapse\`, \`ct_ic_deeplink\`, \`ct_ic_expand\`, \`ct_ic_pause\`, \`ct_ic_play\`, \`ct_ic_volume_off_tint\`, \`ct_ic_volume_on_tint\`"

Length of output: 544


@piyush-kukadiya: The gh pr edit command is blocked by workflow restriction settings in this repository, so I'm unable to update the PR description directly. Please copy the description below and paste it manually into the PR.

📋 PR Description — click to expand
## Summary

Introduces a new **Picture-in-Picture (PIP)** In-App notification type (`CTInAppTypePIP`) that supports Video (HLS/MP4), animated GIFs, and static images in a draggable, lifecycle-aware floating window.

---

## Motivation

Enables marketers to deliver rich, persistent media In-App messages that float over the host application as a compact PIP widget. Users can expand the widget to full-screen, interact via deep links, and continue navigating the app while the media plays.

---

## New PIP SDK — Architecture Overview

### Public API
| Component | Role |
|---|---|
| `PIPManager` | Singleton entry point — `show()`, `dismiss()`, lifecycle hooks |
| `PIPConfig` / `PIPConfig.Builder` | Immutable configuration (media, position, animation, controls) |
| `PIPCallbacks` | Listener interface for show / close / action / expand / collapse events |
| `PIPAnimation` | Enum: `DISSOLVE`, `SLIDE`, `INSTANT` |
| `PIPPosition` | 9-position enum (top/center/bottom × left/center/right) |
| `PIPMediaType` | `VIDEO`, `GIF`, `IMAGE` |

### Internal Engine
| Component | Role |
|---|---|
| `PIPAnimator` | Entry/exit animations (dissolve, slide with easing, instant) |
| `PIPDragHandler` | Touch intercept + snap-to-9-positions drag (configurable) |
| `PIPPositionResolver` | Maps position enum + margins to window coordinates |
| `PIPLifecycleObserver` | Activity lifecycle callbacks for auto-reattach/dismiss |

### Renderers
| Component | Role |
|---|---|
| `ImageRenderer` | Loads static bitmap with cache-first strategy |
| `GifRenderer` | Renders animated GIFs via `GifImageView`; replaces instance on reparent to avoid race conditions |
| `VideoRenderer` | ExoPlayer/Media3 HLS+MP4 playback; reports state via `RendererStateListener` |
| `PIPVideoPlayerWrapper` | Self-contained ExoPlayer management (surface attach/detach, mute, seek preservation) |
| `FallbackImageLoader` | Shared utility for loading fallback images when primary media fails |

### Views
| Component | Role |
|---|---|
| `PIPRootContainer` | Window-attached root; owns animation, drag, insets, and expand/collapse orchestration |
| `PIPCompactView` | Draggable floating widget with close, mute, expand, and deep-link controls |
| `PIPExpandedView` | Full-screen overlay with close, mute, play/pause, collapse, and deep-link controls |
| `PIPMediaView` | Hosts the active renderer; handles surface rebind on rotation |
| `PIPControlsOverlay` | Fade-in/out controls layer with touch intercept when hidden |
| `PIPIcons` | Centralised stateful icon mapping (play/pause, mute/unmute) |

### Session
| Component | Role |
|---|---|
| `PIPSession` | Mutable runtime state (playback position, mute, expanded flag, player reference) |

---

## Integration with CleverTap Core

- **`CTInAppType`** — Added `CTInAppTypePIP` variant.
- **`CTInAppNotification`** — Exposed PIP-specific fields from the in-app JSON payload.
- **`InAppController`** — Handles display and dismissal of the PIP type via `PIPManager`.
- **`PIPConfigFactory`** — Maps CT In-App JSON (media, position, animation, controls, buttons array) to `PIPConfig`.
- **`PIPInAppCallbacksBridge`** — Bridges `PIPCallbacks` to `InAppListener` (onShow, onClose, onAction with `CTInAppAction` dispatch; expand/collapse/playback/error are log-only).
- **`InAppResponse`** — Added debug injection flag to facilitate PIP testing.
- **`ExoplayerHandle` / `Media3Handle`** — Simplified to support full-container layout for PIP video.

---

## Key Features

- **Draggable compact window** snapping to 9 screen positions with configurable margins.
- **Expand/collapse transitions** with smooth animations between compact and full-screen modes.
- **State preservation** across Activity rotation: playback position, mute state, and expanded mode all survive `onDestroy`/`onCreate`.
- **Edge-to-edge insets** — compact view respects system bars and display cutouts; expanded controls avoid cutouts while keeping media edge-to-edge.
- **All in-app action types** supported via the standard `buttons` array (`OPEN_URL`, `KEY_VALUES`, `CUSTOM_CODE`, `CLOSE`).
- **Video-specific handling** — Instant animation override for entry/exit (SurfaceView ignores parent alpha); muted autoplay; `Player.REPEAT_MODE_ONE`.
- **Fallback to static image** when video playback fails; video controls are hidden automatically.
- **Drop-shadow elevation** on the compact floating window for visual depth.
- **Controls visibility** — touch events are blocked (not just invisible) when the controls overlay is hidden.
- **Rich animation config** — duration, easing curve, and move-in direction configurable from server JSON.
- **Configurable controls** — drag, play/pause, mute, and expand/collapse buttons can each be individually disabled from JSON.

---

## Bug Fixes

| Fix | Description |
|---|---|
| Memory leak — `ActivityLifecycleCallbacks` | Unregistered on PIP dismiss; re-registered on next `show()` |
| Race condition — first-frame callback | Synchronized with a shared lock; prevents permanent black scrim after rotation |
| ViewTreeObserver listener leak | Stored and removed in `detach()` as a safety net |
| Renderer async callbacks post-release | `@Volatile released` flag guards every `view.post` callback |
| ExoPlayer error listener leak | Stored and properly removed in `release()` |
| Video scrim stuck after rotation error | 3-second `postDelayed` safety net removes the scrim even if no first frame fires |
| Stale rotation state | Cleared at the start of `showInternal()` to prevent spurious reattach |
| `createSurface` re-entrance | `check()` guard prevents double-call without releasing the first `PlayerView` |
| Fallback image lost on rotation | `VideoRenderer` releases failed player in `loadFallbackAsImage()`; `PIPMediaView` detects null wrapper and reloads fallback |
| GIF crash on expand/collapse | Fresh `GifImageView` instance created after reparenting |
| Expanded state lost on rotation | `isExpanded` flag stored in `PIPSession` and restored on reattach |
| Height overflow in landscape | Clamped to 60% of container height (`MAX_HEIGHT_PERCENT`) |
| Border/corner rendering on SurfaceView | Removed entirely — SurfaceView renders in a separate hardware layer and ignores `clipToOutline` |
| Invisible controls still clickable | `onInterceptTouchEvent` blocks all touches when controls are hidden |

---

## Refactoring

- Extracted `FallbackImageLoader` to eliminate triplicated fallback-loading code across renderers.
- `PIPSession` changed from `data class` to regular class — removed unsafe auto-generated `copy()`/`equals()`/`hashCode()` for mutable fields.
- `VideoRenderer` decoupled from `PIPSession` via new `RendererStateListener` interface.
- `effectiveAnimConfig()` helper extracted in `PIPRootContainer` to DRY the video dissolve override.
- `FallbackLoadRequest` data class replaces 9 separate parameters across all three renderer callers.
- `PIPIcons` centralises stateful icon-to-drawable mapping shared between compact and expanded views.
- Magic numbers extracted to named constants in `PIPAnimator`, `PIPControlsOverlay`, `PIPCompactView`, and `PIPExpandedView`.

---

## Tests

- **`PIPConfigFactoryTest`** (23 tests) — null/invalid input handling, media type mapping, all 9 position strings, animation variants, config field extraction (margins, width, aspect ratio, controls), and builder validation failure.
- **`PIPInAppCallbacksBridgeTest`** (9 tests) — `onShow`/`onClose`/`onRedirect` forwarding, `onRedirect` action creation, and log-only callback verification.

---

## Vector Drawables Added

`ct_ic_close_pip`, `ct_ic_collapse`, `ct_ic_deeplink`, `ct_ic_expand`, `ct_ic_pause`, `ct_ic_play`, `ct_ic_volume_off_tint`, `ct_ic_volume_on_tint`

I'll also trigger a regeneration of the AI-generated summary at the top of the PR.


🧠 Learnings used
Learnt from: CTLalit
Repo: CleverTap/clevertap-android-sdk PR: 802
File: clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.java:180-182
Timestamp: 2025-05-08T08:31:50.340Z
Learning: The CTInAppNotification class in the CleverTap Android SDK adds new fields `aspectRatio` and `isRequestForPushPermission` to its Parcel implementation in version 7.4.0, but doesn't require parcel versioning as these parcels only exist in memory during a single app session.

Learnt from: Anush-Shand
Repo: CleverTap/clevertap-android-sdk PR: 954
File: clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.kt:422-423
Timestamp: 2026-03-04T06:04:20.652Z
Learning: In CleverTap Android SDK CTInAppNotification.kt, the `CTInAppTypeCoverImageOnly`, `CTInAppTypeHalfInterstitialImageOnly`, and `CTInAppTypeInterstitialImageOnly` template types are "ImageOnly" in name only. They actually support all media types: images, audio, and video. Validation for these templates should only fail when `_mediaList` is empty, not when non-image media types are present.
✅ Actions performed

Summary regeneration triggered.

@CTLalit
Copy link
Copy Markdown
Collaborator

CTLalit commented Mar 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.kt`:
- Around line 975-978: The catch block in InAppController (the Throwable handler
that logs "Failed to show PIP in-app") only clears currentlyDisplayingInApp and
therefore stalls the in-app queue; update that catch to also advance the queue
by calling showNotificationIfAvailable() after setting currentlyDisplayingInApp
= null (same behavior as the pipConfig == null path) so the next queued item can
be processed.

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/PIPInAppCallbacksBridge.kt`:
- Around line 26-53: The onAction() in PIPInAppCallbacksBridge must not infer
the tapped action via buttons.firstOrNull(); instead, locate onAction() in
PIPInAppCallbacksBridge and replace the current logic with a lookup of the PIP
click mapping produced by PIPConfigFactory (the pip.onClick.android → redirect
URL/action) to build the Action passed to
inAppListener.inAppNotificationActionTriggered, falling back to any explicit
button match only if necessary; ensure buttonless media taps create an Action
using the PIPConfigFactory redirect and multi-button payloads map the correct
CTA (include action.type and button text only when a real button was tapped),
then call inAppListener.inAppNotificationActionTriggered(inAppNotification,
resolvedAction, resolvedButtonTextOrNull, null, null).

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/engine/PIPDragHandler.kt`:
- Around line 42-49: Replace the hardcoded DRAG_THRESHOLD_PX with the platform
touch-slop value from ViewConfiguration to make drag detection density-aware:
retrieve ViewConfiguration.get(<the View or context>) .scaledTouchSlop (e.g.,
cache it as a touchSlop property in PIPDragHandler during construction or init)
and use that touchSlop instead of DRAG_THRESHOLD_PX in shouldIntercept and the
other drag-check sites (the other usages referenced by isDragging checks).
Remove or stop using DRAG_THRESHOLD_PX and update comparisons (dx > touchSlop ||
dy > touchSlop) so the class uses scaledTouchSlop for tap-vs-drag
discrimination.

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/FallbackImageLoader.kt`:
- Around line 48-77: The load(...) path in FallbackImageLoader can post stale
work after a shared isReleased() flips back; capture a per-request generation
token at the start of FallbackImageLoader.load (e.g., a unique requestId or
generation from FallbackLoadRequest or by reading/setting a tag on container)
and carry it into the mediaExecutor and container.post closures, then before
mutating UI (inside container.post and before calling addFallbackImageView or
callbacks) compare the captured generation with the current generation on the
container (or request) and abort if they differ; keep the existing isReleased()
check but add this per-request generation equality check in both cached and
fetched branches to prevent overlaying stale fallback UI.

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/ImageRenderer.kt`:
- Around line 47-56: The fallback path can run on a stale/detached ImageView and
crash when casting iv.parent to ViewGroup; inside the mediaExecutor.execute {
... } iv.post { ... } blocks (the one using
resourceProvider.fetchInAppImageV1(config.mediaUrl) and the similar block around
lines 85-97), before calling loadFallback(iv, config) or casting iv.parent,
check that the view is still attached and valid (e.g., iv.isAttachedToWindow or
iv.parent != null) and that released is false; if the view is detached or parent
is null, return early and do not call loadFallback or cast iv.parent to
ViewGroup to avoid the crash.

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/VideoRenderer.kt`:
- Around line 55-65: When reusing an existing session.videoPlayerWrapper (and in
the other reuse sites), the old error-listener lambda (created by
setupErrorListener()) still closes over the previous container; fix by
removing/unregistering the existing player error listener on the reused wrapper
and then re-registering a fresh listener via setupErrorListener() after
assigning wrapper and rebinding the surface (i.e., after wrapper =
existingWrapper and after container.addView(surface)) so the new listener closes
over the active container/PIPMediaView; apply the same unregister-then-setup
pattern wherever the wrapper is reused (e.g., the other reuse blocks mentioned).

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPCompactView.kt`:
- Around line 59-64: Add static contentDescription labels for all clickable
ImageView controls in PIPCompactView (e.g., the deeplink button created as
dlBtn, the close/expand buttons at the other ImageView creation sites, and the
mute toggle) so TalkBack announces them; set contentDescription to appropriate
strings like "action", "close", "expand", and "mute" when creating each
ImageView (alongside setImageResource/visibility/scaleType/setOnClickListener).
Also update updateMuteIcon() to set the mute ImageView's contentDescription to
the current state (e.g., "mute" vs "unmute") whenever the icon is changed so the
label matches the icon state. Ensure you reference the existing ImageView
variables (dlBtn and the close/expand/mute ImageView instances) and modify their
creation and updateMuteIcon() accordingly.
- Around line 128-141: bindVideoControls moves deeplinkBtn to
Gravity.TOP|Gravity.START for video mode but hideVideoControls never restores
its original gravity, leaving the compact layout stuck in video arrangement;
update hideVideoControls to check deeplinkBtn and reset (it.layoutParams as
LayoutParams).gravity back to the original position (e.g., Gravity.CENTER or the
non-video gravity used elsewhere) and call requestLayout(), and also ensure the
same restore logic is applied where controls are removed (the counterpart code
around the other video-control teardown path referenced in the review).

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPExpandedView.kt`:
- Around line 91-97: The ImageView controls (closeBtn, actionBtn, collapseBtn,
playPauseBtn, muteBtn) are missing accessibility labels; set contentDescription
for each when creating them (e.g., context.getString(...) or hardcoded strings),
and ensure playPauseBtn and muteBtn update their contentDescription whenever
their icon/state toggles (update inside the methods that call setImageResource
or the click handlers/state setters that toggle play/pause and mute). Use
descriptive strings ("Close", "Action", "Collapse", "Play"/"Pause",
"Mute"/"Unmute") and keep the description in sync with the current
drawable/state for playPauseBtn and muteBtn. Make changes where these views are
initialized and where their state or image resource is changed so the
accessibility label always matches the visible icon.

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPMediaView.kt`:
- Around line 95-109: The scrim safety timeout is not armed when handler is null
(view not yet attached), so the scrim may never be removed; change the
delayed-post call to use the View.postDelayed mechanism which queues before
attachment: in rebindSurface()/PIPMediaView where you create removeScrim and
call handler?.postDelayed(removeScrim, 3000L), replace that scheduling with
view.postDelayed(removeScrim, 3000L) (i.e., call postDelayed on the PIPMediaView
instance or the scrim view) while keeping the existing
wrapper.notifyWhenFirstFrame handling and handler?.removeCallbacks(removeScrim)
so the first-frame path still cancels the timeout.

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPConfig.kt`:
- Around line 10-41: Add an init block to PIPConfig that enforces the same
invariants as Builder.build(): validate mediaUrl is not blank, if fallbackUrl is
provided it is not blank, aspectRatioNumerator and aspectRatioDenominator are >
0, widthPercent is within a sane range (e.g. 1..100), and horizontalEdgeMarginDp
/ verticalEdgeMarginDp are >= 0; when any check fails throw an
IllegalArgumentException with a descriptive message. Place this init inside the
PIPConfig data class so copies created via copy() or direct construction are
validated (mirroring the checks in Builder.build()).

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPManager.kt`:
- Around line 275-283: cleanupSession() currently releases the video player
twice (it calls s.videoPlayerWrapper?.release() and then calls
container.detach(releaseMedia = true) which will release the same wrapper); to
fix, match the safer ordering used in dismissInternal(): first detach the
pipRootContainer (container.detach(releaseMedia = true)), then release and null
the videoPlayerWrapper (or simply remove the direct release call and let detach
handle it), and ensure you null s.videoPlayerWrapper only after detach to avoid
double-release; update the cleanupSession() implementation (references:
cleanupSession(), s.videoPlayerWrapper, s.pipRootContainer, container.detach and
dismissInternal()) accordingly.
- Around line 138-142: The PIP lifecycle observer added in showInternal()
(PIPLifecycleObserver) is never removed, causing stale observers to call
cleanupSession() on a replaced session; fix by storing the lifecycleOwner and
the observer on the newSession object when you create them in showInternal(),
and then remove the observer via
lifecycleOwner.lifecycle.removeObserver(newSession.lifecycleObserver) (or
equivalent) before releasing resources inside both dismissInternal() and
cleanupSession() in PIPManager so the old observer cannot tear down the active
session.

In
`@clevertap-core/src/main/java/com/clevertap/android/sdk/response/InAppResponse.java`:
- Around line 33-34: The INJECT_TEST_PIP flag in InAppResponse is set to true
and injects a synthetic PIP into every server response; change this to disabled
for shipped builds by setting INJECT_TEST_PIP to false (or remove the mutation
path) and, if a QA hook is still needed, gate it behind a non-shipped mechanism
such as a debug/build-time flag or a local-only test helper (avoid hardcoded
true in the production code). Update the constant INJECT_TEST_PIP and any
branches in InAppResponse that rely on it (and the related code around lines
~152-163) so production releases cannot mutate real server responses.

In
`@clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/PIPInAppCallbacksBridgeTest.kt`:
- Around line 35-53: The tests call a removed method onRedirect; update them to
use the current bridge API by calling PIPInAppCallbacksBridge.onAction(...) with
an InAppAction that represents an open-url redirect (type
InAppActionType.OPEN_URL and payload/url set to "https://www.example.com"), then
verify mockListener.inAppNotificationActionTriggered(mockNotification, any(),
eq(url), isNull(), isNull()) and the second test should assert the passed action
has type == InAppActionType.OPEN_URL (use match { it.type ==
InAppActionType.OPEN_URL }) instead of calling onRedirect; reference
PIPInAppCallbacksBridge.onAction, InAppAction/InAppActionType, and
mockListener.inAppNotificationActionTriggered to locate where to change the
calls and assertions.

In `@sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenViewModel.kt`:
- Around line 1172-1173: Uncomment the commented-out call to
cleverTapAPI?.variants() and the printVar("AB Variants", variants) lines inside
the printAbVariants() function so the method actually retrieves and prints A/B
test variants; locate the printAbVariants() method in HomeScreenViewModel.kt and
restore those two lines (cleverTapAPI?.variants() and printVar call) or, if you
prefer to remove the dead action, remove the menu mapping that invokes
printAbVariants() (menu item 12) so there's no non-functional menu entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f39696d5-f382-4265-a943-2a1ca7d4a711

📥 Commits

Reviewing files that changed from the base of the PR and between d30e179 and de80519.

📒 Files selected for processing (46)
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNotification.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppType.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/PIPConfigFactory.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/PIPInAppCallbacksBridge.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPAnimation.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPAnimationConfig.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPCallbacks.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPConfig.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPManager.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPMediaType.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/PIPPosition.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/engine/Extensions.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/engine/PIPAnimator.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/engine/PIPDragHandler.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/engine/PIPPositionResolver.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/lifecycle/PIPLifecycleObserver.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/FallbackImageLoader.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/GifRenderer.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/ImageRenderer.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/MediaRenderer.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/PIPVideoPlayerWrapper.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/RendererStateListener.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/renderer/VideoRenderer.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/session/PIPSession.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPCompactView.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPControlsOverlay.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPDimens.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPExpandedView.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPIcons.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPMediaView.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/pipsdk/internal/view/PIPRootContainer.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/response/InAppResponse.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/video/inapps/ExoplayerHandle.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/video/inapps/Media3Handle.kt
  • clevertap-core/src/main/res/drawable/ct_ic_close_pip.xml
  • clevertap-core/src/main/res/drawable/ct_ic_collapse.xml
  • clevertap-core/src/main/res/drawable/ct_ic_deeplink.xml
  • clevertap-core/src/main/res/drawable/ct_ic_expand.xml
  • clevertap-core/src/main/res/drawable/ct_ic_pause.xml
  • clevertap-core/src/main/res/drawable/ct_ic_play.xml
  • clevertap-core/src/main/res/drawable/ct_ic_volume_off_tint.xml
  • clevertap-core/src/main/res/drawable/ct_ic_volume_on_tint.xml
  • clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/PIPConfigFactoryTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/PIPInAppCallbacksBridgeTest.kt
  • sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenViewModel.kt


playerView = (LayoutInflater.from(context).inflate(R.layout.ct_exo_inapp_styled_player_view, null) as StyledPlayerView).apply {
playerViewLayoutParamsNormal = FrameLayout.LayoutParams(playerWidth, playerHeight)
playerViewLayoutParamsNormal = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is the initial and full screen size params, ideally we should not change this and remove out full screen logic?

Comment on lines +1021 to +1023
private fun initPIPManagerIfNeeded() {
PIPManager.init(FileResourceProvider.getInstance(context, logger))
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

difficult to UT this code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, initPIPManagerIfNeeded() is hard to unit test directly because PIPManager is an object singleton. The PIP config parsing logic is tested via PIPConfigFactoryTest instead. Converting PIPManager from object to a class (for testability) is noted as a follow-up refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated — PIPManager is now a class injected into InAppController via constructor (390698d78), so it can be mocked in unit tests. The initPIPManagerIfNeeded() method is removed — PIPManager initializes its FileResourceProvider lazily on first show().

val showExpandCollapseButton: Boolean = true,

// Callbacks
val callbacks: PIPCallbacks? = null,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

should be a data class and this should not be a part of PipConfig if this is server data class rep.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

PIPConfig is not a pure server DTO — it's a session configuration that bundles everything needed to show() a PIP (server data + client callbacks). Passing callbacks separately would split related data. This is a common Android SDK pattern (e.g., NotificationCompat.Builder bundles content + actions + listeners). If we want to separate concerns, we can discuss in a follow-up.

internal object PIPManager {

@Volatile private var session: PIPSession? = null
private val mainHandler = Handler(Looper.getMainLooper())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why not use coroutines?

* PIPManager.show(activity, PIPConfig.builder(url, PIPMediaType.IMAGE).build());
* ```
*/
internal object PIPManager {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lets not make this object and have it as a class in scope of PIP display and release it once done? objects are hard for testing and synchronous stuff?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid testability concern. Converting to a class would require wiring DI through InAppController and managing the instance lifecycle. For an internal SDK class with singleton semantics (only one PIP at a time), object is simpler. Noted as a follow-up refactor if we need better testability.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored in 390698d78 — converted PIPManager from object singleton to a regular class injected into InAppController via constructor. Uses a lazy lambda for FileResourceProvider to defer initialization until PIP is actually shown. This enables unit testing and proper scoping.

fun detachSurface() = renderer?.detachSurface()

/** Post-rotation: create new surface and re-bind existing player. */
fun rebindSurface(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

too complex and unreadable func.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored in ee6b51f18 — extracted reloadFallbackIfVideoFailed() and attachVideoScrim() from rebindSurface(). The main function is now ~10 lines. No behavior change.


// ─── Helpers ──────────────────────────────────────────────────────────────────

private fun positionAndShow(s: PIPSession, cv: PIPCompactView, isReattach: Boolean) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

too large func.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Refactored in db165ed49 — extracted createExpandedView(), createMediaView(), and createCompactView() from bindSession(). The main function is now ~20 lines.

* Not a data class: mutable fields make copy()/equals()/hashCode() unsafe
* (shared videoPlayerWrapper reference, identity-based equality needed).
*/
internal class PIPSession(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

InAppController -> PipManager -> holds all the data and handles all the pip feature?

is this more intuitive than the session?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

PIPSession groups all mutable runtime state for one PIP display (position, player, playback state, expanded flag, etc.). If PIPManager held these directly, session fields would mix with manager concerns (callback registration, rotation state, executor management). The separation makes it clear what belongs to "one PIP display" vs "the manager infrastructure". It also makes cleanup straightforward — nulling session drops all display state at once.

Copy link
Copy Markdown
Collaborator

@CTLalit CTLalit left a comment

Choose a reason for hiding this comment

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

Added comments.

Base automatically changed from task/SDK-5584/new_icons_inapp_media_support to task/SDK-5584/inapp_media_support March 31, 2026 07:18
@CTLalit CTLalit force-pushed the feat/pip/SDK-5617 branch from 7e22a4d to 4298997 Compare March 31, 2026 10:28
piyush-kukadiya and others added 5 commits March 31, 2026 16:33
PIP action is now parsed from pip.onClick JSON via CTInAppAction.createFromJson()
instead of buttons.firstOrNull(). This decouples PIP actions from the standard
buttons array and uses a constant PIP_CTA for the action label.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cleanupSession() now delegates to dismissInternal(notifyCallback=false,
animate=false) instead of duplicating cleanup logic. Added animate param
to skip exit animation for non-interactive teardown. Player release uses
view chain when container exists, falls back to direct release during
rotation edge case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Break rebindSurface() into reloadFallbackIfVideoFailed() and
attachVideoScrim() for readability. No behavior change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract createExpandedView(), createMediaView(), createCompactView()
from bindSession() for readability. Replace .parent as? ViewGroup casts
with stored view references in expand/collapse transitions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace Java-style Builder with idiomatic Kotlin named arguments
  (PIPConfig is internal with only Kotlin callers)
- Move validation from Builder.build() to PIPConfig init{} block
  (now also covers copy())
- Remove redundant has() guards before optInt/optBoolean calls
- Fix PIPInAppCallbacksBridge tests to match actual onAction()
  implementation (reads pipConfigJson.onClick, not buttons)
- Add missing contentDescription mock stub in PIPConfigFactoryTest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@Anush-Shand Anush-Shand left a comment

Choose a reason for hiding this comment

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

Minor comments

*/
internal object PIPDimens {
/** Extra bottom offset (dp) to clear a typical bottom navigation bar (Material 3 = 80dp). */
const val BOTTOM_NAV_OFFSET_DP = 80
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this belong to the res folder?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This constant is used only in programmatic layout calculations (PIPPositionResolver, PIPDragHandler), not in XML layouts. Moving it to res/values/dimens.xml would require passing Context to read it, adding overhead for a single constant that's only consumed in code. Keeping it as a Kotlin constant is simpler and avoids the resource lookup cost.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The value could/should be different on basis of tablet devices or wider devices or things like that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

80dp is Material 3's standard NavigationBar height, which is consistent across phones and tablets. This isn't for the system nav bar (that's handled via WindowInsetsCompat insets) — it's an extra offset to avoid overlapping the app's bottom navigation bar. The value is a best-effort heuristic since we can't detect the app's bottom nav from the SDK. On tablets or wider devices, the snap grid still works correctly — the offset just adds extra bottom margin.

Copy link
Copy Markdown
Contributor

@Anush-Shand Anush-Shand left a comment

Choose a reason for hiding this comment

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

Minor Comments

*/
internal class PIPVideoPlayerWrapper {

private var player: ExoPlayer? = null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we not want support for the older exoplayer here?

What happens if someone adds the exoplayer deps and tries to use a PIP?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in 097a4f3ce. PIPVideoPlayerWrapper uses Media3 APIs exclusively, but the check only rejected NONE, so old ExoPlayer would pass and crash at runtime. Now checks for MEDIA3 specifically — old ExoPlayer users get the static fallback image instead of a crash.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we wish to support Exoplayer in this version?
Since we have not marked it as deprecated yet, might be a problem @piyush-kukadiya

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No support for old Exoplayer.

Copy link
Copy Markdown
Contributor

@Anush-Shand Anush-Shand left a comment

Choose a reason for hiding this comment

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

Approved with minor comments

piyush-kukadiya and others added 10 commits April 1, 2026 18:40
- Add cornerRadiusDp, borderEnabled, borderColor, borderWidthDp to PIPConfig
- Parse pip.cornerRadius and pip.border JSON in PIPConfigFactory
- Apply GradientDrawable with stroke + clipToOutline in PIPCompactView
- Skip border styling for video (SurfaceView doesn't support clipToOutline)
- Add padding to prevent media from covering the border stroke
- Use Color.parseColor via toColorInt() with fallback to Color.BLACK
- Add Robolectric runner to PIPConfigFactoryTest for Color support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded HlsMediaSource with DefaultMediaSourceFactory which
auto-detects format (HLS, MP4, DASH, SmoothStreaming) from the URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Change aspectRatioNumerator/Denominator from Int to Double to match
  server response format
- Move border padding addition to after height clamping so media area
  keeps correct aspect ratio in both portrait and landscape

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align with newer codebase convention of coding to the ILogger interface
in PIPConfigFactory and PIPInAppCallbacksBridge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
closeBtn, bottomRow, and rowSpacer are assigned exactly once in init{},
so lateinit var is unnecessary. Changed to val.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace magic number 3000L with SCRIM_SAFETY_TIMEOUT_MS constant in
PIPMediaView.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PIPVideoPlayerWrapper uses Media3 APIs exclusively. The old check only
rejected NONE, allowing old ExoPlayer to pass and crash at runtime.
Now checks for MEDIA3 specifically — old ExoPlayer falls back to
static image.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The variants() call and printVar() were commented out, making the
menu item 12 a no-op in the sample app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enable unit testing by replacing the static `object PIPManager` with a
regular class injected into InAppController via constructor. Uses a
lazy lambda for FileResourceProvider (same pattern as
InAppNotificationInflater) to defer the synchronized getInstance() call
until PIP is actually shown, avoiding ANR risk at SDK init time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Margins now scale proportionally with screen dimensions instead of using
fixed dp values. Horizontal margin is a percentage of screen width,
vertical margin is a percentage of screen height (default 3%).

- Rename horizontalEdgeMarginDp/verticalEdgeMarginDp → horizontalEdgeMarginPercent/verticalEdgeMarginPercent
- Add Int.percentOf(total) extension for idiomatic conversion
- Remove margin validation constraints to allow negative values (compensate bottom nav offset)
- Update PIPConfigFactory, PIPRootContainer, PIPDragHandler, PIPCompactView

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants