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
|
|
|
|
2017-12-25 13:57:26 +01:00
|
|
|
import android.annotation.TargetApi;
|
2015-01-14 12:12:47 +01:00
|
|
|
import android.content.Intent;
|
|
|
|
import android.graphics.Point;
|
2015-04-08 23:28:19 +02:00
|
|
|
import android.net.Uri;
|
2017-12-25 13:57:26 +01:00
|
|
|
import android.os.Build;
|
2015-01-14 12:12:47 +01:00
|
|
|
import android.os.Bundle;
|
2020-04-15 22:46:06 +02:00
|
|
|
|
|
|
|
import androidx.appcompat.app.ActionBar;
|
|
|
|
import androidx.appcompat.widget.Toolbar;
|
|
|
|
import androidx.core.text.TextDirectionHeuristicsCompat;
|
|
|
|
import androidx.drawerlayout.widget.DrawerLayout;
|
|
|
|
import androidx.preference.PreferenceManager;
|
|
|
|
import androidx.viewpager.widget.ViewPager;
|
|
|
|
|
2015-01-14 12:12:47 +01:00
|
|
|
import android.text.TextUtils;
|
2015-02-14 06:26:19 +01:00
|
|
|
import android.view.KeyEvent;
|
2015-01-14 12:12:47 +01:00
|
|
|
import android.view.Menu;
|
|
|
|
import android.view.MenuItem;
|
|
|
|
import android.view.ViewTreeObserver;
|
2016-02-19 22:20:18 +01:00
|
|
|
import android.view.WindowManager;
|
2015-01-14 12:12:47 +01:00
|
|
|
import android.widget.ImageView;
|
|
|
|
import android.widget.Toast;
|
|
|
|
|
2015-03-09 22:35:18 +01:00
|
|
|
import org.xbmc.kore.R;
|
|
|
|
import org.xbmc.kore.Settings;
|
|
|
|
import org.xbmc.kore.host.HostConnectionObserver;
|
2018-09-14 17:47:51 +02:00
|
|
|
import org.xbmc.kore.host.HostInfo;
|
2015-03-09 22:35:18 +01:00
|
|
|
import org.xbmc.kore.host.HostManager;
|
|
|
|
import org.xbmc.kore.jsonrpc.method.Application;
|
|
|
|
import org.xbmc.kore.jsonrpc.method.AudioLibrary;
|
2015-03-20 00:00:31 +01:00
|
|
|
import org.xbmc.kore.jsonrpc.method.GUI;
|
2015-03-09 22:35:18 +01:00
|
|
|
import org.xbmc.kore.jsonrpc.method.Input;
|
|
|
|
import org.xbmc.kore.jsonrpc.method.System;
|
|
|
|
import org.xbmc.kore.jsonrpc.method.VideoLibrary;
|
|
|
|
import org.xbmc.kore.jsonrpc.type.ListType;
|
|
|
|
import org.xbmc.kore.jsonrpc.type.PlayerType;
|
2019-12-16 19:37:46 +01:00
|
|
|
import org.xbmc.kore.jsonrpc.type.PlaylistType;
|
2016-05-20 20:24:34 +02:00
|
|
|
import org.xbmc.kore.service.ConnectionObserversManagerService;
|
2016-12-20 21:47:24 +01:00
|
|
|
import org.xbmc.kore.ui.BaseActivity;
|
|
|
|
import org.xbmc.kore.ui.generic.NavigationDrawerFragment;
|
|
|
|
import org.xbmc.kore.ui.generic.SendTextDialogFragment;
|
2018-04-05 19:22:20 +02:00
|
|
|
import org.xbmc.kore.ui.generic.VolumeControllerDialogFragmentListener;
|
2016-12-20 21:47:24 +01:00
|
|
|
import org.xbmc.kore.ui.sections.hosts.AddHostActivity;
|
2019-12-16 19:37:46 +01:00
|
|
|
import org.xbmc.kore.ui.sections.localfile.HttpApp;
|
2015-03-09 22:35:18 +01:00
|
|
|
import org.xbmc.kore.ui.views.CirclePageIndicator;
|
|
|
|
import org.xbmc.kore.utils.LogUtils;
|
|
|
|
import org.xbmc.kore.utils.TabsAdapter;
|
|
|
|
import org.xbmc.kore.utils.UIUtils;
|
2017-12-24 18:49:40 +01:00
|
|
|
import org.xbmc.kore.utils.Utils;
|
2020-05-25 20:38:50 +02:00
|
|
|
import org.xbmc.kore.utils.PluginUrlUtils;
|
2015-04-08 23:28:19 +02:00
|
|
|
|
2019-12-16 19:37:46 +01:00
|
|
|
import java.io.IOException;
|
2015-04-08 23:28:19 +02:00
|
|
|
import java.net.MalformedURLException;
|
|
|
|
import java.net.URL;
|
2017-01-30 20:06:14 +01:00
|
|
|
import java.net.URLEncoder;
|
2018-04-17 20:55:26 +02:00
|
|
|
import java.util.concurrent.Callable;
|
2018-01-26 15:03:30 +01:00
|
|
|
import java.util.concurrent.ExecutionException;
|
2018-04-17 20:55:26 +02:00
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
|
import java.util.concurrent.Executors;
|
2018-01-26 15:03:30 +01:00
|
|
|
import java.util.concurrent.Future;
|
2015-04-08 23:28:19 +02:00
|
|
|
import java.util.regex.Matcher;
|
|
|
|
import java.util.regex.Pattern;
|
2015-01-14 12:12:47 +01:00
|
|
|
|
2018-04-05 19:22:20 +02:00
|
|
|
import butterknife.BindView;
|
2015-01-14 12:12:47 +01:00
|
|
|
import butterknife.ButterKnife;
|
|
|
|
|
2015-02-11 00:17:16 +01:00
|
|
|
public class RemoteActivity extends BaseActivity
|
2015-01-14 12:12:47 +01:00
|
|
|
implements HostConnectionObserver.PlayerEventsObserver,
|
|
|
|
NowPlayingFragment.NowPlayingListener,
|
2018-01-30 10:13:21 +01:00
|
|
|
SendTextDialogFragment.SendTextDialogListener {
|
2017-10-08 20:48:00 +02:00
|
|
|
private static final String TAG = LogUtils.makeLogTag(RemoteActivity.class);
|
2015-01-14 12:12:47 +01:00
|
|
|
|
2016-03-24 15:26:34 +01:00
|
|
|
|
|
|
|
private static final int NOWPLAYING_FRAGMENT_ID = 1;
|
|
|
|
private static final int REMOTE_FRAGMENT_ID = 2;
|
|
|
|
private static final int PLAYLIST_FRAGMENT_ID = 3;
|
|
|
|
|
2015-01-14 12:12:47 +01:00
|
|
|
/**
|
|
|
|
* Host manager singleton
|
|
|
|
*/
|
|
|
|
private HostManager hostManager = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* To register for observing host events
|
|
|
|
*/
|
|
|
|
private HostConnectionObserver hostConnectionObserver;
|
|
|
|
|
|
|
|
private NavigationDrawerFragment navigationDrawerFragment;
|
|
|
|
|
2018-01-26 15:03:30 +01:00
|
|
|
private Future<Boolean> pendingShare;
|
|
|
|
|
|
|
|
private Future<Void> awaitingShare;
|
|
|
|
|
2018-04-05 19:22:20 +02:00
|
|
|
@BindView(R.id.background_image) ImageView backgroundImage;
|
|
|
|
@BindView(R.id.pager_indicator) CirclePageIndicator pageIndicator;
|
|
|
|
@BindView(R.id.pager) ViewPager viewPager;
|
|
|
|
@BindView(R.id.default_toolbar) Toolbar toolbar;
|
2015-01-14 12:12:47 +01:00
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
|
|
super.onCreate(savedInstanceState);
|
|
|
|
|
|
|
|
// Set default values for the preferences
|
|
|
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
|
|
|
|
|
|
|
setContentView(R.layout.activity_remote);
|
2018-04-05 19:22:20 +02:00
|
|
|
ButterKnife.bind(this);
|
2015-01-14 12:12:47 +01:00
|
|
|
|
|
|
|
hostManager = HostManager.getInstance(this);
|
|
|
|
|
|
|
|
// Check if we have any hosts setup
|
|
|
|
if (hostManager.getHostInfo() == null) {
|
|
|
|
final Intent intent = new Intent(this, AddHostActivity.class);
|
|
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
|
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
|
|
startActivity(intent);
|
|
|
|
finish();
|
2016-11-16 13:10:20 +01:00
|
|
|
return;
|
2015-01-14 12:12:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set up the drawer.
|
|
|
|
navigationDrawerFragment = (NavigationDrawerFragment) getSupportFragmentManager()
|
|
|
|
.findFragmentById(R.id.navigation_drawer);
|
|
|
|
navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout));
|
|
|
|
|
|
|
|
// Set up pager and fragments
|
|
|
|
TabsAdapter tabsAdapter = new TabsAdapter(this, getSupportFragmentManager())
|
2016-03-24 15:26:34 +01:00
|
|
|
.addTab(NowPlayingFragment.class, null, R.string.now_playing, NOWPLAYING_FRAGMENT_ID)
|
|
|
|
.addTab(RemoteFragment.class, null, R.string.remote, REMOTE_FRAGMENT_ID)
|
|
|
|
.addTab(PlaylistFragment.class, null, R.string.playlist, PLAYLIST_FRAGMENT_ID);
|
2015-01-14 12:12:47 +01:00
|
|
|
|
|
|
|
viewPager.setAdapter(tabsAdapter);
|
|
|
|
pageIndicator.setViewPager(viewPager);
|
|
|
|
pageIndicator.setOnPageChangeListener(defaultOnPageChangeListener);
|
|
|
|
|
|
|
|
viewPager.setCurrentItem(1);
|
|
|
|
viewPager.setOffscreenPageLimit(2);
|
|
|
|
|
|
|
|
setupActionBar();
|
|
|
|
|
2016-11-15 20:09:23 +01:00
|
|
|
// Periodic Check of Kodi version
|
2019-02-08 18:25:21 +01:00
|
|
|
hostManager.checkAndUpdateKodiVersion();
|
2016-11-15 20:09:23 +01:00
|
|
|
|
2015-04-08 23:28:19 +02:00
|
|
|
// If we should start playing something
|
|
|
|
|
2015-01-14 12:12:47 +01:00
|
|
|
// // Setup system bars and content padding
|
|
|
|
// setupSystemBarsColors();
|
|
|
|
// // Set the padding of views.
|
|
|
|
// // Only set top and right, to allow bottom to overlap in each fragment
|
|
|
|
// UIUtils.setPaddingForSystemBars(this, viewPager, true, true, false);
|
|
|
|
// UIUtils.setPaddingForSystemBars(this, pageIndicator, true, true, false);
|
2018-01-26 15:03:30 +01:00
|
|
|
|
|
|
|
//noinspection unchecked
|
|
|
|
pendingShare = (Future<Boolean>) getLastCustomNonConfigurationInstance();
|
2015-01-14 12:12:47 +01:00
|
|
|
}
|
|
|
|
|
2015-05-21 21:08:05 +02:00
|
|
|
@Override
|
|
|
|
public void onStart() {
|
|
|
|
super.onStart();
|
|
|
|
handleStartIntent(getIntent());
|
|
|
|
}
|
|
|
|
|
2015-01-14 12:12:47 +01:00
|
|
|
@Override
|
|
|
|
public void onResume() {
|
|
|
|
super.onResume();
|
|
|
|
hostConnectionObserver = hostManager.getHostConnectionObserver();
|
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);
|
|
|
|
// Force a refresh, specifically to update the time elapsed on the fragments
|
|
|
|
hostConnectionObserver.refreshWhatsPlaying();
|
|
|
|
hostConnectionObserver.refreshPlaylists();
|
2015-12-15 20:08:19 +01:00
|
|
|
|
2016-02-19 22:20:18 +01:00
|
|
|
// Check whether we should keep the remote activity above the lockscreen
|
|
|
|
boolean keepAboveLockscreen = PreferenceManager
|
|
|
|
.getDefaultSharedPreferences(this)
|
|
|
|
.getBoolean(Settings.KEY_PREF_KEEP_REMOTE_ABOVE_LOCKSCREEN,
|
|
|
|
Settings.DEFAULT_KEY_PREF_KEEP_REMOTE_ABOVE_LOCKSCREEN);
|
|
|
|
if (keepAboveLockscreen) {
|
|
|
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
|
|
|
} else {
|
|
|
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
|
|
|
}
|
|
|
|
|
2016-09-07 20:33:58 +02:00
|
|
|
// Check whether we should keep the screen on
|
|
|
|
boolean keepScreenOn = PreferenceManager
|
|
|
|
.getDefaultSharedPreferences(this)
|
|
|
|
.getBoolean(Settings.KEY_PREF_KEEP_SCREEN_ON,
|
|
|
|
Settings.DEFAULT_KEY_PREF_KEEP_SCREEN_ON);
|
|
|
|
if (keepScreenOn) {
|
|
|
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
|
|
} else {
|
|
|
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
|
|
}
|
2015-01-14 12:12:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPause() {
|
|
|
|
super.onPause();
|
2015-12-02 20:39:18 +01:00
|
|
|
if (hostConnectionObserver != null) hostConnectionObserver.unregisterPlayerObserver(this);
|
2015-01-14 12:12:47 +01:00
|
|
|
hostConnectionObserver = null;
|
2018-01-26 15:03:30 +01:00
|
|
|
if (awaitingShare != null) awaitingShare.cancel(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Object onRetainCustomNonConfigurationInstance() {
|
|
|
|
return pendingShare;
|
2015-01-14 12:12:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onCreateOptionsMenu(Menu menu) {
|
2017-10-08 20:48:00 +02:00
|
|
|
if (!navigationDrawerFragment.isDrawerOpen()) {
|
|
|
|
// Only show items in the action bar relevant to this screen if the drawer is not showing.
|
|
|
|
// Otherwise, let the drawer decide what to show in the action bar.
|
2015-01-14 12:12:47 +01:00
|
|
|
getMenuInflater().inflate(R.menu.remote, menu);
|
2017-10-08 20:48:00 +02:00
|
|
|
}
|
2015-01-14 12:12:47 +01:00
|
|
|
return super.onCreateOptionsMenu(menu);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
|
|
// Handle action bar item clicks here.
|
2017-10-08 20:48:00 +02:00
|
|
|
switch (item.getItemId()) {
|
2015-01-14 12:12:47 +01:00
|
|
|
case R.id.action_wake_up:
|
|
|
|
UIUtils.sendWolAsync(this, hostManager.getHostInfo());
|
|
|
|
return true;
|
|
|
|
case R.id.action_quit:
|
|
|
|
Application.Quit actionQuit = new Application.Quit();
|
|
|
|
// Fire and forget
|
|
|
|
actionQuit.execute(hostManager.getConnection(), null, null);
|
|
|
|
return true;
|
|
|
|
case R.id.action_suspend:
|
|
|
|
System.Suspend actionSuspend = new System.Suspend();
|
|
|
|
// Fire and forget
|
|
|
|
actionSuspend.execute(hostManager.getConnection(), null, null);
|
|
|
|
return true;
|
2015-03-29 13:48:51 +02:00
|
|
|
case R.id.action_reboot:
|
|
|
|
System.Reboot actionReboot = new System.Reboot();
|
|
|
|
// Fire and forget
|
|
|
|
actionReboot.execute(hostManager.getConnection(), null, null);
|
|
|
|
return true;
|
2015-01-14 12:12:47 +01:00
|
|
|
case R.id.action_shutdown:
|
|
|
|
System.Shutdown actionShutdown = new System.Shutdown();
|
|
|
|
// Fire and forget
|
|
|
|
actionShutdown.execute(hostManager.getConnection(), null, null);
|
|
|
|
return true;
|
|
|
|
case R.id.send_text:
|
|
|
|
SendTextDialogFragment dialog =
|
|
|
|
SendTextDialogFragment.newInstance(getString(R.string.send_text));
|
|
|
|
dialog.show(getSupportFragmentManager(), null);
|
2015-01-23 23:23:59 +01:00
|
|
|
return true;
|
2015-01-25 15:17:22 +01:00
|
|
|
case R.id.toggle_fullscreen:
|
2015-03-20 00:00:31 +01:00
|
|
|
GUI.SetFullscreen actionSetFullscreen = new GUI.SetFullscreen();
|
|
|
|
// Input.ExecuteAction actionSetFullscreen = new Input.ExecuteAction(Input.ExecuteAction.TOGGLEFULLSCREEN);
|
2015-01-25 15:17:22 +01:00
|
|
|
actionSetFullscreen.execute(hostManager.getConnection(), null, null);
|
|
|
|
return true;
|
2015-01-23 23:23:59 +01:00
|
|
|
case R.id.clean_video_library:
|
|
|
|
VideoLibrary.Clean actionCleanVideo = new VideoLibrary.Clean();
|
|
|
|
actionCleanVideo.execute(hostManager.getConnection(), null, null);
|
|
|
|
return true;
|
|
|
|
case R.id.clean_audio_library:
|
|
|
|
AudioLibrary.Clean actionCleanAudio = new AudioLibrary.Clean();
|
|
|
|
actionCleanAudio.execute(hostManager.getConnection(), null, null);
|
|
|
|
return true;
|
|
|
|
case R.id.update_video_library:
|
|
|
|
VideoLibrary.Scan actionScanVideo = new VideoLibrary.Scan();
|
|
|
|
actionScanVideo.execute(hostManager.getConnection(), null, null);
|
|
|
|
return true;
|
|
|
|
case R.id.update_audio_library:
|
|
|
|
AudioLibrary.Scan actionScanAudio = new AudioLibrary.Scan();
|
|
|
|
actionScanAudio.execute(hostManager.getConnection(), null, null);
|
2015-01-14 12:12:47 +01:00
|
|
|
return true;
|
2017-10-08 20:48:00 +02:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2015-01-14 12:12:47 +01:00
|
|
|
|
2017-10-08 20:48:00 +02:00
|
|
|
return super.onOptionsItemSelected(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Override hardware volume keys and send to Kodi
|
|
|
|
*/
|
|
|
|
@Override
|
|
|
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
2018-07-20 13:37:25 +02:00
|
|
|
boolean handled = false;
|
|
|
|
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
|
|
handled = VolumeControllerDialogFragmentListener.handleVolumeKeyEvent(this, event);
|
|
|
|
|
|
|
|
// Show volume change dialog if the event was handled and we are not in
|
|
|
|
// first page, which already contains a volume control
|
|
|
|
if (handled && (viewPager.getCurrentItem() != 0)) {
|
|
|
|
new VolumeControllerDialogFragmentListener()
|
|
|
|
.show(getSupportFragmentManager(), VolumeControllerDialogFragmentListener.class.getName());
|
|
|
|
}
|
2018-01-30 10:13:21 +01:00
|
|
|
}
|
|
|
|
return handled || super.dispatchKeyEvent(event);
|
2015-01-14 12:12:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callbacks from Send text dialog
|
|
|
|
*/
|
|
|
|
public void onSendTextFinished(String text, boolean done) {
|
2016-01-25 00:55:34 +01:00
|
|
|
if (TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR.isRtl(text, 0, text.length())) {
|
|
|
|
text = new StringBuilder(text).reverse().toString();
|
|
|
|
}
|
2015-01-14 12:12:47 +01:00
|
|
|
Input.SendText action = new Input.SendText(text, done);
|
|
|
|
action.execute(hostManager.getConnection(), null, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void onSendTextCancel() {
|
|
|
|
// Nothing to do
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void setupActionBar() {
|
2016-03-24 15:26:34 +01:00
|
|
|
setToolbarTitle(toolbar, NOWPLAYING_FRAGMENT_ID);
|
2015-01-14 12:12:47 +01:00
|
|
|
setSupportActionBar(toolbar);
|
2015-06-23 20:08:45 +02:00
|
|
|
|
|
|
|
ActionBar actionBar = getSupportActionBar();
|
|
|
|
if (actionBar == null) return;
|
|
|
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
2015-01-14 12:12:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private void setToolbarTitle(Toolbar toolbar, int position) {
|
|
|
|
if (toolbar != null) {
|
|
|
|
switch (position) {
|
|
|
|
case 0:
|
|
|
|
toolbar.setTitle(R.string.now_playing);
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
toolbar.setTitle(R.string.remote);
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
toolbar.setTitle(R.string.playlist);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-17 20:55:26 +02:00
|
|
|
/**
|
|
|
|
* Provides the thread where the intent will be handled
|
|
|
|
*/
|
|
|
|
private static ExecutorService SHARE_EXECUTOR = null;
|
|
|
|
private static ExecutorService getShareExecutor() {
|
|
|
|
if (SHARE_EXECUTOR == null) {
|
|
|
|
SHARE_EXECUTOR = Executors.newSingleThreadExecutor();
|
|
|
|
}
|
|
|
|
return SHARE_EXECUTOR;
|
|
|
|
}
|
|
|
|
|
2015-04-08 23:28:19 +02:00
|
|
|
/**
|
|
|
|
* Handles the intent that started this activity, namely to start playing something on Kodi
|
|
|
|
* @param intent Start intent for the activity
|
|
|
|
*/
|
2019-12-16 19:37:46 +01:00
|
|
|
protected void handleStartIntent(Intent intent) {
|
|
|
|
handleStartIntent(intent, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected void handleStartIntent(Intent intent, boolean queue) {
|
2018-01-26 15:03:30 +01:00
|
|
|
if (pendingShare != null) {
|
2019-12-16 19:37:46 +01:00
|
|
|
awaitShare(queue);
|
2018-01-26 15:03:30 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-04-08 23:28:19 +02:00
|
|
|
final String action = intent.getAction();
|
|
|
|
// Check action
|
2015-04-18 19:23:31 +02:00
|
|
|
if ((action == null) ||
|
|
|
|
!(action.equals(Intent.ACTION_SEND) || action.equals(Intent.ACTION_VIEW)))
|
|
|
|
return;
|
|
|
|
|
2017-10-17 20:56:19 +02:00
|
|
|
Uri videoUri;
|
2015-04-18 19:23:31 +02:00
|
|
|
if (action.equals(Intent.ACTION_SEND)) {
|
|
|
|
// Get the URI, which is stored in Extras
|
2016-01-08 17:54:59 +01:00
|
|
|
videoUri = getYouTubeUri(intent.getStringExtra(Intent.EXTRA_TEXT));
|
2017-10-17 20:56:19 +02:00
|
|
|
} else {
|
|
|
|
videoUri = intent.getData();
|
2015-04-18 19:23:31 +02:00
|
|
|
}
|
2015-04-08 23:28:19 +02:00
|
|
|
|
2020-03-22 17:57:49 +01:00
|
|
|
if (videoUri == null) {
|
2020-04-26 11:56:26 +02:00
|
|
|
// Check if `intent` contains a URL or a link to a local file:
|
|
|
|
videoUri = getShareLocalUriOrHiddenUri(intent);
|
2020-03-22 17:57:49 +01:00
|
|
|
}
|
|
|
|
|
2020-04-26 11:56:26 +02:00
|
|
|
String url = toPluginUrl(videoUri);
|
2019-12-16 19:37:46 +01:00
|
|
|
|
|
|
|
if (url == null) {
|
|
|
|
url = videoUri.toString();
|
2016-10-18 20:06:41 +02:00
|
|
|
}
|
|
|
|
|
2018-09-14 17:47:51 +02:00
|
|
|
// If a host was passed from the intent use it
|
|
|
|
int hostId = intent.getIntExtra("hostId", 0);
|
|
|
|
if (hostId > 0) {
|
|
|
|
HostManager hostManager = HostManager.getInstance(this);
|
|
|
|
for (HostInfo host : hostManager.getHosts()) {
|
|
|
|
if (host.getId() == hostId) {
|
|
|
|
hostManager.switchHost(host);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-16 19:37:46 +01:00
|
|
|
// Determine which playlist to use
|
|
|
|
String intentType = intent.getType();
|
|
|
|
int playlistType;
|
|
|
|
if (intentType == null) {
|
|
|
|
playlistType = PlaylistType.VIDEO_PLAYLISTID;
|
|
|
|
} else {
|
|
|
|
if (intentType.matches("audio.*")) {
|
|
|
|
playlistType = PlaylistType.MUSIC_PLAYLISTID;
|
|
|
|
} else if (intentType.matches("video.*")) {
|
|
|
|
playlistType = PlaylistType.VIDEO_PLAYLISTID;
|
|
|
|
} else if (intentType.matches("image.*")) {
|
|
|
|
playlistType = PlaylistType.PICTURE_PLAYLISTID;
|
|
|
|
} else {
|
|
|
|
// Generic links? Default to video:
|
|
|
|
playlistType = PlaylistType.VIDEO_PLAYLISTID;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-26 15:03:30 +01:00
|
|
|
String title = getString(R.string.app_name);
|
|
|
|
String text = getString(R.string.item_added_to_playlist);
|
2019-12-16 19:37:46 +01:00
|
|
|
pendingShare = getShareExecutor().submit(
|
|
|
|
new OpenSharedUrl(hostManager.getConnection(), url, title, text, queue, playlistType));
|
|
|
|
|
|
|
|
awaitShare(queue);
|
2015-10-25 21:56:42 +01:00
|
|
|
intent.setAction(null);
|
2019-12-16 19:37:46 +01:00
|
|
|
|
|
|
|
// Don't display Kore after sharing content from another app:
|
|
|
|
finish();
|
|
|
|
}
|
|
|
|
|
2020-04-26 11:56:26 +02:00
|
|
|
private Uri getUrlInsideIntent(Intent intent) {
|
|
|
|
// Some apps hide the link in the clip, try to detect any link by casting the intent
|
|
|
|
// to string a looking with a regular expression:
|
|
|
|
|
|
|
|
Matcher matcher = Pattern.compile("https?://[^\\s]+").matcher(intent.toString());
|
|
|
|
String matchedString = null;
|
|
|
|
if (matcher.find()) {
|
|
|
|
matchedString = matcher.group(0);
|
|
|
|
if (matchedString.endsWith("}")) {
|
|
|
|
matchedString = matchedString.substring(0, matchedString.length() - 1);
|
|
|
|
}
|
|
|
|
return Uri.parse(matchedString);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private Uri getShareLocalUriOrHiddenUri(Intent intent) {
|
2019-12-16 19:37:46 +01:00
|
|
|
Uri contentUri = intent.getData();
|
|
|
|
|
|
|
|
if (contentUri == null) {
|
|
|
|
Bundle bundle = intent.getExtras();
|
|
|
|
contentUri = (Uri) bundle.get(Intent.EXTRA_STREAM);
|
|
|
|
}
|
2020-04-26 11:56:26 +02:00
|
|
|
if (contentUri == null) {
|
|
|
|
return getUrlInsideIntent(intent);
|
|
|
|
}
|
2019-12-16 19:37:46 +01:00
|
|
|
if (contentUri == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
HttpApp http_app = null;
|
|
|
|
try {
|
|
|
|
http_app = HttpApp.getInstance(getApplicationContext(), 8080);
|
|
|
|
} catch (IOException ioe) {
|
|
|
|
Toast.makeText(getApplicationContext(),
|
|
|
|
getString(R.string.error_starting_http_server),
|
|
|
|
Toast.LENGTH_LONG).show();
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
http_app.addUri(contentUri);
|
|
|
|
String url = http_app.getLinkToFile();
|
|
|
|
|
2020-04-26 11:56:26 +02:00
|
|
|
return Uri.parse(url);
|
2015-05-27 20:37:22 +02:00
|
|
|
}
|
|
|
|
|
2015-04-10 20:51:44 +02:00
|
|
|
/**
|
2018-01-26 15:03:30 +01:00
|
|
|
* Awaits the completion of the share request in the same background thread
|
|
|
|
* where the request is running.
|
|
|
|
* <p>
|
|
|
|
* This needs to run stuff in the UI thread so the activity reference is
|
|
|
|
* inevitable, but unlike the share request this doesn't need to outlive the
|
|
|
|
* activity. The resulting future __must__ be cancelled when the activity is
|
|
|
|
* paused (it will drop itself when cancelled or finished). This should be called
|
|
|
|
* again when the activity is resumed and a {@link #pendingShare} exists.
|
2015-04-10 20:51:44 +02:00
|
|
|
*/
|
2019-12-16 19:37:46 +01:00
|
|
|
private void awaitShare(final boolean queue) {
|
2018-04-17 20:55:26 +02:00
|
|
|
awaitingShare = getShareExecutor().submit(new Callable<Void>() {
|
2015-04-08 23:28:19 +02:00
|
|
|
@Override
|
2018-04-17 20:55:26 +02:00
|
|
|
public Void call() throws Exception {
|
2018-01-26 15:03:30 +01:00
|
|
|
try {
|
|
|
|
final boolean wasAlreadyPlaying = pendingShare.get();
|
|
|
|
pendingShare = null;
|
|
|
|
runOnUiThread(new Runnable() {
|
2015-05-21 21:08:05 +02:00
|
|
|
@Override
|
2018-01-26 15:03:30 +01:00
|
|
|
public void run() {
|
|
|
|
if (wasAlreadyPlaying) {
|
2019-12-16 19:37:46 +01:00
|
|
|
String msg;
|
|
|
|
if (queue) {
|
|
|
|
msg = getString(R.string.item_added_to_playlist);
|
|
|
|
} else {
|
|
|
|
msg = getString(R.string.item_sent_to_kodi);
|
|
|
|
}
|
2018-01-26 15:03:30 +01:00
|
|
|
Toast.makeText(RemoteActivity.this,
|
2019-12-16 19:37:46 +01:00
|
|
|
msg,
|
2018-04-17 20:55:26 +02:00
|
|
|
Toast.LENGTH_SHORT)
|
|
|
|
.show();
|
2018-01-26 15:03:30 +01:00
|
|
|
}
|
2015-05-21 21:08:05 +02:00
|
|
|
}
|
2018-01-26 15:03:30 +01:00
|
|
|
});
|
|
|
|
} catch (InterruptedException ignored) {
|
|
|
|
} catch (ExecutionException ex) {
|
|
|
|
pendingShare = null;
|
|
|
|
final OpenSharedUrl.Error e = (OpenSharedUrl.Error) ex.getCause();
|
|
|
|
LogUtils.LOGE(TAG, "Share failed", e);
|
|
|
|
runOnUiThread(new Runnable() {
|
2015-05-21 21:08:05 +02:00
|
|
|
@Override
|
2018-01-26 15:03:30 +01:00
|
|
|
public void run() {
|
2015-05-21 21:08:05 +02:00
|
|
|
Toast.makeText(RemoteActivity.this,
|
2018-04-17 20:55:26 +02:00
|
|
|
getString(e.stage, e.getMessage()),
|
|
|
|
Toast.LENGTH_SHORT).show();
|
2015-05-21 21:08:05 +02:00
|
|
|
}
|
2018-01-26 15:03:30 +01:00
|
|
|
});
|
|
|
|
} finally {
|
|
|
|
awaitingShare = null;
|
2015-05-21 21:08:05 +02:00
|
|
|
}
|
2018-01-26 15:03:30 +01:00
|
|
|
return null;
|
2015-04-08 23:28:19 +02:00
|
|
|
}
|
2018-01-26 15:03:30 +01:00
|
|
|
});
|
2015-04-08 23:28:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the YouTube Uri that the YouTube app passes in EXTRA_TEXT
|
|
|
|
* YouTube sends something like: [Video title]: [YouTube URL] so we need
|
|
|
|
* to get the second part
|
|
|
|
*
|
|
|
|
* @param extraText EXTRA_TEXT passed in the intent
|
|
|
|
* @return Uri present in extraText if present
|
|
|
|
*/
|
|
|
|
private Uri getYouTubeUri(String extraText) {
|
|
|
|
if (extraText == null) return null;
|
|
|
|
|
|
|
|
for (String word : extraText.split(" ")) {
|
|
|
|
if (word.startsWith("http://") || word.startsWith("https://")) {
|
|
|
|
try {
|
|
|
|
URL validUri = new URL(word);
|
|
|
|
return Uri.parse(word);
|
|
|
|
} catch (MalformedURLException exc) {
|
|
|
|
LogUtils.LOGD(TAG, "Got a malformed URL in an intent: " + word);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-10-17 20:56:19 +02:00
|
|
|
* Converts a video url to a Kodi plugin URL.
|
2015-04-08 23:28:19 +02:00
|
|
|
*
|
2017-10-17 20:56:19 +02:00
|
|
|
* @param playuri some URL
|
|
|
|
* @return plugin URL
|
2015-04-08 23:28:19 +02:00
|
|
|
*/
|
2017-10-17 20:56:19 +02:00
|
|
|
private String toPluginUrl(Uri playuri) {
|
|
|
|
String host = playuri.getHost();
|
2017-10-24 20:24:37 +02:00
|
|
|
if (host.endsWith("youtube.com")) {
|
|
|
|
String videoId = playuri.getQueryParameter("v");
|
|
|
|
String playlistId = playuri.getQueryParameter("list");
|
|
|
|
Uri.Builder pluginUri = new Uri.Builder()
|
|
|
|
.scheme("plugin")
|
|
|
|
.authority("plugin.video.youtube")
|
|
|
|
.path("play/");
|
|
|
|
boolean valid = false;
|
|
|
|
if (videoId != null) {
|
|
|
|
valid = true;
|
|
|
|
pluginUri.appendQueryParameter("video_id", videoId);
|
|
|
|
}
|
|
|
|
if (playlistId != null) {
|
|
|
|
valid = true;
|
|
|
|
pluginUri.appendQueryParameter("playlist_id", playlistId)
|
|
|
|
.appendQueryParameter("order", "default");
|
|
|
|
}
|
|
|
|
if (valid) {
|
|
|
|
return pluginUri.build().toString();
|
|
|
|
}
|
|
|
|
} else if (host.endsWith("youtu.be")) {
|
|
|
|
return "plugin://plugin.video.youtube/play/?video_id="
|
|
|
|
+ playuri.getLastPathSegment();
|
|
|
|
} else if (host.endsWith("vimeo.com")) {
|
|
|
|
String last = playuri.getLastPathSegment();
|
|
|
|
if (last.matches("\\d+")) {
|
|
|
|
return "plugin://plugin.video.vimeo/play/?video_id=" + last;
|
|
|
|
}
|
|
|
|
} else if (host.endsWith("svtplay.se")) {
|
2017-10-17 20:56:19 +02:00
|
|
|
Pattern pattern = Pattern.compile(
|
|
|
|
"^(?:https?:\\/\\/)?(?:www\\.)?svtplay\\.se\\/video\\/(\\d+\\/.*)",
|
|
|
|
Pattern.CASE_INSENSITIVE);
|
|
|
|
Matcher matcher = pattern.matcher(playuri.toString());
|
|
|
|
if (matcher.matches()) {
|
|
|
|
return "plugin://plugin.video.svtplay/?url=%2Fvideo%2F"
|
|
|
|
+ URLEncoder.encode(matcher.group(1)) + "&mode=video";
|
2016-01-23 12:10:55 +01:00
|
|
|
}
|
2019-09-07 13:28:51 +02:00
|
|
|
} else if (host.endsWith("soundcloud.com")) {
|
|
|
|
return "plugin://plugin.audio.soundcloud/play/?url="
|
|
|
|
+ URLEncoder.encode(playuri.toString());
|
2020-03-22 17:57:49 +01:00
|
|
|
} else if (host.startsWith("app.primevideo.com")) {
|
|
|
|
Matcher amazonMatcher = Pattern.compile("gti=([^&]+)").matcher(playuri.toString());
|
|
|
|
if (amazonMatcher.find()) {
|
|
|
|
String gti = amazonMatcher.group(1);
|
|
|
|
return "plugin://plugin.video.amazon-test/?asin=" + gti + "&mode=PlayVideo&adult=0&name=&trailer=0&selbitrate=0";
|
|
|
|
}
|
2020-05-25 20:38:50 +02:00
|
|
|
} else if (PluginUrlUtils.isHostArte(host)) {
|
|
|
|
return PluginUrlUtils.toPluginUrlArte(playuri);
|
2015-04-08 23:28:19 +02:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2015-01-14 12:12:47 +01:00
|
|
|
// Default page change listener, that doesn't scroll images
|
|
|
|
ViewPager.OnPageChangeListener defaultOnPageChangeListener = new ViewPager.OnPageChangeListener() {
|
|
|
|
@Override
|
|
|
|
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPageSelected(int position) {
|
|
|
|
setToolbarTitle(toolbar, position);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPageScrollStateChanged(int state) { }
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets or clear the image background
|
2015-02-15 20:11:32 +01:00
|
|
|
* @param url Image url
|
2015-01-14 12:12:47 +01:00
|
|
|
*/
|
|
|
|
private void setImageViewBackground(String url) {
|
|
|
|
if (url != null) {
|
|
|
|
Point displaySize = new Point();
|
|
|
|
getWindowManager().getDefaultDisplay().getSize(displaySize);
|
|
|
|
|
|
|
|
UIUtils.loadImageIntoImageview(hostManager, url, backgroundImage,
|
|
|
|
displaySize.x, displaySize.y / 2);
|
|
|
|
|
|
|
|
final int pixelsPerPage = displaySize.x / 4;
|
|
|
|
|
|
|
|
backgroundImage.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
|
|
|
@Override
|
|
|
|
public boolean onPreDraw() {
|
|
|
|
backgroundImage.getViewTreeObserver().removeOnPreDrawListener(this);
|
|
|
|
// Position the image
|
|
|
|
int offsetX = (viewPager.getCurrentItem() - 1) * pixelsPerPage;
|
|
|
|
backgroundImage.scrollTo(offsetX, 0);
|
|
|
|
|
|
|
|
pageIndicator.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
|
|
|
@Override
|
|
|
|
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
|
|
|
int offsetX = (int) ((position - 1 + positionOffset) * pixelsPerPage);
|
|
|
|
backgroundImage.scrollTo(offsetX, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPageSelected(int position) {
|
|
|
|
setToolbarTitle(toolbar, position);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onPageScrollStateChanged(int state) { }
|
|
|
|
});
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
backgroundImage.setImageDrawable(null);
|
|
|
|
pageIndicator.setOnPageChangeListener(defaultOnPageChangeListener);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* HostConnectionObserver.PlayerEventsObserver interface callbacks
|
|
|
|
*/
|
|
|
|
private String lastImageUrl = null;
|
2017-07-13 20:10:49 +02:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public void playerOnPropertyChanged(org.xbmc.kore.jsonrpc.notification.Player.NotificationsData notificationsData) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2017-12-25 13:57:26 +01:00
|
|
|
@TargetApi(Build.VERSION_CODES.O)
|
2015-01-14 12:12:47 +01:00
|
|
|
public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
|
|
|
|
PlayerType.PropertyValue getPropertiesResult,
|
|
|
|
ListType.ItemsAll getItemResult) {
|
|
|
|
String imageUrl = (TextUtils.isEmpty(getItemResult.fanart)) ?
|
|
|
|
getItemResult.thumbnail : getItemResult.fanart;
|
|
|
|
if ((imageUrl != null) && !imageUrl.equals(lastImageUrl)) {
|
|
|
|
setImageViewBackground(imageUrl);
|
|
|
|
}
|
|
|
|
lastImageUrl = imageUrl;
|
2015-02-15 20:11:32 +01:00
|
|
|
|
2017-12-24 18:49:40 +01:00
|
|
|
// Check whether we should show a notification
|
|
|
|
boolean showNotification = PreferenceManager
|
|
|
|
.getDefaultSharedPreferences(this)
|
|
|
|
.getBoolean(Settings.KEY_PREF_SHOW_NOTIFICATION,
|
|
|
|
Settings.DEFAULT_PREF_SHOW_NOTIFICATION);
|
|
|
|
if (showNotification) {
|
|
|
|
// Start service that manages connection observers
|
|
|
|
LogUtils.LOGD(TAG, "Starting observer service");
|
|
|
|
if (Utils.isOreoOrLater()) {
|
|
|
|
startForegroundService(new Intent(this, ConnectionObserversManagerService.class));
|
|
|
|
} else {
|
|
|
|
startService(new Intent(this, ConnectionObserversManagerService.class));
|
|
|
|
}
|
|
|
|
}
|
2015-01-14 12:12:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
|
|
|
|
PlayerType.PropertyValue getPropertiesResult,
|
|
|
|
ListType.ItemsAll getItemResult) {
|
|
|
|
playerOnPlay(getActivePlayerResult, getPropertiesResult, getItemResult);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void playerOnStop() {
|
2016-05-20 20:24:34 +02:00
|
|
|
LogUtils.LOGD(TAG, "Player stopping");
|
2015-01-14 12:12:47 +01:00
|
|
|
if (lastImageUrl != null) {
|
|
|
|
setImageViewBackground(null);
|
|
|
|
}
|
|
|
|
lastImageUrl = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void playerNoResultsYet() {
|
|
|
|
// Do nothing
|
|
|
|
}
|
|
|
|
|
|
|
|
public void playerOnConnectionError(int errorCode, String description) {
|
|
|
|
playerOnStop();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void systemOnQuit() {
|
|
|
|
Toast.makeText(this, R.string.xbmc_quit, Toast.LENGTH_SHORT).show();
|
|
|
|
playerOnStop();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void inputOnInputRequested(String title, String type, String value) {
|
|
|
|
SendTextDialogFragment dialog =
|
|
|
|
SendTextDialogFragment.newInstance(title);
|
|
|
|
dialog.show(getSupportFragmentManager(), null);
|
|
|
|
}
|
|
|
|
|
2015-02-15 20:11:32 +01:00
|
|
|
public void observerOnStopObserving() {}
|
|
|
|
|
2015-01-14 12:12:47 +01:00
|
|
|
/**
|
|
|
|
* Now playing fragment listener
|
|
|
|
*/
|
|
|
|
public void SwitchToRemotePanel() {
|
|
|
|
viewPager.setCurrentItem(1);
|
|
|
|
}
|
|
|
|
}
|