diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4e091a74..33a12b5e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3,8 +3,9 @@ android:versionCode="8" android:versionName="1.4.2" > - - + + diff --git a/README.md b/README.md index 2a64b7dd..49ab8c94 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#Connect SDK Core (Android) +# Connect SDK Core (Android) The Connect SDK Core contains all of the core classes required for basic operation of Connect SDK. The core also includes support for some select protocols which do not have any heavy and/or external dependencies. These protocols include: - Apple TV - DIAL @@ -7,13 +7,20 @@ The Connect SDK Core contains all of the core classes required for basic operati - LG webOS - Roku -##General Information +## General Information For more information about Connect SDK, visit the [main repository](https://github.com/ConnectSDK/Connect-SDK-Android). -##Setup +## Setup Unless you are doing very specialized work to extend the SDK, you should not need to make direct use of this repository. Instead, clone the [main repository](https://github.com/ConnectSDK/Connect-SDK-Android), which includes this repository as a submodule. -##License +## External libraries + +libgstreamer_android.so is shared object library and links dynamically GStreamer open-source multimedia framework that is licensed under Lesser General Public License. + +You can download and rebuild libgstreamer_android.so at [libgstreamer_android.tar](https://github.com/ConnectSDK/Connect-SDK-Android-Core/blob/master/jniLibs/libgstreamer_android.tar) + + +## License Copyright (c) 2013-2015 LG Electronics. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/build.gradle b/build.gradle index 75f3ee02..90ad279f 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,8 @@ allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 21 + compileSdkVersion 31 + buildToolsVersion '30.0.3' packagingOptions { exclude 'LICENSE.txt' @@ -40,6 +41,7 @@ android { renderscript.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['jniLibs'] } androidTest { java.srcDirs = [ @@ -54,17 +56,18 @@ android { } } defaultConfig { - minSdkVersion 16 - targetSdkVersion 25 + minSdkVersion 24 + targetSdkVersion 31 } + + useLibrary 'org.apache.http.legacy' } dependencies { implementation files('libs/Java-WebSocket-1.3.7.jar') implementation files('libs/javax.jmdns_3.4.1-patch2.jar') - implementation 'com.android.support:support-v4:21.0.3' - implementation 'com.android.support:mediarouter-v7:21.0.3' - implementation 'com.android.support:appcompat-v7:21.0.3' + implementation files('libs/lgcast_sdk_v2.1.1.aar') + implementation 'androidx.preference:preference:1.1.1' implementation 'com.google.android.gms:play-services-cast:19.0.0' implementation 'com.googlecode.plist:dd-plist:1.23' implementation group: 'net.i2p.crypto', name: 'eddsa', version: '0.3.0' diff --git a/jniLibs/arm64-v8a/libc++_shared.so b/jniLibs/arm64-v8a/libc++_shared.so new file mode 100644 index 00000000..da1df600 Binary files /dev/null and b/jniLibs/arm64-v8a/libc++_shared.so differ diff --git a/jniLibs/arm64-v8a/libgstreamer-appcast.so b/jniLibs/arm64-v8a/libgstreamer-appcast.so new file mode 100644 index 00000000..bdd768b5 Binary files /dev/null and b/jniLibs/arm64-v8a/libgstreamer-appcast.so differ diff --git a/jniLibs/arm64-v8a/libgstreamer_android.so b/jniLibs/arm64-v8a/libgstreamer_android.so new file mode 100644 index 00000000..cd6ec062 Binary files /dev/null and b/jniLibs/arm64-v8a/libgstreamer_android.so differ diff --git a/jniLibs/armeabi-v7a/libc++_shared.so b/jniLibs/armeabi-v7a/libc++_shared.so new file mode 100644 index 00000000..a6c8e870 Binary files /dev/null and b/jniLibs/armeabi-v7a/libc++_shared.so differ diff --git a/jniLibs/armeabi-v7a/libgstreamer-appcast.so b/jniLibs/armeabi-v7a/libgstreamer-appcast.so new file mode 100644 index 00000000..614a0891 Binary files /dev/null and b/jniLibs/armeabi-v7a/libgstreamer-appcast.so differ diff --git a/jniLibs/armeabi-v7a/libgstreamer_android.so b/jniLibs/armeabi-v7a/libgstreamer_android.so new file mode 100644 index 00000000..b1385c31 Binary files /dev/null and b/jniLibs/armeabi-v7a/libgstreamer_android.so differ diff --git a/jniLibs/libgstreamer_android.tar b/jniLibs/libgstreamer_android.tar new file mode 100644 index 00000000..ee5851ae Binary files /dev/null and b/jniLibs/libgstreamer_android.tar differ diff --git a/libs/lgcast_sdk_v2.1.1.aar b/libs/lgcast_sdk_v2.1.1.aar new file mode 100644 index 00000000..cc4747f5 Binary files /dev/null and b/libs/lgcast_sdk_v2.1.1.aar differ diff --git a/src/com/connectsdk/service/WebOSTVService.java b/src/com/connectsdk/service/WebOSTVService.java index 23f17c5e..e9e49db0 100644 --- a/src/com/connectsdk/service/WebOSTVService.java +++ b/src/com/connectsdk/service/WebOSTVService.java @@ -21,7 +21,10 @@ package com.connectsdk.service; import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; import android.graphics.PointF; + import androidx.annotation.NonNull; import android.util.Log; @@ -34,6 +37,8 @@ import com.connectsdk.discovery.DiscoveryFilter; import com.connectsdk.discovery.DiscoveryManager; import com.connectsdk.discovery.DiscoveryManager.PairingLevel; +import com.connectsdk.service.capability.LGCastControl; +import com.connectsdk.service.lgcast.screenmirroring.ScreenMirroringApi; import com.connectsdk.service.capability.CapabilityMethods; import com.connectsdk.service.capability.ExternalInputControl; import com.connectsdk.service.capability.KeyControl; @@ -57,6 +62,7 @@ import com.connectsdk.service.config.ServiceConfig; import com.connectsdk.service.config.ServiceDescription; import com.connectsdk.service.config.WebOSTVServiceConfig; +import com.connectsdk.service.lgcast.screenmirroring.ScreenMirroringHelper; import com.connectsdk.service.sessions.LaunchSession; import com.connectsdk.service.sessions.LaunchSession.LaunchSessionType; import com.connectsdk.service.sessions.WebAppSession; @@ -68,6 +74,7 @@ import com.connectsdk.service.webos.WebOSTVServiceSocketClient; import com.connectsdk.service.webos.WebOSTVServiceSocketClient.WebOSTVServiceSocketClientListener; +import com.lge.lgcast.common.utils.XmlUtil; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -88,7 +95,7 @@ import javax.xml.parsers.DocumentBuilderFactory; @SuppressLint("DefaultLocale") -public class WebOSTVService extends WebOSTVDeviceService implements Launcher, MediaPlayer, PlaylistControl, VolumeControl, TVControl, ToastControl, ExternalInputControl, MouseControl, KeyControl, TextInputControl, WebAppLauncher { +public class WebOSTVService extends WebOSTVDeviceService implements Launcher, MediaPlayer, PlaylistControl, VolumeControl, TVControl, ToastControl, ExternalInputControl, MouseControl, KeyControl, TextInputControl, WebAppLauncher, LGCastControl { public static final String ID = "webOS TV"; private static final String MEDIA_PLAYER_ID = "MediaPlayer"; @@ -2448,6 +2455,16 @@ protected void updateCapabilities() { capabilities.add(MediaPlayer.Loop); } + String locationXML = serviceDescription.getLocationXML(); + String appCasting = (locationXML != null) ? XmlUtil.findElement(locationXML, "appCasting") : null; + String appCastingFeature = (locationXML != null) ? XmlUtil.findElement(locationXML, "appCastingFeature") : null; + + if (appCastingFeature != null) { + if (appCastingFeature.contains("mirroring")) capabilities.add(LGCastControl.ScreenMirroring); + if (appCastingFeature.contains("camera")) capabilities.add(LGCastControl.RemoteCamera); + } else if (appCasting != null) { + if ("support".equals(appCasting)) capabilities.add(LGCastControl.ScreenMirroring); + } } setCapabilities(capabilities); @@ -2552,4 +2569,47 @@ public void sendPairingKey(String pairingKey) { public static interface ServiceInfoListener extends ResponseListener { } public static interface SystemInfoListener extends ResponseListener { } + + /********************************************************************************************** + * LG CAST - SCREEN MIRRORING + *********************************************************************************************/ + @Override + public void startScreenMirroring(Context context, Intent projectionData, ScreenMirroringStartListener listener) { + startScreenMirroring(context, projectionData, null, listener); + } + + @Override + public void startScreenMirroring(Context context, Intent projectionData, Class secondScreenClass, ScreenMirroringStartListener listener) { + try { + if (hasCapability(LGCastControl.ScreenMirroring) == false) throw new Exception("This device does not support LG Cast"); + if (ScreenMirroringHelper.isOsCompatible() == false) throw new Exception("Incompatible OS version (LG Cast is compatible from Android Q)."); + if (ScreenMirroringHelper.isRunning(context) == true) throw new Exception("Screen Mirroring is already running"); + ScreenMirroringApi.getInstance().startMirroring(context, projectionData, getServiceDescription().getIpAddress(), secondScreenClass, listener); + } catch (Exception e) { + listener.onError(new ServiceCommandError(LGCastControl.SCREEN_MIRRORING_ERROR_GENERIC, e.getMessage())); + } + } + + @Override + public void stopScreenMirroring(Context context, ScreenMirroringStopListener listener) { + try { + if (ScreenMirroringHelper.isRunning(context) == false) throw new Exception("Screen Mirroring is not running"); + ScreenMirroringApi.getInstance().stopMirroring(context, listener); + } catch (Exception e) { + listener.onError(new ServiceCommandError(e.getMessage())); + } + } + + /********************************************************************************************** + * LG CAST - REMOTE CAMERA + *********************************************************************************************/ + @Override + public void startRemoteCamera() { + // TODO + } + + @Override + public void stopRemoteCamera() { + // TODO + } } diff --git a/src/com/connectsdk/service/capability/LGCastControl.java b/src/com/connectsdk/service/capability/LGCastControl.java new file mode 100644 index 00000000..4489202e --- /dev/null +++ b/src/com/connectsdk/service/capability/LGCastControl.java @@ -0,0 +1,56 @@ +/* + * ScreenMirroringApi + * Connect SDK + * + * Copyright (c) 2020 LG Electronics. + * + * 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. + */ + +package com.connectsdk.service.capability; + +import android.content.Context; +import android.content.Intent; +import com.connectsdk.service.capability.listeners.ResponseListener; +import com.connectsdk.service.lgcast.screenmirroring.SecondScreen; + +public interface LGCastControl extends CapabilityMethods { + String Any = "LGCast.Any"; + String ScreenMirroring = "LGCast.ScreenMirroring"; + String RemoteCamera = "LGCast.RemoteCamera"; + + String[] Capabilities = { + ScreenMirroring, + RemoteCamera, + }; + + int SCREEN_MIRRORING_ERROR_GENERIC = 0; + int SCREEN_MIRRORING_ERROR_CONNECTION_CLOSED = 1; + int SCREEN_MIRRORING_ERROR_DEVICE_SHUTDOWN = 2; + int SCREEN_MIRRORING_ERROR_RENDERER_TERMINATED = 3; + int SCREEN_MIRRORING_ERROR_STOPPED_BY_NOTIFICATION = 4; + + void startScreenMirroring(Context context, Intent projectionData, ScreenMirroringStartListener listener); + void startScreenMirroring(Context context, Intent projectionData, Class secondScreenClass, ScreenMirroringStartListener listener); + void stopScreenMirroring(Context context, ScreenMirroringStopListener listener); + + void startRemoteCamera(); + void stopRemoteCamera(); + + interface ScreenMirroringStartListener extends ResponseListener { + void onPairing(); + } + + interface ScreenMirroringStopListener extends ResponseListener { + } +} diff --git a/src/com/connectsdk/service/lgcast/remotecamera/RemoteCameraApi.java b/src/com/connectsdk/service/lgcast/remotecamera/RemoteCameraApi.java new file mode 100644 index 00000000..c550b547 --- /dev/null +++ b/src/com/connectsdk/service/lgcast/remotecamera/RemoteCameraApi.java @@ -0,0 +1,48 @@ +/* + * RemoteCameraApi + * Connect SDK + * + * Copyright (c) 2020 LG Electronics. + * + * 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. + */ + +package com.connectsdk.service.lgcast.remotecamera; + +import android.app.ActivityManager; +import android.content.Context; +import android.view.Surface; +import com.lge.lgcast.remotecamera.service.RemoteCameraService; +import com.lge.lgcast.remotecamera.service.RemoteCameraServiceIF; +import com.lge.lgcast.common.utils.AppUtil; + +public class RemoteCameraApi { + private Context mContext; + + public RemoteCameraApi(Context context) { + mContext = context; + } + + public void startRemoteCamera(Surface previewSurface) { + RemoteCameraServiceIF.requestStart(mContext, previewSurface); + } + + public void stopRemoteCamera() { + RemoteCameraServiceIF.requestStop(mContext); + } + + public boolean isRunning() { + ActivityManager.RunningServiceInfo serviceInfo = AppUtil.getServiceInfo(mContext, RemoteCameraService.class.getName()); + return (serviceInfo != null) ? serviceInfo.foreground : false; + } +} diff --git a/src/com/connectsdk/service/lgcast/screenmirroring/ScreenMirroringApi.java b/src/com/connectsdk/service/lgcast/screenmirroring/ScreenMirroringApi.java new file mode 100644 index 00000000..d87d6430 --- /dev/null +++ b/src/com/connectsdk/service/lgcast/screenmirroring/ScreenMirroringApi.java @@ -0,0 +1,152 @@ +/* + * ScreenMirroringApi + * Connect SDK + * + * Copyright (c) 2020 LG Electronics. + * + * 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. + */ + +package com.connectsdk.service.lgcast.screenmirroring; + +import android.content.Context; +import android.content.Intent; +import android.hardware.display.DisplayManager; +import android.view.Display; +import com.connectsdk.service.capability.LGCastControl; +import com.connectsdk.service.command.ServiceCommandError; +import com.lge.lgcast.common.utils.LocalBroadcastUtil; +import com.lge.lgcast.common.utils.Logger; +import com.lge.lgcast.screenmirroring.service.MirroringServiceError; +import com.lge.lgcast.screenmirroring.service.MirroringServiceIF; +import java.lang.reflect.Constructor; + +public class ScreenMirroringApi { + private DisplayManager.DisplayListener mDisplayListener; + private SecondScreen mSecondScreen; + + private ScreenMirroringApi() { + } + + private static class LazyHolder { + private static final ScreenMirroringApi INSTANCE = new ScreenMirroringApi(); + } + + public static ScreenMirroringApi getInstance() { + return LazyHolder.INSTANCE; + } + + public void startMirroring(Context context, Intent projectionData, String address, Class secondScreenClass, LGCastControl.ScreenMirroringStartListener listener) { + LocalBroadcastUtil.registerOneTimeReceiver(context, MirroringServiceIF.ACTION_NOTIFY_PAIRING, intent -> { + if (listener != null) listener.onPairing(); + }); + + LocalBroadcastUtil.registerOneTimeReceiver(context, MirroringServiceIF.ACTION_START_RESPONSE, intent -> { + if (listener == null) return; + boolean result = intent.getBooleanExtra(MirroringServiceIF.EXTRA_RESULT, false); + if (result) listener.onSuccess(mSecondScreen); + else listener.onError(new ServiceCommandError(LGCastControl.SCREEN_MIRRORING_ERROR_GENERIC, "Failed to start mirroring")); + }); + + LocalBroadcastUtil.registerOneTimeReceiver(context, MirroringServiceIF.ACTION_NOTIFY_ERROR, intent -> { + if (listener == null) return; + MirroringServiceError serviceError = (MirroringServiceError) intent.getSerializableExtra(MirroringServiceIF.EXTRA_ERROR); + if (serviceError == MirroringServiceError.ERROR_CONNECTION_CLOSED) listener.onError(new ServiceCommandError(LGCastControl.SCREEN_MIRRORING_ERROR_CONNECTION_CLOSED, "Connection closed")); + else if (serviceError == MirroringServiceError.ERROR_DEVICE_SHUTDOWN) listener.onError(new ServiceCommandError(LGCastControl.SCREEN_MIRRORING_ERROR_DEVICE_SHUTDOWN, "Device shutdown")); + else if (serviceError == MirroringServiceError.ERROR_RENDERER_TERMINATED) listener.onError(new ServiceCommandError(LGCastControl.SCREEN_MIRRORING_ERROR_RENDERER_TERMINATED, "Renderer terminated")); + else if (serviceError == MirroringServiceError.ERROR_STOPPED_BY_NOTIFICATION) listener.onError(new ServiceCommandError(LGCastControl.SCREEN_MIRRORING_ERROR_STOPPED_BY_NOTIFICATION, "User stopped mirroring by clicking notification")); + else listener.onError(new ServiceCommandError(LGCastControl.SCREEN_MIRRORING_ERROR_GENERIC, "Unknown error")); + }); + + if (projectionData == null || address == null) { + Logger.error("Invalid arguments (projectionData=%s, address=%s)", projectionData, address); + MirroringServiceIF.respondStart(context, false, false); + return; + } + + if (ScreenMirroringHelper.isRunning(context) == true) { + Logger.error("Mirroring is ALREADY running."); + MirroringServiceIF.respondStart(context, true, secondScreenClass != null); + return; + } + + if (secondScreenClass != null) { + mDisplayListener = createDisplayListener(context, secondScreenClass); + getDisplayManager(context).registerDisplayListener(mDisplayListener, null); + } + + Logger.debug("Request start"); + MirroringServiceIF.requestStart(context, projectionData, address, secondScreenClass != null); + } + + public void stopMirroring(Context context, LGCastControl.ScreenMirroringStopListener listener) { + Logger.print("stopMirroring"); + + LocalBroadcastUtil.registerOneTimeReceiver(context, MirroringServiceIF.ACTION_STOP_RESPONSE, intent -> { + if (listener != null) listener.onSuccess("stopMirroring Success"); + }); + + if (ScreenMirroringHelper.isRunning(context) == false) { + Logger.error("Mirroring is NOT running."); + MirroringServiceIF.respondStop(context); + return; + } + + if (mDisplayListener != null) getDisplayManager(context).unregisterDisplayListener(mDisplayListener); + mDisplayListener = null; + + if (mSecondScreen != null) mSecondScreen.dismiss(); + mSecondScreen = null; + + Logger.debug("Request stop"); + MirroringServiceIF.requestStop(context); + } + + private DisplayManager getDisplayManager(Context context) { + return (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + } + + private DisplayManager.DisplayListener createDisplayListener(Context context, Class secondScreenClass) { + return new DisplayManager.DisplayListener() { + @Override + public void onDisplayAdded(int id) { + Logger.debug("onDisplayAdded (id=%d)", id); + mSecondScreen = createSecondScreenInstance(context, secondScreenClass, getDisplayManager(context).getDisplay(id)); + if (mSecondScreen != null) mSecondScreen.show(); + } + + @Override + public void onDisplayRemoved(int id) { + Logger.debug("onDisplayRemoved (id=%d)", id); + } + + @Override + public void onDisplayChanged(int id) { + Logger.debug("onDisplayChanged (id=%d)", id); + } + }; + } + + private SecondScreen createSecondScreenInstance(Context context, Class secondScreenClass, Display display) { + try { + if (secondScreenClass == null) throw new Exception("Invalid class"); + if (display == null) throw new Exception("Invalid display"); + + Constructor constructor = secondScreenClass.getConstructor(new Class[]{Context.class, Display.class}); + return (SecondScreen) constructor.newInstance(context, display); + } catch (Exception e) { + Logger.error(e); + return null; + } + } +} diff --git a/src/com/connectsdk/service/lgcast/screenmirroring/ScreenMirroringHelper.java b/src/com/connectsdk/service/lgcast/screenmirroring/ScreenMirroringHelper.java new file mode 100644 index 00000000..2e446031 --- /dev/null +++ b/src/com/connectsdk/service/lgcast/screenmirroring/ScreenMirroringHelper.java @@ -0,0 +1,37 @@ +/* + * ScreenMirroringHelper + * Connect SDK + * + * Copyright (c) 2020 LG Electronics. + * + * 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. + */ + +package com.connectsdk.service.lgcast.screenmirroring; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.Build; +import com.lge.lgcast.common.utils.AppUtil; +import com.lge.lgcast.screenmirroring.service.MirroringService; + +public class ScreenMirroringHelper { + public static boolean isOsCompatible() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + } + + public static boolean isRunning(Context context) { + ActivityManager.RunningServiceInfo serviceInfo = AppUtil.getServiceInfo(context, MirroringService.class.getName()); + return (serviceInfo != null) ? serviceInfo.foreground : false; + } +} diff --git a/src/com/connectsdk/service/lgcast/screenmirroring/SecondScreen.java b/src/com/connectsdk/service/lgcast/screenmirroring/SecondScreen.java new file mode 100644 index 00000000..e7edde46 --- /dev/null +++ b/src/com/connectsdk/service/lgcast/screenmirroring/SecondScreen.java @@ -0,0 +1,31 @@ +/* + * SecondScreen + * Connect SDK + * + * Copyright (c) 2020 LG Electronics. + * + * 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. + */ + +package com.connectsdk.service.lgcast.screenmirroring; + +import android.app.Presentation; +import android.content.Context; +import android.view.Display; + +public class SecondScreen extends Presentation { + public SecondScreen(Context context, Display display) { + super(context, display); + } +} +