2015-01-14 12:12:47 +01:00
/ *
* Copyright 2015 Synced Synapse . All rights reserved .
*
* Licensed under the Apache License , Version 2 . 0 ( the " License " ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an " AS IS " BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2016-12-20 21:47:24 +01:00
package org.xbmc.kore.ui.sections.remote ;
2015-01-14 12:12:47 +01:00
2015-06-23 19:43:57 +02:00
import android.content.res.Resources ;
import android.content.res.TypedArray ;
2015-01-14 12:12:47 +01:00
import android.os.Bundle ;
import android.os.Handler ;
2015-04-08 23:28:19 +02:00
import android.text.TextUtils ;
2015-01-14 12:12:47 +01:00
import android.view.LayoutInflater ;
import android.view.Menu ;
import android.view.MenuInflater ;
import android.view.MenuItem ;
import android.view.View ;
import android.view.ViewGroup ;
import android.widget.AdapterView ;
import android.widget.BaseAdapter ;
import android.widget.ImageView ;
import android.widget.PopupMenu ;
import android.widget.RelativeLayout ;
import android.widget.TextView ;
2016-01-11 10:32:07 +01:00
import android.widget.Toast ;
2015-01-14 12:12:47 +01:00
2020-04-15 22:46:06 +02:00
import androidx.cardview.widget.CardView ;
import androidx.fragment.app.Fragment ;
2015-03-09 22:35:18 +01:00
import org.xbmc.kore.R ;
import org.xbmc.kore.host.HostConnectionObserver ;
import org.xbmc.kore.host.HostConnectionObserver.PlayerEventsObserver ;
2019-03-30 12:08:58 +01:00
import org.xbmc.kore.host.HostConnectionObserver.PlaylistEventsObserver ;
2015-03-09 22:35:18 +01:00
import org.xbmc.kore.host.HostInfo ;
import org.xbmc.kore.host.HostManager ;
2019-03-30 12:08:58 +01:00
import org.xbmc.kore.host.actions.GetPlaylist ;
2015-03-09 22:35:18 +01:00
import org.xbmc.kore.jsonrpc.ApiCallback ;
import org.xbmc.kore.jsonrpc.ApiMethod ;
2016-01-11 10:32:07 +01:00
import org.xbmc.kore.jsonrpc.HostConnection ;
2015-03-09 22:35:18 +01:00
import org.xbmc.kore.jsonrpc.method.Player ;
import org.xbmc.kore.jsonrpc.method.Playlist ;
import org.xbmc.kore.jsonrpc.type.ListType ;
import org.xbmc.kore.jsonrpc.type.PlayerType ;
import org.xbmc.kore.jsonrpc.type.PlaylistType ;
2016-01-11 10:32:07 +01:00
import org.xbmc.kore.ui.viewgroups.DynamicListView ;
2019-03-30 12:08:58 +01:00
import org.xbmc.kore.ui.widgets.PlaylistsBar ;
2015-03-09 22:35:18 +01:00
import org.xbmc.kore.utils.LogUtils ;
import org.xbmc.kore.utils.UIUtils ;
import org.xbmc.kore.utils.Utils ;
2015-01-14 12:12:47 +01:00
2019-03-30 12:08:58 +01:00
import java.util.ArrayList ;
import java.util.HashMap ;
import java.util.Iterator ;
2015-01-14 12:12:47 +01:00
import java.util.List ;
2018-04-05 19:22:20 +02:00
import butterknife.BindView ;
2019-03-30 12:08:58 +01:00
import butterknife.ButterKnife ;
2018-04-05 19:22:20 +02:00
import butterknife.Unbinder ;
2015-01-14 12:12:47 +01:00
/ * *
* Playlist view
* /
public class PlaylistFragment extends Fragment
2019-03-30 12:08:58 +01:00
implements PlayerEventsObserver , PlaylistEventsObserver {
2015-01-14 12:12:47 +01:00
private static final String TAG = LogUtils . makeLogTag ( PlaylistFragment . class ) ;
/ * *
* Host manager from which to get info about the current XBMC
* /
private HostManager hostManager ;
/ * *
* Activity to communicate potential actions that change what ' s playing
* /
private HostConnectionObserver hostConnectionObserver ;
/ * *
* Handler on which to post RPC callbacks
* /
private Handler callbackHandler = new Handler ( ) ;
/ * *
2019-03-30 12:08:58 +01:00
* Playlist adapter
2015-01-14 12:12:47 +01:00
* /
2019-03-30 12:08:58 +01:00
private PlayListAdapter playListAdapter ;
2015-01-14 12:12:47 +01:00
2019-03-30 12:08:58 +01:00
private Unbinder unbinder ;
2015-01-14 12:12:47 +01:00
/ * *
2019-03-30 12:08:58 +01:00
* Last call results
2015-01-14 12:12:47 +01:00
* /
2019-03-30 12:08:58 +01:00
private ListType . ItemsAll lastGetItemResult = null ;
private PlayerType . GetActivePlayersReturnType lastGetActivePlayerResult ;
private HashMap < String , PlaylistHolder > playlists = new HashMap < > ( ) ;
private enum PLAYER_STATE {
CONNECTION_ERROR ,
NO_RESULTS_YET ,
PLAYING ,
PAUSED ,
STOPPED
}
2015-01-14 12:12:47 +01:00
2019-03-30 12:08:58 +01:00
private PLAYER_STATE playerState ;
private boolean userSelectedTab ;
2018-04-05 19:22:20 +02:00
2015-01-14 12:12:47 +01:00
/ * *
* Injectable views
* /
2018-04-05 19:22:20 +02:00
@BindView ( R . id . info_panel ) RelativeLayout infoPanel ;
@BindView ( R . id . playlist ) DynamicListView playlistListView ;
2015-01-14 12:12:47 +01:00
2018-04-05 19:22:20 +02:00
@BindView ( R . id . info_title ) TextView infoTitle ;
@BindView ( R . id . info_message ) TextView infoMessage ;
2015-01-14 12:12:47 +01:00
2019-03-30 12:08:58 +01:00
@BindView ( R . id . playlists_bar ) PlaylistsBar playlistsBar ;
2015-01-14 12:12:47 +01:00
@Override
public void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState ) ;
hostManager = HostManager . getInstance ( getActivity ( ) ) ;
hostConnectionObserver = hostManager . getHostConnectionObserver ( ) ;
}
@Override
public View onCreateView ( LayoutInflater inflater , ViewGroup container , Bundle savedInstanceState ) {
ViewGroup root = ( ViewGroup ) inflater . inflate ( R . layout . fragment_playlist , container , false ) ;
2018-04-05 19:22:20 +02:00
unbinder = ButterKnife . bind ( this , root ) ;
2015-01-14 12:12:47 +01:00
playListAdapter = new PlayListAdapter ( ) ;
2016-01-11 10:32:07 +01:00
playlistListView . setAdapter ( playListAdapter ) ;
2015-01-14 12:12:47 +01:00
// When clicking on an item, play it
2016-01-11 10:32:07 +01:00
playlistListView . setOnItemClickListener ( new AdapterView . OnItemClickListener ( ) {
2015-01-14 12:12:47 +01:00
@Override
public void onItemClick ( AdapterView < ? > parent , View view , int position , long id ) {
2019-03-30 12:08:58 +01:00
int playlistId = playlists . get ( playlistsBar . getSelectedPlaylistType ( ) ) . getPlaylistId ( ) ;
Player . Open action = new Player . Open ( Player . Open . TYPE_PLAYLIST , playlistId , position ) ;
2015-01-14 12:12:47 +01:00
action . execute ( hostManager . getConnection ( ) , defaultStringActionCallback , callbackHandler ) ;
}
} ) ;
2019-03-30 12:08:58 +01:00
playlistsBar . setOnPlaylistSelectedListener ( new PlaylistsBar . OnPlaylistSelectedListener ( ) {
@Override
public void onPlaylistSelected ( String playlistType ) {
userSelectedTab = true ; // do not switch to active playlist when user selected a tab
displayPlaylist ( ) ;
}
@Override
public void onPlaylistDeselected ( String playlistType ) {
View v = playlistListView . getChildAt ( 0 ) ;
int top = ( v = = null ) ? 0 : ( v . getTop ( ) - playlistListView . getPaddingTop ( ) ) ;
PlaylistHolder playlistHolder = playlists . get ( playlistType ) ;
if ( playlistHolder ! = null )
playlistHolder . setListViewPosition ( playlistListView . getFirstVisiblePosition ( ) , top ) ;
}
} ) ;
2015-01-14 12:12:47 +01:00
return root ;
}
@Override
public void onActivityCreated ( Bundle savedInstanceState ) {
super . onActivityCreated ( savedInstanceState ) ;
// We have options
setHasOptionsMenu ( true ) ;
}
@Override
public void onResume ( ) {
super . onResume ( ) ;
2019-03-30 12:08:58 +01:00
Tweak connection threads
This PR fixes some issues with connections and threading. Specifically, the change in #618 introduced threading in `HostConnection`, which had some issues. To fix them, the following changes were made:
- A specific TCP listener thread is used and manually started, instead of using one of the threads in the pool. The TCP listener thread is a long lived thread that should always be running (as long as the connection is through TCP), blocked listening on the TCP socket, so it shouldn't be managed in a pool, where, theoretically, it can be paused and reused.
- Changed the number of threads to 5. We shouldn't need more than this, otherwise we can overwhelm some Kodi hardware.
- Had to sprinkle some `synchronized` to avoid race conditions. For instance, through a TCP connection, as soon as Kore is opened (on the remote screen) it will call at least `GetActivePlayers`, `GetProperties`, `Ping`. If the TCP socket isn't set up yet, each of these calls would create a socket (and a TCP listener thread), so we would open 3 or more sockets when we should just open 1. A `synchronized` in `executeThroughTcp` prevents this. The others prevent similar issues.
Aditionally:
- Tweaked the playlist fetching code, so that it happens exclusively in `HostConnectionObserver` and when PlaylistFragment is notified of changes, it already gets the playlist data. This somewhat simplifies the code, and makes it more consistent with the Player Observer code;
- Change `EventServerConnection` to accept a Handler on which to post the result of the connection, so that the caller can control on which thread the result is called;
- Calls to the various `RegisterObserver` loose the reply immediately parameter, as it was always true.
2019-05-28 20:44:02 +02:00
hostConnectionObserver . registerPlayerObserver ( this ) ;
hostConnectionObserver . registerPlaylistObserver ( this ) ;
2015-01-14 12:12:47 +01:00
}
@Override
public void onPause ( ) {
hostConnectionObserver . unregisterPlayerObserver ( this ) ;
2019-03-30 12:08:58 +01:00
hostConnectionObserver . unregisterPlaylistObserver ( this ) ;
super . onPause ( ) ;
2015-01-14 12:12:47 +01:00
}
@Override
public void onCreateOptionsMenu ( Menu menu , MenuInflater inflater ) {
inflater . inflate ( R . menu . playlist , menu ) ;
super . onCreateOptionsMenu ( menu , inflater ) ;
}
2018-04-05 19:22:20 +02:00
@Override
public void onDestroyView ( ) {
super . onDestroyView ( ) ;
unbinder . unbind ( ) ;
}
2015-01-14 12:12:47 +01:00
@Override
public boolean onOptionsItemSelected ( MenuItem item ) {
switch ( item . getItemId ( ) ) {
case R . id . action_clear_playlist :
2019-03-30 12:08:58 +01:00
PlaylistHolder playlistHolder = playlists . get ( playlistsBar . getSelectedPlaylistType ( ) ) ;
int playlistId = playlistHolder . getPlaylistId ( ) ;
playlistOnClear ( playlistId ) ;
Playlist . Clear action = new Playlist . Clear ( playlistId ) ;
2015-01-14 12:12:47 +01:00
action . execute ( hostManager . getConnection ( ) , defaultStringActionCallback , callbackHandler ) ;
break ;
default :
break ;
}
return super . onOptionsItemSelected ( item ) ;
}
2019-03-30 12:08:58 +01:00
private boolean refreshingPlaylist ;
private void refreshPlaylist ( GetPlaylist getPlaylist ) {
if ( refreshingPlaylist )
return ;
refreshingPlaylist = true ;
hostManager . getConnection ( ) . execute ( getPlaylist ,
new ApiCallback < ArrayList < GetPlaylist . GetPlaylistResult > > ( ) {
@Override
public void onSuccess ( ArrayList < GetPlaylist . GetPlaylistResult > result ) {
refreshingPlaylist = false ;
if ( ! isAdded ( ) )
return ;
updatePlaylists ( result ) ;
displayPlaylist ( ) ;
}
@Override
public void onError ( int errorCode , String description ) {
refreshingPlaylist = false ;
Tweak connection threads
This PR fixes some issues with connections and threading. Specifically, the change in #618 introduced threading in `HostConnection`, which had some issues. To fix them, the following changes were made:
- A specific TCP listener thread is used and manually started, instead of using one of the threads in the pool. The TCP listener thread is a long lived thread that should always be running (as long as the connection is through TCP), blocked listening on the TCP socket, so it shouldn't be managed in a pool, where, theoretically, it can be paused and reused.
- Changed the number of threads to 5. We shouldn't need more than this, otherwise we can overwhelm some Kodi hardware.
- Had to sprinkle some `synchronized` to avoid race conditions. For instance, through a TCP connection, as soon as Kore is opened (on the remote screen) it will call at least `GetActivePlayers`, `GetProperties`, `Ping`. If the TCP socket isn't set up yet, each of these calls would create a socket (and a TCP listener thread), so we would open 3 or more sockets when we should just open 1. A `synchronized` in `executeThroughTcp` prevents this. The others prevent similar issues.
Aditionally:
- Tweaked the playlist fetching code, so that it happens exclusively in `HostConnectionObserver` and when PlaylistFragment is notified of changes, it already gets the playlist data. This somewhat simplifies the code, and makes it more consistent with the Player Observer code;
- Change `EventServerConnection` to accept a Handler on which to post the result of the connection, so that the caller can control on which thread the result is called;
- Calls to the various `RegisterObserver` loose the reply immediately parameter, as it was always true.
2019-05-28 20:44:02 +02:00
playerOnConnectionError ( errorCode , description ) ;
2019-03-30 12:08:58 +01:00
}
} , callbackHandler ) ;
2015-01-14 12:12:47 +01:00
}
/ * *
* Default callback for methods that don ' t return anything
* /
private ApiCallback < String > defaultStringActionCallback = ApiMethod . getDefaultActionCallback ( ) ;
2017-07-13 20:10:49 +02:00
@Override
public void playerOnPropertyChanged ( org . xbmc . kore . jsonrpc . notification . Player . NotificationsData notificationsData ) {
if ( notificationsData . property . shuffled ! = null )
2019-03-30 12:08:58 +01:00
refreshPlaylist ( new GetPlaylist ( hostManager . getConnection ( ) , lastGetActivePlayerResult . type ) ) ;
2017-07-13 20:10:49 +02:00
}
2015-01-14 12:12:47 +01:00
/ * *
* HostConnectionObserver . PlayerEventsObserver interface callbacks
* /
2019-03-30 12:08:58 +01:00
@Override
2015-01-14 12:12:47 +01:00
public void playerOnPlay ( PlayerType . GetActivePlayersReturnType getActivePlayerResult ,
PlayerType . PropertyValue getPropertiesResult ,
ListType . ItemsAll getItemResult ) {
2019-03-30 12:08:58 +01:00
playerState = PLAYER_STATE . PLAYING ;
2015-01-14 12:12:47 +01:00
lastGetItemResult = getItemResult ;
2019-03-30 12:08:58 +01:00
lastGetActivePlayerResult = getActivePlayerResult ;
if ( ! userSelectedTab ) {
playlistsBar . selectTab ( getActivePlayerResult . type ) ;
}
playlistsBar . setIsPlaying ( getActivePlayerResult . type , true ) ;
displayPlaylist ( ) ;
PlaylistHolder playlistHolder = playlists . get ( getActivePlayerResult . type ) ;
if ( playlistHolder ! = null & & isPlaying ( playlistHolder . getPlaylistResult ) ) {
highlightCurrentlyPlayingItem ( ) ;
} else {
playlistListView . clearChoices ( ) ;
}
2015-01-14 12:12:47 +01:00
}
2019-03-30 12:08:58 +01:00
@Override
2015-01-14 12:12:47 +01:00
public void playerOnPause ( PlayerType . GetActivePlayersReturnType getActivePlayerResult ,
PlayerType . PropertyValue getPropertiesResult ,
ListType . ItemsAll getItemResult ) {
2019-03-30 12:08:58 +01:00
playerState = PLAYER_STATE . PAUSED ;
2015-01-14 12:12:47 +01:00
lastGetItemResult = getItemResult ;
2019-03-30 12:08:58 +01:00
lastGetActivePlayerResult = getActivePlayerResult ;
if ( ! userSelectedTab ) {
playlistsBar . selectTab ( getActivePlayerResult . type ) ;
}
playlistsBar . setIsPlaying ( getActivePlayerResult . type , false ) ;
2015-01-14 12:12:47 +01:00
}
2019-03-30 12:08:58 +01:00
@Override
2015-01-14 12:12:47 +01:00
public void playerOnStop ( ) {
2019-03-30 12:08:58 +01:00
playerState = PLAYER_STATE . STOPPED ;
if ( lastGetActivePlayerResult ! = null )
playlistsBar . setIsPlaying ( lastGetActivePlayerResult . type , false ) ;
displayPlaylist ( ) ;
2015-01-14 12:12:47 +01:00
2019-03-30 12:08:58 +01:00
playlistListView . clearChoices ( ) ;
2015-01-14 12:12:47 +01:00
}
2019-03-30 12:08:58 +01:00
@Override
2015-01-14 12:12:47 +01:00
public void playerOnConnectionError ( int errorCode , String description ) {
2019-03-30 12:08:58 +01:00
playerState = PLAYER_STATE . CONNECTION_ERROR ;
2015-01-14 12:12:47 +01:00
HostInfo hostInfo = hostManager . getHostInfo ( ) ;
switchToPanel ( R . id . info_panel ) ;
if ( hostInfo ! = null ) {
infoTitle . setText ( R . string . connecting ) ;
// TODO: check error code
infoMessage . setText ( String . format ( getString ( R . string . connecting_to ) , hostInfo . getName ( ) , hostInfo . getAddress ( ) ) ) ;
} else {
infoTitle . setText ( R . string . no_xbmc_configured ) ;
infoMessage . setText ( null ) ;
}
}
2019-03-30 12:08:58 +01:00
@Override
2015-01-14 12:12:47 +01:00
public void playerNoResultsYet ( ) {
2019-03-30 12:08:58 +01:00
playerState = PLAYER_STATE . NO_RESULTS_YET ;
2015-01-14 12:12:47 +01:00
// Initialize info panel
switchToPanel ( R . id . info_panel ) ;
HostInfo hostInfo = hostManager . getHostInfo ( ) ;
if ( hostInfo ! = null ) {
infoTitle . setText ( R . string . connecting ) ;
} else {
infoTitle . setText ( R . string . no_xbmc_configured ) ;
}
infoMessage . setText ( null ) ;
}
2019-03-30 12:08:58 +01:00
@Override
2015-01-14 12:12:47 +01:00
public void systemOnQuit ( ) {
playerNoResultsYet ( ) ;
}
// Ignore this
2019-03-30 12:08:58 +01:00
@Override
2015-01-14 12:12:47 +01:00
public void inputOnInputRequested ( String title , String type , String value ) { }
2019-03-30 12:08:58 +01:00
@Override
2015-02-15 20:11:32 +01:00
public void observerOnStopObserving ( ) { }
2015-01-14 12:12:47 +01:00
2019-03-30 12:08:58 +01:00
@Override
public void playlistOnClear ( int playlistId ) {
Iterator < String > it = playlists . keySet ( ) . iterator ( ) ;
while ( it . hasNext ( ) ) {
String key = it . next ( ) ;
Tweak connection threads
This PR fixes some issues with connections and threading. Specifically, the change in #618 introduced threading in `HostConnection`, which had some issues. To fix them, the following changes were made:
- A specific TCP listener thread is used and manually started, instead of using one of the threads in the pool. The TCP listener thread is a long lived thread that should always be running (as long as the connection is through TCP), blocked listening on the TCP socket, so it shouldn't be managed in a pool, where, theoretically, it can be paused and reused.
- Changed the number of threads to 5. We shouldn't need more than this, otherwise we can overwhelm some Kodi hardware.
- Had to sprinkle some `synchronized` to avoid race conditions. For instance, through a TCP connection, as soon as Kore is opened (on the remote screen) it will call at least `GetActivePlayers`, `GetProperties`, `Ping`. If the TCP socket isn't set up yet, each of these calls would create a socket (and a TCP listener thread), so we would open 3 or more sockets when we should just open 1. A `synchronized` in `executeThroughTcp` prevents this. The others prevent similar issues.
Aditionally:
- Tweaked the playlist fetching code, so that it happens exclusively in `HostConnectionObserver` and when PlaylistFragment is notified of changes, it already gets the playlist data. This somewhat simplifies the code, and makes it more consistent with the Player Observer code;
- Change `EventServerConnection` to accept a Handler on which to post the result of the connection, so that the caller can control on which thread the result is called;
- Calls to the various `RegisterObserver` loose the reply immediately parameter, as it was always true.
2019-05-28 20:44:02 +02:00
if ( playlists . get ( key ) . getPlaylistResult . id = = playlistId ) {
2019-03-30 12:08:58 +01:00
it . remove ( ) ;
playlistsBar . setHasPlaylistAvailable ( key , false ) ;
playlistsBar . setIsPlaying ( key , false ) ;
}
}
displayPlaylist ( ) ;
}
2015-01-14 12:12:47 +01:00
2019-03-30 12:08:58 +01:00
@Override
public void playlistsAvailable ( ArrayList < GetPlaylist . GetPlaylistResult > playlists ) {
updatePlaylists ( playlists ) ;
2015-01-14 12:12:47 +01:00
Tweak connection threads
This PR fixes some issues with connections and threading. Specifically, the change in #618 introduced threading in `HostConnection`, which had some issues. To fix them, the following changes were made:
- A specific TCP listener thread is used and manually started, instead of using one of the threads in the pool. The TCP listener thread is a long lived thread that should always be running (as long as the connection is through TCP), blocked listening on the TCP socket, so it shouldn't be managed in a pool, where, theoretically, it can be paused and reused.
- Changed the number of threads to 5. We shouldn't need more than this, otherwise we can overwhelm some Kodi hardware.
- Had to sprinkle some `synchronized` to avoid race conditions. For instance, through a TCP connection, as soon as Kore is opened (on the remote screen) it will call at least `GetActivePlayers`, `GetProperties`, `Ping`. If the TCP socket isn't set up yet, each of these calls would create a socket (and a TCP listener thread), so we would open 3 or more sockets when we should just open 1. A `synchronized` in `executeThroughTcp` prevents this. The others prevent similar issues.
Aditionally:
- Tweaked the playlist fetching code, so that it happens exclusively in `HostConnectionObserver` and when PlaylistFragment is notified of changes, it already gets the playlist data. This somewhat simplifies the code, and makes it more consistent with the Player Observer code;
- Change `EventServerConnection` to accept a Handler on which to post the result of the connection, so that the caller can control on which thread the result is called;
- Calls to the various `RegisterObserver` loose the reply immediately parameter, as it was always true.
2019-05-28 20:44:02 +02:00
if ( ( playerState = = PLAYER_STATE . PLAYING ) & &
( hostManager . getConnection ( ) . getProtocol ( ) = = HostConnection . PROTOCOL_TCP ) )
// if item is currently playing displaying is already handled by playerOnPlay callback
2019-03-30 12:08:58 +01:00
return ;
// BUG: When playing movies playlist stops, audio tab gets selected when it contains a playlist.
// We might want a separate var to check if something has already played and turn off automatic
// playlist switching if playback stops
if ( playerState = = PLAYER_STATE . STOPPED & & lastGetActivePlayerResult = = null & & ! userSelectedTab ) { // do not automatically switch to first available playlist if user manually selected a playlist
playlistsBar . selectTab ( playlists . get ( 0 ) . type ) ;
}
displayPlaylist ( ) ;
}
@Override
public void playlistOnError ( int errorCode , String description ) {
Tweak connection threads
This PR fixes some issues with connections and threading. Specifically, the change in #618 introduced threading in `HostConnection`, which had some issues. To fix them, the following changes were made:
- A specific TCP listener thread is used and manually started, instead of using one of the threads in the pool. The TCP listener thread is a long lived thread that should always be running (as long as the connection is through TCP), blocked listening on the TCP socket, so it shouldn't be managed in a pool, where, theoretically, it can be paused and reused.
- Changed the number of threads to 5. We shouldn't need more than this, otherwise we can overwhelm some Kodi hardware.
- Had to sprinkle some `synchronized` to avoid race conditions. For instance, through a TCP connection, as soon as Kore is opened (on the remote screen) it will call at least `GetActivePlayers`, `GetProperties`, `Ping`. If the TCP socket isn't set up yet, each of these calls would create a socket (and a TCP listener thread), so we would open 3 or more sockets when we should just open 1. A `synchronized` in `executeThroughTcp` prevents this. The others prevent similar issues.
Aditionally:
- Tweaked the playlist fetching code, so that it happens exclusively in `HostConnectionObserver` and when PlaylistFragment is notified of changes, it already gets the playlist data. This somewhat simplifies the code, and makes it more consistent with the Player Observer code;
- Change `EventServerConnection` to accept a Handler on which to post the result of the connection, so that the caller can control on which thread the result is called;
- Calls to the various `RegisterObserver` loose the reply immediately parameter, as it was always true.
2019-05-28 20:44:02 +02:00
playerOnConnectionError ( errorCode , description ) ;
2019-03-30 12:08:58 +01:00
}
private void updatePlaylists ( ArrayList < GetPlaylist . GetPlaylistResult > playlists ) {
for ( GetPlaylist . GetPlaylistResult getPlaylistResult : playlists ) {
playlistsBar . setHasPlaylistAvailable ( getPlaylistResult . type , true ) ;
PlaylistHolder playlistHolder = this . playlists . get ( getPlaylistResult . type ) ;
if ( playlistHolder = = null ) {
playlistHolder = new PlaylistHolder ( ) ;
this . playlists . put ( getPlaylistResult . type , playlistHolder ) ;
}
playlistHolder . setPlaylist ( getPlaylistResult ) ;
2015-01-14 12:12:47 +01:00
}
}
2019-03-30 12:08:58 +01:00
private void displayPlaylist ( ) {
switchToPanel ( R . id . playlist ) ;
PlaylistHolder playlistHolder = playlists . get ( playlistsBar . getSelectedPlaylistType ( ) ) ;
if ( playlistHolder = = null ) {
displayEmptyPlaylistMessage ( ) ;
return ;
}
GetPlaylist . GetPlaylistResult getPlaylistResult = playlistHolder . getPlaylistResult ;
if ( getPlaylistResult = = null ) {
2015-01-14 12:12:47 +01:00
displayEmptyPlaylistMessage ( ) ;
return ;
}
2019-03-30 12:08:58 +01:00
// JSON RPC does not support picture items in Playlist.Item so we disable item movement
// for the picture playlist
if ( getPlaylistResult . type . contentEquals ( ListType . ItemBase . TYPE_PICTURE ) )
playlistListView . enableItemDragging ( false ) ;
else
playlistListView . enableItemDragging ( true ) ;
2015-01-14 12:12:47 +01:00
2016-01-11 10:32:07 +01:00
//If a user is dragging a list item we must not modify the adapter to prevent
//the dragged item's adapter position from diverging from its listview position
if ( ! playlistListView . isItemBeingDragged ( ) ) {
2019-03-30 12:08:58 +01:00
playListAdapter . setPlaylistItems ( getPlaylistResult . items ) ;
2016-01-11 10:32:07 +01:00
}
2019-03-30 12:08:58 +01:00
playlistListView . setSelectionFromTop ( playlistHolder . index , playlistHolder . top ) ;
}
private boolean isPlaying ( GetPlaylist . GetPlaylistResult getPlaylistResult ) {
return playerState = = PLAYER_STATE . PLAYING & & lastGetActivePlayerResult ! = null & &
getPlaylistResult . id = = lastGetActivePlayerResult . playerid ;
2016-01-11 10:32:07 +01:00
}
2019-03-30 12:08:58 +01:00
private void highlightCurrentlyPlayingItem ( ) {
if ( ! playlistsBar . getSelectedPlaylistType ( ) . contentEquals ( lastGetActivePlayerResult . type ) )
return ;
List < ListType . ItemsAll > playlistItems = playlists . get ( playlistsBar . getSelectedPlaylistType ( ) ) . getPlaylistResult . items ;
2015-01-14 12:12:47 +01:00
for ( int i = 0 ; i < playlistItems . size ( ) ; i + + ) {
2019-03-30 12:08:58 +01:00
if ( ( playlistItems . get ( i ) . id = = lastGetItemResult . id ) & &
( playlistItems . get ( i ) . type . equals ( lastGetItemResult . type ) ) ) {
2016-01-11 10:32:07 +01:00
//When user is dragging an item it is very annoying when we change the list position
if ( ! playlistListView . isItemBeingDragged ( ) ) {
playlistListView . setSelection ( i ) ;
}
2019-03-30 12:08:58 +01:00
playlistListView . setItemChecked ( i , true ) ;
2015-01-14 12:12:47 +01:00
}
}
}
/ * *
* Switches the info panel shown ( they are exclusive )
* @param panelResId The panel to show
* /
private void switchToPanel ( int panelResId ) {
switch ( panelResId ) {
case R . id . info_panel :
infoPanel . setVisibility ( View . VISIBLE ) ;
2016-01-11 10:32:07 +01:00
playlistListView . setVisibility ( View . GONE ) ;
2015-01-14 12:12:47 +01:00
break ;
case R . id . playlist :
infoPanel . setVisibility ( View . GONE ) ;
2016-01-11 10:32:07 +01:00
playlistListView . setVisibility ( View . VISIBLE ) ;
2015-01-14 12:12:47 +01:00
break ;
}
}
/ * *
* Displays empty playlist
* /
private void displayEmptyPlaylistMessage ( ) {
2019-03-30 12:08:58 +01:00
HostInfo hostInfo = hostManager . getHostInfo ( ) ;
2015-01-14 12:12:47 +01:00
switchToPanel ( R . id . info_panel ) ;
infoTitle . setText ( R . string . playlist_empty ) ;
2019-03-30 12:08:58 +01:00
infoMessage . setText ( String . format ( getString ( R . string . connected_to ) , hostInfo . getName ( ) ) ) ;
2015-01-14 12:12:47 +01:00
}
/ * *
* Adapter used to show the hosts in the ListView
* /
private class PlayListAdapter extends BaseAdapter
2016-01-11 10:32:07 +01:00
implements DynamicListView . DynamicListAdapter {
2015-01-14 12:12:47 +01:00
private View . OnClickListener playlistItemMenuClickListener = new View . OnClickListener ( ) {
@Override
public void onClick ( View v ) {
final int position = ( Integer ) v . getTag ( ) ;
final PopupMenu popupMenu = new PopupMenu ( getActivity ( ) , v ) ;
popupMenu . getMenuInflater ( ) . inflate ( R . menu . playlist_item , popupMenu . getMenu ( ) ) ;
popupMenu . setOnMenuItemClickListener ( new PopupMenu . OnMenuItemClickListener ( ) {
@Override
public boolean onMenuItemClick ( MenuItem item ) {
switch ( item . getItemId ( ) ) {
case R . id . action_remove_playlist_item :
// Remove this item from the playlist
2019-03-30 12:08:58 +01:00
int playlistId = playlists . get ( playlistsBar . getSelectedPlaylistType ( ) ) . getPlaylistId ( ) ;
Playlist . Remove action = new Playlist . Remove ( playlistId , position ) ;
2015-01-14 12:12:47 +01:00
action . execute ( hostManager . getConnection ( ) , defaultStringActionCallback , callbackHandler ) ;
return true ;
}
return false ;
}
} ) ;
popupMenu . show ( ) ;
}
} ;
/ * *
* The playlist items
* /
List < ListType . ItemsAll > playlistItems ;
2015-03-28 19:08:07 +01:00
int artWidth = getResources ( ) . getDimensionPixelSize ( R . dimen . playlist_art_width ) ;
int artHeight = getResources ( ) . getDimensionPixelSize ( R . dimen . playlist_art_heigth ) ;
2015-01-14 12:12:47 +01:00
2015-06-23 19:43:57 +02:00
int cardBackgroundColor , selectedCardBackgroundColor ;
2015-01-14 12:12:47 +01:00
public PlayListAdapter ( List < ListType . ItemsAll > playlistItems ) {
super ( ) ;
this . playlistItems = playlistItems ;
2015-06-23 19:43:57 +02:00
Resources . Theme theme = getActivity ( ) . getTheme ( ) ;
TypedArray styledAttributes = theme . obtainStyledAttributes ( new int [ ] {
R . attr . appCardBackgroundColor ,
R . attr . appSelectedCardBackgroundColor } ) ;
2016-09-16 20:09:06 +02:00
Resources resources = getResources ( ) ;
cardBackgroundColor =
styledAttributes . getColor ( styledAttributes . getIndex ( 0 ) , resources . getColor ( R . color . dark_content_background ) ) ;
selectedCardBackgroundColor =
styledAttributes . getColor ( styledAttributes . getIndex ( 1 ) , resources . getColor ( R . color . dark_selected_content_background ) ) ;
2015-06-23 19:43:57 +02:00
styledAttributes . recycle ( ) ;
2015-01-14 12:12:47 +01:00
}
public PlayListAdapter ( ) {
2015-03-28 19:08:07 +01:00
this ( null ) ;
2015-01-14 12:12:47 +01:00
}
/ * *
* Manually set the items on the adapter
* Calls notifyDataSetChanged ( )
*
2015-03-17 19:45:21 +01:00
* @param playlistItems Items
2015-01-14 12:12:47 +01:00
* /
public void setPlaylistItems ( List < ListType . ItemsAll > playlistItems ) {
this . playlistItems = playlistItems ;
notifyDataSetChanged ( ) ;
}
@Override
public int getCount ( ) {
if ( playlistItems = = null ) {
return 0 ;
} else {
return playlistItems . size ( ) ;
}
}
@Override
public ListType . ItemsAll getItem ( int position ) {
if ( playlistItems = = null ) {
return null ;
} else {
return playlistItems . get ( position ) ;
}
}
@Override
public long getItemId ( int position ) {
2016-01-11 10:32:07 +01:00
if ( position < 0 | | position > = playlistItems . size ( ) ) {
return - 1 ;
}
return playlistItems . get ( position ) . id ;
2015-01-14 12:12:47 +01:00
}
@Override
public int getViewTypeCount ( ) {
return 1 ;
}
2016-01-11 10:32:07 +01:00
@Override
public void onSwapItems ( int positionOne , int positionTwo ) {
ListType . ItemsAll tmp = playlistItems . get ( positionOne ) ;
playlistItems . set ( positionOne , playlistItems . get ( positionTwo ) ) ;
playlistItems . set ( positionTwo , tmp ) ;
}
@Override
public void onSwapFinished ( final int originalPosition , final int finalPosition ) {
final HostConnection hostConnection = hostManager . getConnection ( ) ;
if ( playlistItems . get ( finalPosition ) . id = = lastGetItemResult . id ) {
Toast . makeText ( getActivity ( ) , R . string . cannot_move_playing_item , Toast . LENGTH_SHORT )
. show ( ) ;
rollbackSwappedItems ( originalPosition , finalPosition ) ;
notifyDataSetChanged ( ) ;
return ;
}
2019-03-30 12:08:58 +01:00
final int playlistId = playlists . get ( playlistsBar . getSelectedPlaylistType ( ) ) . getPlaylistId ( ) ;
Playlist . Remove remove = new Playlist . Remove ( playlistId , originalPosition ) ;
2016-01-11 10:32:07 +01:00
remove . execute ( hostConnection , new ApiCallback < String > ( ) {
@Override
public void onSuccess ( String result ) {
2019-03-30 12:08:58 +01:00
Playlist . Insert insert = new Playlist . Insert ( playlistId , finalPosition , createPlaylistTypeItem ( playlistItems . get ( finalPosition ) ) ) ;
2016-01-11 10:32:07 +01:00
insert . execute ( hostConnection , new ApiCallback < String > ( ) {
@Override
public void onSuccess ( String result ) {
}
@Override
public void onError ( int errorCode , String description ) {
//Remove succeeded but insert failed, so we need to remove item from playlist at final position
playlistItems . remove ( finalPosition ) ;
notifyDataSetChanged ( ) ;
if ( ! isAdded ( ) ) return ;
// Got an error, show toast
Toast . makeText ( getActivity ( ) , R . string . unable_to_move_item , Toast . LENGTH_SHORT )
. show ( ) ;
}
} , callbackHandler ) ;
}
@Override
public void onError ( int errorCode , String description ) {
rollbackSwappedItems ( originalPosition , finalPosition ) ;
notifyDataSetChanged ( ) ;
if ( ! isAdded ( ) ) return ;
// Got an error, show toast
Toast . makeText ( getActivity ( ) , R . string . unable_to_move_item , Toast . LENGTH_SHORT )
. show ( ) ;
}
} , callbackHandler ) ;
}
2015-01-14 12:12:47 +01:00
@Override
public View getView ( int position , View convertView , ViewGroup parent ) {
ViewHolder viewHolder ;
if ( convertView = = null ) {
convertView = LayoutInflater . from ( getActivity ( ) )
2016-01-11 10:32:07 +01:00
. inflate ( R . layout . grid_item_playlist , parent , false ) ;
2015-01-14 12:12:47 +01:00
// ViewHolder pattern
viewHolder = new ViewHolder ( ) ;
2019-07-10 20:19:51 +02:00
viewHolder . art = convertView . findViewById ( R . id . art ) ;
viewHolder . title = convertView . findViewById ( R . id . playlist_item_title ) ;
viewHolder . details = convertView . findViewById ( R . id . details ) ;
viewHolder . contextMenu = convertView . findViewById ( R . id . list_context_menu ) ;
viewHolder . duration = convertView . findViewById ( R . id . duration ) ;
viewHolder . card = convertView . findViewById ( R . id . card ) ;
2015-01-14 12:12:47 +01:00
convertView . setTag ( viewHolder ) ;
} else {
2015-06-23 19:43:57 +02:00
viewHolder = ( ViewHolder ) convertView . getTag ( ) ;
2015-01-14 12:12:47 +01:00
}
final ListType . ItemsAll item = this . getItem ( position ) ;
// Differentiate between media
String title , details , artUrl ;
int duration ;
2015-02-16 12:36:21 +01:00
switch ( item . type ) {
case ListType . ItemsAll . TYPE_MOVIE :
2017-01-27 16:33:16 +01:00
title = TextUtils . isEmpty ( item . title ) ? item . label : item . title ;
2015-02-16 12:36:21 +01:00
details = item . tagline ;
artUrl = item . thumbnail ;
duration = item . runtime ;
break ;
case ListType . ItemsAll . TYPE_EPISODE :
2017-01-27 16:33:16 +01:00
title = TextUtils . isEmpty ( item . title ) ? item . label : item . title ;
2015-02-16 12:36:21 +01:00
String season = String . format ( getString ( R . string . season_episode_abbrev ) , item . season , item . episode ) ;
details = String . format ( " %s | %s " , item . showtitle , season ) ;
artUrl = item . art . poster ;
duration = item . runtime ;
break ;
case ListType . ItemsAll . TYPE_SONG :
2017-01-27 16:33:16 +01:00
title = TextUtils . isEmpty ( item . title ) ? item . label : item . title ;
2015-02-16 12:36:21 +01:00
details = item . displayartist + " | " + item . album ;
artUrl = item . thumbnail ;
duration = item . duration ;
break ;
case ListType . ItemsAll . TYPE_MUSIC_VIDEO :
2017-01-27 16:33:16 +01:00
title = TextUtils . isEmpty ( item . title ) ? item . label : item . title ;
2015-02-16 12:36:21 +01:00
details = Utils . listStringConcat ( item . artist , " , " ) + " | " + item . album ;
artUrl = item . thumbnail ;
duration = item . runtime ;
break ;
2019-03-30 12:08:58 +01:00
case ListType . ItemsAll . TYPE_PICTURE :
title = TextUtils . isEmpty ( item . label ) ? item . file : item . label ;
details = item . type ;
artUrl = item . thumbnail ;
duration = 0 ;
break ;
2015-02-16 12:36:21 +01:00
default :
// Don't yet recognize this type
2015-04-08 23:28:19 +02:00
title = TextUtils . isEmpty ( item . label ) ? item . file : item . label ;
2015-02-16 12:36:21 +01:00
details = item . type ;
artUrl = item . thumbnail ;
duration = item . runtime ;
break ;
2015-01-14 12:12:47 +01:00
}
2019-10-07 16:14:48 +02:00
viewHolder . title . setText ( UIUtils . applyMarkup ( getContext ( ) , title ) ) ;
2015-01-14 12:12:47 +01:00
viewHolder . details . setText ( details ) ;
viewHolder . duration . setText ( ( duration > 0 ) ? UIUtils . formatTime ( duration ) : " " ) ;
viewHolder . position = position ;
2016-01-11 10:32:07 +01:00
int cardColor = ( position = = playlistListView . getCheckedItemPosition ( ) ) ?
selectedCardBackgroundColor : cardBackgroundColor ;
2015-06-23 19:43:57 +02:00
viewHolder . card . setCardBackgroundColor ( cardColor ) ;
2015-02-16 12:36:21 +01:00
// If not video, change aspect ration of poster to a square
boolean isVideo = ( item . type . equals ( ListType . ItemsAll . TYPE_MOVIE ) ) | |
2016-01-11 10:32:07 +01:00
( item . type . equals ( ListType . ItemsAll . TYPE_EPISODE ) ) ;
2015-02-16 12:36:21 +01:00
if ( ! isVideo ) {
ViewGroup . LayoutParams layoutParams = viewHolder . art . getLayoutParams ( ) ;
layoutParams . width = layoutParams . height ;
viewHolder . art . setLayoutParams ( layoutParams ) ;
artWidth = artHeight ;
}
2015-01-24 12:14:40 +01:00
UIUtils . loadImageWithCharacterAvatar ( getActivity ( ) , hostManager ,
2015-06-23 19:43:57 +02:00
artUrl , title ,
viewHolder . art , artWidth , artHeight ) ;
2015-01-14 12:12:47 +01:00
2019-03-30 12:08:58 +01:00
if ( ! item . type . contentEquals ( ListType . ItemsAll . TYPE_PICTURE ) ) {
// For the popupmenu
viewHolder . contextMenu . setVisibility ( View . VISIBLE ) ;
viewHolder . contextMenu . setTag ( position ) ;
viewHolder . contextMenu . setOnClickListener ( playlistItemMenuClickListener ) ;
} else {
viewHolder . contextMenu . setVisibility ( View . INVISIBLE ) ;
}
2015-01-14 12:12:47 +01:00
return convertView ;
}
2016-01-11 10:32:07 +01:00
private PlaylistType . Item createPlaylistTypeItem ( ListType . ItemsAll item ) {
PlaylistType . Item playlistItem = new PlaylistType . Item ( ) ;
switch ( item . type ) {
case ListType . ItemsAll . TYPE_MOVIE :
playlistItem . movieid = item . id ;
break ;
case ListType . ItemsAll . TYPE_EPISODE :
playlistItem . episodeid = item . id ;
break ;
case ListType . ItemsAll . TYPE_SONG :
playlistItem . songid = item . id ;
break ;
case ListType . ItemsAll . TYPE_MUSIC_VIDEO :
playlistItem . musicvideoid = item . id ;
break ;
default :
LogUtils . LOGE ( TAG , " createPlaylistTypeItem, failed to create item for " + item . type ) ;
break ;
}
return playlistItem ;
}
private void rollbackSwappedItems ( int originalPosition , int newPosition ) {
if ( originalPosition > newPosition ) {
for ( int i = newPosition ; i < originalPosition ; i + + ) {
onSwapItems ( i , i + 1 ) ;
}
} else if ( originalPosition < newPosition ) {
for ( int i = newPosition ; i > originalPosition ; i - - ) {
onSwapItems ( i , i - 1 ) ;
}
}
}
2015-01-14 12:12:47 +01:00
private class ViewHolder {
ImageView art ;
TextView title ;
TextView details ;
ImageView contextMenu ;
TextView duration ;
2015-06-23 19:43:57 +02:00
CardView card ;
2015-01-14 12:12:47 +01:00
int position ;
}
}
2019-03-30 12:08:58 +01:00
private static class PlaylistHolder {
private GetPlaylist . GetPlaylistResult getPlaylistResult ;
private int top ;
private int index ;
private PlaylistHolder ( ) { }
public void setPlaylist ( GetPlaylist . GetPlaylistResult getPlaylistResult ) {
this . getPlaylistResult = getPlaylistResult ;
}
public GetPlaylist . GetPlaylistResult getPlaylist ( ) {
return getPlaylistResult ;
}
void setListViewPosition ( int index , int top ) {
this . index = index ;
this . top = top ;
}
public int getTop ( ) {
return top ;
}
public int getIndex ( ) {
return index ;
}
public int getPlaylistId ( ) { return getPlaylistResult . id ; }
}
2015-01-14 12:12:47 +01:00
}