diff --git a/.gitignore b/.gitignore index 82984787..4baa2c15 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ local.properties # Proguard folder generated by Eclipse proguard/ - *.pydevproject .project .metadata @@ -50,3 +49,5 @@ local.properties .buildpath *.DS_Store + +.idea/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index f7d89931..f77cc0f1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,8 @@ [submodule "modules/google_cast"] path = modules/google_cast - url = https://github.com/ConnectSDK/Connect-SDK-Android-Google-Cast.git + url = https://github.com/rahulfancode/Connect-SDK-Android-Google-Cast.git branch = master [submodule "core"] path = core - url = https://github.com/ConnectSDK/Connect-SDK-Android-Core.git - branch = master -[submodule "modules/firetv"] - path = modules/firetv - url = https://github.com/ConnectSDK/Connect-SDK-Android-FireTV.git - branch = master + url = https://github.com/rahulfancode/Connect-SDK-Android-Core.git + branch = master \ No newline at end of file diff --git a/AndroidManifest.xml b/AndroidManifest.xml index dba65c45..6102054e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,13 +1,4 @@ - - - - + - diff --git a/build.gradle b/build.gradle index bd791d77..5e1e7427 100644 --- a/build.gradle +++ b/build.gradle @@ -2,9 +2,10 @@ buildscript { repositories { google() jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:4.2.2' } } @@ -12,6 +13,7 @@ allprojects { repositories { google() jcenter() + mavenCentral() } } @@ -23,7 +25,7 @@ jacoco { toolVersion = "0.8.5" } -task jacocoTestReport(type:JacocoReport, dependsOn: "check") { +task jacocoTestReport(type: JacocoReport, dependsOn: "check") { group = "Reporting" description = "Generate Jacoco coverage reports" @@ -51,9 +53,7 @@ task jacocoTestReport(type:JacocoReport, dependsOn: "check") { build.dependsOn jacocoTestReport android { - compileSdkVersion 22 - buildToolsVersion '22.0.1' - + compileSdkVersion 28 packagingOptions { exclude 'LICENSE.txt' exclude 'META-INF/LICENSE' @@ -61,6 +61,10 @@ android { exclude 'META-INF/NOTICE' } + defaultConfig { + minSdkVersion 21 // This over her + } + sourceSets { main { manifest.srcFile 'AndroidManifest.xml' @@ -104,25 +108,23 @@ android.testOptions.unitTests.all { } dependencies { - compile files('core/libs/Java-WebSocket-1.3.7.jar') - compile files('core/libs/javax.jmdns_3.4.1-patch2.jar') - - api 'com.android.support:appcompat-v7:22.2.1' - + implementation 'org.java-websocket:Java-WebSocket:1.5.3' + implementation 'javax.jmdns:jmdns:3.4.1' + api 'com.android.support:appcompat-v7:28.0.0' implementation fileTree(dir: 'modules/firetv/libs', include: '*.jar') - - implementation 'com.android.support:support-v4:22.2.1' - implementation 'com.android.support:mediarouter-v7:22.2.1' - implementation 'com.google.android.gms:play-services-cast:7.8.0' + implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.android.support:mediarouter-v7:28.0.0' + implementation 'com.google.android.gms:play-services-cast:19.0.0' implementation 'com.googlecode.plist:dd-plist:1.23' implementation 'com.nimbusds:srp6a-android:2.0.2' implementation 'net.i2p.crypto:eddsa:0.2.0' - testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-all:1.10.19' testImplementation 'org.powermock:powermock-api-mockito:1.6.2' testImplementation 'xmlunit:xmlunit:1.4' + implementation group: 'org.apache.httpcomponents', name: 'httpcore', version: '4.4.10' + implementation(group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.3.5') { + exclude module: 'org.apache.httpcomponents:httpclient' + } } - -apply from: 'maven-push.gradle' diff --git a/core b/core index 11fa2e44..57c368ef 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 11fa2e44db25644806fab13024da37edac91b127 +Subproject commit 57c368ef35d0c87af2ef03c3608aac0aaf6f61bc diff --git a/gradle.properties b/gradle.properties index 6b5a82f0..74dc9d8e 100755 --- a/gradle.properties +++ b/gradle.properties @@ -24,4 +24,5 @@ POM_DEVELOPER_NAME=change SNAPSHOT_REPOSITORY_URL=https://oss.sonatype.org/content/repositories/snapshots RELEASE_REPOSITORY_URL=https://oss.sonatype.org/service/local/staging/deploy/maven2 - +android.enableJetifier=true +android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..5c2d1cf0 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..48f68869 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Oct 27 15:34:52 IST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/modules/firetv b/modules/firetv deleted file mode 160000 index 0c07d038..00000000 --- a/modules/firetv +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0c07d03880f1f457107aa8dbc078bbd6404437fc diff --git a/modules/google_cast b/modules/google_cast index 809e5b48..fc90af5c 160000 --- a/modules/google_cast +++ b/modules/google_cast @@ -1 +1 @@ -Subproject commit 809e5b48f081dab93801ba17b86c1b29ec6ad89c +Subproject commit fc90af5c47c1017419d0ed9ca3474a2228aa47c2 diff --git a/res/drawable/ic_notification.png b/res/drawable/ic_notification.png new file mode 100644 index 00000000..a839e57f Binary files /dev/null and b/res/drawable/ic_notification.png differ diff --git a/res/drawable/tv_icon.xml b/res/drawable/tv_icon.xml new file mode 100644 index 00000000..18d5073a --- /dev/null +++ b/res/drawable/tv_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/res/font/roboto.xml b/res/font/roboto.xml new file mode 100644 index 00000000..1af22546 --- /dev/null +++ b/res/font/roboto.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/res/font/roboto_bold.ttf b/res/font/roboto_bold.ttf new file mode 100644 index 00000000..43da14d8 Binary files /dev/null and b/res/font/roboto_bold.ttf differ diff --git a/res/font/roboto_italic.ttf b/res/font/roboto_italic.ttf new file mode 100644 index 00000000..1b5eaa36 Binary files /dev/null and b/res/font/roboto_italic.ttf differ diff --git a/res/font/roboto_medium.ttf b/res/font/roboto_medium.ttf new file mode 100644 index 00000000..ac0f908b Binary files /dev/null and b/res/font/roboto_medium.ttf differ diff --git a/res/font/roboto_regular.ttf b/res/font/roboto_regular.ttf new file mode 100644 index 00000000..ddf4bfac Binary files /dev/null and b/res/font/roboto_regular.ttf differ diff --git a/res/font/roboto_semibold.ttf b/res/font/roboto_semibold.ttf new file mode 100644 index 00000000..ac0f908b Binary files /dev/null and b/res/font/roboto_semibold.ttf differ diff --git a/res/layout/cast_button.xml b/res/layout/cast_button.xml new file mode 100644 index 00000000..7581bb9f --- /dev/null +++ b/res/layout/cast_button.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/res/layout/header_layout.xml b/res/layout/header_layout.xml new file mode 100644 index 00000000..74c962a2 --- /dev/null +++ b/res/layout/header_layout.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/res/layout/item_layout.xml b/res/layout/item_layout.xml new file mode 100644 index 00000000..6db83c5b --- /dev/null +++ b/res/layout/item_layout.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/tv_icon.xml b/res/layout/tv_icon.xml new file mode 100644 index 00000000..41d2ef14 --- /dev/null +++ b/res/layout/tv_icon.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 00000000..42b49f5b --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #000000 + #ff5000 + \ No newline at end of file diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 00000000..bde7635a --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/src/com/connectsdk/CastButton.java b/src/com/connectsdk/CastButton.java new file mode 100644 index 00000000..3829ccc4 --- /dev/null +++ b/src/com/connectsdk/CastButton.java @@ -0,0 +1,25 @@ +package com.connectsdk; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +public class CastButton extends LinearLayout { + private ImageView mImage; + + public CastButton(Context context) { + super(context); + } + + public CastButton(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.cast_button, this); + mImage = (ImageView) getChildAt(0); + } + +} diff --git a/src/com/connectsdk/CastDevice.java b/src/com/connectsdk/CastDevice.java new file mode 100644 index 00000000..567a3791 --- /dev/null +++ b/src/com/connectsdk/CastDevice.java @@ -0,0 +1,41 @@ +package com.connectsdk; + +import com.connectsdk.device.ConnectableDevice; + +public class CastDevice { + private String id = ""; + private String name = ""; + private String type = ""; + + private CastDevice() {} + + public static CastDevice fromConnectableDevice(ConnectableDevice connectableDevice) { + CastDevice device = new CastDevice(); + if (connectableDevice != null) { + device.id = connectableDevice.getId(); + if (connectableDevice.getFriendlyName() != null) { + device.name = connectableDevice.getFriendlyName(); + } else { + device.name = connectableDevice.getModelName(); + } + device.type = connectableDevice.getServiceId(); + } + return device; + } + + public static CastDevice forDeviceNotAvailble() { + return new CastDevice(); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } +} diff --git a/src/com/connectsdk/CastMediaInfo.java b/src/com/connectsdk/CastMediaInfo.java new file mode 100644 index 00000000..5df6109c --- /dev/null +++ b/src/com/connectsdk/CastMediaInfo.java @@ -0,0 +1,11 @@ +package com.connectsdk; + +public class CastMediaInfo { + public String mediaURL; + public String iconURL; + public String title; + public String description; + public String mimeType; + public String licenceUrl; + public String receiverPlayerId; +} diff --git a/src/com/connectsdk/CastMediaPlayer.java b/src/com/connectsdk/CastMediaPlayer.java new file mode 100644 index 00000000..837e5150 --- /dev/null +++ b/src/com/connectsdk/CastMediaPlayer.java @@ -0,0 +1,107 @@ +package com.connectsdk; + +import android.content.Context; +import android.util.Log; + +import com.connectsdk.core.MediaInfo; +import com.connectsdk.service.capability.MediaPlayer; +import com.connectsdk.service.capability.listeners.ResponseListener; +import com.connectsdk.service.command.ServiceCommandError; +import com.connectsdk.utils.ILogger; + +public class CastMediaPlayer { + private MediaPlayer mediaPlayer; + private RemoteMediaControl mediaControl; + private ILogger logger; + private static final String TAG = "CastMediaPlayer"; + private Context mContext; + + public CastMediaPlayer(Context context, MediaPlayer mediaPlayer, ILogger logger) { + this.mediaPlayer = mediaPlayer; + this.logger = logger; + this.mContext = context; + } + + public void playMedia(CastMediaInfo castMediaInfo, final IMediaLifeCycleListener listener) { + final MediaInfo mediaInfo = new MediaInfo.Builder(castMediaInfo.mediaURL, castMediaInfo.mimeType) + .setTitle(castMediaInfo.title) + .setDescription(castMediaInfo.description) + .setIcon(castMediaInfo.iconURL) + .setLicenseUrl(castMediaInfo.licenceUrl) + .setReceiverPlayerId(castMediaInfo.receiverPlayerId) + .build(); + + clearCurrentMedia(new ResponseListener() { + @Override + public void onError(ServiceCommandError error) { + logger.log(Log.INFO, TAG, "stopCurrentMedia onError " + error.getMessage()); + startNewMedia(mediaInfo, listener); + } + + @Override + public void onSuccess(Object object) { + logger.log(Log.INFO, TAG, "stopCurrentMedia onSuccess "); + startNewMedia(mediaInfo, listener); + } + } + ); + } + + public void clearCurrentMedia(ResponseListener listener) { + logger.log(Log.INFO, TAG, "stopCurrentMedia " + (mediaControl != null)); + if (mediaControl != null) { + mediaControl.stop(listener); + mediaControl = null; + } else if (listener != null) { + listener.onSuccess(null); + } + } + + private void startNewMedia(final MediaInfo mediaInfo, final IMediaLifeCycleListener listener) { + if (mediaPlayer != null) { + mediaPlayer.playMedia( + mediaInfo, + false, + new MediaPlayer.LaunchListener() { + + @Override + public void onSuccess(MediaPlayer.MediaLaunchObject object) { + logger.log(Log.INFO, TAG, "playMedia onSuccess"); + CastMediaPlayer.this.mediaControl = + new RemoteMediaControl(object.mediaControl, mediaInfo, logger); + listener.onMediaReady(CastMediaPlayer.this.mediaControl); + } + + @Override + public void onError(ServiceCommandError error) { + logger.log(Log.INFO, TAG, "playMedia onError"); + listener.onMediaError(); + } + } + ); + } else { + logger.log(Log.INFO, TAG, "playMedia onError"); + listener.onMediaError(); + } + } + + public void destroyPlayer(final ResponseListener listener) { + logger.log(Log.INFO, TAG, "destroyPlayer Start"); + clearCurrentMedia( + new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + logger.log(Log.INFO, TAG, "destroyPlayer clearCurrentMedia error " + error.getMessage()); + if (listener != null) listener.onError(error); + } + + @Override + public void onSuccess(Object object) { + logger.log(Log.INFO, TAG, "destroyPlayer clearCurrentMedia onSuccess "); + if (listener != null) listener.onSuccess(object); + } + } + ); + } +} diff --git a/src/com/connectsdk/CastState.java b/src/com/connectsdk/CastState.java new file mode 100644 index 00000000..b4897ccf --- /dev/null +++ b/src/com/connectsdk/CastState.java @@ -0,0 +1,8 @@ +package com.connectsdk; + +public class CastState { + public static final int NO_DEVICE_AVAILABLE = 0; + public static final int NOT_CONNECTED = 1; + public static final int CONNECTING = 2; + public static final int CONNECTED = 3; +} diff --git a/src/com/connectsdk/CastStatus.java b/src/com/connectsdk/CastStatus.java new file mode 100644 index 00000000..833fa684 --- /dev/null +++ b/src/com/connectsdk/CastStatus.java @@ -0,0 +1,37 @@ +package com.connectsdk; + +public class CastStatus { + private int castState; + private CastDevice castDevice; + + public CastStatus() { + this(CastState.NO_DEVICE_AVAILABLE, CastDevice.fromConnectableDevice(null)); + } + + public CastStatus(int castState, CastDevice castDevice) { + this.castState = castState; + this.castDevice = castDevice; + } + + public int getCastState() { + return castState; + } + + public void setCastState(int castState) { + this.castState = castState; + if ( + castState == CastState.NO_DEVICE_AVAILABLE || + castState == CastState.NOT_CONNECTED + ) { + castDevice = CastDevice.fromConnectableDevice(null); + } + } + + public CastDevice getCastDevice() { + return castDevice; + } + + public void setCastDevice(CastDevice castDevice) { + this.castDevice = castDevice; + } +} diff --git a/src/com/connectsdk/ConnectSDKManager.java b/src/com/connectsdk/ConnectSDKManager.java new file mode 100644 index 00000000..0560fa9c --- /dev/null +++ b/src/com/connectsdk/ConnectSDKManager.java @@ -0,0 +1,221 @@ +package com.connectsdk; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Application; +import android.content.Context; +import android.os.Build; +import android.view.View; +import android.widget.AdapterView; + +import androidx.annotation.RequiresApi; + +import com.connectsdk.core.Util; +import com.connectsdk.device.ConnectableDevice; +import com.connectsdk.device.DevicePicker; +import com.connectsdk.discovery.DiscoveryProvider; +import com.connectsdk.notification.MediaNotificationManager; +import com.connectsdk.service.capability.MediaPlayer; +import com.connectsdk.utils.ActivityTracker; +import com.connectsdk.utils.DefaultLogger; +import com.connectsdk.utils.ILogger; + +import java.util.ArrayList; +import java.util.List; + +public class ConnectSDKManager { + private static final Object lock = new Object(); + private static ConnectSDKManager instance = null; + private Context mAppContext; + private DeviceManager mDeviceManager; + private DiscoveryManagerWrapper mDiscoveryManager; + private MediaNotificationManager mNotificationManager; + private ArrayList castStateListeners = new ArrayList<>(); + private List capabilities = new ArrayList<>(); + private CastStatus castStatus = new CastStatus(); + private ILogger logger; + private ActivityTracker activityTracker; + private DevicePicker mDevicePicker; + + private ConnectSDKManager() { + capabilities.add(MediaPlayer.Play_Video); + logger = new DefaultLogger(); + } + + public static ConnectSDKManager getInstance() { + if (instance == null) { + synchronized (lock) { + if (instance == null) { + instance = new ConnectSDKManager(); + } + } + } + return instance; + } + + @RequiresApi(api = Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public void init(Application application, Activity currentActivity) { + this.mAppContext = application; + activityTracker = new ActivityTracker(currentActivity); + application.registerActivityLifecycleCallbacks(activityTracker); + mDeviceManager = new DeviceManager(application, logger); + mDiscoveryManager = new DiscoveryManagerWrapper(application, capabilities, logger); + mDiscoveryManager.setDeviceAvailabilityListener(availabilityListener); + mDeviceManager.setStatusListener(statusListener); + mDiscoveryManager.start(); + mNotificationManager = new MediaNotificationManager(application, logger, notificationListener); + } + + public List getAvailableDevices() { + return mDiscoveryManager.getAvailableDevices(); + } + + public void pickDevice(CastDevice device) { + ConnectableDevice connectableDevice = mDiscoveryManager.getConnectableDevice(device); + mDeviceManager.pickDevice(connectableDevice); + } + + public void playMedia(CastMediaInfo mediaInfo, final IMediaLifeCycleListener listener) { + if (castStatus.getCastState() == CastState.CONNECTED) { + mDeviceManager.playMedia(mediaInfo, new IMediaLifeCycleListener() { + + @Override + public void onMediaReady(RemoteMediaControl mediaControl) { + mNotificationManager.setCurrentRemoteMediaControl(mediaControl); + listener.onMediaReady(mediaControl); + } + + @Override + public void onMediaError() { + listener.onMediaError(); + } + } + ); + } else { + listener.onMediaError(); + } + } + + public void showDeviceSelector() { + Util.runOnUI(new Runnable() { + + @Override + public void run() { + mDiscoveryManager.setScanIntensity(DiscoveryProvider.ScanIntensity.ACTIVE); + if (isDialogHostAvailable()) { + if (mDevicePicker == null) { + mDevicePicker = new DevicePicker(activityTracker.getCurrentActivity()); + } + final AlertDialog dialog = mDevicePicker.getPickerDialog( + R.layout.header_layout, + R.layout.item_layout, + R.id.deviceName, + new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick( + AdapterView adapter, + View parent, + int position, + long id + ) { + ConnectableDevice device = (ConnectableDevice) adapter.getItemAtPosition( + position + ); + if (device != null) { + mDevicePicker.pickDevice(device); + mDeviceManager.pickDevice( + (ConnectableDevice) adapter.getItemAtPosition(position) + ); + } + mDiscoveryManager.setScanIntensity(DiscoveryProvider.ScanIntensity.PASSIVE); + } + } + ); + dialog.show(); + } + } + } + ); + } + + public void showConnectedDevice() { + if (isDialogHostAvailable() && mDevicePicker != null) { + AlertDialog dialog = mDeviceManager.getConnectedDeviceDialog(activityTracker.getCurrentActivity()); + if (dialog != null) dialog.show(); + } + } + + public CastStatus getCurrentCastInfo() { + return castStatus; + } + + public void addCastStateListener(ICastStateListener listener) { + castStateListeners.add(listener); + } + + public void removeCastStateListener(ICastStateListener listener) { + castStateListeners.remove(listener); + } + + private IDeviceAvailabilityListener availabilityListener = new IDeviceAvailabilityListener() { + + @Override + public void onAvailabilityChange(boolean available) { + if (castStatus.getCastState() == CastState.NO_DEVICE_AVAILABLE && available) { + castStatus.setCastState(CastState.NOT_CONNECTED); + postCurrentCastState(); + } else if (castStatus.getCastState() != CastState.NO_DEVICE_AVAILABLE && !available) { + castStatus.setCastState(CastState.NO_DEVICE_AVAILABLE); + postCurrentCastState(); + } + } + }; + + private IDeviceStatusListener statusListener = new IDeviceStatusListener() { + + @Override + public void onDeviceConnecting(int castState, CastDevice castDevice) { + if (castStatus.getCastState() != CastState.NO_DEVICE_AVAILABLE) { + castStatus.setCastState(castState); + castStatus.setCastDevice(castDevice); + postCurrentCastState(); + } + } + }; + + private MediaNotificationManager.INotificationListener notificationListener = new MediaNotificationManager.INotificationListener() { + @Override + public void onCastMediaStopped() { + if (mDeviceManager != null) { + mDeviceManager.clearDevice(); + } + } + }; + + private void postCurrentCastState() { + for (ICastStateListener listener : castStateListeners) { + listener.onCastStateChanged(castStatus); + } + } + + private boolean isDialogHostAvailable() { + return activityTracker.getCurrentActivity() != null + && !activityTracker.getCurrentActivity().isDestroyed() + && !activityTracker.getCurrentActivity().isFinishing(); + } + + public void handleExit() { + if (mNotificationManager != null) { + mNotificationManager.handleExit(); + } + if (mDeviceManager != null) { + mDeviceManager.clearDevice(); + } + } + + public void onDestroy() { + mDiscoveryManager.onDestory(); + mDevicePicker = null; + } +} diff --git a/src/com/connectsdk/ConnectedDeviceDialog.java b/src/com/connectsdk/ConnectedDeviceDialog.java new file mode 100644 index 00000000..5514029a --- /dev/null +++ b/src/com/connectsdk/ConnectedDeviceDialog.java @@ -0,0 +1,69 @@ +package com.connectsdk; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +public class ConnectedDeviceDialog { + private AlertDialog pickerDialog; + private IConnectedDeviceDialogListener deviceDialogListener; + private Activity activity; + + public AlertDialog getConnectedDeviceDialog(Activity activity, String deviceName) { + ViewGroup titleContainer = (ViewGroup) activity.getLayoutInflater().inflate(R.layout.header_layout, null); + this.activity = activity; + TextView title = titleContainer.findViewById(R.id.headerView); + title.setText(deviceName); + pickerDialog = new AlertDialog.Builder(activity) + .setCustomTitle(titleContainer) + .setCancelable(false) + .setPositiveButton("STOP CASTING", positiveButtonListener) + .setNegativeButton("CANCEL", negativeButtonListener) + .create(); + pickerDialog.setOnShowListener(onShowListener); + return pickerDialog; + } + + private DialogInterface.OnClickListener positiveButtonListener = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialogInterface, int i) { + deviceDialogListener.disconnectClick(); + dialogInterface.dismiss(); + } + }; + + private DialogInterface.OnClickListener negativeButtonListener = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialogInterface, int i) { + dialogInterface.dismiss(); + } + }; + + private DialogInterface.OnShowListener onShowListener = new DialogInterface.OnShowListener() { + + @Override + public void onShow(DialogInterface dialog) { + Button nbutton = pickerDialog.getButton(DialogInterface.BUTTON_NEGATIVE); + if (nbutton != null) { + nbutton.setTextAppearance(activity, R.style.buttonText); + } + Button pbutton = pickerDialog.getButton(DialogInterface.BUTTON_POSITIVE); + if (pbutton != null) { + pbutton.setTextAppearance(activity, R.style.buttonText); + } + } + }; + + public void setDeviceDialogListener(IConnectedDeviceDialogListener deviceDialogListener) { + this.deviceDialogListener = deviceDialogListener; + } + + public interface IConnectedDeviceDialogListener { + public void disconnectClick(); + } +} diff --git a/src/com/connectsdk/DeviceManager.java b/src/com/connectsdk/DeviceManager.java new file mode 100644 index 00000000..6ca564a8 --- /dev/null +++ b/src/com/connectsdk/DeviceManager.java @@ -0,0 +1,151 @@ +package com.connectsdk; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import com.connectsdk.device.ConnectableDevice; +import com.connectsdk.device.ConnectableDeviceListener; +import com.connectsdk.service.DeviceService; +import com.connectsdk.service.capability.MediaPlayer; +import com.connectsdk.service.capability.listeners.ResponseListener; +import com.connectsdk.service.command.ServiceCommandError; +import com.connectsdk.utils.ILogger; + +import java.util.List; + +public class DeviceManager implements ConnectableDeviceListener { + private ConnectableDevice mDevice; + private IDeviceStatusListener statusListener; + private ILogger logger; + private String TAG = "DeviceManager"; + private CastMediaPlayer mCastMediaPlayer; + private ConnectedDeviceDialog connectedDeviceDialog; + private Context mContext; + + public DeviceManager(Context context, ILogger logger) { + this.logger = logger; + this.mContext = context; + this.connectedDeviceDialog = new ConnectedDeviceDialog(); + this.connectedDeviceDialog.setDeviceDialogListener( + connectedDeviceDialogListener + ); + } + + public void pickDevice(ConnectableDevice device) { + if (mCastMediaPlayer != null) { + mCastMediaPlayer.destroyPlayer(null); + } + clearDevice(); + mDevice = device; + mCastMediaPlayer = new CastMediaPlayer(this.mContext, mDevice.getCapability(MediaPlayer.class), logger); + mDevice.addListener(this); + mDevice.connect(); + postDeviceStatus(CastState.CONNECTING, CastDevice.fromConnectableDevice(mDevice)); + } + + @Override + public void onDeviceReady(ConnectableDevice device) { + logger.log(Log.INFO, TAG, "onDeviceReady"); + mDevice = device; + postDeviceStatus(CastState.CONNECTED, CastDevice.fromConnectableDevice(device)); + } + + @Override + public void onDeviceDisconnected(ConnectableDevice device) { + logger.log(Log.INFO, TAG, "onDeviceDisconnected"); + mCastMediaPlayer.destroyPlayer(null); + mDevice.removeListener(this); + mDevice = null; + postDeviceStatus(CastState.NOT_CONNECTED, CastDevice.forDeviceNotAvailble()); + } + + @Override + public void onPairingRequired(ConnectableDevice device, DeviceService service, DeviceService.PairingType pairingType) { + logger.log(Log.INFO, TAG, "onPairingRequired"); + } + + @Override + public void onCapabilityUpdated(ConnectableDevice device, List added, List removed) { + logger.log(Log.INFO, TAG, "onCapabilityUpdated"); + } + + @Override + public void onConnectionFailed(ConnectableDevice device, ServiceCommandError error) { + logger.log(Log.INFO, TAG, "onConnectionFailed"); + } + + public void playMedia(CastMediaInfo castMediaInfo, final IMediaLifeCycleListener listener) { + mCastMediaPlayer.playMedia(castMediaInfo, new IMediaLifeCycleListener() { + + @Override + public void onMediaReady(RemoteMediaControl mediaControl) { + mediaControl.setDevice(CastDevice.fromConnectableDevice(mDevice)); + listener.onMediaReady(mediaControl); + } + + @Override + public void onMediaError() { + listener.onMediaError(); + } + } + ); + } + + public void setStatusListener(IDeviceStatusListener statusListener) { + this.statusListener = statusListener; + } + + @RequiresApi(api = Build.VERSION_CODES.CUPCAKE) + public AlertDialog getConnectedDeviceDialog(Activity activity) { + if (mDevice != null) { + return connectedDeviceDialog.getConnectedDeviceDialog(activity, mDevice.getFriendlyName()); + } + return null; + } + + private void postDeviceStatus(int status, CastDevice castDevice) { + if (statusListener != null) { + statusListener.onDeviceConnecting(status, castDevice); + } + } + + public void clearDevice() { + logger.log(Log.INFO, TAG, "clearDevice " + (mDevice != null)); + if (mDevice != null) { + mDevice.cancelPairing(); + mDevice.disconnect(); + } + } + + public void disconnect() { + if (mCastMediaPlayer != null) { + mCastMediaPlayer.destroyPlayer(new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + clearDevice(); + } + + @Override + public void onSuccess(Object object) { + clearDevice(); + } + } + ); + } + } + + private ConnectedDeviceDialog.IConnectedDeviceDialogListener connectedDeviceDialogListener = new ConnectedDeviceDialog.IConnectedDeviceDialogListener() { + + @Override + public void disconnectClick() { + logger.log(Log.INFO, TAG, "disconnect " + (mCastMediaPlayer != null)); + DeviceManager.this.disconnect(); + } + }; +} diff --git a/src/com/connectsdk/DiscoveryManagerWrapper.java b/src/com/connectsdk/DiscoveryManagerWrapper.java new file mode 100644 index 00000000..43f6eecb --- /dev/null +++ b/src/com/connectsdk/DiscoveryManagerWrapper.java @@ -0,0 +1,157 @@ +package com.connectsdk; + +import android.content.Context; +import android.util.Log; + +import com.connectsdk.device.ConnectableDevice; +import com.connectsdk.discovery.CapabilityFilter; +import com.connectsdk.discovery.DiscoveryManager; +import com.connectsdk.discovery.DiscoveryManagerListener; +import com.connectsdk.discovery.DiscoveryProvider; +import com.connectsdk.discovery.provider.CastDiscoveryProvider; +import com.connectsdk.service.CastService; +import com.connectsdk.service.command.ServiceCommandError; +import com.connectsdk.utils.ILogger; + +import java.util.ArrayList; +import java.util.List; + +class DiscoveryManagerWrapper implements DiscoveryManagerListener { + private ArrayList availableDevices = new ArrayList<>(); + private DiscoveryManager mDiscoveryManager; + private IDeviceAvailabilityListener availabilityListener; + private boolean isDeviceAvailable = false; + private ILogger logger; + private String TAG = "DiscoveryManager"; + + DiscoveryManagerWrapper(Context context, List capabilities, ILogger logger) { + this.logger = logger; + DiscoveryManager.init(context); + mDiscoveryManager = DiscoveryManager.getInstance(); + mDiscoveryManager.registerDeviceService(CastService.class, CastDiscoveryProvider.class); + mDiscoveryManager.setPairingLevel(DiscoveryManager.PairingLevel.ON); + mDiscoveryManager.setServiceIntegration(true); + mDiscoveryManager.addListener(this); + mDiscoveryManager.setCapabilityFilters(new CapabilityFilter(capabilities)); + mDiscoveryManager.start(); + } + + public void start() { + mDiscoveryManager.start(); + } + + public void stop() { + mDiscoveryManager.stop(); + } + + public void rescan() { + mDiscoveryManager.rescan(); + } + + public void setScanIntensity(DiscoveryProvider.ScanIntensity intensity) { + mDiscoveryManager.setScanIntensity(intensity); + } + + public void onDestory() { + mDiscoveryManager.onDestroy(); + } + + @Override + public void onDeviceAdded(DiscoveryManager manager, ConnectableDevice device) { + this.logger.log(Log.INFO, TAG, "OnDeviceAdded " + device.getFriendlyName()); + int index = -1; + for (int i = 0; i < availableDevices.size(); i++) { + ConnectableDevice d = availableDevices.get(i); + + String newDeviceName = device.getFriendlyName(); + String dName = d.getFriendlyName(); + + if (newDeviceName == null) { + newDeviceName = device.getModelName(); + } + + if (dName == null) { + dName = d.getModelName(); + } + + if (d.getIpAddress().equals(device.getIpAddress())) { + if (d.getFriendlyName().equals(device.getFriendlyName())) { + if (!manager.isServiceIntegrationEnabled() && d.getServiceId().equals(device.getServiceId())) { + availableDevices.remove(d); + availableDevices.add(i, device); + return; + } + } + } + + if (newDeviceName.compareToIgnoreCase(dName) < 0) { + index = i; + availableDevices.add(index, device); + break; + } + } + + if (index == -1) availableDevices.add(device); + setDeviceAvailability(); + } + + @Override + public void onDeviceUpdated(DiscoveryManager manager, ConnectableDevice device) { + this.logger.log(Log.INFO, TAG, "onDeviceUpdated " + device.getFriendlyName()); + for (String capability : device.getCapabilities()) { + this.logger.log(Log.INFO, TAG, capability); + } + setDeviceAvailability(); + } + + @Override + public void onDeviceRemoved(DiscoveryManager manager, ConnectableDevice device) { + this.logger.log(Log.INFO, TAG, "onDeviceRemoved " + device.getFriendlyName()); + for (String capability : device.getCapabilities()) { + this.logger.log(Log.INFO, TAG, capability); + } + availableDevices.remove(device); + setDeviceAvailability(); + } + + @Override + public void onDiscoveryFailed(DiscoveryManager manager, ServiceCommandError error) { + availableDevices.clear(); + this.logger.log(Log.INFO, TAG, "onDiscoveryFailed " + error.getMessage()); + setDeviceAvailability(); + } + + public void setDeviceAvailabilityListener(IDeviceAvailabilityListener listener) { + this.availabilityListener = listener; + } + + private void setDeviceAvailability() { + boolean available = this.availableDevices.size() > 0; + if (available != isDeviceAvailable) { + isDeviceAvailable = available; + if (this.availabilityListener != null) { + this.availabilityListener.onAvailabilityChange(isDeviceAvailable); + } + } + } + + public List getAvailableDevices() { + List devices = new ArrayList<>(); + for (ConnectableDevice connectableDevice : availableDevices) { + CastDevice device = CastDevice.fromConnectableDevice(connectableDevice); + devices.add(device); + } + return devices; + } + + public ConnectableDevice getConnectableDevice(CastDevice device) { + if (device != null) { + for (ConnectableDevice connectableDevice : availableDevices) { + if (connectableDevice.getId() != null && connectableDevice.getId().equals(device.getId())) { + return connectableDevice; + } + } + } + return null; + } +} diff --git a/src/com/connectsdk/ICastStateListener.java b/src/com/connectsdk/ICastStateListener.java new file mode 100644 index 00000000..3ea0e7db --- /dev/null +++ b/src/com/connectsdk/ICastStateListener.java @@ -0,0 +1,5 @@ +package com.connectsdk; + +public interface ICastStateListener { + void onCastStateChanged(CastStatus castStatus); +} diff --git a/src/com/connectsdk/IDeviceAvailabilityListener.java b/src/com/connectsdk/IDeviceAvailabilityListener.java new file mode 100644 index 00000000..aec3612c --- /dev/null +++ b/src/com/connectsdk/IDeviceAvailabilityListener.java @@ -0,0 +1,5 @@ +package com.connectsdk; + +interface IDeviceAvailabilityListener { + void onAvailabilityChange(boolean available); +} diff --git a/src/com/connectsdk/IDeviceStatusListener.java b/src/com/connectsdk/IDeviceStatusListener.java new file mode 100644 index 00000000..a5dffa63 --- /dev/null +++ b/src/com/connectsdk/IDeviceStatusListener.java @@ -0,0 +1,5 @@ +package com.connectsdk; + +public interface IDeviceStatusListener { + public void onDeviceConnecting(int castState, CastDevice castDevice); +} diff --git a/src/com/connectsdk/IMediaLifeCycleListener.java b/src/com/connectsdk/IMediaLifeCycleListener.java new file mode 100644 index 00000000..b3b7eb41 --- /dev/null +++ b/src/com/connectsdk/IMediaLifeCycleListener.java @@ -0,0 +1,7 @@ +package com.connectsdk; + +public interface IMediaLifeCycleListener { + public void onMediaReady(RemoteMediaControl mediaControl); + + public void onMediaError(); +} diff --git a/src/com/connectsdk/IRemoteMediaEventListener.java b/src/com/connectsdk/IRemoteMediaEventListener.java new file mode 100644 index 00000000..e2bacd82 --- /dev/null +++ b/src/com/connectsdk/IRemoteMediaEventListener.java @@ -0,0 +1,5 @@ +package com.connectsdk; + +public interface IRemoteMediaEventListener { + public void onEvent(RemoteMediaEvent mediaEvent); +} diff --git a/src/com/connectsdk/MediaEventType.java b/src/com/connectsdk/MediaEventType.java new file mode 100644 index 00000000..8b4409bf --- /dev/null +++ b/src/com/connectsdk/MediaEventType.java @@ -0,0 +1,10 @@ +package com.connectsdk; + +public class MediaEventType { + public static final String IDLE = "IDLE"; + public static final String DID_PLAY = "DID_PLAY"; + public static final String BUFFERING = "BUFFERING"; + public static final String PROGRESS = "PROGRESS"; + public static final String PAUSED = "PAUSED"; + public static final String FINISHED = "FINISHED"; +} diff --git a/src/com/connectsdk/RemoteMediaControl.java b/src/com/connectsdk/RemoteMediaControl.java new file mode 100644 index 00000000..cf4e41f4 --- /dev/null +++ b/src/com/connectsdk/RemoteMediaControl.java @@ -0,0 +1,269 @@ +package com.connectsdk; + +import android.util.Log; + +import com.connectsdk.core.MediaInfo; +import com.connectsdk.service.capability.MediaControl; +import com.connectsdk.service.capability.listeners.ResponseListener; +import com.connectsdk.service.command.ServiceCommandError; +import com.connectsdk.utils.ILogger; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class RemoteMediaControl { + private MediaControl mediaControl; + private ILogger logger; + protected ScheduledFuture updater; + private String TAG = "RemoteMediaControl"; + private ScheduledExecutorService EXECUTOR; + private int progressInterval = 1000; + private long playheadPosition = 0; + private long duration = 0; + private boolean userRequestPending = false; + private boolean playing = false; + private MediaInfo mediaInfo; + private CastDevice device; + private List> mediaEventListeners; + + public RemoteMediaControl(MediaControl mediaControl, MediaInfo mediaInfo, ILogger logger) { + this.mediaControl = mediaControl; + this.mediaControl.getDuration(remoteDurationListener); + this.logger = logger; + this.mediaInfo = mediaInfo; + EXECUTOR = Executors.newScheduledThreadPool(1); + this.mediaControl.subscribePlayState(stateListener); + this.mediaEventListeners = new ArrayList<>(); + } + + public MediaInfo getMediaInfo() { + return mediaInfo; + } + + public CastDevice getDevice() { + return device; + } + + public void setDevice(CastDevice device) { + this.device = device; + } + + private void startUpdater() { + if (this.updater == null) { + this.updater = + EXECUTOR.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + if (mediaControl != null && playing) { + if (!userRequestPending) mediaControl.getPosition( + positionListener + ); + postEvent(MediaEventType.PROGRESS, null); + } + } + }, + 0L, + (long) this.progressInterval, + TimeUnit.MILLISECONDS + ); + } + } + + protected void stopUpdater() { + logger.log(Log.INFO, TAG, "stopUpdater: " + this.updater); + if (this.updater != null) { + this.updater.cancel(false); + this.updater = null; + } + } + + public void stop(final ResponseListener listener) { + stopUpdater(); + userRequestPending = true; + mediaControl.stop(new ResponseListener() { + @Override + public void onError(ServiceCommandError error) { + postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + postSuccess(listener, object); + } + } + ); + } + + public void seek(final long position, final ResponseListener listener) { + logger.log(Log.INFO, TAG, "seek: " + position); + userRequestPending = true; + mediaControl.seek( + position, + new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + postSuccess(listener, object); + RemoteMediaControl.this.playheadPosition = position; + postEvent(MediaEventType.PROGRESS, null); + } + } + ); + } + + public void play(final ResponseListener listener) { + logger.log(Log.INFO, TAG, "play"); + userRequestPending = true; + mediaControl.play( + new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + mediaControl.getDuration(remoteDurationListener); + postSuccess(listener, object); + } + } + ); + } + + public void pause(final ResponseListener listener) { + logger.log(Log.INFO, TAG, "pause"); + userRequestPending = true; + this.mediaControl.pause( + new ResponseListener() { + + @Override + public void onError(ServiceCommandError error) { + postError(listener, error); + } + + @Override + public void onSuccess(Object object) { + postSuccess(listener, object); + } + } + ); + } + + private MediaControl.DurationListener remoteDurationListener = new MediaControl.DurationListener() { + + @Override + public void onSuccess(Long duration) { + RemoteMediaControl.this.duration = duration; + logger.log(Log.VERBOSE, TAG, "DurationListener: Success" + duration); + } + + @Override + public void onError(ServiceCommandError error) { + logger.log(Log.WARN, TAG, "DurationListener: Error" + error.getMessage()); + } + }; + + private MediaControl.PositionListener positionListener = new MediaControl.PositionListener() { + + @Override + public void onSuccess(Long position) { + RemoteMediaControl.this.playheadPosition = position; + } + + @Override + public void onError(ServiceCommandError error) { + logger.log(Log.WARN, TAG, "PositionListener: Error"); + } + }; + + private MediaControl.PlayStateListener stateListener = new MediaControl.PlayStateListener() { + + @Override + public void onSuccess(MediaControl.PlayStateStatus status) { + switch (status) { + case Idle: + postEvent(MediaEventType.IDLE, null); + break; + case Paused: + playing = false; + stopUpdater(); + postEvent(MediaEventType.PAUSED, null); + break; + case Playing: + if (!playing) { + startUpdater(); + playing = true; + } + postEvent(MediaEventType.PROGRESS, null); + break; + case Buffering: + postEvent(MediaEventType.BUFFERING, null); + break; + case Finished: + playing = false; + stopUpdater(); + postEvent(MediaEventType.FINISHED, null); + break; + } + } + + @Override + public void onError(ServiceCommandError error) { + } + }; + + public void addRemoteMediaEventListener(IRemoteMediaEventListener mediaEventListener) { + this.mediaEventListeners.add(new WeakReference<>(mediaEventListener)); + } + + public void removeRemoteMediaEventListener(IRemoteMediaEventListener mediaEventListener) { + this.mediaEventListeners.remove(new WeakReference<>(mediaEventListener)); + } + + private Map getDefaultEventProperties() { + Map properties = new HashMap(2); + properties.put("playheadPosition", this.playheadPosition + 2000); + properties.put("duration", duration); + return properties; + } + + private void postError(ResponseListener listener, ServiceCommandError error) { + userRequestPending = false; + if (listener != null) { + listener.onError(error); + } + } + + private void postSuccess(ResponseListener listener, Object success) { + userRequestPending = false; + if (listener != null) { + listener.onSuccess(success); + } + } + + private void postEvent(String eventType, Map extraProperties) { + Map basicProperties = getDefaultEventProperties(); + if (extraProperties != null) { + basicProperties.putAll(extraProperties); + } + RemoteMediaEvent mediaEvent = new RemoteMediaEvent(eventType, basicProperties); + for (WeakReference listener : mediaEventListeners) { + IRemoteMediaEventListener eventListener = listener.get(); + if (eventListener != null) + eventListener.onEvent(mediaEvent); + } + } +} diff --git a/src/com/connectsdk/RemoteMediaEvent.java b/src/com/connectsdk/RemoteMediaEvent.java new file mode 100644 index 00000000..404a47c0 --- /dev/null +++ b/src/com/connectsdk/RemoteMediaEvent.java @@ -0,0 +1,33 @@ +package com.connectsdk; + +import java.util.Map; + +public class RemoteMediaEvent { + private String eventType; + private Map properties; + + public RemoteMediaEvent(String eventType, Map properties) { + this.eventType = eventType; + this.properties = properties; + } + + public String getEventType() { + return eventType; + } + + public long getLong(String propertyName) { + if (properties != null && properties.containsKey(propertyName)) + return (long) properties.get(propertyName); + return 0; + } + + public String getString(String propertyName) { + if (properties != null && properties.containsKey(propertyName)) + return (String) properties.get(propertyName); + return null; + } + + public Map getProperties() { + return properties; + } +} diff --git a/src/com/connectsdk/notification/MediaNotificationManager.java b/src/com/connectsdk/notification/MediaNotificationManager.java new file mode 100644 index 00000000..d0728126 --- /dev/null +++ b/src/com/connectsdk/notification/MediaNotificationManager.java @@ -0,0 +1,239 @@ +package com.connectsdk.notification; + +import android.app.Application; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import com.connectsdk.CastState; +import com.connectsdk.CastStatus; +import com.connectsdk.ICastStateListener; +import com.connectsdk.IRemoteMediaEventListener; +import com.connectsdk.MediaEventType; +import com.connectsdk.R; +import com.connectsdk.RemoteMediaControl; +import com.connectsdk.RemoteMediaEvent; +import com.connectsdk.service.capability.listeners.ResponseListener; +import com.connectsdk.service.command.ServiceCommandError; +import com.connectsdk.utils.ILogger; + +import java.net.URL; + +public class MediaNotificationManager implements IRemoteMediaEventListener, ICastStateListener { + private Application application; + private static int notificationId = 1111; + NotificationManager notificationManager; + private RemoteMediaControl remoteMediaControl; + private ILogger logger; + private static final String TAG = "MediaNotificationManager"; + public static final String ACTION_PLAY = "action_play"; + public static final String ACTION_PAUSE = "action_pause"; + public static final String ACTION_STOP = "action_stop"; + private static final String channelId = "FanCodeCast"; + private boolean isPlayingUpdated = true; + private Notification.Action stopAction; + private INotificationListener notificationListener; + + public MediaNotificationManager(Application application, ILogger logger, INotificationListener listener) { + this.logger = logger; + this.application = application; + this.notificationListener = listener; + notificationManager = (NotificationManager) application.getSystemService(Context.NOTIFICATION_SERVICE); + this.stopAction = generateAction(android.R.drawable.ic_menu_delete, "Stop", ACTION_STOP); + } + + public void setCurrentRemoteMediaControl(RemoteMediaControl remoteMediaControl) { + if (remoteMediaControl != null + && remoteMediaControl.getDevice() != null + && "firetv".equalsIgnoreCase(remoteMediaControl.getDevice().getType())) { + if (this.remoteMediaControl != null) { + this.remoteMediaControl.removeRemoteMediaEventListener(this); + } + this.remoteMediaControl = remoteMediaControl; + buildNotification(generateAction(android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE)); + this.remoteMediaControl.addRemoteMediaEventListener(this); + } + } + + private void startNotificationService(Notification notification) { + MediaNotificationService.notificationManager = this; + MediaNotificationService.currentNotification = notification; + Intent serviceIntent = new Intent(application, MediaNotificationService.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + application.startForegroundService(serviceIntent); + } else { + application.startService(serviceIntent); + } + } + + private void stopNotificationService() { + application.stopService(new Intent(application, MediaNotificationService.class)); + } + + private Notification buildNotification(Notification.Action action) { + String title = this.remoteMediaControl.getMediaInfo().getTitle(); + String subTitle = "Casting on " + this.remoteMediaControl.getDevice().getName(); + Intent intent = new Intent(application, MediaNotificationService.class); + intent.setAction(ACTION_STOP); + Notification.BigPictureStyle notificationStyle = new Notification.BigPictureStyle(); + PendingIntent pendingIntent = PendingIntent.getService(application, 1, intent, PendingIntent.FLAG_IMMUTABLE); + + Notification.Builder builder = new Notification.Builder(application) + .setContentTitle(title) + .setContentText(subTitle) + .setDeleteIntent(pendingIntent) + .setSmallIcon(R.drawable.ic_notification) + .setOnlyAlertOnce(true) + .addAction(action) + .setColor(Color.argb(100, 255, 80, 0)) + .addAction(stopAction); + + try { + URL url = new URL(this.remoteMediaControl.getMediaInfo().getImages().get(0).getUrl()); + Bitmap image = BitmapFactory.decodeStream(url.openConnection().getInputStream()); + builder.setStyle(notificationStyle.bigPicture(image)); + } catch (Exception e) { + System.out.println(e); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setChannelId(channelId); + } + Notification notification = builder.build(); + MediaNotificationService.notificationManager = this; + MediaNotificationService.currentNotification = notification; + notificationManager.notify(notificationId, notification); + return notification; + } + + private Notification.Action generateAction(int icon, String title, String intentAction) { + Intent intent = new Intent(application, MediaNotificationService.class); + intent.setAction(intentAction); + PendingIntent pendingIntent = PendingIntent.getService(application, 1, intent, PendingIntent.FLAG_IMMUTABLE); + return new Notification.Action.Builder(icon, title, pendingIntent).build(); + } + + + public void handleIntent(Intent intent) { + if (intent == null || intent.getAction() == null || this.remoteMediaControl == null) return; + String action = intent.getAction(); + if (action.equalsIgnoreCase(ACTION_PLAY)) { + this.remoteMediaControl.play(null); + } else if (action.equalsIgnoreCase(ACTION_PAUSE)) { + this.remoteMediaControl.pause(null); + } else if (action.equalsIgnoreCase(ACTION_STOP)) { + stopMedia(); + } + } + + private void stopMedia() { + if (this.remoteMediaControl != null) { + this.remoteMediaControl.stop(new ResponseListener() { + @Override + public void onError(ServiceCommandError error) { + stopCasting(); + } + + @Override + public void onSuccess(Object object) { + stopCasting(); + } + }); + } + } + + + @Override + public void onEvent(RemoteMediaEvent mediaEvent) { + if (mediaEvent.getEventType() == MediaEventType.PROGRESS && !isPlayingUpdated) { + isPlayingUpdated = true; + buildNotification(generateAction(android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE) + ); + } else if (mediaEvent.getEventType() == MediaEventType.PAUSED) { + isPlayingUpdated = false; + buildNotification(generateAction(android.R.drawable.ic_media_play, "Play", ACTION_PLAY)); + } else if (mediaEvent.getEventType() == MediaEventType.FINISHED) { + isPlayingUpdated = false; + notificationManager.cancel(notificationId); + } + } + + @Override + public void onCastStateChanged(CastStatus castStatus) { + if (castStatus.getCastState() == CastState.NOT_CONNECTED || castStatus.getCastState() == CastState.NO_DEVICE_AVAILABLE) { + isPlayingUpdated = false; + notificationManager.cancel(notificationId); + } + } + + private void stopCasting() { + isPlayingUpdated = false; + notificationManager.cancel(notificationId); + if (notificationListener != null) + notificationListener.onCastMediaStopped(); + } + + public static class MediaNotificationService extends Service { + private static MediaNotificationManager notificationManager; + private static Notification currentNotification; + + @Override + public void onCreate() { + super.onCreate(); + if (currentNotification != null) { + startForeground(notificationId, currentNotification); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (notificationManager != null) { + notificationManager.handleIntent(intent); + } + return START_NOT_STICKY; + } + + @Override + public boolean onUnbind(Intent intent) { + return super.onUnbind(intent); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + if (notificationManager != null) { + stopSelf(); + notificationManager.logger.log(Log.INFO, TAG, "onTaskRemoved"); + notificationManager.cleanNotification(); + } + super.onTaskRemoved(rootIntent); + } + } + + private void cleanNotification() { + if (notificationManager != null) { + notificationManager.cancel(notificationId); + stopNotificationService(); + } + } + + public void handleExit() { + cleanNotification(); + } + + public interface INotificationListener { + public void onCastMediaStopped(); + } +} diff --git a/src/com/connectsdk/utils/ActivityTracker.java b/src/com/connectsdk/utils/ActivityTracker.java new file mode 100644 index 00000000..9f0a34cf --- /dev/null +++ b/src/com/connectsdk/utils/ActivityTracker.java @@ -0,0 +1,42 @@ +package com.connectsdk.utils; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +public class ActivityTracker implements Application.ActivityLifecycleCallbacks { + private Activity currentActivity; + + public ActivityTracker(Activity activity) { + this.currentActivity = activity; + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} + + @Override + public void onActivityStarted(Activity activity) {} + + @Override + public void onActivityResumed(Activity activity) { + currentActivity = activity; + } + + @Override + public void onActivityPaused(Activity activity) { + currentActivity = null; + } + + @Override + public void onActivityStopped(Activity activity) {} + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + + @Override + public void onActivityDestroyed(Activity activity) {} + + public Activity getCurrentActivity() { + return currentActivity; + } +} diff --git a/src/com/connectsdk/utils/DefaultLogger.java b/src/com/connectsdk/utils/DefaultLogger.java new file mode 100644 index 00000000..25919efe --- /dev/null +++ b/src/com/connectsdk/utils/DefaultLogger.java @@ -0,0 +1,102 @@ +package com.connectsdk.utils; + +import android.util.Log; + +public class DefaultLogger implements ILogger { + private int logLevel; + + public DefaultLogger(int logLevel) { + this.logLevel = logLevel; + } + + public DefaultLogger() { + this.logLevel = Log.INFO; + } + + @Override + public boolean isLoggable(String tag, int level) { + return logLevel <= level; + } + + @Override + public int getLogLevel() { + return logLevel; + } + + @Override + public void setLogLevel(int logLevel) { + this.logLevel = logLevel; + } + + @Override + public void d(String tag, String text, Throwable throwable) { + if (isLoggable(tag, Log.DEBUG)) { + Log.d(tag, text, throwable); + } + } + + @Override + public void v(String tag, String text, Throwable throwable) { + if (isLoggable(tag, Log.VERBOSE)) { + Log.v(tag, text, throwable); + } + } + + @Override + public void i(String tag, String text, Throwable throwable) { + if (isLoggable(tag, Log.INFO)) { + Log.i(tag, text, throwable); + } + } + + @Override + public void w(String tag, String text, Throwable throwable) { + if (isLoggable(tag, Log.WARN)) { + Log.w(tag, text, throwable); + } + } + + @Override + public void e(String tag, String text, Throwable throwable) { + if (isLoggable(tag, Log.ERROR)) { + Log.e(tag, text, throwable); + } + } + + @Override + public void d(String tag, String text) { + d(tag, text, null); + } + + @Override + public void v(String tag, String text) { + v(tag, text, null); + } + + @Override + public void i(String tag, String text) { + i(tag, text, null); + } + + @Override + public void w(String tag, String text) { + w(tag, text, null); + } + + @Override + public void e(String tag, String text) { + e(tag, text, null); + } + + @Override + public void log(int priority, String tag, String msg) { + log(priority, tag, msg, false); + } + + @Override + public void log(int priority, String tag, String msg, boolean forceLog) { + if (forceLog || isLoggable(tag, priority)) { + Log.println(priority, tag, msg); + } + } +} diff --git a/src/com/connectsdk/utils/ILogger.java b/src/com/connectsdk/utils/ILogger.java new file mode 100644 index 00000000..61fc00b7 --- /dev/null +++ b/src/com/connectsdk/utils/ILogger.java @@ -0,0 +1,33 @@ +package com.connectsdk.utils; + +public interface ILogger { + boolean isLoggable(String tag, int level); + + int getLogLevel(); + + void setLogLevel(int logLevel); + + void d(String tag, String text, Throwable throwable); + + void v(String tag, String text, Throwable throwable); + + void i(String tag, String text, Throwable throwable); + + void w(String tag, String text, Throwable throwable); + + void e(String tag, String text, Throwable throwable); + + void d(String tag, String text); + + void v(String tag, String text); + + void i(String tag, String text); + + void w(String tag, String text); + + void e(String tag, String text); + + void log(int priority, String tag, String msg); + + void log(int priority, String tag, String msg, boolean forceLog); +}