Repository: expo/expo
File: packages/expo-video/android/src/main/java/expo/modules/video/managers/VideoManager.kt (Kotlin)

Code before:
import expo.modules.video.player.VideoPlayer
import expo.modules.video.utils.weakMutableHashSetOf
import java.lang.ref.WeakReference

// Helper class used to keep track of all existing VideoViews and VideoPlayers
@OptIn(UnstableApi::class)
object VideoManager {
  const val INTENT_PLAYER_KEY = "player_uuid"
  private var appContext: WeakReference<AppContext?> = WeakReference(null)
  lateinit var pictureInPicture: PictureInPictureManager

  // Used for sharing videoViews between VideoView and FullscreenPlayerActivity
  private var videoViews = mutableMapOf<String, VideoView>()
  private var fullscreenPlayerActivities = mutableMapOf<String, WeakReference<FullscreenPlayerActivity>>()

  // Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView
  private var videoPlayersToVideoViews = mutableMapOf<VideoPlayer, MutableList<VideoView>>()

  private var playersRequestingKeepAwake = weakMutableHashSetOf<VideoPlayer>()

  private lateinit var audioFocusManager: AudioFocusManager
  lateinit var cache: VideoCache

  private var listeners = mutableListOf<WeakReference<VideoManagerListener>>()

  @Synchronized
  fun onModuleCreated(appContext: AppContext) {
    val context = appContext.reactContext ?: throw Exceptions.ReactContextLost()
    this.appContext = WeakReference(appContext)

    this.pictureInPicture = PictureInPictureManager(appContext)

    if (!this::audioFocusManager.isInitialized) {
      audioFocusManager = AudioFocusManager(appContext)
    }
    if (!this::cache.isInitialized) {
      cache = VideoCache(context)
    }
  }

  fun onModuleDestroyed(appContext: AppContext) {
    pictureInPicture.release()
    listeners.clear()
  }

  fun registerListener(listener: VideoManagerListener) {
    listeners.add(WeakReference(listener))
  }

  fun unregisterListener(listener: VideoManagerListener) {
    listeners.retainAll { it.get() != listener }

Review comment (performance):
We use `synchronized(this)` instead of the keyword. Let's also handle changing react context.

```suggestion
  fun onModuleCreated(appContext: AppContext) = synchronized(this) {
    val context = appContext.reactContext ?: throw Exceptions.ReactContextLost()
    
    this.pictureInPicture = PictureInPictureManager(appContext)

    if (!this::audioFocusManager.isInitialized) {
      audioFocusManager = AudioFocusManager(appContext)
    }

    if (!this::cache.isInitialized) {
      cache = VideoCache(context)
    } else if(this.appContext.get()?.reactContext != appContext.reactContext) {
      cache.release()
      cache = VideoCache(context)
    }
    this.appContext = WeakReference(appContext)
  }
```

Code after:
import expo.modules.video.player.VideoPlayer
import expo.modules.video.utils.weakMutableHashSetOf
import java.lang.ref.WeakReference

// Helper class used to keep track of all existing VideoViews and VideoPlayers
@OptIn(UnstableApi::class)
object VideoManager {
  const val INTENT_PLAYER_KEY = "player_uuid"
  private var appContext: WeakReference<AppContext?> = WeakReference(null)
  lateinit var pictureInPicture: PictureInPictureManager

  // Used for sharing videoViews between VideoView and FullscreenPlayerActivity
  private var videoViews = mutableMapOf<String, VideoView>()
  private var fullscreenPlayerActivities = mutableMapOf<String, WeakReference<FullscreenPlayerActivity>>()

  // Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView
  private var videoPlayersToVideoViews = mutableMapOf<VideoPlayer, MutableList<VideoView>>()

  private var playersRequestingKeepAwake = weakMutableHashSetOf<VideoPlayer>()

  private lateinit var audioFocusManager: AudioFocusManager
  lateinit var cache: VideoCache

  private var listeners = mutableListOf<WeakReference<VideoManagerListener>>()

  fun onModuleCreated(appContext: AppContext) = synchronized(this) {
    val context = appContext.reactContext ?: throw Exceptions.ReactContextLost()

    this.pictureInPicture = PictureInPictureManager(appContext)
    if (!this::audioFocusManager.isInitialized) {
      audioFocusManager = AudioFocusManager(appContext)
    }
    if (!this::cache.isInitialized) {
      cache = VideoCache(context)
    } else if (this.appContext.get()?.reactContext != appContext.reactContext) {
      cache.release()
      cache = VideoCache(context)
    }
    this.appContext = WeakReference(appContext)
  }

  fun onModuleDestroyed(appContext: AppContext) {
    pictureInPicture.release()
    listeners.clear()
  }

  fun registerListener(listener: VideoManagerListener) {
    listeners.add(WeakReference(listener))
  }

  fun unregisterListener(listener: VideoManagerListener) {