FyreMemories is an Android memory SDK that adds a photo and video memories experience to your app. It gives you three surfaces: a thumbnail carousel, a swipeable full-screen feed, and a headless repository for building your own UI. The SDK handles the player, caching, analytics, and per-user data isolation.
One line in build.gradle.kts
implementation("com.fyrememories:fyre-memories-sdk:1.0.1").
Call FyreMemories.init() once a Firebase user is signed in.
Drop MemoriesCarouselView into any layout, or call FyreMemories.openFeed().
Everything funnels through one entry point: FyreMemories. Everything beyond step 3 is optional.
| Surface | Class | Use it for |
|---|---|---|
| Carousel | MemoriesCarouselView | A horizontal thumbnail strip you embed in your own screens. |
| Full-screen feed | MemoriesFeedActivity | A swipeable, auto-playing video player with like, save, share, and download. |
| Headless | MemoriesRepository | Fetch memories yourself and render a custom UI. |
The SDK is published to Maven Central as com.fyrememories:fyre-memories-sdk. Make sure your repositories include it.
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}dependencies {
implementation("com.fyrememories:fyre-memories-sdk:1.0.1")
}The SDK targets Java 17. Your module must compile against a matching toolchain.
android {
compileSdk = 35
defaultConfig { minSdk = 24 }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
}Add INTERNET to your AndroidManifest.xmlif it isn't already present â don't rely on manifest merging from the SDK. If you enable the in-feed Download button, request WRITE_EXTERNAL_STORAGE at runtime on API 28 and below before the download. On API 29 and above the SDK writes to app-scoped storage and no runtime permission is needed.
FyreMemories.init()requires a signed-in Firebase user. Where you call it depends on your app's auth flow.
On returning launches, Firebase restores the session synchronously. Initialize the SDK right after your splash / auth check, once you confirm the user is signed in:
// In your SplashActivity, MainActivity, or wherever you check auth state
if (FirebaseAuth.getInstance().currentUser != null) {
FyreMemories.init(
context = applicationContext,
config = FyreMemoriesConfig(enableLogging = BuildConfig.DEBUG)
)
// proceed to main screen
} else {
// send user to login screen
}Call init() in your sign-in success callback, right after the user is authenticated:
// After signInWithCredential, signInWithEmailAndPassword, signInAnonymously â any provider
firebaseAuth.signIn(...).addOnSuccessListener {
FyreMemories.init(
context = applicationContext,
config = FyreMemoriesConfig(enableLogging = BuildConfig.DEBUG)
)
// proceed to main screen
}If your app manages auth state reactively, register the listener once (e.g. in Application.onCreate()) â the init() call itself only fires inside the callback, once a user is confirmed:
// Register once, e.g. in Application.onCreate()
FirebaseAuth.getInstance().addAuthStateListener { auth ->
if (auth.currentUser != null && !FyreMemories.isInitialized) {
FyreMemories.init(context = applicationContext)
}
}Calling init() again after success logs a warning and is ignored. To use the repository before init has finished, suspend on awaitInitialized() first.
lifecycleScope.launch {
FyreMemories.awaitInitialized()
val userId = FyreMemories.currentUserId() ?: return@launch
val result = FyreMemories.repository.fetchMemories(userId)
}All FyreMemoriesConfig fields have defaults. Override only what you need.
FyreMemories.init(
context = this,
config = FyreMemoriesConfig(
baseUrl = "", // empty = built-in production URL
customization = FyreCustomization(/* ... */), // optional UI overrides
enableLogging = BuildConfig.DEBUG,
cacheSizeBytes = 200L * 1024 * 1024, // video cache (200 MB)
thumbnailCacheSizeBytes = 50L * 1024 * 1024, // thumbnail cache (50 MB)
themeOverlay = R.style.MyFyreOverlay, // optional theme overlay
logger = MyTimberLogger(), // optional log sink
connectTimeoutMs = 10_000L,
readTimeoutMs = 15_000L,
writeTimeoutMs = 15_000L,
)
)| Field | Default | Notes |
|---|---|---|
baseUrl | "" | Backend base URL. Empty uses the built-in production URL. Include the scheme, no trailing slash. |
customization | null | Full UI customization tree. null uses defaults. |
enableLogging | false | Verbose SDK logs. Pass BuildConfig.DEBUG so logs stay off in release. |
cacheSizeBytes | 200 MB | ExoPlayer video disk cache, synced to PlayerOptions at init. No live update*Options setter, but MemoryPlayerCache recreates itself if you pass a different size on re-init. |
thumbnailCacheSizeBytes | 50 MB | Coil thumbnail disk cache size. |
themeOverlay | 0 | Theme overlay applied to all SDK views and the feed activity. 0 means none. |
logger | null | Forward SDK logs to Timber or Crashlytics. Only called when enableLogging is true. |
connectTimeoutMs | 10 000 | OkHttp connect timeout. |
readTimeoutMs | 15 000 | OkHttp read timeout. |
writeTimeoutMs | 15 000 | OkHttp write timeout. |
Each user ID gets isolated in-memory state and an on-disk cache. One user's memories are never shown to another, which keeps shared devices and account switching safe.
val userId: String? = FyreMemories.currentUserId()
// email for authenticated users, uid for anonymous, null if signed outCall setUser()only for a custom auth flow or to override the auto-detected ID. It activates that user's state and auto-fetches their memories when the ID changes. The SDK keeps the 3 most-recently-used users in memory (LRU).
FyreMemories.setUser(newUserId)The feed marks views automatically. For custom UIs, update the seen or unseen ring state yourself.
FyreMemories.markAsViewed(memory.id)Call clearUser() on sign-out. It:
- Purges the current user's repository memory and disk cache (fyre_memories_cache_{userId}.json)
- Flushes and clears queued analytics, and shuts down the analytics batcher
- Clears the global liked/viewed MemoriesPreferences (not scoped per user)
- Nulls the internal service locator and resets isInitialized to false, so you can call init() again on the next sign-in
FyreMemories.clearUser()
FirebaseAuth.getInstance().signOut()MemoriesCarouselView is a LinearLayout subclass. It works fully programmatically, and XML is also supported.bind() is required: the view stays hidden until you bind it, and it auto-hides if the user has no memories.
<io.fyre.memories.ui.MemoriesCarouselView
android:id="@+id/memoriesCarousel"
android:layout_width="match_parent"
android:layout_height="wrap_content" />val userId = FyreMemories.currentUserId() ?: return
val carousel = findViewById<MemoriesCarouselView>(R.id.memoriesCarousel)
carousel.bind(lifecycleOwner = this, userId = userId)The local listener consumes the tap. Without it, the carousel opens the feed at the tapped position by default â unless a MemoryClickInterceptor is set (see Paywall Interceptor), which can also gate the default open.
carousel.onMemoryClickListener = OnMemoryClickListener { memory, position ->
MemoriesFeedActivity.start(this, userId, startPosition = position)
}The easiest way to open the swipeable player. If no user is signed in, the call logs a warning and does nothing.
FyreMemories.openFeed(context) // start at position 0
FyreMemories.openFeed(context, startPosition = 3) // jump to a memoryMemoriesFeedActivity.start(context, userId, startPosition = 3)
MemoriesFeedActivity.isActive() // is the feed on screen?
MemoriesFeedActivity.closeCurrent() // close it programmatically
// Host the fragment inside your own screen
supportFragmentManager.beginTransaction()
.replace(R.id.container, MemoriesFeedFragment.newInstance(userId, startPosition = 0))
.commit()Access FyreMemories.repository to fetch data and render your own UI. It exposes a reactive, per-user Flow. Accessing it before init() throws IllegalStateException.
val userId = FyreMemories.currentUserId() ?: return
lifecycleScope.launch {
FyreMemories.repository.fetchMemories(userId) // updates state and cache
}
lifecycleScope.launch {
FyreMemories.repository.memories(userId).collect { result ->
result
.onSuccess { memories -> renderList(memories) }
.onFailure { error -> showError(error) }
}
}| Member | Description |
|---|---|
fetchMemories(userId) | Fetches from backend, updates state and cache. Caps at 200 items and deduplicates concurrent fetches per user. |
memories(userId) | Reactive stream of the latest list for that user, seeded from disk cache. |
saveMemory(sourceKey) | Saves an existing memory key to the user's account. |
markAsViewed(memoryId) | Marks a memory viewed in-memory so carousel rings update live. |
clearUserCaches(userId?) | Clears disk and memory caches for one user, or all if null. |
FeedOptions(
showLike = true, showSave = true, showDownload = true,
showShare = true, showClose = true, showSeekBar = true,
showPositionText = true, // "3 / 20" counter
// Custom icons (@DrawableRes; 0 = SDK default)
iconLikeOutline = R.drawable.ic_heart_outline,
iconLikeFilled = R.drawable.ic_heart_filled,
iconSave = R.drawable.ic_save, iconDownload = R.drawable.ic_download,
iconShare = R.drawable.ic_share, iconClose = R.drawable.ic_close,
iconPlay = R.drawable.ic_play,
// Colors (ARGB; 0 = theme attr)
likedTint = Color.RED, unlikedTint = Color.WHITE,
chromeTint = Color.WHITE, backgroundColor = Color.BLACK,
// Optional message overrides (null = resource string)
shareChooserTitle = null, downloadFailedText = null,
storagePermissionDeniedText = null,
// Playback
autoAdvanceOnComplete = true, autoAdvanceDelayMs = 300L,
loopCurrent = false, tapToPlayPause = true,
)FyreMemories.updateFeedOptions { it.copy(showSave = false, likedTint = Color.RED) }PlayerOptions(
autoPlay = true, loop = false, mutedByDefault = false,
playbackSpeed = 1.0f, // must be > 0
resizeMode = ResizeMode.FIT, // FIT | FILL | ZOOM
prefetchNext = true, // pre-buffer the next video
cacheSizeBytes = 200L * 1024 * 1024, // 200 MB default; must be >= 0
)FyreMemories.updatePlayerOptions { it.copy(mutedByDefault = true, playbackSpeed = 1.5f) }CardOptions(
aspectRatio = AspectRatio.PORTRAIT_9_16, // LANDSCAPE_16_9 | SQUARE_1_1 | INSTAGRAM_4_5
thumbnailQuality = ThumbnailQuality.HIGH, // LOW | MEDIUM | HIGH | ORIGINAL
cardElevationDp = 0f,
cardCornerRadiusDp = 12f,
cardBackgroundColor = 0xFF000000.toInt(),
)FyreMemories.updateCardOptions { it.copy(cardCornerRadiusDp = 0f) }LayoutOptions(
carouselPaddingDp = 8,
carouselMarginStartDp = 0, carouselMarginEndDp = 0,
carouselMarginTopDp = 0, carouselMarginBottomDp = 0,
headerPaddingStartDp = 12, headerPaddingEndDp = 12,
headerPaddingTopDp = 12, headerPaddingBottomDp = 4,
headerBackgroundColor = Color.TRANSPARENT,
viewAllPaddingDp = 4, viewAllBackgroundColor = Color.TRANSPARENT,
thumbnailCornerRadiusDp = 12f,
showLoadingIndicator = true,
loadingIndicatorColor = Color.WHITE,
loadingIndicatorSizeDp = 24f,
showPlayButton = true,
playButtonColor = 0xFF6750A4.toInt(),
playButtonBackgroundColor = Color.TRANSPARENT,
playButtonSizeDp = 24f,
feedButtonPaddingDp = 8, feedButtonMarginDp = 12,
)FyreMemories.updateLayoutOptions { it.copy(thumbnailCornerRadiusDp = 0f) }Controls the header title, header button ("View All"), and feed position text.
TypographyOptions(
fontFamily = Typeface.createFromAsset(assets, "fonts/MyFont.ttf"),
headerTitleTypeface = null, // null = use fontFamily
headerTitleSizeSp = 18f,
headerTitleWeight = Typeface.BOLD,
headerTitleColor = Color.WHITE,
headerTitleLetterSpacing = 0f,
headerButtonTypeface = null,
headerButtonSizeSp = 14f,
headerButtonWeight = Typeface.NORMAL,
headerButtonColor = Color.WHITE,
headerButtonAllCaps = false,
positionTextSizeSp = 14f,
positionTextWeight = Typeface.NORMAL,
)FyreMemories.updateTypographyOptions { it.copy(headerTitleColor = Color.BLACK) }GradientOptions(
showGradient = true,
gradientOpacity = 0.4f, // 0.0 - 1.0
gradientTopColor = 0x00000000,
gradientBottomColor = 0xFF000000.toInt(),
gradientDirection = GradientDirection.TOP_TO_BOTTOM, // TOP_TO_BOTTOM | BOTTOM_TO_TOP | LEFT_TO_RIGHT | DIAGONAL
)FyreMemories.updateGradientOptions { it.copy(gradientOpacity = 0.6f) }A master toggle plus per-effect controls for the like pop, loading shimmer, and button-press feedback.
AnimationOptions(
enableAnimations = true, // master toggle for all animations
enableHeartAnimation = true, // like pop
enableShimmerAnimation = true, // loading shimmer
enableButtonPressAnimation = true, // button press feedback
heartAnimationDurationMs = 300L,
shimmerAnimationDurationMs = 1000L,
buttonPressAnimationDurationMs = 150L,
)FyreMemories.updateAnimationOptions { it.copy(enableHeartAnimation = false) }Controls the feed seekbar and the loading indicator.
ProgressOptions(
seekBarThicknessDp = 4f,
seekBarScrubberSizeDp = 12f,
seekBarCornerRadiusDp = 2f,
loadingIndicatorSizeDp = 24f,
loadingIndicatorThicknessDp = 3f,
loadingIndicatorColor = Color.WHITE,
progressIndicatorStyle = ProgressIndicatorStyle.LINEAR, // LINEAR | CIRCULAR
)FyreMemories.updateProgressOptions { it.copy(progressIndicatorStyle = ProgressIndicatorStyle.CIRCULAR) }HapticOptions(
enableHaptics = true,
hapticOnLike = true,
hapticOnActionButtons = true,
hapticOnNavigation = false,
hapticOnLongPress = true,
)FyreMemories.updateHapticOptions { it.copy(enableHaptics = false) }SoundOptions(
enableSounds = true,
soundOnSwipe = false,
soundOnLike = true,
soundOnActionSuccess = true,
likeSoundResId = R.raw.pop, // @RawRes
soundVolume = 0.7f, // 0.0 - 1.0
)FyreMemories.updateSoundOptions { it.copy(enableSounds = true, soundVolume = 0.5f) }Per-button visibility, color, size, and custom Drawable icons for the feed Like, Save, Share, and Play buttons.
IconButtonOptions(
showLikeButton = true,
likeButtonColor = Color.WHITE, likedButtonColor = Color.RED,
likeButtonSizeDp = 28f,
showSaveButton = true, saveButtonColor = Color.WHITE,
showShareButton = true, shareButtonColor = Color.WHITE,
showPlayButton = true, playButtonSizeDp = 48f,
buttonPaddingDp = 8, buttonMarginDp = 12,
)val heart = ContextCompat.getDrawable(context, R.drawable.ic_heart)
FyreMemories.updateIconButtonOptions {
it.copy(showSaveButton = false, customLikedIcon = heart, likedButtonColor = Color.RED)
}AnalyticsOptions(
enabled = true,
enabledEvents = emptySet(), // empty = collect all; or setOf("PLAYED", "LIKED")
flushIntervalMs = 30_000L,
flushBatchSize = 20,
debugOnly = false, // log events but don't send them
)FyreMemories.setAnalyticsEnabled(false) // consent screen helper
FyreMemories.updateAnalyticsOptions { it.copy(debugOnly = true) }Set styling on the MemoriesCarouselView instance directly. Each instance has independent state, so you can show several carousels with different looks.
// Compact cards, subtle gradient
val featuredCarousel = MemoriesCarouselView(context).apply {
bind(this@Activity, userId)
withLayout(thumbnailCornerRadiusDp = 4f, carouselPaddingDp = 4)
withGradient(gradientOpacity = 0.2f)
}
// Larger cards, custom title, stronger gradient
val recentCarousel = MemoriesCarouselView(context).apply {
bind(this@Activity, userId)
setTitle("Recent Memories")
setShowViewAll(true)
withLayout(thumbnailCornerRadiusDp = 16f, carouselPaddingDp = 12)
withTypography(headerTitleSizeSp = 20f, headerTitleColor = Color.BLACK)
withGradient(gradientOpacity = 0.7f, gradientDirection = GradientDirection.BOTTOM_TO_TOP)
withCard(aspectRatio = AspectRatio.PORTRAIT_9_16, cardElevationDp = 4f)
}The with* methods return the view, so you can chain them and end with bind().
carousel
.withLayout(thumbnailCornerRadiusDp = 8f)
.withGradient(showGradient = false)
.withTypography(headerTitleColor = Color.WHITE)
.withHaptics(enableHaptics = true, hapticOnLike = true)
.bind(lifecycleOwner, userId)carousel.setPresentationMode(PresentationMode.CIRCLE) // default: rings
carousel.setPresentationMode(PresentationMode.STORIES) // card tilescarousel.setTitle("Recent Memories")
carousel.setShowViewAll(true)
carousel.setViewAllText("See All")
carousel.setTitleColor(Color.WHITE)
carousel.setHeaderBackground(Color.TRANSPARENT)
carousel.setTitleSize(18f)carousel.setThumbSize(80)
carousel.setThumbSpacing(8)
carousel.setRingColor(Color.WHITE)
carousel.setShowRing(true)
carousel.setRingWidth(2)
carousel.setCarouselHeight(120)
carousel.setCarouselMargins(start = 16, top = 8, end = 16, bottom = 8)
carousel.setHeaderPaddingStart(12)
carousel.configureCarousel(thumbSizeDp = 80, thumbSpacingDp = 8, ringColor = Color.WHITE, ringWidthDp = 2)Register listeners to observe events. Pass nullto remove one. All callbacks run on the main thread, so don't block them.
// Called when user taps a memory (before feed opens)
FyreMemories.setOnMemoryClickListener { memory -> }
// Called when a memory starts playing
FyreMemories.setOnMemoryViewListener { memory -> }
FyreMemories.setOnPlaybackCompletedListener { memory, position, durationMs -> }
// Called when feed is opened or closed
FyreMemories.setOnFeedLifecycleListener(object : OnFeedLifecycleListener {
override fun onFeedOpened() { }
override fun onFeedClosed() { }
})
// Called when user taps like / save / share / download
FyreMemories.setOnFeedActionClickListener { memory, action -> }
// SDK errors
FyreMemories.setOnFyreErrorListener { error, context -> }An interceptor can prevent or defer the feed from opening â for example to gate it behind a paywall or ad. It receives the tapped memory and a proceed callback; call proceed() to let the feed open.
FyreMemories.setMemoryClickInterceptor { memory, proceed ->
if (userHasPremium) {
proceed()
} else {
showUpgradePrompt()
}
}To show an interstitial ad first, call proceed() when the ad is dismissed.
Providers supply behavior the SDK needs. Return null to fall back to the SDK default.
FyreMemories.setShareIntentBuilder { memory, contentUri ->
Intent(Intent.ACTION_SEND).apply {
type = "video/mp4"
putExtra(Intent.EXTRA_STREAM, contentUri)
putExtra(Intent.EXTRA_TEXT, "Check this out")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}FyreMemories.setDownloadLocationProvider(object : DownloadLocationProvider {
override fun directory(memory: MemoryItem): File? =
File(filesDir, "fyre-downloads").apply { mkdirs() }
override fun fileName(memory: MemoryItem): String? =
"memory_" + memory.id + ".mp4"
override fun onDownloadComplete(memory: MemoryItem, file: File) { }
override fun onDownloadError(memory: MemoryItem, error: Throwable) { }
})Replace the default POST /analytics delivery with your own pipeline. Return true when the batch is delivered, or false to keep it for retry. It runs off the main thread, so blocking network I/O is fine here.
FyreMemories.setAnalyticsDispatcher { events: List<MemoryEvent> ->
try {
myAnalytics.upload(events)
true // delivered
} catch (e: Exception) {
false // keep for the next flush
}
}
// Reset to the SDK default:
FyreMemories.setAnalyticsDispatcher(null)Standard interactions are logged for you automatically. Events are batched, persisted to Room, and flushed by size, timer, or when the app is backgrounded.
enum class EventType {
VIEWED, PLAYED, PAUSED, COMPLETED, REPLAYED,
LIKED, UNLIKED, SHARED, DOWNLOADED, SAVED_TO_CLOUD, RATED, ERRORED
}Use this only for partner-defined engagement dimensions. Most events are automatic.
FyreMemories.logEvent(
videoId = memory.id,
eventType = EventType.RATED,
properties = mapOf("stars" to 5, "surface" to "home_carousel")
)Every memory implements MemoryItem, a sealed interface.VideoMemory is the only type in this release. Handle types with a when and an else branch so future types stay forward-compatible.
sealed interface MemoryItem {
val id: String // stable id for UI and analytics
val sourceKey: String // raw backend key (use with saveMemory)
val thumbnailUrl: String? // CDN thumbnail URL, or null
val createdAt: Instant?
val isViewed: Boolean?
}
data class VideoMemory(
override val id: String,
override val sourceKey: String,
override val thumbnailUrl: String?,
override val createdAt: Instant?,
override val isViewed: Boolean?,
val videoUrl: String, // CDN MP4 URL, ready for ExoPlayer
val durationMs: Long? = null,
val isLiked: Boolean? = null,
val viewCount: Long? = null,
) : MemoryItem
when (memory) {
is VideoMemory -> player.load(memory.videoUrl)
else -> { /* future memory types */ }
}Data calls return a FyreResult<T> instead of throwing. Every failure maps to a FyreError subtype, so raw exceptions never escape the public API.
| Error | Meaning |
|---|---|
Unauthorized | Token missing or expired and refresh failed. |
Forbidden | Server rejected the request (HTTP 403). |
NotFound | Resource not found (HTTP 404). Carries an optional resourceId. |
Transient | Timeout or 408/5xx. Safe to retry. Carries an optional httpCode. |
Network | I/O failure such as no connectivity or DNS. |
Protocol | Response could not be parsed. |
Unknown | Anything else. |
fun FyreError.toUserMessage(): String = when (this) {
is FyreError.Network -> "You're offline. Check your connection."
is FyreError.Transient -> "Something went wrong. Pull to retry."
is FyreError.Unauthorized -> "Please sign in again."
is FyreError.Forbidden -> "You don't have access to this content."
is FyreError.NotFound -> "That memory is no longer available."
is FyreError.Protocol -> "We received an unexpected response."
is FyreError.Unknown -> "An unexpected error occurred."
}Route SDK logs anywhere with a FyreLogger. It is only called when enableLogging is true.
FyreMemoriesConfig(
enableLogging = BuildConfig.DEBUG,
logger = FyreLogger { level, tag, message, throwable ->
when (level) {
FyreLogLevel.ERROR -> Timber.tag(tag).e(throwable, message)
FyreLogLevel.WARN -> Timber.tag(tag).w(message)
else -> Timber.tag(tag).d(message)
}
}
)No rules needed. The SDK ships consumer rules inside the AAR. They are applied automatically when building with R8 or ProGuard. After enabling minification, do a release smoke test of feed playback, the action buttons, analytics delivery, and JSON parsing.
Verify these before shipping a release build.
- Firebase Auth is configured and a user is always signed in before init().
- enableLogging = BuildConfig.DEBUG so verbose logs stay off in release.
- Release logger routes SDK logs to your crash reporter.
- Analytics consent is wired with setAnalyticsEnabled() or updateAnalyticsOptions if your jurisdiction requires opt-in.
- clearUser() is called on sign-out, and re-init() on the next sign-in is verified.
- Account switching is tested with setUser() so there is no cross-user data leakage.
- baseUrl points at the right environment, or is empty for production.
- Cache sizes are tuned to your storage budget and set before init().
- No FileProvider authority clash in the merged manifest.
- R8 or ProGuard release build is smoke-tested: playback, actions, analytics, JSON parsing.
- WRITE_EXTERNAL_STORAGE runtime request is handled on API 28 and below if Download is enabled.
- Empty state is handled: the carousel auto-hides when there are no memories.
- Network and transient errors are surfaced with retry affordances.
- A global OnFyreErrorListener is registered.
| Requirement | Details |
|---|---|
| Min SDK | 24 (Android 7.0+) |
| Compile SDK | 35 |
| Java / Kotlin | Toolchain target 17 |
| Repository | Maven Central (com.fyrememories:fyre-memories-sdk:1.0.1) |
| Firebase Auth | User must be signed in before FyreMemories.init() |
| Internet permission | Add to AndroidManifest.xml if not already present. |