mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	Merge pull request #11829 from Profpatsch/PlayerUIList-to-kotlin
Player UI list to kotlin
This commit is contained in:
		| @@ -246,7 +246,7 @@ public final class VideoDetailFragment | ||||
|         // It will do nothing if the player is not in fullscreen mode | ||||
|         hideSystemUiIfNeeded(); | ||||
|  | ||||
|         final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class); | ||||
|         final Optional<MainPlayerUi> playerUi = player.UIs().getOpt(MainPlayerUi.class); | ||||
|         if (!player.videoPlayerSelected() && !playAfterConnect) { | ||||
|             return; | ||||
|         } | ||||
| @@ -529,7 +529,7 @@ public final class VideoDetailFragment | ||||
|         binding.overlayPlayPauseButton.setOnClickListener(v -> { | ||||
|             if (playerIsNotStopped()) { | ||||
|                 player.playPause(); | ||||
|                 player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0)); | ||||
|                 player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0)); | ||||
|                 showSystemUi(); | ||||
|             } else { | ||||
|                 autoPlayEnabled = true; // forcefully start playing | ||||
| @@ -688,7 +688,7 @@ public final class VideoDetailFragment | ||||
|     @Override | ||||
|     public boolean onKeyDown(final int keyCode) { | ||||
|         return isPlayerAvailable() | ||||
|                 && player.UIs().get(VideoPlayerUi.class) | ||||
|                 && player.UIs().getOpt(VideoPlayerUi.class) | ||||
|                 .map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false); | ||||
|     } | ||||
|  | ||||
| @@ -1028,7 +1028,7 @@ public final class VideoDetailFragment | ||||
|         // If a user watched video inside fullscreen mode and than chose another player | ||||
|         // return to non-fullscreen mode | ||||
|         if (isPlayerAvailable()) { | ||||
|             player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> { | ||||
|             player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> { | ||||
|                 if (playerUi.isFullscreen()) { | ||||
|                     playerUi.toggleFullscreen(); | ||||
|                 } | ||||
| @@ -1244,7 +1244,7 @@ public final class VideoDetailFragment | ||||
|             // setup the surface view height, so that it fits the video correctly | ||||
|             setHeightThumbnail(); | ||||
|  | ||||
|             player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> { | ||||
|             player.UIs().getOpt(MainPlayerUi.class).ifPresent(playerUi -> { | ||||
|                 // sometimes binding would be null here, even though getView() != null above u.u | ||||
|                 if (binding != null) { | ||||
|                     // prevent from re-adding a view multiple times | ||||
| @@ -1260,7 +1260,7 @@ public final class VideoDetailFragment | ||||
|         makeDefaultHeightForVideoPlaceholder(); | ||||
|  | ||||
|         if (player != null) { | ||||
|             player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent); | ||||
|             player.UIs().getOpt(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -1327,7 +1327,7 @@ public final class VideoDetailFragment | ||||
|         binding.detailThumbnailImageView.setMinimumHeight(newHeight); | ||||
|         if (isPlayerAvailable()) { | ||||
|             final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT); | ||||
|             player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> | ||||
|             player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> | ||||
|                     ui.getBinding().surfaceView.setHeights(newHeight, | ||||
|                             ui.isFullscreen() ? newHeight : maxHeight)); | ||||
|         } | ||||
| @@ -1861,7 +1861,7 @@ public final class VideoDetailFragment | ||||
|     public void onFullscreenStateChanged(final boolean fullscreen) { | ||||
|         setupBrightness(); | ||||
|         if (!isPlayerAndPlayerServiceAvailable() | ||||
|                 || player.UIs().get(MainPlayerUi.class).isEmpty() | ||||
|                 || player.UIs().getOpt(MainPlayerUi.class).isEmpty() | ||||
|                 || getRoot().map(View::getParent).isEmpty()) { | ||||
|             return; | ||||
|         } | ||||
| @@ -1890,7 +1890,7 @@ public final class VideoDetailFragment | ||||
|         final boolean isLandscape = DeviceUtils.isLandscape(requireContext()); | ||||
|         if (DeviceUtils.isTablet(activity) | ||||
|                 && (!globalScreenOrientationLocked(activity) || isLandscape)) { | ||||
|             player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen); | ||||
|             player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -1990,7 +1990,7 @@ public final class VideoDetailFragment | ||||
|     } | ||||
|  | ||||
|     private boolean isFullscreen() { | ||||
|         return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class) | ||||
|         return isPlayerAvailable() && player.UIs().getOpt(VideoPlayerUi.class) | ||||
|                 .map(VideoPlayerUi::isFullscreen).orElse(false); | ||||
|     } | ||||
|  | ||||
| @@ -2067,7 +2067,7 @@ public final class VideoDetailFragment | ||||
|             setAutoPlay(true); | ||||
|         } | ||||
|  | ||||
|         player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape); | ||||
|         player.UIs().getOpt(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape); | ||||
|         // Let's give a user time to look at video information page if video is not playing | ||||
|         if (globalScreenOrientationLocked(activity) && !player.isPlaying()) { | ||||
|             player.play(); | ||||
| @@ -2332,7 +2332,7 @@ public final class VideoDetailFragment | ||||
|                                 && player.isPlaying() | ||||
|                                 && !isFullscreen() | ||||
|                                 && !DeviceUtils.isTablet(activity)) { | ||||
|                             player.UIs().get(MainPlayerUi.class) | ||||
|                             player.UIs().getOpt(MainPlayerUi.class) | ||||
|                                     .ifPresent(MainPlayerUi::toggleFullscreen); | ||||
|                         } | ||||
|                         setOverlayLook(binding.appBarLayout, behavior, 1); | ||||
| @@ -2346,7 +2346,7 @@ public final class VideoDetailFragment | ||||
|                         // Re-enable clicks | ||||
|                         setOverlayElementsClickable(true); | ||||
|                         if (isPlayerAvailable()) { | ||||
|                             player.UIs().get(MainPlayerUi.class) | ||||
|                             player.UIs().getOpt(MainPlayerUi.class) | ||||
|                                     .ifPresent(MainPlayerUi::closeItemsList); | ||||
|                         } | ||||
|                         setOverlayLook(binding.appBarLayout, behavior, 0); | ||||
| @@ -2357,7 +2357,7 @@ public final class VideoDetailFragment | ||||
|                             showSystemUi(); | ||||
|                         } | ||||
|                         if (isPlayerAvailable()) { | ||||
|                             player.UIs().get(MainPlayerUi.class).ifPresent(ui -> { | ||||
|                             player.UIs().getOpt(MainPlayerUi.class).ifPresent(ui -> { | ||||
|                                 if (ui.isControlsVisible()) { | ||||
|                                     ui.hideControls(0, 0); | ||||
|                                 } | ||||
| @@ -2454,7 +2454,7 @@ public final class VideoDetailFragment | ||||
|  | ||||
|     public Optional<View> getRoot() { | ||||
|         return Optional.ofNullable(player) | ||||
|                 .flatMap(player1 -> player1.UIs().get(VideoPlayerUi.class)) | ||||
|                 .flatMap(player1 -> player1.UIs().getOpt(VideoPlayerUi.class)) | ||||
|                 .map(playerUi -> playerUi.getBinding().getRoot()); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -473,14 +473,15 @@ public final class Player implements PlaybackListener, Listener { | ||||
|     } | ||||
|  | ||||
|     private void initUIsForCurrentPlayerType() { | ||||
|         if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN) | ||||
|                 || (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) { | ||||
|         if ((UIs.getOpt(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN) | ||||
|                 || (UIs.getOpt(PopupPlayerUi.class).isPresent() | ||||
|                     && playerType == PlayerType.POPUP)) { | ||||
|             // correct UI already in place | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // try to reuse binding if possible | ||||
|         final PlayerBinding binding = UIs.get(VideoPlayerUi.class).map(VideoPlayerUi::getBinding) | ||||
|         final PlayerBinding binding = UIs.getOpt(VideoPlayerUi.class).map(VideoPlayerUi::getBinding) | ||||
|                 .orElseGet(() -> { | ||||
|                     if (playerType == PlayerType.AUDIO) { | ||||
|                         return null; | ||||
|   | ||||
| @@ -148,7 +148,7 @@ public final class PlayerService extends MediaBrowserServiceCompat { | ||||
|             // no one already and starting the service in foreground should not create any issues. | ||||
|             // If the service is already started in foreground, requesting it to be started | ||||
|             // shouldn't do anything. | ||||
|             player.UIs().get(NotificationPlayerUi.class) | ||||
|             player.UIs().getOpt(NotificationPlayerUi.class) | ||||
|                     .ifPresent(NotificationPlayerUi::createNotificationAndStartForeground); | ||||
|  | ||||
|             if (playerWasNull && onPlayerStartedOrStopped != null) { | ||||
| @@ -173,7 +173,7 @@ public final class PlayerService extends MediaBrowserServiceCompat { | ||||
|  | ||||
|         if (player != null) { | ||||
|             player.handleIntent(intent); | ||||
|             player.UIs().get(MediaSessionPlayerUi.class) | ||||
|             player.UIs().getOpt(MediaSessionPlayerUi.class) | ||||
|                     .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -138,7 +138,7 @@ public class MediaSessionPlayerUi extends PlayerUi | ||||
|             public void play() { | ||||
|                 player.play(); | ||||
|                 // hide the player controls even if the play command came from the media session | ||||
|                 player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0)); | ||||
|                 player.UIs().getOpt(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0)); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|   | ||||
| @@ -102,7 +102,7 @@ public final class NotificationUtil { | ||||
|             mediaStyle.setShowActionsInCompactView(compactSlots); | ||||
|         } | ||||
|         player.UIs() | ||||
|                 .get(MediaSessionPlayerUi.class) | ||||
|                 .getOpt(MediaSessionPlayerUi.class) | ||||
|                 .flatMap(MediaSessionPlayerUi::getSessionToken) | ||||
|                 .ifPresent(mediaStyle::setMediaSession); | ||||
|  | ||||
|   | ||||
| @@ -1,90 +0,0 @@ | ||||
| package org.schabi.newpipe.player.ui; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
| import java.util.function.Consumer; | ||||
|  | ||||
| public final class PlayerUiList { | ||||
|     final List<PlayerUi> playerUis = new ArrayList<>(); | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link PlayerUiList} starting with the provided player uis. The provided player uis | ||||
|      * will not be prepared like those passed to {@link #addAndPrepare(PlayerUi)}, because when | ||||
|      * the {@link PlayerUiList} constructor is called, the player is still not running and it | ||||
|      * wouldn't make sense to initialize uis then. Instead the player will initialize them by doing | ||||
|      * proper calls to {@link #call(Consumer)}. | ||||
|      * | ||||
|      * @param initialPlayerUis the player uis this list should start with; the order will be kept | ||||
|      */ | ||||
|     public PlayerUiList(final PlayerUi... initialPlayerUis) { | ||||
|         playerUis.addAll(List.of(initialPlayerUis)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adds the provided player ui to the list and calls on it the initialization functions that | ||||
|      * apply based on the current player state. The preparation step needs to be done since when UIs | ||||
|      * are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer | ||||
|      * is already initialized, but we need to notify the newly built UI that the player is ready | ||||
|      * nonetheless. | ||||
|      * @param playerUi the player ui to prepare and add to the list; its {@link | ||||
|      *                 PlayerUi#getPlayer()} will be used to query information about the player | ||||
|      *                 state | ||||
|      */ | ||||
|     public void addAndPrepare(final PlayerUi playerUi) { | ||||
|         if (playerUi.getPlayer().getFragmentListener().isPresent()) { | ||||
|             // make sure UIs know whether a service is connected or not | ||||
|             playerUi.onFragmentListenerSet(); | ||||
|         } | ||||
|  | ||||
|         if (!playerUi.getPlayer().exoPlayerIsNull()) { | ||||
|             playerUi.initPlayer(); | ||||
|             if (playerUi.getPlayer().getPlayQueue() != null) { | ||||
|                 playerUi.initPlayback(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         playerUis.add(playerUi); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Destroys all matching player UIs and removes them from the list. | ||||
|      * @param playerUiType the class of the player UI to destroy; the {@link | ||||
|      *                     Class#isInstance(Object)} method will be used, so even subclasses will be | ||||
|      *                     destroyed and removed | ||||
|      * @param <T>          the class type parameter | ||||
|      */ | ||||
|     public <T> void destroyAll(final Class<T> playerUiType) { | ||||
|         playerUis.stream() | ||||
|                 .filter(playerUiType::isInstance) | ||||
|                 .forEach(playerUi -> { | ||||
|                     playerUi.destroyPlayer(); | ||||
|                     playerUi.destroy(); | ||||
|                 }); | ||||
|         playerUis.removeIf(playerUiType::isInstance); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param playerUiType the class of the player UI to return; the {@link | ||||
|      *                     Class#isInstance(Object)} method will be used, so even subclasses could | ||||
|      *                     be returned | ||||
|      * @param <T>          the class type parameter | ||||
|      * @return the first player UI of the required type found in the list, or an empty {@link | ||||
|      *         Optional} otherwise | ||||
|      */ | ||||
|     public <T> Optional<T> get(final Class<T> playerUiType) { | ||||
|         return playerUis.stream() | ||||
|                 .filter(playerUiType::isInstance) | ||||
|                 .map(playerUiType::cast) | ||||
|                 .findFirst(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Calls the provided consumer on all player UIs in the list, in order of addition. | ||||
|      * @param consumer the consumer to call with player UIs | ||||
|      */ | ||||
|     public void call(final Consumer<PlayerUi> consumer) { | ||||
|         //noinspection SimplifyStreamApiCallChains | ||||
|         playerUis.stream().forEachOrdered(consumer); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										124
									
								
								app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| package org.schabi.newpipe.player.ui | ||||
|  | ||||
| import org.schabi.newpipe.util.GuardedByMutex | ||||
| import java.util.Optional | ||||
|  | ||||
| class PlayerUiList(vararg initialPlayerUis: PlayerUi) { | ||||
|     var playerUis = GuardedByMutex(mutableListOf<PlayerUi>()) | ||||
|  | ||||
|     /** | ||||
|      * Creates a [PlayerUiList] starting with the provided player uis. The provided player uis | ||||
|      * will not be prepared like those passed to [.addAndPrepare], because when | ||||
|      * the [PlayerUiList] constructor is called, the player is still not running and it | ||||
|      * wouldn't make sense to initialize uis then. Instead the player will initialize them by doing | ||||
|      * proper calls to [.call]. | ||||
|      * | ||||
|      * @param initialPlayerUis the player uis this list should start with; the order will be kept | ||||
|      */ | ||||
|     init { | ||||
|         playerUis.runWithLockSync { | ||||
|             lockData.addAll(listOf(*initialPlayerUis)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adds the provided player ui to the list and calls on it the initialization functions that | ||||
|      * apply based on the current player state. The preparation step needs to be done since when UIs | ||||
|      * are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer | ||||
|      * is already initialized, but we need to notify the newly built UI that the player is ready | ||||
|      * nonetheless. | ||||
|      * @param playerUi the player ui to prepare and add to the list; its [PlayerUi.getPlayer] | ||||
|      * will be used to query information about the player state | ||||
|      */ | ||||
|     fun addAndPrepare(playerUi: PlayerUi) { | ||||
|         if (playerUi.getPlayer().fragmentListener.isPresent) { | ||||
|             // make sure UIs know whether a service is connected or not | ||||
|             playerUi.onFragmentListenerSet() | ||||
|         } | ||||
|  | ||||
|         if (!playerUi.getPlayer().exoPlayerIsNull()) { | ||||
|             playerUi.initPlayer() | ||||
|             if (playerUi.getPlayer().playQueue != null) { | ||||
|                 playerUi.initPlayback() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         playerUis.runWithLockSync { | ||||
|             lockData.add(playerUi) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Destroys all matching player UIs and removes them from the list. | ||||
|      * @param playerUiType the class of the player UI to destroy; | ||||
|      * the [Class.isInstance] method will be used, so even subclasses will be | ||||
|      * destroyed and removed | ||||
|      * @param T the class type parameter </T> | ||||
|      * */ | ||||
|     fun <T> destroyAll(playerUiType: Class<T?>) { | ||||
|         val toDestroy = mutableListOf<PlayerUi>() | ||||
|  | ||||
|         // short blocking removal from class to prevent interfering from other threads | ||||
|         playerUis.runWithLockSync { | ||||
|             val new = mutableListOf<PlayerUi>() | ||||
|             for (ui in lockData) { | ||||
|                 if (playerUiType.isInstance(ui)) { | ||||
|                     toDestroy.add(ui) | ||||
|                 } else { | ||||
|                     new.add(ui) | ||||
|                 } | ||||
|             } | ||||
|             lockData = new | ||||
|         } | ||||
|         // then actually destroy the UIs | ||||
|         for (ui in toDestroy) { | ||||
|             ui.destroyPlayer() | ||||
|             ui.destroy() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param playerUiType the class of the player UI to return; | ||||
|      * the [Class.isInstance] method will be used, so even subclasses could be returned | ||||
|      * @param T the class type parameter | ||||
|      * @return the first player UI of the required type found in the list, or null | ||||
|      </T> */ | ||||
|     fun <T> get(playerUiType: Class<T>): T? = | ||||
|         playerUis.runWithLockSync { | ||||
|             for (ui in lockData) { | ||||
|                 if (playerUiType.isInstance(ui)) { | ||||
|                     when (val r = playerUiType.cast(ui)) { | ||||
|                         // try all UIs before returning null | ||||
|                         null -> continue | ||||
|                         else -> return@runWithLockSync r | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             return@runWithLockSync null | ||||
|         } | ||||
|  | ||||
|     /** | ||||
|      * @param playerUiType the class of the player UI to return; | ||||
|      * the [Class.isInstance] method will be used, so even subclasses could be returned | ||||
|      * @param T the class type parameter | ||||
|      * @return the first player UI of the required type found in the list, or an empty | ||||
|      * [Optional] otherwise | ||||
|      </T> */ | ||||
|     @Deprecated("use get", ReplaceWith("get(playerUiType)")) | ||||
|     fun <T> getOpt(playerUiType: Class<T>): Optional<T & Any> = | ||||
|         Optional.ofNullable(get(playerUiType)) | ||||
|  | ||||
|     /** | ||||
|      * Calls the provided consumer on all player UIs in the list, in order of addition. | ||||
|      * @param consumer the consumer to call with player UIs | ||||
|      */ | ||||
|     fun call(consumer: java.util.function.Consumer<PlayerUi>) { | ||||
|         // copy the list out of the mutex before calling the consumer which might block | ||||
|         val new = playerUis.runWithLockSync { | ||||
|             lockData.toMutableList() | ||||
|         } | ||||
|         for (ui in new) { | ||||
|             consumer.accept(ui) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										47
									
								
								app/src/main/java/org/schabi/newpipe/util/GuardedByMutex.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								app/src/main/java/org/schabi/newpipe/util/GuardedByMutex.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| package org.schabi.newpipe.util | ||||
|  | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
|  | ||||
| /** Guard the given data so that it can only be accessed by locking the mutex first. | ||||
|  * | ||||
|  * Inspired by [this blog post](https://jonnyzzz.com/blog/2017/03/01/guarded-by-lock/) | ||||
|  * */ | ||||
| class GuardedByMutex<T>( | ||||
|     private var data: T, | ||||
|     private val lock: Mutex = Mutex(locked = false), | ||||
| ) { | ||||
|  | ||||
|     /** Lock the mutex and access the data, blocking the current thread. | ||||
|      * @param action to run with locked mutex | ||||
|      * */ | ||||
|     fun <Y> runWithLockSync( | ||||
|         action: MutexData<T>.() -> Y | ||||
|     ) = | ||||
|         runBlocking { | ||||
|             lock.withLock { | ||||
|                 MutexData(data, { d -> data = d }).action() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     /** Lock the mutex and access the data, suspending the coroutine. | ||||
|      * @param action to run with locked mutex | ||||
|      * */ | ||||
|     suspend fun <Y> runWithLock(action: MutexData<T>.() -> Y) = | ||||
|         lock.withLock { | ||||
|             MutexData(data, { d -> data = d }).action() | ||||
|         } | ||||
| } | ||||
|  | ||||
| /** The data inside a [GuardedByMutex], which can be accessed via [lockData]. | ||||
|  *  [lockData] is a `var`, so you can `set` it as well. | ||||
|  * */ | ||||
| class MutexData<T>(data: T, val setFun: (T) -> Unit) { | ||||
|     /** The data inside this [GuardedByMutex] */ | ||||
|     var lockData: T = data | ||||
|         set(t) { | ||||
|             setFun(t) | ||||
|             field = t | ||||
|         } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Stypox
					Stypox