diff --git a/.gitignore b/.gitignore index f7c1221..a8a0c07 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /app/build/ /local.properties /app/schemas/ +/.project +/.settings/org.eclipse.buildship.core.prefs diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/Browser.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/Browser.java index ba06b51..afe47c2 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/browser/Browser.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/Browser.java @@ -1,125 +1,132 @@ -package codes.nh.streambrowser.screens.browser; - -import android.content.Context; -import android.graphics.Bitmap; -import android.util.AttributeSet; +package codes.nh.streambrowser.screens.browser; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.AttributeSet; import android.view.MotionEvent; import android.webkit.WebSettings; import android.webkit.WebView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Map; - -import codes.nh.streambrowser.screens.stream.Stream; -import codes.nh.streambrowser.utils.AppUtils; - -public class Browser extends WebView { - - public Browser(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - - initializeBrowser(); - } - - private void initializeBrowser() { - WebSettings settings = getSettings(); - - settings.setBuiltInZoomControls(true); - settings.setDisplayZoomControls(false); - settings.setUseWideViewPort(true); - settings.setLoadWithOverviewMode(true); - +import android.os.Message; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +import codes.nh.streambrowser.screens.stream.Stream; +import codes.nh.streambrowser.utils.AppUtils; + +public class Browser extends WebView { + + public Browser(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + initializeBrowser(); + } + + private void initializeBrowser() { + WebSettings settings = getSettings(); + + settings.setBuiltInZoomControls(true); + settings.setDisplayZoomControls(false); + settings.setUseWideViewPort(true); + settings.setLoadWithOverviewMode(true); + settings.setJavaScriptEnabled(true); + settings.setSupportMultipleWindows(true); + settings.setJavaScriptCanOpenWindowsAutomatically(true); settings.setMediaPlaybackRequiresUserGesture(false); - settings.setDomStorageEnabled(true); - settings.setCacheMode(WebSettings.LOAD_NO_CACHE); - - BrowserClient browserClient = new BrowserClient(); - setWebViewClient(browserClient); - - BrowserChromeClient browserChromeClient = new BrowserChromeClient(); - setWebChromeClient(browserChromeClient); - - //CookieManager.getInstance().setAcceptThirdPartyCookies(this, true); - } - - private boolean desktopMode = false; - - public boolean getDesktopMode() { - return desktopMode; - } - - public void setDesktopMode(boolean enabled) { - desktopMode = enabled; - - String userAgent = getSettings().getUserAgentString(); - if (enabled) { - userAgent = userAgent - .replace(" Mobile ", " Dummy1 ") - .replace(" Android ", " Dummy2 "); - } else { - userAgent = userAgent - .replace(" Dummy1 ", " Mobile ") - .replace(" Dummy2 ", " Android "); - } - getSettings().setUserAgentString(userAgent); - AppUtils.log("new user agent = " + userAgent); - - //getSettings().setUseWideViewPort(enabled); - } - - @Override - public void loadUrl(@NonNull String url) { - super.loadUrl(url); - getListener().onRequestPage(url); - } - - @Override - public void loadUrl(@NonNull String url, @NonNull Map additionalHttpHeaders) { - super.loadUrl(url, additionalHttpHeaders); - getListener().onRequestPage(url); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - getListener().onTouch(); - return super.onTouchEvent(event); - } - - //listener - - private Listener listener; - - public Listener getListener() { - return listener; - } - - public void setListener(Listener listener) { - this.listener = listener; - } - - interface Listener { - - void onRequestPage(String url); - - void onStartLoadPage(String url); - - void onFinishLoadPage(String url); - - void onUpdateUrl(String url); - - void onUpdateTitle(String title); - - void onUpdateFavicon(Bitmap favicon); - - void onUpdateProgress(int progress); - - boolean onRedirect(String oldUrl, String newUrl); + settings.setDomStorageEnabled(true); + settings.setCacheMode(WebSettings.LOAD_NO_CACHE); + + BrowserClient browserClient = new BrowserClient(); + setWebViewClient(browserClient); + + BrowserChromeClient browserChromeClient = new BrowserChromeClient(); + setWebChromeClient(browserChromeClient); + + //CookieManager.getInstance().setAcceptThirdPartyCookies(this, true); + } + + private boolean desktopMode = false; + + public boolean getDesktopMode() { + return desktopMode; + } + + public void setDesktopMode(boolean enabled) { + desktopMode = enabled; + + String userAgent = getSettings().getUserAgentString(); + if (enabled) { + userAgent = userAgent + .replace(" Mobile ", " Dummy1 ") + .replace(" Android ", " Dummy2 "); + } else { + userAgent = userAgent + .replace(" Dummy1 ", " Mobile ") + .replace(" Dummy2 ", " Android "); + } + getSettings().setUserAgentString(userAgent); + AppUtils.log("new user agent = " + userAgent); + + //getSettings().setUseWideViewPort(enabled); + } + + @Override + public void loadUrl(@NonNull String url) { + super.loadUrl(url); + getListener().onRequestPage(url); + } + + @Override + public void loadUrl(@NonNull String url, @NonNull Map additionalHttpHeaders) { + super.loadUrl(url, additionalHttpHeaders); + getListener().onRequestPage(url); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + getListener().onTouch(); + return super.onTouchEvent(event); + } + + //listener + + private Listener listener; + + public Listener getListener() { + return listener; + } + + public void setListener(Listener listener) { + this.listener = listener; + } + + interface Listener { + + void onRequestPage(String url); + + void onStartLoadPage(String url); + + void onFinishLoadPage(String url); + + void onUpdateUrl(String url); + + void onUpdateTitle(String title); + + void onUpdateFavicon(Bitmap favicon); + + void onUpdateProgress(int progress); + + boolean onRedirect(String oldUrl, String newUrl); + + void onFindStream(Stream stream); + + void onTouch(); - void onFindStream(Stream stream); + boolean onCreateWindow(Browser parentBrowser, Message resultMsg); - void onTouch(); + void onCloseWindow(Browser browser); } } diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserChromeClient.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserChromeClient.java index 20eba76..dc6326b 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserChromeClient.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserChromeClient.java @@ -1,33 +1,49 @@ -package codes.nh.streambrowser.screens.browser; - +package codes.nh.streambrowser.screens.browser; + import android.graphics.Bitmap; +import android.os.Message; import android.webkit.WebChromeClient; import android.webkit.WebView; - -public class BrowserChromeClient extends WebChromeClient { - - @Override - public void onReceivedTitle(WebView webView, String title) { - super.onReceivedTitle(webView, title); - + +public class BrowserChromeClient extends WebChromeClient { + + @Override + public void onReceivedTitle(WebView webView, String title) { + super.onReceivedTitle(webView, title); + + Browser browser = (Browser) webView; + browser.getListener().onUpdateTitle(title); + } + + @Override + public void onReceivedIcon(WebView webView, Bitmap favicon) { + super.onReceivedIcon(webView, favicon); + + Browser browser = (Browser) webView; + browser.getListener().onUpdateFavicon(favicon); + } + + @Override + public void onProgressChanged(WebView webView, int progress) { + super.onProgressChanged(webView, progress); + Browser browser = (Browser) webView; - browser.getListener().onUpdateTitle(title); + browser.getListener().onUpdateProgress(progress); } @Override - public void onReceivedIcon(WebView webView, Bitmap favicon) { - super.onReceivedIcon(webView, favicon); - + public boolean onCreateWindow(WebView webView, boolean isDialog, boolean isUserGesture, Message resultMsg) { Browser browser = (Browser) webView; - browser.getListener().onUpdateFavicon(favicon); + return browser.getListener().onCreateWindow(browser, resultMsg); } @Override - public void onProgressChanged(WebView webView, int progress) { - super.onProgressChanged(webView, progress); - - Browser browser = (Browser) webView; - browser.getListener().onUpdateProgress(progress); + public void onCloseWindow(WebView window) { + super.onCloseWindow(window); + if (window instanceof Browser) { + Browser browser = (Browser) window; + browser.getListener().onCloseWindow(browser); + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserClient.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserClient.java index f500c04..7d72935 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserClient.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserClient.java @@ -1,150 +1,279 @@ -package codes.nh.streambrowser.screens.browser; - -import android.graphics.Bitmap; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebView; -import android.webkit.WebViewClient; - +package codes.nh.streambrowser.screens.browser; + +import android.graphics.Bitmap; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; + import androidx.annotation.Nullable; +import org.json.JSONArray; + import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; - -import codes.nh.streambrowser.screens.stream.Stream; -import codes.nh.streambrowser.utils.UrlUtils; - + +import codes.nh.streambrowser.screens.stream.Stream; +import codes.nh.streambrowser.utils.UrlUtils; + public class BrowserClient extends WebViewClient { - @Override - public boolean shouldOverrideUrlLoading(WebView webView, WebResourceRequest request) { - Browser browser = (Browser) webView; - - String oldUrl = webView.getUrl(); - String oldDomain = UrlUtils.getDomainNameFromURL(oldUrl); - - String newUrl = request.getUrl().toString(); - String newDomain = UrlUtils.getDomainNameFromURL(newUrl); - - //true = block request, false = allow request; - boolean blockRequest = false; - if (!newDomain.equalsIgnoreCase(oldDomain)) { - blockRequest = !browser.getListener().onRedirect(oldUrl, newUrl); - } - - if (!blockRequest) { - browser.getListener().onRequestPage(newUrl); - } - - return blockRequest; - } - - //private final List cachedThumbnails = new ArrayList<>(); - - private final List cachedSubtitles = new ArrayList<>(); - - @Nullable - @Override - public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) { - - String url = request.getUrl().toString(); - String fileName = UrlUtils.getFileNameFromUrl(url); - - Map headers = request.getRequestHeaders(); - - /*if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") || fileName.endsWith(".png")) { //thumbnails - - synchronized (cachedThumbnails) { - cachedThumbnails.add(url); - } - - } else */ - if (fileName.endsWith(".vtt") || fileName.endsWith(".srt")) { //subtitles - - synchronized (cachedSubtitles) { - cachedSubtitles.add(url); - } - - } else if (fileName.endsWith(".m3u8") || fileName.endsWith(".mp4")) { //streams - - onFindStream(webView, url, headers); - - }/* else if (fileName.endsWith("videoplayback")) { // youtube - url = url.replaceAll("&range=\\d*-\\d*", "&"); - onFindStream(webView, url, headers); - } else {*/ - - //onFindStream(webView, url, headers); - /* else { //streams 2 - - String range = UrlUtils.getHeaderValue(headers, "range"); - if (range != null && range.startsWith("bytes=")) { - onFindStream(webView, url, headers); - } - - } - }*/ - - return super.shouldInterceptRequest(webView, request); - } - - private void onFindStream(WebView webView, String url, Map headers) { - webView.post(() -> { - - Stream stream = new Stream( - url, - webView.getUrl(), - webView.getTitle(), - headers, - new ArrayList<>(/*cachedThumbnails*/), - new ArrayList<>(cachedSubtitles), - 0 - ); - - Browser browser = (Browser) webView; - browser.getListener().onFindStream(stream); - - }); - } - - @Override - public void onPageStarted(WebView webView, String url, Bitmap favicon) { - super.onPageStarted(webView, url, favicon); - - //cachedThumbnails.clear(); - cachedSubtitles.clear(); - - Browser browser = (Browser) webView; - browser.getListener().onStartLoadPage(url); - } - - @Override + private static final String DOM_VIDEO_SOURCES_SCRIPT = "(function() {" + + "const results = [];" + + "const seen = Object.create(null);" + + "const allowed = /\\.(m3u8|mp4)(?:$|[?#])/i;" + + "const add = function(raw) {" + + "if (!raw || typeof raw !== 'string') return;" + + "try {" + + "const absolute = new URL(raw, document.baseURI || window.location.href).href;" + + "if (!allowed.test(absolute)) return;" + + "if (seen[absolute]) return;" + + "seen[absolute] = true;" + + "results.push(absolute);" + + "} catch (e) {}" + + "};" + + "const videos = document.querySelectorAll('video');" + + "for (let i = 0; i < videos.length; i++) {" + + "const video = videos[i];" + + "add(video.currentSrc);" + + "add(video.src);" + + "const sources = video.querySelectorAll('source');" + + "for (let j = 0; j < sources.length; j++) {" + + "add(sources[j].src);" + + "}" + + "}" + + "return results;" + + "})();"; + + private static final String PAGE_STREAM_CANDIDATES_SCRIPT = "(function() {" + + "const results = [];" + + "const seen = Object.create(null);" + + "const allowed = /\\.(m3u8|mp4)(?:$|[?#])/i;" + + "const add = function(raw) {" + + "if (!raw || typeof raw !== 'string') return;" + + "try {" + + "const absolute = new URL(raw, document.baseURI || window.location.href).href;" + + "if (!allowed.test(absolute)) return;" + + "if (seen[absolute]) return;" + + "seen[absolute] = true;" + + "results.push(absolute);" + + "} catch (e) {}" + + "};" + + "const videos = document.querySelectorAll('video');" + + "for (let i = 0; i < videos.length; i++) {" + + "const video = videos[i];" + + "add(video.currentSrc);" + + "add(video.src);" + + "const sources = video.querySelectorAll('source');" + + "for (let j = 0; j < sources.length; j++) {" + + "add(sources[j].src);" + + "}" + + "}" + + "if (typeof window.source === 'string') {" + + "add(window.source);" + + "}" + + "if (typeof window.jwplayer === 'function') {" + + "try {" + + "const player = window.jwplayer();" + + "if (player && typeof player.getPlaylist === 'function') {" + + "const playlist = player.getPlaylist();" + + "if (Array.isArray(playlist)) {" + + "for (let i = 0; i < playlist.length; i++) {" + + "const item = playlist[i] || {};" + + "add(item.file);" + + "const sources = item.sources;" + + "if (Array.isArray(sources)) {" + + "for (let j = 0; j < sources.length; j++) {" + + "const source = sources[j] || {};" + + "add(source.file);" + + "}" + + "}" + + "}" + + "}" + + "}" + + "if (player && typeof player.getConfig === 'function') {" + + "const config = player.getConfig() || {};" + + "add(config.file);" + + "const playlist = config.playlist;" + + "if (Array.isArray(playlist)) {" + + "for (let i = 0; i < playlist.length; i++) {" + + "const item = playlist[i] || {};" + + "add(item.file);" + + "const sources = item.sources;" + + "if (Array.isArray(sources)) {" + + "for (let j = 0; j < sources.length; j++) {" + + "const source = sources[j] || {};" + + "add(source.file);" + + "}" + + "}" + + "}" + + "}" + + "}" + + "} catch (e) {}" + + "}" + + "return results;" + + "})();"; + + @Override + public boolean shouldOverrideUrlLoading(WebView webView, WebResourceRequest request) { + Browser browser = (Browser) webView; + + String oldUrl = webView.getUrl(); + String oldDomain = UrlUtils.getDomainNameFromURL(oldUrl); + + String newUrl = request.getUrl().toString(); + String newDomain = UrlUtils.getDomainNameFromURL(newUrl); + + //true = block request, false = allow request; + boolean blockRequest = false; + if (!newDomain.equalsIgnoreCase(oldDomain)) { + blockRequest = !browser.getListener().onRedirect(oldUrl, newUrl); + } + + if (!blockRequest) { + browser.getListener().onRequestPage(newUrl); + } + + return blockRequest; + } + + //private final List cachedThumbnails = new ArrayList<>(); + + private final List cachedSubtitles = new ArrayList<>(); + + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) { + + String url = request.getUrl().toString(); + String fileName = UrlUtils.getFileNameFromUrl(url); + + Map headers = request.getRequestHeaders(); + + /*if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") || fileName.endsWith(".png")) { //thumbnails + + synchronized (cachedThumbnails) { + cachedThumbnails.add(url); + } + + } else */ + if (fileName.endsWith(".vtt") || fileName.endsWith(".srt")) { //subtitles + + synchronized (cachedSubtitles) { + cachedSubtitles.add(url); + } + + } else if (fileName.endsWith(".m3u8") || fileName.endsWith(".mp4")) { //streams + + onFindStream(webView, url, headers); + + }/* else if (fileName.endsWith("videoplayback")) { // youtube + url = url.replaceAll("&range=\\d*-\\d*", "&"); + onFindStream(webView, url, headers); + } else {*/ + + //onFindStream(webView, url, headers); + /* else { //streams 2 + + String range = UrlUtils.getHeaderValue(headers, "range"); + if (range != null && range.startsWith("bytes=")) { + onFindStream(webView, url, headers); + } + + } + }*/ + + return super.shouldInterceptRequest(webView, request); + } + + private void onFindStream(WebView webView, String url, Map headers) { + webView.post(() -> { + + Stream stream = new Stream( + url, + webView.getUrl(), + webView.getTitle(), + headers, + new ArrayList<>(/*cachedThumbnails*/), + new ArrayList<>(cachedSubtitles), + 0 + ); + + Browser browser = (Browser) webView; + browser.getListener().onFindStream(stream); + + }); + } + + @Override + public void onPageStarted(WebView webView, String url, Bitmap favicon) { + super.onPageStarted(webView, url, favicon); + + //cachedThumbnails.clear(); + cachedSubtitles.clear(); + + Browser browser = (Browser) webView; + browser.getListener().onStartLoadPage(url); + } + + @Override public void onPageFinished(WebView webView, String url) { super.onPageFinished(webView, url); + detectStreamCandidates(webView); + webView.postDelayed(() -> detectStreamCandidates(webView), 500); + webView.postDelayed(() -> detectStreamCandidates(webView), 1500); + webView.postDelayed(() -> detectStreamCandidates(webView), 3000); + Browser browser = (Browser) webView; browser.getListener().onFinishLoadPage(url); } - @Override - public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) { - super.doUpdateVisitedHistory(webView, url, isReload); - - Browser browser = (Browser) webView; - browser.getListener().onUpdateUrl(url); + private void detectStreamCandidates(WebView webView) { + detectFromScript(webView, DOM_VIDEO_SOURCES_SCRIPT); + detectFromScript(webView, PAGE_STREAM_CANDIDATES_SCRIPT); } - /* - @Override - public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { - super.onReceivedError(view, request, error); - AppUtils.log("onReceivedError: " + error.getErrorCode() + " - " + error.getErrorCode()); - } + private void detectFromScript(WebView webView, String script) { + webView.evaluateJavascript(script, json -> { + if (json == null || json.isEmpty() || "null".equals(json)) { + return; + } - @Override //todo get webresourceresponse for non error requests - public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { - super.onReceivedHttpError(view, request, errorResponse); - AppUtils.log("onReceivedHttpError: " + errorResponse.getStatusCode() + " - " + errorResponse.getReasonPhrase() + " - " + request.getUrl() + " - " + AppUtils.mapToJson(errorResponse.getResponseHeaders()).toString()); - }*/ -} \ No newline at end of file + try { + JSONArray streamUrls = new JSONArray(json); + for (int i = 0; i < streamUrls.length(); i++) { + String streamUrl = streamUrls.optString(i, null); + if (streamUrl == null || streamUrl.isEmpty()) { + continue; + } + onFindStream(webView, streamUrl, new HashMap<>()); + } + } catch (Exception ignored) { + } + }); + } + + @Override + public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) { + super.doUpdateVisitedHistory(webView, url, isReload); + + Browser browser = (Browser) webView; + browser.getListener().onUpdateUrl(url); + } + + /* + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + super.onReceivedError(view, request, error); + AppUtils.log("onReceivedError: " + error.getErrorCode() + " - " + error.getErrorCode()); + } + + @Override //todo get webresourceresponse for non error requests + public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + AppUtils.log("onReceivedHttpError: " + errorResponse.getStatusCode() + " - " + errorResponse.getReasonPhrase() + " - " + request.getUrl() + " - " + AppUtils.mapToJson(errorResponse.getResponseHeaders()).toString()); + }*/ +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserFragment.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserFragment.java index ef3c081..c4a3b2d 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserFragment.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserFragment.java @@ -1,246 +1,536 @@ -package codes.nh.streambrowser.screens.browser; - +package codes.nh.streambrowser.screens.browser; + import android.graphics.Bitmap; import android.os.Bundle; +import android.os.Message; import android.view.Menu; import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; +import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.PopupMenu; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; -import androidx.mediarouter.app.MediaRouteButton; - +import android.widget.CheckBox; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.mediarouter.app.MediaRouteButton; + import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.button.MaterialButton; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.textfield.TextInputEditText; - -import java.util.List; - + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import codes.nh.streambrowser.R; import codes.nh.streambrowser.screens.main.MainViewModel; -import codes.nh.streambrowser.screens.main.SnackbarRequest; +import codes.nh.streambrowser.screens.settings.SettingsManager; +import codes.nh.streambrowser.screens.sheet.SheetRequest; import codes.nh.streambrowser.screens.stream.Stream; -import codes.nh.streambrowser.utils.AppUtils; -import codes.nh.streambrowser.utils.ImageUtils; -import codes.nh.streambrowser.utils.UrlUtils; - -public class BrowserFragment extends Fragment { - - public BrowserFragment() { - super(R.layout.fragment_browser); - AppUtils.log("init BrowserFragment"); - } - - private BrowserViewModel browserViewModel; - - private MainViewModel mainViewModel; - - private Browser webView; - - private LinearProgressIndicator progressBar; - - private TextInputEditText urlInput; - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - +import codes.nh.streambrowser.utils.AppUtils; +import codes.nh.streambrowser.utils.ImageUtils; +import codes.nh.streambrowser.utils.UrlUtils; + +public class BrowserFragment extends Fragment { + + public BrowserFragment() { + super(R.layout.fragment_browser); + AppUtils.log("init BrowserFragment"); + } + + private BrowserViewModel browserViewModel; + + private MainViewModel mainViewModel; + + private FrameLayout webViewContainer; + + private final Map webViewsByTabId = new HashMap<>(); + + private final Map tabIdsByWebView = new HashMap<>(); + + private LinearProgressIndicator progressBar; + + private TextInputEditText urlInput; + + private MaterialButton tabsButton; + + private SettingsManager settingsManager; + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + browserViewModel = new ViewModelProvider(requireActivity()).get(BrowserViewModel.class); - - browserViewModel.getRequestLoadUrl().observe(getViewLifecycleOwner(), request -> { - if (request != null) { - AppUtils.log("getRequestLoadUrl"); - browserViewModel.setRequestLoadUrl(null); - webView.loadUrl(request.getUrl(), request.getHeaders()); - } - }); - - browserViewModel.getDesktopMode().observe(getViewLifecycleOwner(), desktopMode -> { - if (webView.getDesktopMode() != desktopMode) { - AppUtils.log("getDesktopMode"); - webView.setDesktopMode(desktopMode); - webView.reload(); - } - }); - - browserViewModel.getDestinationList().observe(getViewLifecycleOwner(), destinations -> { - if (webView.getUrl() == null) { - AppUtils.log("getDestinationList"); - String url = !destinations.isEmpty() ? destinations.get(0).getUrl() : "https://google.com/"; - browserViewModel.setRequestLoadUrl(new BrowserRequest(url));//webView.loadUrl(url); - } - }); - mainViewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); - - webView = view.findViewById(R.id.fragment_browser_webview); - webView.setDesktopMode(browserViewModel.getDesktopMode().getValue()); - webView.setListener(new Browser.Listener() { - - @Override - public void onRequestPage(String url) { - urlInput.setText(url); - - progressBar.setIndeterminate(true); - progressBar.setVisibility(View.VISIBLE); - } - - @Override - public void onStartLoadPage(String url) { - urlInput.setText(url); - - progressBar.setProgress(0, false); - progressBar.setVisibility(View.VISIBLE); - - browserViewModel.clearStreams(); - } - - @Override - public void onFinishLoadPage(String url) { - progressBar.setProgress(100, true); - progressBar.setVisibility(View.GONE); - } - - @Override - public void onUpdateUrl(String url) { - urlInput.setText(url); - - BrowserDestination destination = new BrowserDestination(url, webView.getTitle(), ImageUtils.bytesFromBitmap(webView.getFavicon()), System.currentTimeMillis()); - addDestination(destination); - } - + settingsManager = new SettingsManager(requireContext().getApplicationContext()); + + webViewContainer = view.findViewById(R.id.fragment_browser_webview_container); + progressBar = view.findViewById(R.id.fragment_browser_loader); + urlInput = view.findViewById(R.id.fragment_browser_input_url); + tabsButton = view.findViewById(R.id.fragment_browser_button_tabs); + + browserViewModel.getTabs().observe(getViewLifecycleOwner(), tabs -> { + ensureWebViewsForTabs(tabs); + switchToTab(browserViewModel.getActiveTabId().getValue()); + updateTabsButton(); + }); + + browserViewModel.getActiveTabId().observe(getViewLifecycleOwner(), tabId -> { + switchToTab(tabId); + updateTabsButton(); + }); + + browserViewModel.getRequestLoadUrl().observe(getViewLifecycleOwner(), request -> { + if (request == null) { + return; + } + browserViewModel.setRequestLoadUrl(null); + + if (request.shouldOpenInNewTab()) { + String groupId = request.getGroupId() != null ? request.getGroupId() : browserViewModel.getActiveGroupId(); + BrowserTab tab = browserViewModel.createTabInGroup(groupId, true); + Browser webView = getWebView(tab.getId()); + if (webView == null) { + webView = createWebView(tab.getId()); + } + if (webView != null) { + webView.loadUrl(request.getUrl(), request.getHeaders()); + } + return; + } + + String targetTabId = request.getTabId() != null ? request.getTabId() : browserViewModel.getActiveTabId().getValue(); + Browser webView = getWebView(targetTabId); + if (webView != null) { + browserViewModel.activateTab(targetTabId); + webView.loadUrl(request.getUrl(), request.getHeaders()); + } + }); + + browserViewModel.getDesktopMode().observe(getViewLifecycleOwner(), desktopMode -> { + for (Browser webView : webViewsByTabId.values()) { + boolean enabled = Boolean.TRUE.equals(desktopMode); + if (webView.getDesktopMode() != enabled) { + webView.setDesktopMode(enabled); + if (webView.getUrl() != null) { + webView.reload(); + } + } + } + }); + + browserViewModel.getDestinationList().observe(getViewLifecycleOwner(), destinations -> { + if (!browserViewModel.consumeInitialPageLoadPending()) { + return; + } + Browser activeWebView = getActiveWebView(); + if (activeWebView == null || activeWebView.getUrl() != null) { + return; + } + String url = !destinations.isEmpty() ? destinations.get(0).getUrl() : "https://google.com/"; + browserViewModel.setRequestLoadUrl(new BrowserRequest(url)); + }); + + urlInput.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + openUrl(v.getText() != null ? v.getText().toString() : ""); + } + return false; + }); + urlInput.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) { + urlInput.post(urlInput::selectAll); + } + }); + + ImageButton backButton = view.findViewById(R.id.fragment_browser_button_back); + backButton.setOnClickListener(this::openBrowserHistoryDialog); + + ImageButton newTabButton = view.findViewById(R.id.fragment_browser_button_new_tab); + newTabButton.setOnClickListener(v -> browserViewModel.createTabInActiveGroup(true)); + + tabsButton.setOnClickListener(v -> mainViewModel.openSheet(new SheetRequest(TabSwitcherFragment.class))); + + MediaRouteButton mediaRouteButton = view.findViewById(R.id.media_route_button); + CastButtonFactory.setUpMediaRouteButton(requireContext().getApplicationContext(), mediaRouteButton); + } + + @Override + public void onPause() { + for (Browser webView : webViewsByTabId.values()) { + webView.onPause(); + } + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + for (Browser webView : webViewsByTabId.values()) { + webView.onResume(); + } + } + + @Override + public void onDestroyView() { + for (Browser webView : webViewsByTabId.values()) { + webViewContainer.removeView(webView); + webView.destroy(); + } + webViewsByTabId.clear(); + tabIdsByWebView.clear(); + super.onDestroyView(); + } + + public boolean handleBackPressed() { + Browser activeWebView = getActiveWebView(); + if (activeWebView == null) { + return false; + } + if (activeWebView.canGoBack()) { + activeWebView.goBack(); + return true; + } + if (browserViewModel.getTabCount() > 1) { + String tabId = browserViewModel.getActiveTabId().getValue(); + browserViewModel.closeTab(tabId); + return true; + } + return false; + } + + private void ensureWebViewsForTabs(List tabs) { + List existingIds = new ArrayList<>(webViewsByTabId.keySet()); + for (BrowserTab tab : tabs) { + if (!webViewsByTabId.containsKey(tab.getId())) { + createWebView(tab.getId()); + } + existingIds.remove(tab.getId()); + } + + for (String staleId : existingIds) { + Browser staleView = webViewsByTabId.remove(staleId); + if (staleView != null) { + tabIdsByWebView.remove(staleView); + webViewContainer.removeView(staleView); + staleView.destroy(); + } + } + } + + private Browser createWebView(String tabId) { + Browser webView = new Browser(requireContext(), null); + webView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + Boolean desktopMode = browserViewModel.getDesktopMode().getValue(); + webView.setDesktopMode(Boolean.TRUE.equals(desktopMode)); + webView.setVisibility(View.GONE); + webView.setListener(new Browser.Listener() { + @Override + public void onRequestPage(String url) { + if (!isActiveTab(tabId)) { + return; + } + urlInput.setText(url); + progressBar.setIndeterminate(true); + progressBar.setVisibility(View.VISIBLE); + } + + @Override + public void onStartLoadPage(String url) { + browserViewModel.updateTabUrl(tabId, url); + if (!isActiveTab(tabId)) { + return; + } + urlInput.setText(url); + progressBar.setProgress(0, false); + progressBar.setVisibility(View.VISIBLE); + browserViewModel.clearStreams(tabId); + } + + @Override + public void onFinishLoadPage(String url) { + if (!isActiveTab(tabId)) { + return; + } + progressBar.setProgress(100, true); + progressBar.setVisibility(View.GONE); + } + + @Override + public void onUpdateUrl(String url) { + browserViewModel.updateTabUrl(tabId, url); + if (isActiveTab(tabId)) { + urlInput.setText(url); + } + BrowserDestination destination = new BrowserDestination(url, webView.getTitle(), ImageUtils.bytesFromBitmap(webView.getFavicon()), System.currentTimeMillis()); + addDestination(destination); + } + + @Override + public void onUpdateTitle(String title) { + browserViewModel.updateTabTitle(tabId, title); + BrowserDestination destination = new BrowserDestination(webView.getUrl(), title, ImageUtils.bytesFromBitmap(webView.getFavicon()), System.currentTimeMillis()); + addDestination(destination); + } + + @Override + public void onUpdateFavicon(Bitmap favicon) { + browserViewModel.updateTabFavicon(tabId, ImageUtils.bytesFromBitmap(favicon)); + BrowserDestination destination = new BrowserDestination(webView.getUrl(), webView.getTitle(), ImageUtils.bytesFromBitmap(favicon), System.currentTimeMillis()); + addDestination(destination); + } + + @Override + public void onUpdateProgress(int progress) { + if (!isActiveTab(tabId)) { + return; + } + progressBar.setIndeterminate(false); + progressBar.setProgress(progress); + } + @Override - public void onUpdateTitle(String title) { - BrowserDestination destination = new BrowserDestination(webView.getUrl(), title, ImageUtils.bytesFromBitmap(webView.getFavicon()), System.currentTimeMillis()); - addDestination(destination); - } - - @Override - public void onUpdateFavicon(Bitmap favicon) { - BrowserDestination destination = new BrowserDestination(webView.getUrl(), webView.getTitle(), ImageUtils.bytesFromBitmap(favicon), System.currentTimeMillis()); - addDestination(destination); - } + public boolean onRedirect(String oldUrl, String newUrl) { + String domain = UrlUtils.getDomainNameFromURL(newUrl); + SettingsManager.DomainPolicy policy = settingsManager.getRedirectPolicy(domain); - @Override - public void onUpdateProgress(int progress) { - progressBar.setIndeterminate(false); - progressBar.setProgress(progress); - } + if (policy == SettingsManager.DomainPolicy.ALLOW) { + return true; + } + if (policy == SettingsManager.DomainPolicy.BLOCK) { + showBottomInfo(getString(R.string.snackbar_redirect_blocked_domain, domain)); + return false; + } - @Override - public boolean onRedirect(String oldUrl, String newUrl) { if (!browserViewModel.getBlockRedirects()) { return true; } - String domain = UrlUtils.getDomainNameFromURL(newUrl); - - SnackbarRequest snackbarRequest = new SnackbarRequest( - getString(R.string.snackbar_redirect_message, domain), - new SnackbarRequest.SnackbarAction( - getString(R.string.snackbar_redirect_action), - () -> { - browserViewModel.setRequestLoadUrl(new BrowserRequest(newUrl)); //webView.loadUrl(newUrl); - } - ) - ); - mainViewModel.showSnackbar(snackbarRequest); - + openRedirectPermissionDialog(domain, newUrl); return false; } - - @Override - public void onFindStream(Stream stream) { - browserViewModel.addStream(stream); - } - - @Override - public void onTouch() { - if (urlInput.isFocused()) { - urlInput.clearFocus(); - - AppUtils.closeKeyboard(urlInput); + + @Override + public void onFindStream(Stream stream) { + browserViewModel.addStream(tabId, stream); + } + + @Override + public void onTouch() { + if (urlInput.isFocused()) { + urlInput.clearFocus(); + AppUtils.closeKeyboard(urlInput); + } + } + + @Override + public boolean onCreateWindow(Browser parentBrowser, Message resultMsg) { + BrowserTab sourceTab = browserViewModel.getTab(tabId); + String groupId = sourceTab != null ? sourceTab.getGroupId() : browserViewModel.getActiveGroupId(); + + String currentUrl = parentBrowser.getUrl(); + String domain = UrlUtils.getDomainNameFromURL(currentUrl); + SettingsManager.DomainPolicy popupPolicy = settingsManager.getPopupPolicy(domain); + + if (popupPolicy == SettingsManager.DomainPolicy.BLOCK) { + showBottomInfo(getString(R.string.snackbar_popup_blocked_domain, domain)); + return false; } - } - }); - progressBar = view.findViewById(R.id.fragment_browser_loader); + if (popupPolicy == SettingsManager.DomainPolicy.ALLOW) { + Browser child = createChildWindowWebView(groupId, resultMsg); + return child != null; + } - urlInput = view.findViewById(R.id.fragment_browser_input_url); - urlInput.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_DONE) { - openUrl(v.getText().toString()); + openPopupPermissionDialog(domain, + () -> { + createChildWindowWebView(groupId, resultMsg); + }, + () -> settingsManager.setPopupPolicy(domain, SettingsManager.DomainPolicy.ALLOW), + () -> settingsManager.setPopupPolicy(domain, SettingsManager.DomainPolicy.BLOCK)); + return true; } - return false; - }); - - ImageButton backButton = view.findViewById(R.id.fragment_browser_button_back); - backButton.setOnClickListener(v -> openBrowserHistoryDialog(v)); - - MediaRouteButton mediaRouteButton = view.findViewById(R.id.media_route_button); - CastButtonFactory.setUpMediaRouteButton(requireContext().getApplicationContext(), mediaRouteButton); - + + @Override + public void onCloseWindow(Browser browser) { + String closeTabId = tabIdsByWebView.get(browser); + browserViewModel.closeTab(closeTabId); + } + }); + + webViewsByTabId.put(tabId, webView); + tabIdsByWebView.put(webView, tabId); + webViewContainer.addView(webView); + + return webView; + } + + private void switchToTab(String tabId) { + for (Map.Entry entry : webViewsByTabId.entrySet()) { + entry.getValue().setVisibility(entry.getKey().equals(tabId) ? View.VISIBLE : View.GONE); + } + + Browser webView = getWebView(tabId); + if (webView != null) { + if (webView.getUrl() == null) { + BrowserTab tab = browserViewModel.getTab(tabId); + if (tab != null && tab.getUrl() != null && !tab.getUrl().isEmpty()) { + webView.loadUrl(tab.getUrl()); + } + } + if (webView.getUrl() != null) { + urlInput.setText(webView.getUrl()); + } else { + urlInput.setText(""); + } + } + } + + private void openUrl(String query) { + if (!query.startsWith("https://") && !query.startsWith("http://")) { + if (query.contains(" ") || !query.contains(".")) { + query = "https://google.com/search?q=" + query.replace(" ", "+"); + } else { + query = "https://" + query; + } + } + urlInput.clearFocus(); + browserViewModel.setRequestLoadUrl(new BrowserRequest(query)); + } + + private void addDestination(BrowserDestination destination) { + if (destination.getUrl() == null) { + return; + } + browserViewModel.addDestination(destination, rowId -> { + }); + } + + private void openBrowserHistoryDialog(View view) { + List allDestinations = browserViewModel.getDestinationList().getValue(); + if (allDestinations == null || allDestinations.isEmpty()) { + return; + } + List destinations = allDestinations.subList(0, Math.min(5, allDestinations.size())); + + PopupMenu popup = new PopupMenu(view.getContext(), view); + Menu menu = popup.getMenu(); + + int i = 0; + for (BrowserDestination destination : destinations) { + String domainName = UrlUtils.getDomainNameFromURL(destination.getUrl()); + String text = domainName + " | " + destination.getTitle(); + menu.add(Menu.NONE, i, i, text); + i++; + } + + popup.setOnMenuItemClickListener(item -> { + BrowserDestination destination = destinations.get(item.getItemId()); + browserViewModel.setRequestLoadUrl(new BrowserRequest(destination.getUrl())); + return true; + }); + + popup.show(); + } + + private boolean isActiveTab(String tabId) { + String activeTabId = browserViewModel.getActiveTabId().getValue(); + return tabId != null && tabId.equals(activeTabId); } - @Override - public void onPause() { - webView.onPause(); - super.onPause(); + private Browser createChildWindowWebView(String groupId, Message resultMsg) { + BrowserTab newTab = browserViewModel.createTabInGroup(groupId, true); + Browser webView = getWebView(newTab.getId()); + if (webView == null) { + webView = createWebView(newTab.getId()); + } + if (webView == null) { + return null; + } + android.webkit.WebView.WebViewTransport transport = (android.webkit.WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(webView); + resultMsg.sendToTarget(); + return webView; } - @Override - public void onResume() { - super.onResume(); - webView.onResume(); + private void openRedirectPermissionDialog(String domain, String newUrl) { + if (!isAdded()) { + return; + } + CheckBox rememberCheck = new CheckBox(requireContext()); + rememberCheck.setText(R.string.dialog_action_remember_domain); + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.dialog_redirect_title) + .setMessage(getString(R.string.dialog_redirect_message, domain)) + .setView(rememberCheck) + .setPositiveButton(R.string.dialog_action_allow, (dialog, which) -> { + if (rememberCheck.isChecked()) { + settingsManager.setRedirectPolicy(domain, SettingsManager.DomainPolicy.ALLOW); + } + browserViewModel.setRequestLoadUrl(new BrowserRequest(newUrl)); + }) + .setNeutralButton(R.string.dialog_action_block, (dialog, which) -> { + if (rememberCheck.isChecked()) { + settingsManager.setRedirectPolicy(domain, SettingsManager.DomainPolicy.BLOCK); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); } - private void openUrl(String query) { - if (!query.startsWith("https://") && !query.startsWith("http://")) { - if (query.contains(" ") || !query.contains(".")) { - query = "https://google.com/search?q=" + query.replace(" ", "+"); - } else { - query = "https://" + query; - } + private void openPopupPermissionDialog(String domain, Runnable allowOnceAction, Runnable alwaysAllowAction, Runnable alwaysBlockAction) { + if (!isAdded()) { + return; } - urlInput.clearFocus(); - browserViewModel.setRequestLoadUrl(new BrowserRequest(query));//webView.loadUrl(query); + CheckBox rememberCheck = new CheckBox(requireContext()); + rememberCheck.setText(R.string.dialog_action_remember_domain); + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.dialog_popup_title) + .setMessage(getString(R.string.dialog_popup_message, domain)) + .setView(rememberCheck) + .setPositiveButton(R.string.dialog_action_allow, (dialog, which) -> { + if (rememberCheck.isChecked()) { + alwaysAllowAction.run(); + } + allowOnceAction.run(); + }) + .setNeutralButton(R.string.dialog_action_block, (dialog, which) -> { + if (rememberCheck.isChecked()) { + alwaysBlockAction.run(); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); } - - private void addDestination(BrowserDestination destination) { - browserViewModel.addDestination(destination, rowId -> { - }); + + private Browser getWebView(String tabId) { + return webViewsByTabId.get(tabId); + } + + private Browser getActiveWebView() { + return getWebView(browserViewModel.getActiveTabId().getValue()); } - private void openBrowserHistoryDialog(View view) { - List allDestinations = browserViewModel.getDestinationList().getValue(); - /*if (allDestinations.isEmpty()) { - mainViewModel.showSnackbar("browser history empty"); + private void showBottomInfo(String message) { + View root = getView(); + if (root == null) { return; - }*/ - List destinations = allDestinations.subList(0, Math.min(5, allDestinations.size())); - - PopupMenu popup = new PopupMenu(view.getContext(), view); - Menu menu = popup.getMenu(); - - int i = 0; - for (BrowserDestination destination : destinations) { - String domainName = UrlUtils.getDomainNameFromURL(destination.getUrl()); - String text = domainName + " | " + destination.getTitle(); - menu.add(Menu.NONE, i, i, text); - i++; } - - popup.setOnMenuItemClickListener(item -> { - BrowserDestination destination = destinations.get(item.getItemId()); - browserViewModel.goBack(destination); - return true; - }); - - popup.show(); + Snackbar snackbar = Snackbar.make(root, message, Snackbar.LENGTH_SHORT); + snackbar.setDuration(1000); + snackbar.show(); } - -} + + private void updateTabsButton() { + tabsButton.setContentDescription(getString(R.string.browser_tab_count_description, browserViewModel.getTabCount())); + tabsButton.setText(String.valueOf(browserViewModel.getTabCount())); + } +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserRequest.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserRequest.java index cd38aef..5351a33 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserRequest.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserRequest.java @@ -1,28 +1,53 @@ -package codes.nh.streambrowser.screens.browser; - -import java.util.Collections; -import java.util.Map; - +package codes.nh.streambrowser.screens.browser; + +import java.util.Collections; +import java.util.Map; + public class BrowserRequest { + + private final String url; + + private final Map headers; - private final String url; + private final String tabId; - private final Map headers; + private final boolean openInNewTab; - public BrowserRequest(String url, Map headers) { + private final String groupId; + + public BrowserRequest(String url, Map headers, String tabId, boolean openInNewTab, String groupId) { this.url = url; this.headers = headers; + this.tabId = tabId; + this.openInNewTab = openInNewTab; + this.groupId = groupId; } + public BrowserRequest(String url, Map headers) { + this(url, headers, null, false, null); + } + public BrowserRequest(String url) { this(url, Collections.emptyMap()); } + + public String getUrl() { + return url; + } + + public Map getHeaders() { + return headers; + } - public String getUrl() { - return url; + public String getTabId() { + return tabId; } - public Map getHeaders() { - return headers; + public boolean shouldOpenInNewTab() { + return openInNewTab; + } + + public String getGroupId() { + return groupId; } } diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserTab.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserTab.java new file mode 100644 index 0000000..079077e --- /dev/null +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserTab.java @@ -0,0 +1,75 @@ +package codes.nh.streambrowser.screens.browser; + +import java.util.UUID; + +public class BrowserTab { + + private final String id; + + private String groupId; + + private String url; + + private String title; + + private byte[] favicon; + + private long updatedAt; + + public BrowserTab(String groupId) { + this(UUID.randomUUID().toString(), groupId, null, null, null, System.currentTimeMillis()); + } + + public BrowserTab(String id, String groupId, String url, String title, byte[] favicon, long updatedAt) { + this.id = id; + this.groupId = groupId; + this.url = url; + this.title = title; + this.favicon = favicon; + this.updatedAt = updatedAt; + } + + public String getId() { + return id; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + this.updatedAt = System.currentTimeMillis(); + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + this.updatedAt = System.currentTimeMillis(); + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + this.updatedAt = System.currentTimeMillis(); + } + + public byte[] getFavicon() { + return favicon; + } + + public void setFavicon(byte[] favicon) { + this.favicon = favicon; + this.updatedAt = System.currentTimeMillis(); + } + + public long getUpdatedAt() { + return updatedAt; + } +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserTabGroup.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserTabGroup.java new file mode 100644 index 0000000..9d1e091 --- /dev/null +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserTabGroup.java @@ -0,0 +1,31 @@ +package codes.nh.streambrowser.screens.browser; + +import java.util.UUID; + +public class BrowserTabGroup { + + private final String id; + + private String name; + + public BrowserTabGroup(String name) { + this(UUID.randomUUID().toString(), name); + } + + public BrowserTabGroup(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserViewModel.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserViewModel.java index 3c3f5d2..089ac92 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserViewModel.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/BrowserViewModel.java @@ -1,6 +1,9 @@ package codes.nh.streambrowser.screens.browser; +import android.content.Context; +import android.content.SharedPreferences; import android.app.Application; +import android.util.Base64; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; @@ -8,135 +11,584 @@ import androidx.lifecycle.MutableLiveData; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONObject; + +import codes.nh.streambrowser.database.AppDatabase; +import codes.nh.streambrowser.database.BrowserHistoryDao; +import codes.nh.streambrowser.screens.settings.SettingsManager; +import codes.nh.streambrowser.screens.stream.Stream; +import codes.nh.streambrowser.utils.AppUtils; +import codes.nh.streambrowser.utils.async.Async; + +public class BrowserViewModel extends AndroidViewModel { -import codes.nh.streambrowser.database.AppDatabase; -import codes.nh.streambrowser.database.BrowserHistoryDao; -import codes.nh.streambrowser.screens.settings.SettingsManager; -import codes.nh.streambrowser.screens.stream.Stream; -import codes.nh.streambrowser.utils.AppUtils; -import codes.nh.streambrowser.utils.async.Async; + private static final String PREFS_NAME = "browser_session"; + private static final String KEY_SESSION = "tabs_groups_v1"; + + private final BrowserHistoryDao historyDao; + + private final LiveData> destinationList; -public class BrowserViewModel extends AndroidViewModel { + private final List rawGroups = new ArrayList<>(); - private final BrowserHistoryDao historyDao; + private final MutableLiveData> tabGroups = new MutableLiveData<>(new ArrayList<>()); - private final LiveData> destinationList; + private final List rawTabs = new ArrayList<>(); + + private final MutableLiveData> tabs = new MutableLiveData<>(new ArrayList<>()); + + private final MutableLiveData activeTabId = new MutableLiveData<>(); + + private final Map> streamsByTabId = new HashMap<>(); + + private boolean initialPageLoadPending = true; + private String selectedGroupId; + public BrowserViewModel(@NonNull Application application) { super(application); AppUtils.log("init BrowserViewModel"); - - AppDatabase database = AppDatabase.getInstance(application); - historyDao = database.getBrowserHistoryDao(); + + AppDatabase database = AppDatabase.getInstance(application); + historyDao = database.getBrowserHistoryDao(); destinationList = historyDao.getAll(); - + clearDestinations(20, count -> { AppUtils.log("deleted " + count + " BrowserDestinations"); }); + + boolean restored = restoreSession(); + if (!restored) { + BrowserTabGroup defaultGroup = createGroupInternal("Group 1"); + BrowserTab initialTab = createTabInternal(defaultGroup.getId()); + activeTabId.setValue(initialTab.getId()); + selectedGroupId = defaultGroup.getId(); + } else { + initialPageLoadPending = false; + } + updateStreamsForActiveTab(); + notifyTabsUpdate(); } - //settings + public MutableLiveData> getTabGroups() { + return tabGroups; + } - private final SettingsManager settingsManager = new SettingsManager(getApplication()); + public MutableLiveData> getTabs() { + return tabs; + } - public boolean getBlockRedirects() { - return settingsManager.getBlockRedirects(); + public MutableLiveData getActiveTabId() { + return activeTabId; } - //streams + public BrowserTabGroup createGroup(String name) { + BrowserTabGroup group = createGroupInternal(name); + if (selectedGroupId == null) { + selectedGroupId = group.getId(); + } + notifyTabsUpdate(); + return group; + } - private final List rawStreams = new ArrayList<>(); - private final MutableLiveData> streams = new MutableLiveData<>(rawStreams); + public String getSelectedGroupId() { + return selectedGroupId; + } - public void addStream(Stream stream) { - Stream duplicate = getStream(stream); - if (duplicate != null) { - rawStreams.remove(duplicate); + public void setSelectedGroupId(String groupId) { + if (groupId == null || getGroup(groupId) == null) { + return; } - rawStreams.add(stream); - notifyUpdate(); + selectedGroupId = groupId; + saveSession(); } - public void clearStreams() { - rawStreams.clear(); - notifyUpdate(); + public void renameGroup(String groupId, String newName) { + BrowserTabGroup group = getGroup(groupId); + if (group == null) { + return; + } + if (newName == null) { + return; + } + String normalizedName = newName.trim(); + if (normalizedName.isEmpty()) { + return; + } + group.setName(normalizedName); + notifyTabsUpdate(); } - private void notifyUpdate() { - streams.setValue(rawStreams); + public boolean consumeInitialPageLoadPending() { + if (!initialPageLoadPending) { + return false; + } + initialPageLoadPending = false; + return true; } - private Stream getStream(Stream stream) { - return rawStreams.stream().filter(s -> s.equals(stream)).findFirst().orElse(null); + public BrowserTab createTabInGroup(String groupId, boolean activate) { + if (getGroup(groupId) == null) { + groupId = getActiveGroupId(); + } + if (groupId == null) { + groupId = createGroupInternal("Group " + (rawGroups.size() + 1)).getId(); + } + BrowserTab tab = createTabInternal(groupId); + if (activate) { + activeTabId.setValue(tab.getId()); + updateStreamsForActiveTab(); + } + notifyTabsUpdate(); + return tab; } - public MutableLiveData> getStreams() { - return streams; + public BrowserTab createTabInActiveGroup(boolean activate) { + String groupId = getActiveGroupId(); + if (groupId == null) { + groupId = createGroupInternal("Group " + (rawGroups.size() + 1)).getId(); + } + BrowserTab tab = createTabInternal(groupId); + if (activate) { + activeTabId.setValue(tab.getId()); + updateStreamsForActiveTab(); + } + notifyTabsUpdate(); + return tab; } - //request load url + public BrowserTabGroup createGroupWithTab(boolean activate) { + BrowserTabGroup group = createGroupInternal("Group " + (rawGroups.size() + 1)); + BrowserTab tab = createTabInternal(group.getId()); + if (activate) { + activeTabId.setValue(tab.getId()); + updateStreamsForActiveTab(); + } + notifyTabsUpdate(); + return group; + } - private final MutableLiveData requestLoadUrl = new MutableLiveData<>(); + public void activateTab(String tabId) { + if (getTab(tabId) == null) { + return; + } + activeTabId.setValue(tabId); + updateStreamsForActiveTab(); + notifyTabsUpdate(); + } + + public void closeTab(String tabId) { + BrowserTab tab = getTab(tabId); + if (tab == null) { + return; + } + + rawTabs.remove(tab); + streamsByTabId.remove(tab.getId()); + List groupTabs = getTabsInGroup(tab.getGroupId()); + if (groupTabs.isEmpty()) { + BrowserTabGroup group = getGroup(tab.getGroupId()); + if (group != null) { + rawGroups.remove(group); + } + } + + if (rawTabs.isEmpty()) { + BrowserTabGroup defaultGroup = createGroupInternal("Group 1"); + BrowserTab newTab = createTabInternal(defaultGroup.getId()); + activeTabId.setValue(newTab.getId()); + } else if (Objects.equals(activeTabId.getValue(), tabId)) { + activeTabId.setValue(rawTabs.get(rawTabs.size() - 1).getId()); + } - public MutableLiveData getRequestLoadUrl() { - return requestLoadUrl; + updateStreamsForActiveTab(); + notifyTabsUpdate(); } - public void setRequestLoadUrl(BrowserRequest request) { - requestLoadUrl.setValue(request); + public void moveTabToGroup(String tabId, String groupId) { + BrowserTab tab = getTab(tabId); + BrowserTabGroup group = getGroup(groupId); + if (tab == null || group == null) { + return; + } + tab.setGroupId(groupId); + notifyTabsUpdate(); } - //request desktop mode change + public void reorderTabInGroup(String groupId, int fromIndex, int toIndex) { + List groupTabs = getTabsInGroup(groupId); + if (fromIndex < 0 || toIndex < 0 || fromIndex >= groupTabs.size() || toIndex >= groupTabs.size() || fromIndex == toIndex) { + return; + } - private final MutableLiveData desktopMode = new MutableLiveData<>(settingsManager.getDesktopMode()); + BrowserTab movingTab = groupTabs.get(fromIndex); + BrowserTab targetTab = groupTabs.get(toIndex); + int sourceRawIndex = rawTabs.indexOf(movingTab); + int targetRawIndex = rawTabs.indexOf(targetTab); + if (sourceRawIndex == -1 || targetRawIndex == -1) { + return; + } - public MutableLiveData getDesktopMode() { - return desktopMode; + rawTabs.remove(sourceRawIndex); + if (sourceRawIndex < targetRawIndex) { + targetRawIndex--; + } + rawTabs.add(targetRawIndex, movingTab); + notifyTabsUpdate(); } - public void setDesktopMode(boolean enabled) { - desktopMode.setValue(enabled); + public List getTabsInGroup(String groupId) { + return rawTabs.stream().filter(tab -> Objects.equals(tab.getGroupId(), groupId)).collect(Collectors.toList()); } - //browser history + public String getActiveGroupId() { + BrowserTab tab = getActiveTab(); + return tab != null ? tab.getGroupId() : null; + } - public LiveData> getDestinationList() { - return destinationList; + public BrowserTab getActiveTab() { + return getTab(activeTabId.getValue()); } - public BrowserDestination getDestination(int index) { - List destinations = getDestinationList().getValue(); - return destinations.size() > index ? destinations.get(index) : null; + public BrowserTab getTab(String tabId) { + if (tabId == null) { + return null; + } + return rawTabs.stream().filter(tab -> tabId.equals(tab.getId())).findFirst().orElse(null); } - public boolean goBack() { - BrowserDestination previousDestination = getDestination(1); - if (previousDestination == null) return false; - goBack(previousDestination); - return true; + public BrowserTabGroup getGroup(String groupId) { + if (groupId == null) { + return null; + } + return rawGroups.stream().filter(group -> groupId.equals(group.getId())).findFirst().orElse(null); } - public void goBack(BrowserDestination destination) { - deleteDestinationsAfter(destination, success -> { - }); - BrowserRequest browserRequest = new BrowserRequest(destination.getUrl()); - setRequestLoadUrl(browserRequest); + public void updateTabUrl(String tabId, String url) { + BrowserTab tab = getTab(tabId); + if (tab == null) { + return; + } + tab.setUrl(url); + notifyTabsUpdate(); } - //browser history database + public void updateTabTitle(String tabId, String title) { + BrowserTab tab = getTab(tabId); + if (tab == null) { + return; + } + tab.setTitle(title); + notifyTabsUpdate(); + } + + public void updateTabFavicon(String tabId, byte[] favicon) { + BrowserTab tab = getTab(tabId); + if (tab == null) { + return; + } + tab.setFavicon(favicon); + notifyTabsUpdate(); + } + + public int getTabCount() { + return rawTabs.size(); + } + + private BrowserTabGroup createGroupInternal(String name) { + BrowserTabGroup group = new BrowserTabGroup(name); + rawGroups.add(group); + return group; + } + + private BrowserTab createTabInternal(String groupId) { + BrowserTab tab = new BrowserTab(groupId); + rawTabs.add(tab); + return tab; + } + + private void notifyTabsUpdate() { + tabGroups.setValue(Collections.unmodifiableList(new ArrayList<>(rawGroups))); + tabs.setValue(Collections.unmodifiableList(new ArrayList<>(rawTabs))); + saveSession(); + } + + private SharedPreferences getSessionPrefs() { + return getApplication().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + private void saveSession() { + try { + JSONObject root = new JSONObject(); + + JSONArray groupsJson = new JSONArray(); + for (BrowserTabGroup group : rawGroups) { + JSONObject item = new JSONObject(); + item.put("id", group.getId()); + item.put("name", group.getName()); + groupsJson.put(item); + } + + JSONArray tabsJson = new JSONArray(); + for (BrowserTab tab : rawTabs) { + JSONObject item = new JSONObject(); + item.put("id", tab.getId()); + item.put("groupId", tab.getGroupId()); + item.put("url", tab.getUrl()); + item.put("title", tab.getTitle()); + item.put("updatedAt", tab.getUpdatedAt()); + byte[] favicon = tab.getFavicon(); + if (favicon != null) { + item.put("favicon", Base64.encodeToString(favicon, Base64.NO_WRAP)); + } + tabsJson.put(item); + } + + root.put("groups", groupsJson); + root.put("tabs", tabsJson); + root.put("activeTabId", activeTabId.getValue()); + root.put("selectedGroupId", selectedGroupId); + + getSessionPrefs().edit().putString(KEY_SESSION, root.toString()).apply(); + } catch (Exception e) { + AppUtils.log("saveSession()", e); + } + } + + private boolean restoreSession() { + try { + String raw = getSessionPrefs().getString(KEY_SESSION, null); + if (raw == null || raw.isEmpty()) { + return false; + } + + JSONObject root = new JSONObject(raw); + JSONArray groupsJson = root.optJSONArray("groups"); + JSONArray tabsJson = root.optJSONArray("tabs"); + if (groupsJson == null || tabsJson == null || groupsJson.length() == 0 || tabsJson.length() == 0) { + return false; + } + + rawGroups.clear(); + rawTabs.clear(); + + for (int i = 0; i < groupsJson.length(); i++) { + JSONObject item = groupsJson.optJSONObject(i); + if (item == null) { + continue; + } + String id = item.optString("id", null); + String name = item.optString("name", "Group " + (i + 1)); + if (id == null) { + continue; + } + rawGroups.add(new BrowserTabGroup(id, name)); + } + + for (int i = 0; i < tabsJson.length(); i++) { + JSONObject item = tabsJson.optJSONObject(i); + if (item == null) { + continue; + } + String id = item.optString("id", null); + String groupId = item.optString("groupId", null); + if (id == null || groupId == null || getGroup(groupId) == null) { + continue; + } + String favicon64 = item.optString("favicon", null); + byte[] favicon = null; + if (favicon64 != null && !favicon64.isEmpty()) { + favicon = Base64.decode(favicon64, Base64.DEFAULT); + } + rawTabs.add(new BrowserTab( + id, + groupId, + item.optString("url", null), + item.optString("title", null), + favicon, + item.optLong("updatedAt", System.currentTimeMillis()) + )); + } + + if (rawGroups.isEmpty() || rawTabs.isEmpty()) { + return false; + } + + String restoredActiveTabId = root.optString("activeTabId", null); + if (getTab(restoredActiveTabId) == null) { + restoredActiveTabId = rawTabs.get(rawTabs.size() - 1).getId(); + } + activeTabId.setValue(restoredActiveTabId); + + String restoredSelectedGroupId = root.optString("selectedGroupId", null); + if (getGroup(restoredSelectedGroupId) == null) { + BrowserTab activeTab = getTab(restoredActiveTabId); + restoredSelectedGroupId = activeTab != null ? activeTab.getGroupId() : rawGroups.get(0).getId(); + } + selectedGroupId = restoredSelectedGroupId; + return true; + } catch (Exception e) { + AppUtils.log("restoreSession()", e); + return false; + } + } + + //settings + + private final SettingsManager settingsManager = new SettingsManager(getApplication()); + + public boolean getBlockRedirects() { + return settingsManager.getBlockRedirects(); + } + + //streams + + private List rawStreams = new ArrayList<>(); + private final MutableLiveData> streams = new MutableLiveData<>(rawStreams); + + public void addStream(String tabId, Stream stream) { + if (tabId == null) { + return; + } + List tabStreams = streamsByTabId.get(tabId); + if (tabStreams == null) { + tabStreams = new ArrayList<>(); + streamsByTabId.put(tabId, tabStreams); + } + + Stream duplicate = tabStreams.stream().filter(s -> s.equals(stream)).findFirst().orElse(null); + if (duplicate != null) { + tabStreams.remove(duplicate); + } + tabStreams.add(stream); - public void addDestination(BrowserDestination destination, Consumer callback) { - Async.execute(() -> historyDao.insert(destination), callback); + if (Objects.equals(tabId, activeTabId.getValue())) { + rawStreams = tabStreams; + notifyUpdate(); + } } - private void deleteDestinationsAfter(BrowserDestination destination, Consumer callback) { - Async.execute(() -> historyDao.deleteAfter(destination.getTime()), callback); + public void addStream(Stream stream) { + addStream(activeTabId.getValue(), stream); } - private void clearDestinations(int keepCount, Consumer callback) { - Async.execute(() -> historyDao.clear(keepCount), callback); + public void clearStreams(String tabId) { + if (tabId == null) { + return; + } + List tabStreams = streamsByTabId.get(tabId); + if (tabStreams == null) { + tabStreams = new ArrayList<>(); + streamsByTabId.put(tabId, tabStreams); + } else { + tabStreams.clear(); + } + + if (Objects.equals(tabId, activeTabId.getValue())) { + rawStreams = tabStreams; + notifyUpdate(); + } + } + + public void clearStreams() { + clearStreams(activeTabId.getValue()); } -} \ No newline at end of file + private void updateStreamsForActiveTab() { + String tabId = activeTabId.getValue(); + if (tabId == null) { + rawStreams = new ArrayList<>(); + notifyUpdate(); + return; + } + List tabStreams = streamsByTabId.get(tabId); + if (tabStreams == null) { + tabStreams = new ArrayList<>(); + streamsByTabId.put(tabId, tabStreams); + } + rawStreams = tabStreams; + notifyUpdate(); + } + + private void notifyUpdate() { + streams.setValue(rawStreams); + } + + public MutableLiveData> getStreams() { + return streams; + } + + //request load url + + private final MutableLiveData requestLoadUrl = new MutableLiveData<>(); + + public MutableLiveData getRequestLoadUrl() { + return requestLoadUrl; + } + + public void setRequestLoadUrl(BrowserRequest request) { + requestLoadUrl.setValue(request); + } + + //request desktop mode change + + private final MutableLiveData desktopMode = new MutableLiveData<>(settingsManager.getDesktopMode()); + + public MutableLiveData getDesktopMode() { + return desktopMode; + } + + public void setDesktopMode(boolean enabled) { + desktopMode.setValue(enabled); + } + + //browser history + + public LiveData> getDestinationList() { + return destinationList; + } + + public BrowserDestination getDestination(int index) { + List destinations = getDestinationList().getValue(); + if (destinations == null) return null; + return destinations.size() > index ? destinations.get(index) : null; + } + + public boolean goBack() { + BrowserDestination previousDestination = getDestination(1); + if (previousDestination == null) return false; + goBack(previousDestination); + return true; + } + + public void goBack(BrowserDestination destination) { + deleteDestinationsAfter(destination, success -> { + }); + BrowserRequest browserRequest = new BrowserRequest(destination.getUrl()); + setRequestLoadUrl(browserRequest); + } + + //browser history database + + public void addDestination(BrowserDestination destination, Consumer callback) { + Async.execute(() -> historyDao.insert(destination), callback); + } + + private void deleteDestinationsAfter(BrowserDestination destination, Consumer callback) { + Async.execute(() -> historyDao.deleteAfter(destination.getTime()), callback); + } + + private void clearDestinations(int keepCount, Consumer callback) { + Async.execute(() -> historyDao.clear(keepCount), callback); + } + +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/TabSwitcherAdapter.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/TabSwitcherAdapter.java new file mode 100644 index 0000000..48c28ce --- /dev/null +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/TabSwitcherAdapter.java @@ -0,0 +1,149 @@ +package codes.nh.streambrowser.screens.browser; + +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.color.MaterialColors; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import codes.nh.streambrowser.R; +import codes.nh.streambrowser.utils.ImageUtils; + +public class TabSwitcherAdapter extends RecyclerView.Adapter { + + public static class Row { + private final BrowserTab tab; + private final String groupName; + private final boolean active; + + public Row(BrowserTab tab, String groupName, boolean active) { + this.tab = tab; + this.groupName = groupName; + this.active = active; + } + + public BrowserTab getTab() { + return tab; + } + } + + private final List rows = new ArrayList<>(); + + public void setRows(List nextRows) { + rows.clear(); + rows.addAll(nextRows); + notifyDataSetChanged(); + } + + public int getRowCount() { + return rows.size(); + } + + public Row getRow(int index) { + return rows.get(index); + } + + public void moveRow(int from, int to) { + if (from < 0 || to < 0 || from >= rows.size() || to >= rows.size() || from == to) { + return; + } + Collections.swap(rows, from, to); + notifyItemMoved(from, to); + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_browser_tab, parent, false); + return new Holder(view); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + Row row = rows.get(position); + + String title = row.tab.getTitle(); + if (title == null || title.trim().isEmpty()) { + title = holder.itemView.getContext().getString(R.string.browser_tab_untitled); + } + + String url = row.tab.getUrl(); + if (url == null || url.trim().isEmpty()) { + url = holder.itemView.getContext().getString(R.string.browser_tab_empty_url); + } + + holder.titleText.setText(title); + holder.urlText.setText(url); + holder.groupText.setText(holder.itemView.getContext().getString(R.string.browser_tab_group_label, row.groupName)); + + Bitmap favicon = ImageUtils.bitmapFromBytes(row.tab.getFavicon()); + if (favicon != null) { + holder.iconView.setImageBitmap(favicon); + } else { + holder.iconView.setImageResource(R.drawable.logo_icon); + } + + int strokeColor = row.active + ? MaterialColors.getColor(holder.itemView, com.google.android.material.R.attr.colorPrimary, 0xFF2196F3) + : MaterialColors.getColor(holder.itemView, com.google.android.material.R.attr.colorOnSurface, 0xFF888888); + holder.cardView.setStrokeColor(strokeColor); + holder.cardView.setStrokeWidth(row.active ? 3 : 1); + + holder.itemView.setOnClickListener(v -> listener.onOpen(row.tab)); + holder.closeButton.setOnClickListener(v -> listener.onClose(row.tab)); + holder.itemView.setOnLongClickListener(v -> { + listener.onMove(row.tab); + return true; + }); + } + + @Override + public int getItemCount() { + return rows.size(); + } + + private Listener listener; + + public void setListener(Listener listener) { + this.listener = listener; + } + + public interface Listener { + void onOpen(BrowserTab tab); + + void onClose(BrowserTab tab); + + void onMove(BrowserTab tab); + } + + static class Holder extends RecyclerView.ViewHolder { + private final MaterialCardView cardView; + private final ImageView iconView; + private final TextView titleText; + private final TextView urlText; + private final TextView groupText; + private final ImageButton closeButton; + + public Holder(@NonNull View itemView) { + super(itemView); + cardView = itemView.findViewById(R.id.card_browser_tab_root); + iconView = itemView.findViewById(R.id.card_browser_tab_icon); + titleText = itemView.findViewById(R.id.card_browser_tab_text_title); + urlText = itemView.findViewById(R.id.card_browser_tab_text_url); + groupText = itemView.findViewById(R.id.card_browser_tab_text_group); + closeButton = itemView.findViewById(R.id.card_browser_tab_button_close); + } + } +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/browser/TabSwitcherFragment.java b/app/src/main/java/codes/nh/streambrowser/screens/browser/TabSwitcherFragment.java new file mode 100644 index 0000000..a620b00 --- /dev/null +++ b/app/src/main/java/codes/nh/streambrowser/screens/browser/TabSwitcherFragment.java @@ -0,0 +1,213 @@ +package codes.nh.streambrowser.screens.browser; + +import android.os.Bundle; +import android.view.View; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import codes.nh.streambrowser.R; +import codes.nh.streambrowser.screens.main.MainViewModel; +import codes.nh.streambrowser.screens.main.SnackbarRequest; +import codes.nh.streambrowser.screens.sheet.SheetFragment; + +public class TabSwitcherFragment extends SheetFragment { + + public TabSwitcherFragment() { + super(R.layout.fragment_tab_switcher, R.string.browser_tab_manager_title); + } + + private BrowserViewModel browserViewModel; + private MainViewModel mainViewModel; + + private ChipGroup groupChips; + private TabSwitcherAdapter adapter; + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + browserViewModel = new ViewModelProvider(requireActivity()).get(BrowserViewModel.class); + mainViewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); + + groupChips = view.findViewById(R.id.fragment_tab_switcher_group_chips); + MaterialButton newTabButton = view.findViewById(R.id.fragment_tab_switcher_button_new_tab); + MaterialButton newGroupButton = view.findViewById(R.id.fragment_tab_switcher_button_new_group); + RecyclerView listView = view.findViewById(R.id.fragment_tab_switcher_list_tabs); + + adapter = new TabSwitcherAdapter(); + listView.setLayoutManager(new LinearLayoutManager(requireContext())); + listView.setAdapter(adapter); + + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP | ItemTouchHelper.DOWN, + 0 + ) { + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + int from = viewHolder.getBindingAdapterPosition(); + int to = target.getBindingAdapterPosition(); + adapter.moveRow(from, to); + browserViewModel.reorderTabInGroup(browserViewModel.getSelectedGroupId(), from, to); + return true; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + } + }); + itemTouchHelper.attachToRecyclerView(listView); + + adapter.setListener(new TabSwitcherAdapter.Listener() { + @Override + public void onOpen(BrowserTab tab) { + browserViewModel.activateTab(tab.getId()); + mainViewModel.closeSheet(); + } + + @Override + public void onClose(BrowserTab tab) { + if (browserViewModel.getTabCount() <= 1) { + mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.browser_tab_close_last_blocked))); + return; + } + browserViewModel.closeTab(tab.getId()); + } + + @Override + public void onMove(BrowserTab tab) { + openMoveTabDialog(tab); + } + }); + + newTabButton.setOnClickListener(v -> { + browserViewModel.createTabInActiveGroup(true); + mainViewModel.closeSheet(); + }); + newGroupButton.setOnClickListener(v -> { + browserViewModel.createGroupWithTab(true); + mainViewModel.closeSheet(); + }); + + browserViewModel.getTabs().observe(getViewLifecycleOwner(), tabs -> render()); + browserViewModel.getTabGroups().observe(getViewLifecycleOwner(), groups -> render()); + browserViewModel.getActiveTabId().observe(getViewLifecycleOwner(), tabId -> render()); + } + + private void render() { + List groups = browserViewModel.getTabGroups().getValue(); + List allTabs = browserViewModel.getTabs().getValue(); + if (groups == null || groups.isEmpty() || allTabs == null || allTabs.isEmpty()) { + return; + } + + String selectedGroupId = browserViewModel.getSelectedGroupId(); + if (selectedGroupId == null || browserViewModel.getGroup(selectedGroupId) == null) { + selectedGroupId = browserViewModel.getActiveGroupId(); + } + if (selectedGroupId == null && !groups.isEmpty()) { + selectedGroupId = groups.get(0).getId(); + } + browserViewModel.setSelectedGroupId(selectedGroupId); + + groupChips.removeAllViews(); + int checkedChipId = View.NO_ID; + for (BrowserTabGroup group : groups) { + Chip chip = new Chip(requireContext()); + chip.setId(View.generateViewId()); + chip.setText(group.getName()); + chip.setCheckable(true); + if (Objects.equals(group.getId(), selectedGroupId)) { + checkedChipId = chip.getId(); + } + chip.setOnClickListener(v -> { + browserViewModel.setSelectedGroupId(group.getId()); + renderTabList(); + }); + chip.setOnLongClickListener(v -> { + openRenameGroupDialog(group); + return true; + }); + groupChips.addView(chip); + } + + if (checkedChipId != View.NO_ID) { + groupChips.check(checkedChipId); + } + + renderTabList(); + } + + private void renderTabList() { + String selectedGroupId = browserViewModel.getSelectedGroupId(); + List tabs = browserViewModel.getTabsInGroup(selectedGroupId); + tabs = new ArrayList<>(tabs); + + String activeTabId = browserViewModel.getActiveTabId().getValue(); + List rows = new ArrayList<>(); + for (BrowserTab tab : tabs) { + BrowserTabGroup group = browserViewModel.getGroup(tab.getGroupId()); + String groupName = group != null ? group.getName() : getString(R.string.browser_tab_group_unknown); + rows.add(new TabSwitcherAdapter.Row(tab, groupName, Objects.equals(activeTabId, tab.getId()))); + } + adapter.setRows(rows); + } + + private void openMoveTabDialog(BrowserTab sourceTab) { + List groups = browserViewModel.getTabGroups().getValue(); + if (groups == null) { + return; + } + + List targets = new ArrayList<>(); + for (BrowserTabGroup group : groups) { + if (!group.getId().equals(sourceTab.getGroupId())) { + targets.add(group); + } + } + if (targets.isEmpty()) { + mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.browser_tab_move_no_group))); + return; + } + + CharSequence[] items = new CharSequence[targets.size()]; + for (int i = 0; i < targets.size(); i++) { + items[i] = targets.get(i).getName(); + } + + new com.google.android.material.dialog.MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.browser_tab_move) + .setItems(items, (dialog, which) -> browserViewModel.moveTabToGroup(sourceTab.getId(), targets.get(which).getId())) + .show(); + } + + private void openRenameGroupDialog(BrowserTabGroup group) { + EditText input = new EditText(requireContext()); + input.setSingleLine(true); + input.setText(group.getName()); + input.setSelection(input.getText().length()); + + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.browser_tab_group_rename) + .setView(input) + .setPositiveButton(R.string.browser_tab_group_rename_save, (dialog, which) -> { + browserViewModel.renameGroup(group.getId(), input.getText().toString()); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/main/MainActivity.java b/app/src/main/java/codes/nh/streambrowser/screens/main/MainActivity.java index bdf7f72..7f8c3eb 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/main/MainActivity.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/main/MainActivity.java @@ -1,366 +1,372 @@ -package codes.nh.streambrowser.screens.main; - -import android.content.res.ColorStateList; -import android.os.Bundle; -import android.view.View; - -import androidx.activity.OnBackPressedCallback; -import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.ViewModelProvider; - -import com.google.android.gms.cast.MediaStatus; -import com.google.android.gms.cast.framework.media.RemoteMediaClient; -import com.google.android.material.bottomnavigation.BottomNavigationView; -import com.google.android.material.color.MaterialColors; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; - -import codes.nh.streambrowser.R; +package codes.nh.streambrowser.screens.main; + +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.View; + +import androidx.activity.OnBackPressedCallback; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProvider; + +import com.google.android.gms.cast.MediaStatus; +import com.google.android.gms.cast.framework.media.RemoteMediaClient; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.color.MaterialColors; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; + +import codes.nh.streambrowser.R; import codes.nh.streambrowser.screens.bookmark.BookmarksFragment; +import codes.nh.streambrowser.screens.browser.BrowserFragment; import codes.nh.streambrowser.screens.browser.BrowserViewModel; -import codes.nh.streambrowser.screens.cast.CastFullControllerFragment; -import codes.nh.streambrowser.screens.cast.CastManager; -import codes.nh.streambrowser.screens.cast.CastViewModel; -import codes.nh.streambrowser.screens.help.HelpFragment; -import codes.nh.streambrowser.screens.history.HistoryFragment; -import codes.nh.streambrowser.screens.history.HistoryViewModel; -import codes.nh.streambrowser.screens.settings.SettingsFragment; -import codes.nh.streambrowser.screens.settings.SettingsManager; -import codes.nh.streambrowser.screens.sheet.SheetManager; -import codes.nh.streambrowser.screens.sheet.SheetRequest; -import codes.nh.streambrowser.screens.stream.Stream; -import codes.nh.streambrowser.screens.stream.StreamViewModel; -import codes.nh.streambrowser.screens.stream.StreamsFragment; -import codes.nh.streambrowser.utils.AppUtils; - -public class MainActivity extends AppCompatActivity { - - private BrowserViewModel browserViewModel; - - private HistoryViewModel historyViewModel; - - private MainViewModel mainViewModel; - - private CastViewModel castViewModel; - - private StreamViewModel streamViewModel; - - private SheetManager sheetManager; - - private BottomNavigationView bottomNavigation; - - private View rootView; - - private FloatingActionButton streamsButton; - - //private FragmentContainerView miniControllerFragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - AppUtils.log("onCreate MainActivity"); - - browserViewModel = new ViewModelProvider(this).get(BrowserViewModel.class); - browserViewModel.getStreams().observe(this, streams -> { - updateStreamsAvailable(streams.size()); - }); - - historyViewModel = new ViewModelProvider(this).get(HistoryViewModel.class); - - mainViewModel = new ViewModelProvider(this).get(MainViewModel.class); - mainViewModel.getCurrentSheet().observe(this, sheet -> { - if (sheet == null) { - sheetManager.close(); - } else { - sheetManager.open(sheet); - } - }); +import codes.nh.streambrowser.screens.cast.CastFullControllerFragment; +import codes.nh.streambrowser.screens.cast.CastManager; +import codes.nh.streambrowser.screens.cast.CastViewModel; +import codes.nh.streambrowser.screens.help.HelpFragment; +import codes.nh.streambrowser.screens.history.HistoryFragment; +import codes.nh.streambrowser.screens.history.HistoryViewModel; +import codes.nh.streambrowser.screens.settings.SettingsFragment; +import codes.nh.streambrowser.screens.settings.SettingsManager; +import codes.nh.streambrowser.screens.sheet.SheetManager; +import codes.nh.streambrowser.screens.sheet.SheetRequest; +import codes.nh.streambrowser.screens.stream.Stream; +import codes.nh.streambrowser.screens.stream.StreamViewModel; +import codes.nh.streambrowser.screens.stream.StreamsFragment; +import codes.nh.streambrowser.utils.AppUtils; + +public class MainActivity extends AppCompatActivity { + + private BrowserViewModel browserViewModel; + + private HistoryViewModel historyViewModel; + + private MainViewModel mainViewModel; + + private CastViewModel castViewModel; + + private StreamViewModel streamViewModel; + + private SheetManager sheetManager; + + private BottomNavigationView bottomNavigation; + + private View rootView; + + private FloatingActionButton streamsButton; + + //private FragmentContainerView miniControllerFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + AppUtils.log("onCreate MainActivity"); + + browserViewModel = new ViewModelProvider(this).get(BrowserViewModel.class); + browserViewModel.getStreams().observe(this, streams -> { + updateStreamsAvailable(streams.size()); + }); + + historyViewModel = new ViewModelProvider(this).get(HistoryViewModel.class); + + mainViewModel = new ViewModelProvider(this).get(MainViewModel.class); + mainViewModel.getCurrentSheet().observe(this, sheet -> { + if (sheet == null) { + sheetManager.close(); + } else { + sheetManager.open(sheet); + } + }); mainViewModel.getSnackbarMessage().observe(this, request -> { Snackbar snackbar = Snackbar.make(rootView, request.getMessage(), Snackbar.LENGTH_SHORT); + if (request.getDurationMs() > 0) { + snackbar.setDuration(request.getDurationMs()); + } if (request.getAction() != null) { snackbar.setAction(request.getAction().getMessage(), view -> request.getAction().getAction().run()); } snackbar.show(); }); - - castViewModel = new ViewModelProvider(this).get(CastViewModel.class); - castViewModel.getCastManager().setListener(castListener); - - streamViewModel = new ViewModelProvider(this).get(StreamViewModel.class); - streamViewModel.getStreamRequest().observe(this, streamRequest -> { - if (streamRequest != null) { - AppUtils.log("getStreamRequest"); - startStream(streamRequest.getStream()); - streamViewModel.play(null); - } - }); - - rootView = findViewById(R.id.activity_main_layout_root); - - sheetManager = new SheetManager(this); - sheetManager.setListener(new SheetManager.Listener() { - @Override - public void onOpen(SheetRequest sheetRequest) { - if (sheetRequest.getFragmentClass() == StreamsFragment.class) { - clearNavigationSelection(); - } - } - - @Override - public void onRequestGoBack() { - mainViewModel.goBackToPreviousSheet(); - } - - @Override - public void onRequestClose() { - mainViewModel.closeSheet(); - } - - @Override - public void onClosed() { - clearNavigationSelection(); - } - }); - - streamsButton = findViewById(R.id.activity_main_button_streams); - streamsButton.setOnClickListener(view -> { - SheetRequest request = new SheetRequest(StreamsFragment.class); - mainViewModel.openSheet(request); - }); - - /*miniControllerFragment = findViewById(R.id.activity_main_fragment_minicontroller); - miniControllerFragment.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - SheetRequest request = new SheetRequest(CastFullControllerFragment.class); - mainViewModel.openSheet(request); - } - }); - - if (castViewModel.getCastManager().isPlaying()) { - miniControllerFragment.setVisibility(View.VISIBLE); - }*/ - - bottomNavigation = findViewById(R.id.activity_main_navigation_bottom); - clearNavigationSelection(); - bottomNavigation.setOnItemSelectedListener(item -> { - int id = item.getItemId(); - SheetRequest request; - if (id == R.id.action_navigation_bookmarks) { - request = new SheetRequest(BookmarksFragment.class); - } else if (id == R.id.action_navigation_history) { - request = new SheetRequest(HistoryFragment.class); - } else if (id == R.id.action_navigation_help) { - request = new SheetRequest(HelpFragment.class); - } else if (id == R.id.action_navigation_settings) { - request = new SheetRequest(SettingsFragment.class); - } else { - return false; - } - mainViewModel.openSheet(request); - return true; - }); - - getOnBackPressedDispatcher().addCallback(backPressedCallback); - - castViewModel.getCastManager().startSessionListener(); - } - - @Override - protected void onDestroy() { - AppUtils.log("onDestroy MainActivity"); - - backPressedCallback.remove(); - - castViewModel.getCastManager().stopSessionListener(); - - super.onDestroy(); - } - - //navigation - - public void clearNavigationSelection() { - bottomNavigation.setSelectedItemId(R.id.action_navigation_divider); - } - - //streams available - - private void updateStreamsAvailable(int count) { - int primaryColor = MaterialColors.getColor(this, R.attr.colorPrimary, "colorPrimary missing"); - int surfaceColor = MaterialColors.getColor(this, R.attr.colorSurface, "colorSurface missing"); - int color = count > 0 ? primaryColor : surfaceColor; - streamsButton.setBackgroundTintList(ColorStateList.valueOf(color)); - } - - //back button - - private long lastClickedBack = System.currentTimeMillis(); - - private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - - if (mainViewModel.isSheetOpen()) { - mainViewModel.goBackToPreviousSheet(); + + castViewModel = new ViewModelProvider(this).get(CastViewModel.class); + castViewModel.getCastManager().setListener(castListener); + + streamViewModel = new ViewModelProvider(this).get(StreamViewModel.class); + streamViewModel.getStreamRequest().observe(this, streamRequest -> { + if (streamRequest != null) { + AppUtils.log("getStreamRequest"); + startStream(streamRequest.getStream()); + streamViewModel.play(null); + } + }); + + rootView = findViewById(R.id.activity_main_layout_root); + + sheetManager = new SheetManager(this); + sheetManager.setListener(new SheetManager.Listener() { + @Override + public void onOpen(SheetRequest sheetRequest) { + if (sheetRequest.getFragmentClass() == StreamsFragment.class) { + clearNavigationSelection(); + } + } + + @Override + public void onRequestGoBack() { + mainViewModel.goBackToPreviousSheet(); + } + + @Override + public void onRequestClose() { + mainViewModel.closeSheet(); + } + + @Override + public void onClosed() { + clearNavigationSelection(); + } + }); + + streamsButton = findViewById(R.id.activity_main_button_streams); + streamsButton.setOnClickListener(view -> { + SheetRequest request = new SheetRequest(StreamsFragment.class); + mainViewModel.openSheet(request); + }); + + /*miniControllerFragment = findViewById(R.id.activity_main_fragment_minicontroller); + miniControllerFragment.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + SheetRequest request = new SheetRequest(CastFullControllerFragment.class); + mainViewModel.openSheet(request); + } + }); + + if (castViewModel.getCastManager().isPlaying()) { + miniControllerFragment.setVisibility(View.VISIBLE); + }*/ + + bottomNavigation = findViewById(R.id.activity_main_navigation_bottom); + clearNavigationSelection(); + bottomNavigation.setOnItemSelectedListener(item -> { + int id = item.getItemId(); + SheetRequest request; + if (id == R.id.action_navigation_bookmarks) { + request = new SheetRequest(BookmarksFragment.class); + } else if (id == R.id.action_navigation_history) { + request = new SheetRequest(HistoryFragment.class); + } else if (id == R.id.action_navigation_help) { + request = new SheetRequest(HelpFragment.class); + } else if (id == R.id.action_navigation_settings) { + request = new SheetRequest(SettingsFragment.class); + } else { + return false; + } + mainViewModel.openSheet(request); + return true; + }); + + getOnBackPressedDispatcher().addCallback(backPressedCallback); + + castViewModel.getCastManager().startSessionListener(); + } + + @Override + protected void onDestroy() { + AppUtils.log("onDestroy MainActivity"); + + backPressedCallback.remove(); + + castViewModel.getCastManager().stopSessionListener(); + + super.onDestroy(); + } + + //navigation + + public void clearNavigationSelection() { + bottomNavigation.setSelectedItemId(R.id.action_navigation_divider); + } + + //streams available + + private void updateStreamsAvailable(int count) { + int primaryColor = MaterialColors.getColor(this, R.attr.colorPrimary, "colorPrimary missing"); + int surfaceColor = MaterialColors.getColor(this, R.attr.colorSurface, "colorSurface missing"); + int color = count > 0 ? primaryColor : surfaceColor; + streamsButton.setBackgroundTintList(ColorStateList.valueOf(color)); + } + + //back button + + private long lastClickedBack = System.currentTimeMillis(); + + private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + + if (mainViewModel.isSheetOpen()) { + mainViewModel.goBackToPreviousSheet(); + return; + } + + BrowserFragment browserFragment = (BrowserFragment) getSupportFragmentManager().findFragmentById(R.id.activity_main_container_browser); + if (browserFragment != null && browserFragment.handleBackPressed()) { return; } - - boolean success = browserViewModel.goBack(); - if (success) return; - - int delay = getResources().getInteger(R.integer.back_click_close_delay); - - long now = System.currentTimeMillis(); - if (now - lastClickedBack < delay) { - finishAffinity(); - return; - } - - lastClickedBack = now; - mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.toast_close_app))); - - } - }; - - //stream - - /* - public void playStream(Stream stream) { - historyViewModel.getHistory(stream.getStreamUrl(), historyList -> { - - if (historyList.isEmpty()) { - startStream(stream); - return; - } - - Stream history = historyList.get(0); - if (history.getStartTime() < 1000) { - startStream(stream); - return; - } - - stream.setStartTime(history.getStartTime()); - openStreamResumeDialog(stream); - - }); - } - */ - - /* - private void openStreamResumeDialog(Stream stream) { - String time = AppUtils.millisToMinutesSeconds(stream.getStartTime()); - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.dialog_stream_resume_title) - .setMessage(getString(R.string.dialog_stream_resume_message, time)) - .setNeutralButton("No", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - stream.setStartTime(0); - startStream(stream); - } - }) - .setPositiveButton("Yes", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - startStream(stream); - } - }) - .show(); - } - */ - - private void startStream(Stream stream) { - castViewModel.getCastManager().requestStream(this, stream); - } - - private final CastManager.Listener castListener = new CastManager.Listener() { - - @Override - public void onSessionUpdate(CastManager.SessionStatus sessionStatus, Object... data) { - if (sessionStatus == CastManager.SessionStatus.STARTING) { - mainViewModel.showSnackbar(new SnackbarRequest("starting cast session...")); - } - } - - @Override - public void onPlaybackRequested(Stream stream) { - mainViewModel.showSnackbar(new SnackbarRequest("playback requested...")); - } - - @Override - public void onPlaybackStarted(Stream stream, String error) { - - if (error != null) { - mainViewModel.closeSheet(CastFullControllerFragment.class); - - mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.snackbar_play_error_message, error))); - - boolean proxyEnabled = new SettingsManager(getApplicationContext()).getUseProxy(); - if (!proxyEnabled || stream.useProxy()) { //if streaming through proxy is disabled or stream is already playing on proxy - return; - } - - //try streaming through proxy - stream.setUseProxy(true); - startStream(stream); - - return; - } - - SheetRequest request = new SheetRequest(CastFullControllerFragment.class); - mainViewModel.openSheet(request); - - historyViewModel.addHistory(stream, rowId -> { - }); - } - - @Override - public void onPlaybackUpdate(RemoteMediaClient remoteMediaClient, int stateId) { - - if (stateId == MediaStatus.PLAYER_STATE_IDLE) { - mainViewModel.closeSheet(CastFullControllerFragment.class); - return; - } - - Stream stream; - try { - stream = Stream.fromJson(remoteMediaClient.getMediaInfo().getCustomData()); - } catch (Exception e) { - AppUtils.log("onPlaybackUpdate getMediaInfo().getCustomData()", e); - return; - } - - long streamPosition = remoteMediaClient.isLiveStream() ? -1 : remoteMediaClient.getApproximateStreamPosition(); - stream.setStartTime(streamPosition); - - historyViewModel.addHistory(stream, rowId -> { //todo updateHistory - }); - - } - - @Override - public void onReceiveMessage(String message) { - AppUtils.log("onMessageReceived: " + message); - } - }; - - /* - @Override - public boolean dispatchTouchEvent(MotionEvent event) { //clear edittext focus on click outside - if (event.getAction() == MotionEvent.ACTION_DOWN) { - View view = getCurrentFocus(); - if (view instanceof EditText) { - Rect outRect = new Rect(); - view.getGlobalVisibleRect(outRect); - if (!outRect.contains((int) event.getRawX(), (int) event.getRawY())) { - view.clearFocus(); - AppUtils.closeKeyboard(view); - } - } - } - return super.dispatchTouchEvent(event); - }*/ - -} \ No newline at end of file + + int delay = getResources().getInteger(R.integer.back_click_close_delay); + + long now = System.currentTimeMillis(); + if (now - lastClickedBack < delay) { + finishAffinity(); + return; + } + + lastClickedBack = now; + mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.toast_close_app))); + + } + }; + + //stream + + /* + public void playStream(Stream stream) { + historyViewModel.getHistory(stream.getStreamUrl(), historyList -> { + + if (historyList.isEmpty()) { + startStream(stream); + return; + } + + Stream history = historyList.get(0); + if (history.getStartTime() < 1000) { + startStream(stream); + return; + } + + stream.setStartTime(history.getStartTime()); + openStreamResumeDialog(stream); + + }); + } + */ + + /* + private void openStreamResumeDialog(Stream stream) { + String time = AppUtils.millisToMinutesSeconds(stream.getStartTime()); + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.dialog_stream_resume_title) + .setMessage(getString(R.string.dialog_stream_resume_message, time)) + .setNeutralButton("No", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + stream.setStartTime(0); + startStream(stream); + } + }) + .setPositiveButton("Yes", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + startStream(stream); + } + }) + .show(); + } + */ + + private void startStream(Stream stream) { + castViewModel.getCastManager().requestStream(this, stream); + } + + private final CastManager.Listener castListener = new CastManager.Listener() { + + @Override + public void onSessionUpdate(CastManager.SessionStatus sessionStatus, Object... data) { + if (sessionStatus == CastManager.SessionStatus.STARTING) { + mainViewModel.showSnackbar(new SnackbarRequest("starting cast session...")); + } + } + + @Override + public void onPlaybackRequested(Stream stream) { + mainViewModel.showSnackbar(new SnackbarRequest("playback requested...")); + } + + @Override + public void onPlaybackStarted(Stream stream, String error) { + + if (error != null) { + mainViewModel.closeSheet(CastFullControllerFragment.class); + + mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.snackbar_play_error_message, error))); + + boolean proxyEnabled = new SettingsManager(getApplicationContext()).getUseProxy(); + if (!proxyEnabled || stream.useProxy()) { //if streaming through proxy is disabled or stream is already playing on proxy + return; + } + + //try streaming through proxy + stream.setUseProxy(true); + startStream(stream); + + return; + } + + SheetRequest request = new SheetRequest(CastFullControllerFragment.class); + mainViewModel.openSheet(request); + + historyViewModel.addHistory(stream, rowId -> { + }); + } + + @Override + public void onPlaybackUpdate(RemoteMediaClient remoteMediaClient, int stateId) { + + if (stateId == MediaStatus.PLAYER_STATE_IDLE) { + mainViewModel.closeSheet(CastFullControllerFragment.class); + return; + } + + Stream stream; + try { + stream = Stream.fromJson(remoteMediaClient.getMediaInfo().getCustomData()); + } catch (Exception e) { + AppUtils.log("onPlaybackUpdate getMediaInfo().getCustomData()", e); + return; + } + + long streamPosition = remoteMediaClient.isLiveStream() ? -1 : remoteMediaClient.getApproximateStreamPosition(); + stream.setStartTime(streamPosition); + + historyViewModel.addHistory(stream, rowId -> { //todo updateHistory + }); + + } + + @Override + public void onReceiveMessage(String message) { + AppUtils.log("onMessageReceived: " + message); + } + }; + + /* + @Override + public boolean dispatchTouchEvent(MotionEvent event) { //clear edittext focus on click outside + if (event.getAction() == MotionEvent.ACTION_DOWN) { + View view = getCurrentFocus(); + if (view instanceof EditText) { + Rect outRect = new Rect(); + view.getGlobalVisibleRect(outRect); + if (!outRect.contains((int) event.getRawX(), (int) event.getRawY())) { + view.clearFocus(); + AppUtils.closeKeyboard(view); + } + } + } + return super.dispatchTouchEvent(event); + }*/ + +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/main/SnackbarRequest.java b/app/src/main/java/codes/nh/streambrowser/screens/main/SnackbarRequest.java index 52b38b2..2cb26c2 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/main/SnackbarRequest.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/main/SnackbarRequest.java @@ -1,46 +1,61 @@ -package codes.nh.streambrowser.screens.main; - +package codes.nh.streambrowser.screens.main; + public class SnackbarRequest { private final String message; private final SnackbarAction action; - public SnackbarRequest(String message, SnackbarAction action) { + private final int durationMs; + + public SnackbarRequest(String message, SnackbarAction action, int durationMs) { this.message = message; this.action = action; + this.durationMs = durationMs; } - public SnackbarRequest(String message) { - this(message, null); + public SnackbarRequest(String message, SnackbarAction action) { + this(message, action, -1); } - public String getMessage() { - return message; + public SnackbarRequest(String message) { + this(message, null, -1); } + public SnackbarRequest(String message, int durationMs) { + this(message, null, durationMs); + } + + public String getMessage() { + return message; + } + public SnackbarAction getAction() { return action; } - public static class SnackbarAction { - - private final String message; - - private final Runnable action; - - public SnackbarAction(String message, Runnable action) { - this.message = message; - this.action = action; - } - - public String getMessage() { - return message; - } - - public Runnable getAction() { - return action; - } - + public int getDurationMs() { + return durationMs; } -} + + public static class SnackbarAction { + + private final String message; + + private final Runnable action; + + public SnackbarAction(String message, Runnable action) { + this.message = message; + this.action = action; + } + + public String getMessage() { + return message; + } + + public Runnable getAction() { + return action; + } + + } +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/settings/SettingsFragment.java b/app/src/main/java/codes/nh/streambrowser/screens/settings/SettingsFragment.java index a895e16..1ce1852 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/settings/SettingsFragment.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/settings/SettingsFragment.java @@ -1,56 +1,157 @@ -package codes.nh.streambrowser.screens.settings; - +package codes.nh.streambrowser.screens.settings; + import android.os.Bundle; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import androidx.preference.Preference; -import codes.nh.streambrowser.R; -import codes.nh.streambrowser.screens.browser.BrowserViewModel; -import codes.nh.streambrowser.screens.main.MainViewModel; -import codes.nh.streambrowser.screens.main.SnackbarRequest; -import codes.nh.streambrowser.screens.sheet.SheetFragment; -import codes.nh.streambrowser.utils.AppUtils; - -public class SettingsFragment extends SheetFragment { - - public SettingsFragment() { - super(R.layout.fragment_settings, R.string.navigation_title_settings); - AppUtils.log("init SettingsFragment"); - } - - private BrowserViewModel browserViewModel; - - private MainViewModel mainViewModel; - - private SettingsManager settingsManager; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; - @Override +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import codes.nh.streambrowser.R; +import codes.nh.streambrowser.screens.browser.BrowserViewModel; +import codes.nh.streambrowser.screens.main.MainViewModel; +import codes.nh.streambrowser.screens.main.SnackbarRequest; +import codes.nh.streambrowser.screens.sheet.SheetFragment; +import codes.nh.streambrowser.utils.AppUtils; + +public class SettingsFragment extends SheetFragment { + + public SettingsFragment() { + super(R.layout.fragment_settings, R.string.navigation_title_settings); + AppUtils.log("init SettingsFragment"); + } + + private BrowserViewModel browserViewModel; + + private MainViewModel mainViewModel; + + private SettingsManager settingsManager; + + @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - - browserViewModel = new ViewModelProvider(requireActivity()).get(BrowserViewModel.class); - - mainViewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); - + + browserViewModel = new ViewModelProvider(requireActivity()).get(BrowserViewModel.class); + + mainViewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class); + settingsManager = new SettingsManager(getApplicationContext()); settingsManager.registerListener((sharedPreferences, key) -> { - if (key.equalsIgnoreCase("preference_desktop_mode")) { - browserViewModel.setDesktopMode(settingsManager.getDesktopMode()); - } else if (key.equalsIgnoreCase("preference_block_redirects")) { - - } else if (key.equalsIgnoreCase("preference_skip_time")) { - mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.toast_restart_to_apply))); + if (key.equalsIgnoreCase("preference_desktop_mode")) { + browserViewModel.setDesktopMode(settingsManager.getDesktopMode()); + } else if (key.equalsIgnoreCase("preference_block_redirects")) { + + } else if (key.equalsIgnoreCase("preference_skip_time")) { + mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.toast_restart_to_apply))); } }); + + bindPreferenceActions(); } @Override + public void onResume() { + super.onResume(); + bindPreferenceActions(); + } + + @Override public void onDestroy() { super.onDestroy(); settingsManager.unregisterListener(); } -} \ No newline at end of file + private void bindPreferenceActions() { + Fragment child = getChildFragmentManager().findFragmentById(R.id.fragment_settings_container_preferences); + if (!(child instanceof PreferenceFragment)) { + return; + } + + PreferenceFragment preferences = (PreferenceFragment) child; + + Preference redirectRules = preferences.findPreference("preference_manage_redirect_policies"); + if (redirectRules != null) { + redirectRules.setOnPreferenceClickListener(preference -> { + showPolicyListDialog(true); + return true; + }); + } + + Preference popupRules = preferences.findPreference("preference_manage_popup_policies"); + if (popupRules != null) { + popupRules.setOnPreferenceClickListener(preference -> { + showPolicyListDialog(false); + return true; + }); + } + } + + private void showPolicyListDialog(boolean redirect) { + Map policies = redirect + ? settingsManager.getRedirectPolicies() + : settingsManager.getPopupPolicies(); + + List domains = new ArrayList<>(policies.keySet()); + Collections.sort(domains); + + if (domains.isEmpty()) { + mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.settings_policy_empty))); + return; + } + + CharSequence[] items = new CharSequence[domains.size()]; + for (int i = 0; i < domains.size(); i++) { + String domain = domains.get(i); + SettingsManager.DomainPolicy policy = policies.get(domain); + String policyText = policy == SettingsManager.DomainPolicy.ALLOW + ? getString(R.string.settings_policy_allow) + : getString(R.string.settings_policy_block); + items[i] = domain + " - " + policyText; + } + + int title = redirect ? R.string.settings_redirect_policies_title : R.string.settings_popup_policies_title; + + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(title) + .setItems(items, (dialog, which) -> { + String domain = domains.get(which); + showRemovePolicyDialog(redirect, domain); + }) + .setNeutralButton(R.string.settings_policy_clear_all, (dialog, which) -> { + if (redirect) { + settingsManager.clearRedirectPolicies(); + } else { + settingsManager.clearPopupPolicies(); + } + mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.settings_policy_cleared))); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void showRemovePolicyDialog(boolean redirect, String domain) { + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.settings_policy_remove_title) + .setMessage(getString(R.string.settings_policy_remove_message, domain)) + .setPositiveButton(R.string.settings_policy_remove, (dialog, which) -> { + if (redirect) { + settingsManager.removeRedirectPolicy(domain); + } else { + settingsManager.removePopupPolicy(domain); + } + mainViewModel.showSnackbar(new SnackbarRequest(getString(R.string.settings_policy_removed))); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/settings/SettingsManager.java b/app/src/main/java/codes/nh/streambrowser/screens/settings/SettingsManager.java index 206ae89..270fdd2 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/settings/SettingsManager.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/settings/SettingsManager.java @@ -1,45 +1,183 @@ -package codes.nh.streambrowser.screens.settings; - +package codes.nh.streambrowser.screens.settings; + import android.content.Context; import android.content.SharedPreferences; import androidx.preference.PreferenceManager; +import org.json.JSONObject; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + public class SettingsManager { - private final SharedPreferences preferences; + public enum DomainPolicy { + ASK, + ALLOW, + BLOCK + } + + private static final String KEY_REDIRECT_POLICIES = "preference_redirect_domain_policies"; - public SettingsManager(Context context) { - preferences = PreferenceManager.getDefaultSharedPreferences(context); + private static final String KEY_POPUP_POLICIES = "preference_popup_domain_policies"; + + private final SharedPreferences preferences; + + public SettingsManager(Context context) { + preferences = PreferenceManager.getDefaultSharedPreferences(context); + } + + public boolean getDesktopMode() { + return preferences.getBoolean("preference_desktop_mode", false); + } + + public boolean getBlockRedirects() { + return preferences.getBoolean("preference_block_redirects", true); + } + + public int getSkipTime() { + return preferences.getInt("preference_skip_time", 15); + } + + public boolean getUseProxy() { + return preferences.getBoolean("preference_use_proxy", true); } - public boolean getDesktopMode() { - return preferences.getBoolean("preference_desktop_mode", false); + public DomainPolicy getRedirectPolicy(String domain) { + return getDomainPolicy(KEY_REDIRECT_POLICIES, domain, DomainPolicy.ASK); } - public boolean getBlockRedirects() { - return preferences.getBoolean("preference_block_redirects", true); + public void setRedirectPolicy(String domain, DomainPolicy policy) { + setDomainPolicy(KEY_REDIRECT_POLICIES, domain, policy); } - public int getSkipTime() { - return preferences.getInt("preference_skip_time", 15); + public Map getRedirectPolicies() { + return getDomainPolicies(KEY_REDIRECT_POLICIES); } - public boolean getUseProxy() { - return preferences.getBoolean("preference_use_proxy", true); + public void removeRedirectPolicy(String domain) { + removeDomainPolicy(KEY_REDIRECT_POLICIES, domain); + } + + public void clearRedirectPolicies() { + preferences.edit().remove(KEY_REDIRECT_POLICIES).apply(); + } + + public DomainPolicy getPopupPolicy(String domain) { + return getDomainPolicy(KEY_POPUP_POLICIES, domain, DomainPolicy.ASK); + } + + public void setPopupPolicy(String domain, DomainPolicy policy) { + setDomainPolicy(KEY_POPUP_POLICIES, domain, policy); + } + + public Map getPopupPolicies() { + return getDomainPolicies(KEY_POPUP_POLICIES); } - //listener + public void removePopupPolicy(String domain) { + removeDomainPolicy(KEY_POPUP_POLICIES, domain); + } - private SharedPreferences.OnSharedPreferenceChangeListener listener; + public void clearPopupPolicies() { + preferences.edit().remove(KEY_POPUP_POLICIES).apply(); + } + + private DomainPolicy getDomainPolicy(String key, String domain, DomainPolicy fallback) { + String normalized = normalizeDomain(domain); + if (normalized.isEmpty()) { + return fallback; + } + Map policies = getDomainPolicies(key); + DomainPolicy policy = policies.get(normalized); + return policy != null ? policy : fallback; + } + + private void setDomainPolicy(String key, String domain, DomainPolicy policy) { + String normalized = normalizeDomain(domain); + if (normalized.isEmpty() || policy == null || policy == DomainPolicy.ASK) { + removeDomainPolicy(key, domain); + return; + } + Map policies = new HashMap<>(getDomainPolicies(key)); + policies.put(normalized, policy); + saveDomainPolicies(key, policies); + } + + private void removeDomainPolicy(String key, String domain) { + String normalized = normalizeDomain(domain); + if (normalized.isEmpty()) { + return; + } + Map policies = new HashMap<>(getDomainPolicies(key)); + if (policies.remove(normalized) == null) { + return; + } + saveDomainPolicies(key, policies); + } + + private Map getDomainPolicies(String key) { + String raw = preferences.getString(key, "{}"); + if (raw == null || raw.trim().isEmpty()) { + return Collections.emptyMap(); + } + try { + JSONObject json = new JSONObject(raw); + Map policies = new HashMap<>(); + Iterator keys = json.keys(); + while (keys.hasNext()) { + String domain = keys.next(); + String policyValue = json.optString(domain, null); + if (policyValue == null) { + continue; + } + try { + policies.put(domain, DomainPolicy.valueOf(policyValue)); + } catch (Exception ignored) { + } + } + return policies; + } catch (Exception ignored) { + return Collections.emptyMap(); + } + } - public void registerListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { - this.listener = listener; - preferences.registerOnSharedPreferenceChangeListener(listener); + private void saveDomainPolicies(String key, Map policies) { + if (policies.isEmpty()) { + preferences.edit().remove(key).apply(); + return; + } + JSONObject json = new JSONObject(); + for (Map.Entry entry : policies.entrySet()) { + try { + json.put(entry.getKey(), entry.getValue().name()); + } catch (Exception ignored) { + } + } + preferences.edit().putString(key, json.toString()).apply(); } - public void unregisterListener() { - preferences.unregisterOnSharedPreferenceChangeListener(listener); - listener = null; + private String normalizeDomain(String domain) { + if (domain == null) { + return ""; + } + return domain.trim().toLowerCase(); } -} + + //listener + + private SharedPreferences.OnSharedPreferenceChangeListener listener; + + public void registerListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + this.listener = listener; + preferences.registerOnSharedPreferenceChangeListener(listener); + } + + public void unregisterListener() { + preferences.unregisterOnSharedPreferenceChangeListener(listener); + listener = null; + } +} diff --git a/app/src/main/java/codes/nh/streambrowser/screens/sheet/SheetManager.java b/app/src/main/java/codes/nh/streambrowser/screens/sheet/SheetManager.java index 3dbe008..c878be1 100644 --- a/app/src/main/java/codes/nh/streambrowser/screens/sheet/SheetManager.java +++ b/app/src/main/java/codes/nh/streambrowser/screens/sheet/SheetManager.java @@ -1,124 +1,128 @@ -package codes.nh.streambrowser.screens.sheet; - -import android.view.View; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; - -import com.google.android.material.bottomsheet.BottomSheetBehavior; - -import codes.nh.streambrowser.R; -import codes.nh.streambrowser.utils.AppUtils; - -public class SheetManager { - - private final AppCompatActivity activity; - - private final ImageButton backButton; - - private final TextView titleText; - - private final ImageButton closeButton; - - private final BottomSheetBehavior behavior; - - public SheetManager(AppCompatActivity activity) { - this.activity = activity; - - LinearLayout sheetLayout = activity.findViewById(R.id.fragment_sheet_layout); - this.behavior = BottomSheetBehavior.from(sheetLayout); - this.backButton = sheetLayout.findViewById(R.id.fragment_sheet_button_back); - this.titleText = sheetLayout.findViewById(R.id.fragment_sheet_text_title); - this.closeButton = sheetLayout.findViewById(R.id.fragment_sheet_button_close); - init(); - } - - private void init() { - behavior.setState(BottomSheetBehavior.STATE_HIDDEN); - - behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { - @Override - public void onStateChanged(@NonNull View bottomSheet, int newState) { - if (newState == BottomSheetBehavior.STATE_HIDDEN) { - listener.onClosed(); - } - } - - @Override - public void onSlide(@NonNull View bottomSheet, float slideOffset) { - - } - }); - - backButton.setOnClickListener(view -> listener.onRequestGoBack()); - closeButton.setOnClickListener(view -> listener.onRequestClose()); - - } - - public void open(SheetRequest sheetRequest) { - - listener.onOpen(sheetRequest); - - SheetFragment fragment = null; +package codes.nh.streambrowser.screens.sheet; + +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; + +import codes.nh.streambrowser.R; +import codes.nh.streambrowser.utils.AppUtils; + +public class SheetManager { + + private final AppCompatActivity activity; + + private final ImageButton backButton; + + private final TextView titleText; + + private final ImageButton closeButton; + + private final BottomSheetBehavior behavior; + + public SheetManager(AppCompatActivity activity) { + this.activity = activity; + + LinearLayout sheetLayout = activity.findViewById(R.id.fragment_sheet_layout); + this.behavior = BottomSheetBehavior.from(sheetLayout); + this.backButton = sheetLayout.findViewById(R.id.fragment_sheet_button_back); + this.titleText = sheetLayout.findViewById(R.id.fragment_sheet_text_title); + this.closeButton = sheetLayout.findViewById(R.id.fragment_sheet_button_close); + init(); + } + + private void init() { + behavior.setState(BottomSheetBehavior.STATE_HIDDEN); + + behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) { + listener.onClosed(); + } + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { + + } + }); + + backButton.setOnClickListener(view -> listener.onRequestGoBack()); + closeButton.setOnClickListener(view -> listener.onRequestClose()); + + } + + public void open(SheetRequest sheetRequest) { + + listener.onOpen(sheetRequest); + + SheetFragment fragment = null; try { fragment = sheetRequest.getFragmentClass().newInstance(); } catch (Exception e) { AppUtils.log("SheetManager.open()", e); } - titleText.setText(fragment.getTitleId()); - - backButton.setVisibility(fragment.canGoBack() ? View.VISIBLE : View.INVISIBLE); - - activity.getSupportFragmentManager() - .beginTransaction() - .setCustomAnimations(R.anim.fade_in, R.anim.fade_out) - .replace(R.id.fragment_sheet_frame_content, fragment) - .runOnCommit(() -> behavior.setState(BottomSheetBehavior.STATE_COLLAPSED)) - .commit(); - - } - - public void close() { - if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { + if (fragment == null) { return; } - behavior.setState(BottomSheetBehavior.STATE_HIDDEN); - - activity.getSupportFragmentManager().getFragments().forEach(fragment -> { - if (fragment.getId() == R.id.fragment_sheet_frame_content) { - activity.getSupportFragmentManager() - .beginTransaction() - .remove(fragment) - .commit(); - } - }); - - //listener.onClosed(); todo already done in onStateChange - } - - //listener - - private Listener listener; - - public void setListener(Listener listener) { - this.listener = listener; - } - - public interface Listener { - - void onOpen(SheetRequest sheetRequest); - - void onRequestGoBack(); - - void onRequestClose(); - - void onClosed(); - - } - -} + titleText.setText(fragment.getTitleId()); + + backButton.setVisibility(fragment.canGoBack() ? View.VISIBLE : View.INVISIBLE); + + activity.getSupportFragmentManager() + .beginTransaction() + .setCustomAnimations(R.anim.fade_in, R.anim.fade_out) + .replace(R.id.fragment_sheet_frame_content, fragment) + .runOnCommit(() -> behavior.setState(BottomSheetBehavior.STATE_COLLAPSED)) + .commit(); + + } + + public void close() { + if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { + return; + } + + behavior.setState(BottomSheetBehavior.STATE_HIDDEN); + + activity.getSupportFragmentManager().getFragments().forEach(fragment -> { + if (fragment.getId() == R.id.fragment_sheet_frame_content) { + activity.getSupportFragmentManager() + .beginTransaction() + .remove(fragment) + .commit(); + } + }); + + //listener.onClosed(); todo already done in onStateChange + } + + //listener + + private Listener listener; + + public void setListener(Listener listener) { + this.listener = listener; + } + + public interface Listener { + + void onOpen(SheetRequest sheetRequest); + + void onRequestGoBack(); + + void onRequestClose(); + + void onClosed(); + + } + +} diff --git a/app/src/main/res/layout/card_browser_tab.xml b/app/src/main/res/layout/card_browser_tab.xml new file mode 100644 index 0000000..a0cad40 --- /dev/null +++ b/app/src/main/res/layout/card_browser_tab.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_browser.xml b/app/src/main/res/layout/fragment_browser.xml index e4837a7..8fd5e0f 100644 --- a/app/src/main/res/layout/fragment_browser.xml +++ b/app/src/main/res/layout/fragment_browser.xml @@ -1,68 +1,88 @@ - - - - - - + + + + + + - - - - - - + + + + + + + + - - - - - + + + + + + diff --git a/app/src/main/res/layout/fragment_tab_switcher.xml b/app/src/main/res/layout/fragment_tab_switcher.xml new file mode 100644 index 0000000..8d000b1 --- /dev/null +++ b/app/src/main/res/layout/fragment_tab_switcher.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 258aede..2a20b7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,75 +1,119 @@ - - StreamBrowser - D1CAAFD8 - + + StreamBrowser + D1CAAFD8 + Redirect to %s? Go - Cast Error: %s - - Try again using proxy? - Yes - - Click again to close app - - Restart app to apply changes - - No page loaded - - Streams - History - Cast - Bookmarks - Settings + Redirect request + %1$s wants to redirect you. + New tab request + %1$s wants to open a new tab. + Allow + Always allow for this domain + Block + Always block for this domain + Do not ask again for this domain + Redirect blocked for %1$s + New tab blocked for %1$s + + Cast Error: %s + + Try again using proxy? + Yes + + Click again to close app + + Restart app to apply changes + + No page loaded + + Streams + History + Cast + Bookmarks + Settings Help - Source: %s - Host: %s - File: %s - %s - - Edit bookmark - Url - Title - Save - Remove - - Edit History - Remove - - Loading url info… - - Copy Url - Open Url - - Resume stream - Start stream at %s? - - Stream Info - - Error while loading video, try again. - Stream using proxy - - Resolutions - Share - Download - Cast - Play - Download started, check notification - - - -

Coming soon

- - ]]> -
- - Local file - + Tabs + Untitled tab + New group with tab + Move active tab + Open tabs: %1$d + New tab + Close active tab + Cannot close the last tab + Group + Create another group to move this tab + Back + Tabs + New tab + Group: %1$s + No URL loaded + Tab icon + Close tab + Rename group + Save + + Source: %s + Host: %s + File: %s + %s + + Edit bookmark + Url + Title + Save + Remove + + Edit History + Remove + + Loading url info… + + Copy Url + Open Url + + Resume stream + Start stream at %s? + + Stream Info + + Error while loading video, try again. + Stream using proxy + + Resolutions + Share + Download + Cast + Play + Download started, check notification + + + +

Coming soon

+ + ]]> +
+ + Local file + Cast through proxy? - Yes - No - -
\ No newline at end of file + No saved rules + Always allow + Always block + Redirect rules + New-tab rules + Clear all + Rules cleared + Remove rule + Remove saved rule for %1$s? + Remove + Rule removed + + Yes + No + +
diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 2afdd44..1749bb2 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -1,50 +1,69 @@ - - - + + + + + + + + + + + - - - - - + app:key="category_playback" + app:title="Playback"> + + + + + + app:key="category_permissions" + app:title="Website permissions"> - + app:key="preference_manage_redirect_policies" + app:summary="Manage saved redirect decisions by domain." + app:title="Manage redirect rules" /> - + app:key="preference_manage_popup_policies" + app:summary="Manage saved new-tab decisions by domain." + app:title="Manage new-tab rules" /> - \ No newline at end of file +