diff --git a/app/build.gradle b/app/build.gradle index 772f76f68..d3668110b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -114,8 +114,10 @@ dependencies { implementation('org.simpleframework:simple-xml:2.7.1') { exclude group: 'stax', module: 'stax-api' exclude group: 'xpp3', module: 'xpp3' } leanbackImplementation 'androidx.leanback:leanback:1.2.0-alpha02' leanbackImplementation 'me.jessyan:autosize:1.2.1' + mobileImplementation 'androidx.mediarouter:mediarouter:1.3.0' mobileImplementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' mobileImplementation 'com.github.devin1014.DLNA-Cast:dlna-dmc:V1.0.0' + mobileImplementation 'com.google.android.gms:play-services-cast-framework:21.3.0' mobileImplementation('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } annotationProcessor 'androidx.room:room-compiler:2.5.1' annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a74243cb5..46f459cfe 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -35,6 +35,9 @@ -keep class com.github.catvod.crawler.** { *; } -keep class * extends com.github.catvod.crawler.Spider +# Cast +-keep class * implements com.google.android.gms.cast.framework.OptionsProvider + # Cling -keep class org.fourthline.cling.** { *; } diff --git a/app/src/main/java/com/fongmi/android/tv/bean/Device.java b/app/src/main/java/com/fongmi/android/tv/bean/Device.java index 3c9fb8a81..c60c6c347 100644 --- a/app/src/main/java/com/fongmi/android/tv/bean/Device.java +++ b/app/src/main/java/com/fongmi/android/tv/bean/Device.java @@ -88,6 +88,10 @@ public class Device { this.type = type; } + public boolean isLeanback() { + return getType() == 0; + } + public boolean isMobile() { return getType() == 1; } @@ -96,8 +100,16 @@ public class Device { return getType() == 2; } + public boolean isCast() { + return getType() == 3; + } + + public boolean isApp() { + return isLeanback() || isMobile(); + } + public String getHost() { - return isDLNA() ? getUuid() : Uri.parse(getIp()).getHost(); + return isDLNA() || isCast() ? getUuid() : Uri.parse(getIp()).getHost(); } public Device save() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0a1f3776b..b47158eb9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,7 +2,7 @@ TV - C0868879 + 08955620 Press back again to exit diff --git a/app/src/mobile/AndroidManifest.xml b/app/src/mobile/AndroidManifest.xml index d59b78ccb..7e9df0c00 100644 --- a/app/src/mobile/AndroidManifest.xml +++ b/app/src/mobile/AndroidManifest.xml @@ -6,6 +6,10 @@ + + > devices; + private final List routers; private static class Loader { static volatile CastDevice INSTANCE = new CastDevice(); @@ -19,6 +25,7 @@ public class CastDevice { public CastDevice() { this.devices = new ArrayList<>(); + this.routers = new ArrayList<>(); } public boolean isEmpty() { @@ -33,16 +40,39 @@ public class CastDevice { return device; } - public List getAll() { + private com.fongmi.android.tv.bean.Device create(MediaRouter.RouteInfo item) { + com.fongmi.android.tv.bean.Device device = new com.fongmi.android.tv.bean.Device(); + device.setName(item.getName()); + device.setUuid(item.getId()); + device.setType(3); + return device; + } + + public List getDLNA() { List items = new ArrayList<>(); - for (Device device : devices) items.add(create(device)); + for (Device item : devices) items.add(create(item)); return items; } - public List add(Device device) { - devices.remove(device); - devices.add(device); - return getAll(); + public List getCast() { + List items = new ArrayList<>(); + for (MediaRouter.RouteInfo item : routers) items.add(create(item)); + return items; + } + + public List add(Device item) { + devices.remove(item); + devices.add(item); + return getDLNA(); + } + + @SuppressLint("RestrictedApi") + public List add(List items) { + ArrayList routes = new ArrayList<>(items); + onFilterRoutes(routes); + routers.clear(); + routers.addAll(routes); + return getCast(); } public com.fongmi.android.tv.bean.Device remove(Device device) { @@ -50,8 +80,22 @@ public class CastDevice { return create(device); } - public Device find(com.fongmi.android.tv.bean.Device item) { + public Device findDLNA(com.fongmi.android.tv.bean.Device item) { for (Device device : devices) if (device.getIdentity().getUdn().getIdentifierString().equals(item.getUuid())) return device; return null; } + + public MediaRouter.RouteInfo findCast(com.fongmi.android.tv.bean.Device item) { + for (MediaRouter.RouteInfo device : routers) if (device.getId().equals(item.getUuid())) return device; + return null; + } + + private void onFilterRoutes(@NonNull List routes) { + for (int i = routes.size(); i-- > 0; ) if (!onFilterRoute(routes.get(i))) routes.remove(i); + } + + @SuppressLint("RestrictedApi") + private boolean onFilterRoute(@NonNull MediaRouter.RouteInfo route) { + return !route.isDefaultOrBluetooth() && route.isEnabled(); + } } diff --git a/app/src/mobile/java/com/fongmi/android/tv/cast/CastOptionsProvider.java b/app/src/mobile/java/com/fongmi/android/tv/cast/CastOptionsProvider.java new file mode 100644 index 000000000..bda5960b9 --- /dev/null +++ b/app/src/mobile/java/com/fongmi/android/tv/cast/CastOptionsProvider.java @@ -0,0 +1,62 @@ +package com.fongmi.android.tv.cast; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.fongmi.android.tv.R; +import com.fongmi.android.tv.ui.activity.DetailActivity; +import com.google.android.gms.cast.LaunchOptions; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.framework.CastOptions; +import com.google.android.gms.cast.framework.OptionsProvider; +import com.google.android.gms.cast.framework.SessionProvider; +import com.google.android.gms.cast.framework.media.CastMediaOptions; +import com.google.android.gms.cast.framework.media.ImageHints; +import com.google.android.gms.cast.framework.media.ImagePicker; +import com.google.android.gms.cast.framework.media.MediaIntentReceiver; +import com.google.android.gms.cast.framework.media.NotificationOptions; +import com.google.android.gms.common.images.WebImage; + +import java.util.Arrays; +import java.util.List; + +public class CastOptionsProvider implements OptionsProvider { + + @NonNull + @Override + public CastOptions getCastOptions(Context context) { + NotificationOptions notificationOptions = new NotificationOptions.Builder().setActions(Arrays.asList(MediaIntentReceiver.ACTION_SKIP_NEXT, MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK, MediaIntentReceiver.ACTION_STOP_CASTING), new int[]{1, 2}).setTargetActivityClassName(DetailActivity.class.getName()).build(); + CastMediaOptions mediaOptions = new CastMediaOptions.Builder().setImagePicker(new ImagePickerImpl()).setNotificationOptions(notificationOptions).setExpandedControllerActivityClassName(DetailActivity.class.getName()).build(); + LaunchOptions launchOptions = new LaunchOptions.Builder().setAndroidReceiverCompatible(true).build(); + return new CastOptions.Builder().setLaunchOptions(launchOptions).setReceiverApplicationId(context.getString(R.string.app_id)).setCastMediaOptions(mediaOptions).build(); + } + + @Nullable + @Override + public List getAdditionalSessionProviders(@NonNull Context context) { + return null; + } + + private static class ImagePickerImpl extends ImagePicker { + + @Override + public WebImage onPickImage(MediaMetadata mediaMetadata, ImageHints hints) { + int type = hints.getType(); + if ((mediaMetadata == null) || !mediaMetadata.hasImages()) { + return null; + } + List images = mediaMetadata.getImages(); + if (images.size() == 1) { + return images.get(0); + } else { + if (type == ImagePicker.IMAGE_TYPE_MEDIA_ROUTE_CONTROLLER_DIALOG_BACKGROUND) { + return images.get(0); + } else { + return images.get(1); + } + } + } + } +} diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/activity/DetailActivity.java b/app/src/mobile/java/com/fongmi/android/tv/ui/activity/DetailActivity.java index 9e22765e1..eb91125fd 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/activity/DetailActivity.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/activity/DetailActivity.java @@ -44,18 +44,17 @@ import com.fongmi.android.tv.event.ErrorEvent; import com.fongmi.android.tv.event.PlayerEvent; import com.fongmi.android.tv.event.RefreshEvent; import com.fongmi.android.tv.model.SiteViewModel; -import com.fongmi.android.tv.utils.PiP; -import com.fongmi.android.tv.receiver.PiPReceiver; import com.fongmi.android.tv.player.ExoUtil; import com.fongmi.android.tv.player.Players; +import com.fongmi.android.tv.receiver.PiPReceiver; import com.fongmi.android.tv.ui.adapter.EpisodeAdapter; import com.fongmi.android.tv.ui.adapter.FlagAdapter; import com.fongmi.android.tv.ui.adapter.ParseAdapter; import com.fongmi.android.tv.ui.adapter.SearchAdapter; import com.fongmi.android.tv.ui.base.BaseActivity; +import com.fongmi.android.tv.ui.base.ViewType; import com.fongmi.android.tv.ui.custom.CustomKeyDownVod; import com.fongmi.android.tv.ui.custom.SpaceItemDecoration; -import com.fongmi.android.tv.ui.base.ViewType; import com.fongmi.android.tv.ui.custom.dialog.CastDialog; import com.fongmi.android.tv.ui.custom.dialog.ControlDialog; import com.fongmi.android.tv.ui.custom.dialog.EpisodeGridDialog; @@ -63,10 +62,12 @@ import com.fongmi.android.tv.ui.custom.dialog.EpisodeListDialog; import com.fongmi.android.tv.ui.custom.dialog.TrackDialog; import com.fongmi.android.tv.utils.Clock; import com.fongmi.android.tv.utils.Notify; +import com.fongmi.android.tv.utils.PiP; import com.fongmi.android.tv.utils.Prefers; import com.fongmi.android.tv.utils.ResUtil; import com.fongmi.android.tv.utils.Traffic; import com.fongmi.android.tv.utils.Utils; +import com.google.android.gms.cast.framework.CastContext; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.permissionx.guolindev.PermissionX; @@ -94,6 +95,7 @@ public class DetailActivity extends BaseActivity implements Clock.Callback, Cust private ExecutorService mExecutor; private SiteViewModel mViewModel; private FlagAdapter mFlagAdapter; + private CastContext mCastContext; private List mDialogs; private PiPReceiver mReceiver; private History mHistory; diff --git a/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/DeviceAdapter.java b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/DeviceAdapter.java index 63d00e742..c70373fc8 100644 --- a/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/DeviceAdapter.java +++ b/app/src/mobile/java/com/fongmi/android/tv/ui/adapter/DeviceAdapter.java @@ -49,7 +49,7 @@ public class DeviceAdapter extends RecyclerView.Adapter getIps() { List ips = new ArrayList<>(); - for (Device item : mItems) if (!item.isDLNA()) ips.add(item.getIp()); + for (Device item : mItems) if (item.isApp()) ips.add(item.getIp()); return ips; } @@ -69,8 +69,14 @@ public class DeviceAdapter extends RecyclerView.Adapter mListener.onItemClick(item)); - holder.binding.type.setImageResource(item.isMobile() ? R.drawable.ic_cast_mobile : R.drawable.ic_cast_tv); + } + + private int getIcon(Device item) { + if (item.isCast()) return R.drawable.ic_control_cast; + else if (item.isMobile()) return R.drawable.ic_cast_mobile; + return R.drawable.ic_cast_tv; } static class ViewHolder extends RecyclerView.ViewHolder {