diff --git a/app/build.gradle b/app/build.gradle index 833e446..8f31828 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,7 +5,11 @@ plugins { android { namespace 'com.eanyatonic.cctvViewer' compileSdk 33 - + sourceSets { + main { + assets.srcDirs = ['src/main/assets', 'src\\main\\assets'] + } + } defaultConfig { applicationId "com.eanyatonic.cctvViewer" minSdk 21 @@ -31,8 +35,8 @@ android { dependencies { implementation 'androidx.leanback:leanback:1.0.0' - implementation 'com.github.bumptech.glide:glide:4.11.0' - implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'com.github.bumptech.glide:glide:4.16.0' + implementation 'androidx.appcompat:appcompat:1.6.1' api 'com.tencent.tbs:tbssdk:44286' implementation 'androidx.recyclerview:recyclerview:1.3.2' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 17409d6..b74b224 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="MissingLeanbackLauncher"> @@ -8,6 +9,9 @@ + { + setTimeout(() => { + resolve(); + }, time); + }); + } + + async function playback(){ + if(document.querySelector("#play_or_pause_play_player").style.display==="none"){ + document.querySelector("#play_or_plause_player").click() + } + await sleep1(3) + const targetElement = document.querySelector("#timeshift_pointer_player") + mouseDragStart(targetElement); + mouseDragEnd(targetElement); + }; + + playback() + console.log('执行了回退'); \ No newline at end of file diff --git a/app/src/main/assets/js/cctvOpen.js b/app/src/main/assets/js/cctvOpen.js new file mode 100644 index 0000000..73c5d3e --- /dev/null +++ b/app/src/main/assets/js/cctvOpen.js @@ -0,0 +1,37 @@ +// 定义休眠函数 +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// 页面加载完成后执行 JavaScript 脚本 +let interval = setInterval(async function executeScript() { + console.log('页面加载完成!'); + + // 休眠 1000 毫秒(1秒) + await sleep(1000); + + // 休眠 50 毫秒 + await sleep(50); + + console.log('点击分辨率按钮'); + var elem = document.querySelector('#resolution_item_720_player'); + elem.click(); + + // 休眠 50 毫秒 + await sleep(50); + + console.log('设置音量并点击音量按钮'); + var btn = document.querySelector('#player_sound_btn_player'); + btn.setAttribute('volume', 100); + btn.click(); + btn.click(); + btn.click(); + + // 休眠 50 毫秒 + await sleep(50); + + console.log('点击全屏按钮'); + var fullscreenBtn = document.querySelector('#player_pagefullscreen_yes_player'); + fullscreenBtn.click(); + clearInterval(interval); +}, 3000); \ No newline at end of file diff --git a/app/src/main/assets/js/forwardScript.js b/app/src/main/assets/js/forwardScript.js new file mode 100644 index 0000000..28b4641 --- /dev/null +++ b/app/src/main/assets/js/forwardScript.js @@ -0,0 +1,93 @@ + function simulate(element, eventName) { + var options = extend(defaultOptions, arguments[2] || {}); + var oEvent, eventType = null; + + for (var name in eventMatchers) { + if (eventMatchers[name].test(eventName)) { + eventType = name; + break; + } + } + + if (!eventType) throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported'); + + if (document.createEvent) { + oEvent = document.createEvent(eventType); + if (eventType == 'HTMLEvents') { + oEvent.initEvent(eventName, options.bubbles, options.cancelable); + } else { + oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView, options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element); + } + element.dispatchEvent(oEvent); + } else { + options.clientX = options.pointerX; + options.clientY = options.pointerY; + var evt = document.createEventObject(); + oEvent = extend(evt, options); + element.fireEvent('on' + eventName, oEvent); + } + return element; + } + + function extend(destination, source) { + for (var property in source) destination[property] = source[property]; + return destination; + } + + var eventMatchers = { + 'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, + 'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/ + } + var defaultOptions = { + pointerX: 0, + pointerY: 0, + button: 0, + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + bubbles: true, + cancelable: true + } + + function triggerMouseEvent (node, eventType) { + var clickEvent = document.createEvent ('MouseEvents'); + clickEvent.initEvent (eventType, true, true); + node.dispatchEvent (clickEvent); + }; + + function mouseDragStart(node) { + console.log("Starting drag..."); + console.log(node.offsetTop); + console.log(node.offsetLeft); + triggerMouseEvent(node, "mousedown") + } + + function mouseDragEnd(node){ + console.log("Ending drag..."); + const rect = node.getBoundingClientRect(); + document.querySelector("#epg_right_shift_player").click() + simulate(node, "mousemove" , {pointerX: rect.x+20 , pointerY: node.offsetTop}) + simulate(node, "mouseup" , {pointerX: rect.x+20, pointerY: node.offsetTop}) + } + function sleep1(time) { + time*=1000 + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, time); + }); + } + + async function playback(){ + if(document.querySelector("#play_or_pause_play_player").style.display==="none"){ + document.querySelector("#play_or_plause_player").click() + } + await sleep1(3) + const targetElement = document.querySelector("#timeshift_pointer_player") + mouseDragStart(targetElement); + mouseDragEnd(targetElement); + }; + + playback() + console.log('执行了前进'); \ No newline at end of file diff --git a/app/src/main/assets/js/getEpgScript.js b/app/src/main/assets/js/getEpgScript.js new file mode 100644 index 0000000..66290a1 --- /dev/null +++ b/app/src/main/assets/js/getEpgScript.js @@ -0,0 +1,39 @@ +{ + // 定义休眠函数 + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // 页面加载完成后执行 JavaScript 脚本 + let interval = setInterval(async function executeScript() { + console.log('页面加载完成!'); + + // 休眠 1000 毫秒(1秒) + await sleep(1000); + + // 休眠 50 毫秒 + await sleep(50); + + console.log('点击分辨率按钮'); + var elem = document.querySelector('#resolution_item_720_player'); + elem.click(); + + // 休眠 50 毫秒 + await sleep(50); + + console.log('设置音量并点击音量按钮'); + var btn = document.querySelector('#player_sound_btn_player'); + btn.setAttribute('volume', 100); + btn.click(); + btn.click(); + btn.click(); + + // 休眠 50 毫秒 + await sleep(50); + + console.log('点击全屏按钮'); + var fullscreenBtn = document.querySelector('#player_pagefullscreen_yes_player'); + fullscreenBtn.click(); + clearInterval(interval); + }, 3000); +} \ No newline at end of file diff --git a/app/src/main/java/com/eanyatonic/cctvViewer/Channel.java b/app/src/main/java/com/eanyatonic/cctvViewer/Channel.java deleted file mode 100644 index 64837cd..0000000 --- a/app/src/main/java/com/eanyatonic/cctvViewer/Channel.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.eanyatonic.cctvViewer; - -public class Channel { - private String name; - - public Channel(String name) { - this.name = name; - } - - public String getName() { - return name; - } -} diff --git a/app/src/main/java/com/eanyatonic/cctvViewer/ChannelAdapter.java b/app/src/main/java/com/eanyatonic/cctvViewer/ChannelAdapter.java index e272511..365287e 100644 --- a/app/src/main/java/com/eanyatonic/cctvViewer/ChannelAdapter.java +++ b/app/src/main/java/com/eanyatonic/cctvViewer/ChannelAdapter.java @@ -1,20 +1,35 @@ package com.eanyatonic.cctvViewer; import android.util.Log; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.eanyatonic.cctvViewer.bean.EpgInfo; + import java.util.List; public class ChannelAdapter extends RecyclerView.Adapter { - private List channelList; + private int selectedItem = -1; // 用于跟踪选中的项 + + private List channelList; + private OnItemClickListener onItemClickListener; + private OnFocusChangeListener onFocusChangeListener; + private RecyclerView recyclerView; + private com.tencent.smtt.sdk.WebView view; - public ChannelAdapter(List channelList) { + public ChannelAdapter(List channelList, OnItemClickListener onItemClickListener,OnFocusChangeListener onFocusChangeListener,RecyclerView recyclerView,com.tencent.smtt.sdk.WebView view) { this.channelList = channelList; + this.onItemClickListener = onItemClickListener; + this.onFocusChangeListener = onFocusChangeListener; + this.recyclerView = recyclerView; + this.view = view; } @Override @@ -25,27 +40,160 @@ public ChannelViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { @Override public void onBindViewHolder(ChannelViewHolder holder, int position) { - Channel channel = channelList.get(position); + // 设置选中状态的背景和字体颜色 + if (position == selectedItem) { + holder.channelNameTextView.setBackgroundResource(R.drawable.channel_background_unselected); + holder.channelNameTextView.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.unselected_color)); + } else { + holder.channelNameTextView.setBackgroundResource(R.drawable.channel_background_selected); + holder.channelNameTextView.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.text_color)); + } + EpgInfo channel = channelList.get(position); holder.bind(channel); - Log.d("ChannelAdapter", "Bound channel: " + channel.getName()); + Log.d("ChannelAdapterxx", "Bound channel: " + channel.getName()); + holder.channelNameTextView.setOnKeyListener((v, keyCode, event) -> { + notifyDataSetChanged(); + int action = event.getAction(); + if (event.getAction() == KeyEvent.ACTION_DOWN){ + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + int positionUp = position; + if (action == KeyEvent.ACTION_DOWN) { + if (positionUp <= 0) { + recyclerView.smoothScrollToPosition(getItemCount() - 1); + return true; + } + } + break; + + case KeyEvent.KEYCODE_DPAD_DOWN: + int positionDown = position; + if (action == KeyEvent.ACTION_DOWN) { + if (positionDown >= getItemCount() - 1) { + recyclerView.smoothScrollToPosition(0); + return true; + } + } + break; + case KeyEvent.KEYCODE_ENTER: + EpgInfo epgInfo = channelList.get(position); + Log.d(epgInfo.getName(),epgInfo.getName()+"xxx"); + view.evaluateJavascript("async function xx(){document.querySelector('#play_or_plause_player').click();await sleep(3000);" + + "document.querySelector('#" + epgInfo.getId() + "')" + ".click();"+"}"+"xx()" + , null); + return true; + } + } + + return false; + }); + + recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { + + //向上或者向下滚动 + boolean toLast = false; + boolean toFirst = false; + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + scroolChange(recyclerView, toFirst, toLast); + } + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + if (dy > 0) { + toLast = true; + } else { + toFirst = true; + } + } + }); } + public void scroolChange(RecyclerView recyclerView, boolean toFirst, boolean toLast) { + LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager(); + int lastVisibleItem = manager.findLastCompletelyVisibleItemPosition(); + int firstVisibleItem = manager.findFirstCompletelyVisibleItemPosition(); + int totalItemCount = manager.getItemCount(); + + //向下滚动,到底部 + if (lastVisibleItem == (totalItemCount - 1) && toLast) { + View view = recyclerView.getChildAt(lastVisibleItem); + LinearLayoutManager llM = (LinearLayoutManager) recyclerView.getLayoutManager(); + if (view != null) { + view.requestFocus(); + } else if (llM.findViewByPosition(lastVisibleItem) != null) { + llM.findViewByPosition(lastVisibleItem).requestFocus(); + } else { + recyclerView.requestFocus(); + } + } + + //向上滚动,到顶部 + if (firstVisibleItem == 0 && toFirst) { + View view =recyclerView.getChildAt(firstVisibleItem); + LinearLayoutManager llM = (LinearLayoutManager) recyclerView.getLayoutManager(); + if (view != null) { + view.requestFocus(); + } else if (llM.findViewByPosition(firstVisibleItem) != null) { + llM.findViewByPosition(firstVisibleItem).requestFocus(); + } else { + recyclerView.requestFocus(); + } + } + } + + @Override public int getItemCount() { return channelList.size(); } public class ChannelViewHolder extends RecyclerView.ViewHolder { - private TextView channelNameTextView; + public TextView channelNameTextView; + public ChannelViewHolder(View itemView) { super(itemView); channelNameTextView = itemView.findViewById(R.id.channelNameTextView); + + channelNameTextView.setOnClickListener(v -> { + onItemClickListener.onItemClick(v,getAbsoluteAdapterPosition()); + }); + + + channelNameTextView.setOnFocusChangeListener((v, hasFocus) -> { + + onFocusChangeListener.onFocusChangeListener(v,hasFocus); + }); + + + + + } - public void bind(Channel channel) { + public void bind(EpgInfo channel) { channelNameTextView.setText(channel.getName()); } + } + + // 接口定义 + public interface OnItemClickListener { + void onItemClick(View view,int position); + } + + public interface OnFocusChangeListener { + void onFocusChangeListener(View view,boolean hasFocus); + } + + + + } diff --git a/app/src/main/java/com/eanyatonic/cctvViewer/MainActivity.java b/app/src/main/java/com/eanyatonic/cctvViewer/MainActivity.java index fc72c69..2ebb7c7 100644 --- a/app/src/main/java/com/eanyatonic/cctvViewer/MainActivity.java +++ b/app/src/main/java/com/eanyatonic/cctvViewer/MainActivity.java @@ -1,9 +1,9 @@ package com.eanyatonic.cctvViewer; +import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; -import android.content.res.Configuration; import android.graphics.Color; import android.os.Build; import android.os.Bundle; @@ -14,37 +14,50 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.webkit.JavascriptInterface; import android.widget.FrameLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.eanyatonic.cctvViewer.bean.EpgInfo; +import com.eanyatonic.cctvViewer.tools.FileTool; import com.tencent.smtt.export.external.TbsCoreSettings; import com.tencent.smtt.export.external.interfaces.IX5WebChromeClient; +import com.tencent.smtt.export.external.interfaces.SslError; +import com.tencent.smtt.export.external.interfaces.SslErrorHandler; import com.tencent.smtt.sdk.QbSdk; -import com.tencent.smtt.sdk.ValueCallback; import com.tencent.smtt.sdk.WebChromeClient; import com.tencent.smtt.sdk.WebSettings; import com.tencent.smtt.sdk.WebView; import com.tencent.smtt.sdk.WebViewClient; +import org.json.JSONArray; +import org.json.JSONException; + import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Objects; + +public class MainActivity extends AppCompatActivity implements ChannelAdapter.OnItemClickListener,ChannelAdapter.OnFocusChangeListener{ -public class MainActivity extends AppCompatActivity { + public FileTool filetool; private RecyclerView recyclerView; private View mCustomView; private FrameLayout mFrameLayout; private IX5WebChromeClient.CustomViewCallback mCustomViewCallback; private com.tencent.smtt.sdk.WebView webView; // 导入 X5 WebView + private com.tencent.smtt.sdk.WebView cctvFinishedView; + private ChannelAdapter.ChannelViewHolder viewHolder; - private String[] liveUrls = { + private ChannelAdapter channelAdapter; + private final String[] liveUrls = { "https://tv.cctv.com/live/cctv1/", "https://tv.cctv.com/live/cctv2/", "https://tv.cctv.com/live/cctv3/", @@ -67,203 +80,6 @@ public class MainActivity extends AppCompatActivity { "https://tv.cctv.com/live/cctvamerica/", }; - private String backwardScript = - """ - function simulate(element, eventName) { - var options = extend(defaultOptions, arguments[2] || {}); - var oEvent, eventType = null; - - for (var name in eventMatchers) { - if (eventMatchers[name].test(eventName)) { - eventType = name; - break; - } - } - - if (!eventType) throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported'); - - if (document.createEvent) { - oEvent = document.createEvent(eventType); - if (eventType == 'HTMLEvents') { - oEvent.initEvent(eventName, options.bubbles, options.cancelable); - } else { - oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView, options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element); - } - element.dispatchEvent(oEvent); - } else { - options.clientX = options.pointerX; - options.clientY = options.pointerY; - var evt = document.createEventObject(); - oEvent = extend(evt, options); - element.fireEvent('on' + eventName, oEvent); - } - return element; - } - - function extend(destination, source) { - for (var property in source) destination[property] = source[property]; - return destination; - } - - var eventMatchers = { - 'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, - 'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/ - } - var defaultOptions = { - pointerX: 0, - pointerY: 0, - button: 0, - ctrlKey: false, - altKey: false, - shiftKey: false, - metaKey: false, - bubbles: true, - cancelable: true - } - - function triggerMouseEvent (node, eventType) { - var clickEvent = document.createEvent ('MouseEvents'); - clickEvent.initEvent (eventType, true, true); - node.dispatchEvent (clickEvent); - }; - - function mouseDragStart(node) { - console.log("Starting drag..."); - console.log(node.offsetTop); - console.log(node.offsetLeft); - triggerMouseEvent(node, "mousedown") - } - - function mouseDragEnd(node){ - console.log("Ending drag..."); - const rect = node.getBoundingClientRect(); - document.querySelector("#epg_right_shift_player").click() - simulate(node, "mousemove" , {pointerX: rect.x-3 , pointerY: node.offsetTop}) - simulate(node, "mouseup" , {pointerX: rect.x-3, pointerY: node.offsetTop}) - } - function sleep1(time) { - time*=1000 - return new Promise(resolve => { - setTimeout(() => { - resolve(); - }, time); - }); - } - - async function playback(){ - if(document.querySelector("#play_or_pause_play_player").style.display==="none"){ - document.querySelector("#play_or_plause_player").click() - } - await sleep1(3) - const targetElement = document.querySelector("#timeshift_pointer_player") - mouseDragStart(targetElement); - mouseDragEnd(targetElement); - }; - - playback() - console.log('执行了回退'); - executeScript(); - """; - - private String forwardScript = - """ - function simulate(element, eventName) { - var options = extend(defaultOptions, arguments[2] || {}); - var oEvent, eventType = null; - - for (var name in eventMatchers) { - if (eventMatchers[name].test(eventName)) { - eventType = name; - break; - } - } - - if (!eventType) throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported'); - - if (document.createEvent) { - oEvent = document.createEvent(eventType); - if (eventType == 'HTMLEvents') { - oEvent.initEvent(eventName, options.bubbles, options.cancelable); - } else { - oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView, options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element); - } - element.dispatchEvent(oEvent); - } else { - options.clientX = options.pointerX; - options.clientY = options.pointerY; - var evt = document.createEventObject(); - oEvent = extend(evt, options); - element.fireEvent('on' + eventName, oEvent); - } - return element; - } - - function extend(destination, source) { - for (var property in source) destination[property] = source[property]; - return destination; - } - - var eventMatchers = { - 'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, - 'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/ - } - var defaultOptions = { - pointerX: 0, - pointerY: 0, - button: 0, - ctrlKey: false, - altKey: false, - shiftKey: false, - metaKey: false, - bubbles: true, - cancelable: true - } - - function triggerMouseEvent (node, eventType) { - var clickEvent = document.createEvent ('MouseEvents'); - clickEvent.initEvent (eventType, true, true); - node.dispatchEvent (clickEvent); - }; - - function mouseDragStart(node) { - console.log("Starting drag..."); - console.log(node.offsetTop); - console.log(node.offsetLeft); - triggerMouseEvent(node, "mousedown") - } - - function mouseDragEnd(node){ - console.log("Ending drag..."); - const rect = node.getBoundingClientRect(); - document.querySelector("#epg_right_shift_player").click() - simulate(node, "mousemove" , {pointerX: rect.x+20 , pointerY: node.offsetTop}) - simulate(node, "mouseup" , {pointerX: rect.x+20, pointerY: node.offsetTop}) - } - function sleep1(time) { - time*=1000 - return new Promise(resolve => { - setTimeout(() => { - resolve(); - }, time); - }); - } - - async function playback(){ - if(document.querySelector("#play_or_pause_play_player").style.display==="none"){ - document.querySelector("#play_or_plause_player").click() - } - await sleep1(3) - const targetElement = document.querySelector("#timeshift_pointer_player") - mouseDragStart(targetElement); - mouseDragEnd(targetElement); - }; - - playback() - console.log('执行了前进'); - executeScript(); - """; - - private String[] channelNames = { "CCTV-1 综合", "CCTV-2 财经", @@ -287,6 +103,7 @@ await sleep1(3) "CCTV America" }; + private int currentLiveIndex; private static final String PREF_NAME = "MyPreferences"; @@ -294,7 +111,7 @@ await sleep1(3) private boolean doubleBackToExitPressedOnce = false; - private StringBuilder digitBuffer = new StringBuilder(); // 用于缓存按下的数字键 + private final StringBuilder digitBuffer = new StringBuilder(); // 用于缓存按下的数字键 private static final long DIGIT_TIMEOUT = 3000; // 超时时间(毫秒) private TextView inputTextView; // 用于显示正在输入的数字的 TextView @@ -307,7 +124,20 @@ await sleep1(3) private String info = ""; - private com.tencent.smtt.sdk.WebView cctvView = null; + public List epgList = new ArrayList<>(); + + @JavascriptInterface + public void onAnnotations(String result) throws JSONException { + epgList.clear(); + JSONArray jsonArray = new JSONArray(result); + for (int i = 0; i < jsonArray.length(); i++) { + EpgInfo epgInfo = new EpgInfo(jsonArray.getJSONObject(i).getString("id"), jsonArray.getJSONObject(i).getString("name")); + epgList.add(epgInfo); + Log.d("epgInfo", epgInfo.getName() + epgInfo.getId()); + } + + } + @Override @@ -317,27 +147,31 @@ protected void onCreate(Bundle savedInstanceState) { mFrameLayout = findViewById(R.id.flVideoContainer); // X5WebView初始化 initX5WebView(); - // 初始化 WebView webView = findViewById(R.id.webView); - // 初始化显示正在输入的数字的 TextView inputTextView = findViewById(R.id.inputTextView); - // 初始化 loadingOverlay loadingOverlay = findViewById(R.id.loadingOverlay); - // 初始化 overlayTextView overlayTextView = findViewById(R.id.overlayTextView); // 加载上次保存的位置 loadLastLiveIndex(); // 添加自动化调试 -// if (Build.VERSION.SDK_INT >=Build.VERSION_CODES.KITKAT) { -// WebView.setWebContentsDebuggingEnabled(true); -// } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(true); + Log.d("remote debug", "远程调试"); + } // 配置 WebView 设置 - com.tencent.smtt.sdk.WebSettings webSettings = webView.getSettings(); +// filetool = new FileTool(this); +// String backwardScript =filetool.readFileContent("js/backwardScript.js"); +// +// String forwardScript =filetool.readFileContent("js/forwardScript.js"); +// +// String cctvOpenScript =filetool.readFileContent("js/getEpgScript.js"); + webView.addJavascriptInterface(this, "bridge"); + WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setDomStorageEnabled(true); // 添加自动播放视频 @@ -347,40 +181,67 @@ protected void onCreate(Bundle savedInstanceState) { webSettings.setUseWideViewPort(true); // 启用 JavaScript 自动点击功能 webSettings.setJavaScriptCanOpenWindowsAutomatically(true); - - //添加js android双向调用 必须大于api19(4.4)才可以使用 -/* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - String jsonParams = "123456"; - //String method = "jsMethod()";//不拼接参数,直接调用js的jsMethod函数 - String method = "jsMethod(" + jsonParams + ")";//拼接参数,就可以把数据传递给js - webView.evaluateJavascript(method, new ValueCallback() { - @Override - public void onReceiveValue(String value) { - Log.i("qcl0228", "js返回的数据" + value); - } - }); - }*/ - - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webSettings.setMixedContentMode(com.tencent.smtt.sdk.WebSettings.LOAD_NORMAL); - } + webSettings.setMixedContentMode(WebSettings.LOAD_NORMAL); // 设置 WebViewClient 和 WebChromeClient - webView.setWebViewClient(new com.tencent.smtt.sdk.WebViewClient() { + webView.setWebViewClient(new WebViewClient() { @Override - public void onReceivedSslError(com.tencent.smtt.sdk.WebView webView, com.tencent.smtt.export.external.interfaces.SslErrorHandler handler, com.tencent.smtt.export.external.interfaces.SslError error) { + public void onReceivedSslError(WebView webView, SslErrorHandler handler, SslError error) { handler.proceed(); // 忽略 SSL 错误 } // 设置 WebViewClient,监听页面加载完成事件 @Override - public void onPageFinished(com.tencent.smtt.sdk.WebView view, String url) { - cctvView = view; + public void onPageFinished(WebView view, String url) { // 页面加载完成后执行 JavaScript 脚本 // 清空info info = ""; + view.evaluateJavascript(""" + // 定义休眠函数 + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // 页面加载完成后执行 JavaScript 脚本 + let interval = setInterval(async function executeScript() { + console.log('页面加载完成!'); + + // 休眠 1000 毫秒(1秒) + await sleep(1000); + + // 休眠 50 毫秒 + await sleep(50); + + console.log('点击分辨率按钮'); + if(document.querySelector('#resolution_item_720_player')===null){ + var elem = document.querySelector("#resolution_item_480_player") + elem.click(); + }else{ + var elem = document.querySelector('#resolution_item_720_player'); + elem.click(); + } + + // 休眠 50 毫秒 + await sleep(50); + + console.log('设置音量并点击音量按钮'); + var btn = document.querySelector('#player_sound_btn_player'); + btn.setAttribute('volume', 100); + btn.click(); + btn.click(); + btn.click(); + + // 休眠 50 毫秒 + await sleep(50); + + console.log('点击全屏按钮'); + var fullscreenBtn = document.querySelector('#player_pagefullscreen_yes_player'); + fullscreenBtn.click(); + clearInterval(interval); + }, 3000); + """, null); + // 获取节目预告和当前节目 view.evaluateJavascript("document.querySelector('#jiemu > li.cur.act').innerText", value -> { // 处理获取到的元素值 @@ -397,51 +258,41 @@ public void onPageFinished(com.tencent.smtt.sdk.WebView view, String url) { } }); - String script = - """ - // 定义休眠函数 - function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - // 页面加载完成后执行 JavaScript 脚本 - let interval=setInterval(async function executeScript() { - console.log('页面加载完成!'); - - // 休眠 1000 毫秒(1秒) - await sleep(1000); - - // 休眠 50 毫秒 - await sleep(50); - - console.log('点击分辨率按钮'); - var elem = document.querySelector('#resolution_item_720_player'); - elem.click(); - - // 休眠 50 毫秒 - await sleep(50); - - console.log('设置音量并点击音量按钮'); - var btn = document.querySelector('#player_sound_btn_player'); - btn.setAttribute('volume', 100); - btn.click(); - btn.click(); - btn.click(); - - // 休眠 50 毫秒 - await sleep(50); - - console.log('点击全屏按钮'); - var fullscreenBtn = document.querySelector('#player_pagefullscreen_yes_player'); - fullscreenBtn.click(); - clearInterval(interval); - }, 3000); - - - - executeScript(); - """; - view.evaluateJavascript(script, null); + view.evaluateJavascript(""" + { + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + let interval = setInterval(async function () { + console.log('页面加载完成!'); + // 休眠 1000 毫秒(1秒) + await sleep(1000); + // 休眠 50 毫秒 + await sleep(50); + console.log('获取节目单'); + var epg_list = [ + { name: "", id: "" }, + ]; + + var epg_child = document.querySelector("#epg_player").childNodes; + for (let index = 0; index < epg_child.length; index++) { + if (index % 2 == 1) { + epg_list.push({ name: epg_child[index].innerText, id: epg_child[index].id }); + } + } + let filteredArray = epg_list.map(obj => { + Object.keys(obj).forEach(key => obj[key] === '' || obj[key] === null ? delete obj[key] : ''); + return obj; + }).filter(obj => Object.keys(obj).length > 0); + console.log(JSON.stringify(filteredArray)); + clearInterval(interval); + bridge.onAnnotations(JSON.stringify(filteredArray)); + }, 3000); + } + """, null); + + cctvFinishedView = view; new Handler().postDelayed(() -> { // 模拟触摸 @@ -451,30 +302,10 @@ function sleep(ms) { loadingOverlay.setVisibility(View.GONE); // 显示覆盖层,传入当前频道信息 - showOverlay(channelNames[currentLiveIndex] + "\n" + info); + showOverlay(channelNames[currentLiveIndex] + "\n" + info); }, 5000); - - // 在 WebView 加载完成后加载 RecyclerView - new Handler().post(() -> { - recyclerView = findViewById(R.id.recyclerView); - List channelList = createChannelList(); - ChannelAdapter channelAdapter = new ChannelAdapter(channelList); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(MainActivity.this); - recyclerView.setLayoutManager(linearLayoutManager); - recyclerView.setAdapter(channelAdapter); - }); } }); - - - // 禁用缩放 - webSettings.setSupportZoom(false); - webSettings.setBuiltInZoomControls(false); - webSettings.setDisplayZoomControls(false); - - // 在 Android TV 上,需要禁用焦点自动导航 - webView.setFocusable(false); - // 设置 WebView 客户端 webView.setWebChromeClient(new WebChromeClient() { @Override @@ -489,7 +320,7 @@ public void onShowCustomView(View view, IX5WebChromeClient.CustomViewCallback ca mCustomViewCallback = callback; webView.setVisibility(View.GONE); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - } + } @Override public void onHideCustomView() { @@ -508,6 +339,16 @@ public void onHideCustomView() { }); + + // 禁用缩放 + webSettings.setSupportZoom(false); + webSettings.setBuiltInZoomControls(false); + webSettings.setDisplayZoomControls(false); + + // 在 Android TV 上,需要禁用焦点自动导航 + webView.setFocusable(false); + + // 加载初始网页 loadLiveUrl(); webView.setBackgroundColor(Color.TRANSPARENT); @@ -515,19 +356,15 @@ public void onHideCustomView() { RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)); - } - + recyclerView = findViewById(R.id.recyclerView); + channelAdapter = new ChannelAdapter(epgList,MainActivity.this,MainActivity.this,recyclerView,webView); + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(MainActivity.this); + recyclerView.setLayoutManager(linearLayoutManager); + recyclerView.setAdapter(channelAdapter); - - private List createChannelList() { - List channels = new ArrayList<>(); - // 添加您的频道数据 - channels.add(new Channel("CCTV-1 综合")); - channels.add(new Channel("CCTV-2 财经")); - // 添加其他频道... - return channels; } + private void initX5WebView() { // 搜集本地tbs内核信息并上报服务器,服务器返回结果决定使用哪个内核。 QbSdk.PreInitCallback cb = new QbSdk.PreInitCallback() { @@ -552,9 +389,103 @@ public void onCoreInitFinished() { QbSdk.initTbsSettings(map); } + @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { + cctvFinishedView.evaluateJavascript( + """ + function simulate(element, eventName) { + var options = extend(defaultOptions, arguments[2] || {}); + var oEvent, eventType = null; + + for (var name in eventMatchers) { + if (eventMatchers[name].test(eventName)) { + eventType = name; + break; + } + } + + if (!eventType) throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported'); + + if (document.createEvent) { + oEvent = document.createEvent(eventType); + if (eventType == 'HTMLEvents') { + oEvent.initEvent(eventName, options.bubbles, options.cancelable); + } else { + oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView, options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element); + } + element.dispatchEvent(oEvent); + } else { + options.clientX = options.pointerX; + options.clientY = options.pointerY; + var evt = document.createEventObject(); + oEvent = extend(evt, options); + element.fireEvent('on' + eventName, oEvent); + } + return element; + } + + function extend(destination, source) { + for (var property in source) destination[property] = source[property]; + return destination; + } + + var eventMatchers = { + 'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, + 'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/ + } + var defaultOptions = { + pointerX: 0, + pointerY: 0, + button: 0, + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + bubbles: true, + cancelable: true + } + + function triggerMouseEvent (node, eventType) { + var clickEvent = document.createEvent ('MouseEvents'); + clickEvent.initEvent (eventType, true, true); + node.dispatchEvent (clickEvent); + }; + + async function mouseDragStart(node) { + console.log("Starting drag..."); + triggerMouseEvent(node, "mousedown") + } + + + var progress = null; + // 200 -1000 + async function mouseDragEnd(node,x,y){ + console.log("Ending drag..."); + await sleep(500) + simulate(node, "mousemove",{pointerX: x-30, pointerY: y}) + await sleep(500) + simulate(node, "mouseup" , {pointerX: x-30, pointerY: y}) + + } + function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + async function playback(offset){ + const targetElement = document.querySelector("#timeshift_pointer_player") + const xy = document.querySelector("#timeshift_pointer_player").getClientRects()[0] + console.log(xy) + mouseDragStart(targetElement); + mouseDragEnd(targetElement,xy.x+offset,xy.y); + }; + """ + ,null); + + + Log.d("recyclerView.hasFocus()",recyclerView.hasFocus()+"xxx"); + if (!recyclerView.hasFocus()&&event.getAction() == KeyEvent.ACTION_DOWN) { + Log.d("ACTION_DOWN","ACTION进入了"); if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT || event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER || event.getKeyCode() == KeyEvent.KEYCODE_MENU) { if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) { // 执行上一个直播地址的操作 @@ -568,14 +499,23 @@ public boolean dispatchKeyEvent(KeyEvent event) { // 执行暂停操作 simulateTouch(webView, 0.5f, 0.5f); return true; // 返回 true 表示事件已处理,不传递给 WebView + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { + cctvFinishedView.evaluateJavascript( + """ + {playback(-30);} + """ + ,null); + + + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { + cctvFinishedView.evaluateJavascript( + """ + {playback(60);} + """,null); } else if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) { recyclerView.setVisibility(View.VISIBLE); recyclerView.requestFocus(); // 将焦点设置到 RecyclerView - return true; // 返回 true 表示事件已处理,不传递给 WebView - } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { - cctvView.evaluateJavascript(backwardScript, null); - } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { - cctvView.evaluateJavascript(forwardScript, null); + return false; // 返回 true 表示事件已处理,不传递给 WebView } return true; // 返回 true 表示事件已处理,不传递给 WebView } else if (event.getKeyCode() >= KeyEvent.KEYCODE_0 && event.getKeyCode() <= KeyEvent.KEYCODE_9) { @@ -585,7 +525,7 @@ public boolean dispatchKeyEvent(KeyEvent event) { digitBuffer.append(numericKey); // 使用 Handler 来在超时后处理输入的数字 - new Handler().postDelayed(() -> handleNumericInput(), DIGIT_TIMEOUT); + new Handler().postDelayed(this::handleNumericInput, DIGIT_TIMEOUT); // 更新显示正在输入的数字的 TextView updateInputTextView(); @@ -593,7 +533,6 @@ public boolean dispatchKeyEvent(KeyEvent event) { return true; // 事件已处理,不传递给 WebView } } - return super.dispatchKeyEvent(event); // 如果不处理,调用父类的方法继续传递事件 } @@ -617,6 +556,7 @@ private void handleNumericInput() { } } + @SuppressLint("SetTextI18n") private void updateInputTextView() { // 在 TextView 中显示当前正在输入的数字 inputTextView.setVisibility(View.VISIBLE); @@ -693,17 +633,18 @@ public void simulateTouch(View view, float x, float y) { @Override public void onBackPressed() { if (recyclerView.getVisibility() == View.VISIBLE) { - // 如果 RecyclerView 可见,按下返回键时隐藏它 recyclerView.setVisibility(View.GONE); - } else if (doubleBackToExitPressedOnce) { - super.onBackPressed(); - return; + } else { + if (doubleBackToExitPressedOnce) { + super.onBackPressed(); + return; + } + this.doubleBackToExitPressedOnce = true; + Toast.makeText(this, "再按一次返回键退出应用", Toast.LENGTH_SHORT).show(); + new Handler().postDelayed(() -> doubleBackToExitPressedOnce = false, 2000); } - this.doubleBackToExitPressedOnce = true; - Toast.makeText(this, "再按一次返回键退出应用", Toast.LENGTH_SHORT).show(); - new Handler().postDelayed(() -> doubleBackToExitPressedOnce = false, 2000); // 如果两秒内再次按返回键,则退出应用 } @@ -727,5 +668,29 @@ protected void onDestroy() { } super.onDestroy(); } + + + @Override + public void onItemClick(View view, int position) { + Log.d("onItemClick", position + "xxxxxxx"); + EpgInfo channel = epgList.get(position); + Log.d("cctvFinishedView",cctvFinishedView.toString()); + cctvFinishedView.evaluateJavascript("async function xx(){document.querySelector('#play_or_plause_player').click();await sleep(3000);" + + "document.querySelector('#" + channel.getId() + "')" + ".click();"+"}"+"xx()" + , null); + } + + + @Override + public void onFocusChangeListener(View view, boolean hasFocus) { + Log.d("onFocusChangeListener", hasFocus + "xxxxxxx"); +// if (hasFocus){ +// EpgInfo channel = epgList.get(position); +// cctvFinishedView.evaluateJavascript("document.querySelector('#" + channel.getId() + "')" + ".click()", null); +// } +// cctvFinishedView.evaluateJavascript("document.querySelector('#" + channel.getId() + "')" + ".click()", null); + + } } + diff --git a/app/src/main/java/com/eanyatonic/cctvViewer/MainFragment.java b/app/src/main/java/com/eanyatonic/cctvViewer/MainFragment.java index 0695894..7811620 100644 --- a/app/src/main/java/com/eanyatonic/cctvViewer/MainFragment.java +++ b/app/src/main/java/com/eanyatonic/cctvViewer/MainFragment.java @@ -136,14 +136,8 @@ private void setupUIElements() { } private void setupEventListeners() { - setOnSearchClickedListener(new View.OnClickListener() { - - @Override - public void onClick(View view) { - Toast.makeText(getActivity(), "Implement your own in-app search", Toast.LENGTH_LONG) - .show(); - } - }); + setOnSearchClickedListener(view -> + Toast.makeText(getActivity(), "Implement your own in-app search", Toast.LENGTH_LONG).show()); setOnItemViewClickedListener(new ItemViewClickedListener()); setOnItemViewSelectedListener(new ItemViewSelectedListener()); diff --git a/app/src/main/java/com/eanyatonic/cctvViewer/bean/EpgInfo.java b/app/src/main/java/com/eanyatonic/cctvViewer/bean/EpgInfo.java new file mode 100644 index 0000000..229ffe3 --- /dev/null +++ b/app/src/main/java/com/eanyatonic/cctvViewer/bean/EpgInfo.java @@ -0,0 +1,27 @@ +package com.eanyatonic.cctvViewer.bean; + +public class EpgInfo { + private String id; + private String name; + + public EpgInfo(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/app/src/main/java/com/eanyatonic/cctvViewer/tools/FileTool.java b/app/src/main/java/com/eanyatonic/cctvViewer/tools/FileTool.java new file mode 100644 index 0000000..e156ccd --- /dev/null +++ b/app/src/main/java/com/eanyatonic/cctvViewer/tools/FileTool.java @@ -0,0 +1,68 @@ + +package com.eanyatonic.cctvViewer.tools; + +import android.content.Context; +import android.content.res.AssetManager; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; + +public class FileTool { + + private Context context; + + public FileTool(Context context) { + this.context = context; + } + + public String readFileContent(String fileName) { + InputStream inputStream = null; + Reader reader = null; + BufferedReader bufferedReader = null; + StringBuilder result = new StringBuilder(); + + try { + //得到资源中的asset数据流 + inputStream = context.getResources().getAssets().open(fileName); + reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);// 字符流 + bufferedReader = new BufferedReader(reader); //缓冲流 + String temp; + while ((temp = bufferedReader.readLine()) != null) { + result.append(temp); + } + Log.i("fileTool", "result:" + result); + } catch (Exception e) { + Log.e("fileError", e.toString()); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (bufferedReader != null) { + try { + bufferedReader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + return result.toString(); + } +} diff --git a/app/src/main/res/drawable/channel_background_selected.xml b/app/src/main/res/drawable/channel_background_selected.xml new file mode 100644 index 0000000..e66df4f --- /dev/null +++ b/app/src/main/res/drawable/channel_background_selected.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/channel_background_unselected.xml b/app/src/main/res/drawable/channel_background_unselected.xml new file mode 100644 index 0000000..7f30199 --- /dev/null +++ b/app/src/main/res/drawable/channel_background_unselected.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 64c16c3..2bcf7c6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -11,21 +11,27 @@ android:id="@+id/webView" android:layout_width="match_parent" android:layout_height="match_parent"> - + + + - - + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 733f3f5..fbf04fc 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,6 +3,16 @@ #DDDDDD #0096a6 #ffaa3f - #ffaa3f #3d3d3d + #ffaa3f + + + + #80FFAA3F + #80D3A229 + #80FF0078 + #FF0078 + #00000000 + #00FFE1 + \ No newline at end of file