diff --git a/.gitignore b/.gitignore index da4cf251..4543d919 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ .cxx app/release/** app/standard/** -app/extended/** \ No newline at end of file +app/extended/** +app/extendedGithub/** diff --git a/README.md b/README.md index 9185af23..fda15c80 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Native Alpha ![OS](https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white&style=plastic) -![OS](https://img.shields.io/badge/MinVersion-8.0-red) -![SDK](https://img.shields.io/badge/SDK-32-yellowgreen) +![OS](https://img.shields.io/badge/MinVersion-9.0-red) +![SDK](https://img.shields.io/badge/SDK-35-yellowgreen) [![GitHub release](https://img.shields.io/github/v/release/cylonid/NativeAlphaForAndroid?include_prereleases&color=blueviolet)](https://github.com/cylonid/NativeAlphaForAndroid/releases) [![Github all releases](https://img.shields.io/github/downloads/cylonid/NativeAlphaForAndroid/total?color=blue&label=GitHub%E2%87%A9&style=plastic)](https://somsubhra.github.io/github-release-stats/?username=cylonid&repository=NativeAlphaForAndroid&page=1&per_page=20) [![GitHub license](https://img.shields.io/github/license/cylonid/NativeAlphaForAndroid?color=orange)](https://github.com/cylonid/NativeAlphaForAndroid/blob/master/LICENSE) @@ -14,32 +14,45 @@ * Create home screen shortcuts and retrieves icons in suitable resolution. * Various settings (JavaScript, cookies, adblocking, location/camera/microphone access) can be set for every web app individually * Navigation with multi-touch gestures while browsing. - * Opt-in adblock using an AdBlock Plus custom webview. + * Opt-in adblock with user-selected filter lists. * Less memory footprint and no privacy-invading app permissions in comparison to native apps * Dark mode for Android 10+ ## Download Options [![IzzyOnDroid Download Badge](graphics/IzzyOnDroid.png)](https://apt.izzysoft.de/fdroid/index/apk/com.cylonid.nativealpha) -[![APK Download Badge](graphics/apk_badge.png)](https://github.com/cylonid/NativeAlphaForAndroid/releases/download/v1.3.0/NativeAlpha-standard-universal-release-v1.3.0.apk) +[![APK Download Badge](graphics/apk_badge.png)](https://github.com/cylonid/NativeAlphaForAndroid/releases/download/v1.5.2/NativeAlpha-extendedGithub-universal-release-v1.5.2.apk) [![Google Play Download Badge](graphics/google_play.png)](https://play.google.com/store/apps/details?id=com.cylonid.nativealpha) ### Paid Download [![Google Play Download Badge](graphics/google_play.png)](https://play.google.com/store/apps/details?id=com.cylonid.nativealpha.pro) + + + +[![LiberaPay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/cylonid/donate) + ## Paid Features + +__Note: From v1.5.0, the GitHub and IzzyOnDroid release is functionally equivalent to Native Alpha Plus.__ + * Sandbox containers: Web Apps are loaded in fully separated sandboxes, cookies or other data are not shared with other Web Apps * Kiosk Mode: Fullscreen with menubars hidden * Biometric Access Protection: For every Web App, you can enable access protection (Fingerprint + fallback to lockscreen PIN) * Experimental "Force Dark Mode" also available for websites (configurable with respect to day-time) -## Latest Changes (v1.3.0) +## Latest Major Changes (v1.5.x) -* Resolved unusual going back behaviour on certain websites -* Added support for Google OAuth-enabled sites -* Context Menu: Long-press context menu with several options (Share, going back/forward, reload...) -* Added pinch-to-zoom setting -* Added option to freely set start URL of Web Apps to support non-standard URLs (expert settings) -* Build for x86 and x86_64 platform included -* Several bugfixes and general improvements +* New adblock engine that allows users to add their own selection of block lists. By default, the app will download and use "Fanboy Ultimate List" from https://fanboy.co.nz. You can change your block list sources at any time. +* Material Design 3-based components and theme +* Cleaner main screen, less buttons: "Delete" and "Open settings" actions are available via swipe, Web Apps are opened by clicking on the label. +* Login using HTTP Auth is supported +* Several bugfixes, most notably regarding the top system bar on devices running Android 15 + +### Small release (v1.5.2) +* Fixed cases where OK button could not be pressed on intro screen +* Changed system top bar to neutral color again +* Fixed an issue with desktop mode on large screens +* Fixed crashes when opening pop up menu +* Information dialog regarding adblock-related crashes ### Native Alpha Plus @@ -48,46 +61,55 @@ ## FAQ -*Q: Why would I need this app if any mobile browser can do the same?* - +
+ Q: Why would I need this app if any mobile browser can do the same? A: Mobile browsers usually only are able to create shortcuts which give a native, borderless fullscreen experience if the website has a Progressive Web App (PWA) manifest. Unfortunately, most websites do not offer this feature yet. Additionally, you cannot set different settings for different websites with an usual browser. +
-*Q: Can I keep multiple log-in sessions of the same website?* - +
+ Q: Can I keep multiple log-in sessions of the same website? A: Yes, this is possible using the sandbox feature of Native Alpha Plus. +
-*Q: Why isn't the sandbox feature in Native Alpha Plus enabled by default?* - +
+ Q: Why isn't the sandbox feature in Native Alpha Plus enabled by default? A: The sandboxing approach is recommended for specific usage rather than general usage because it can limit the performance of the application and increase the disk usage. Therefore, use it for privacy-invasive websites or websites where you want to be logged in twice, but not for any website just because you can. +
-*Q: Is this app a dedicated web browser with its own browser engine?* - +
+ Q: Is this app a dedicated web browser with its own browser engine? A: No. As stated, this app relies on the system built-in Android WebView in order to display the website. For privacy reasons, you can opt to use alternative webviews such as [Bromite](https://www.bromite.org/system_web_view) on rooted phones. Always make sure to use to most recent version of any WebView implementation you use! +
-*Q: Why is it not possible to find an icon for a certain website?* - +
+ Q: Why is it not possible to find an icon for a certain website? A: This problem can occur due to multiple reasons. In most cases, the website does not offer a high-resolution icon. If you are a website maintainer and your website icon cannot be found, look at [RealFaviconGenerator](https://realfavicongenerator.net) for further information. If you think it should work, feel free to post the URL and I will look into it. +
-*Q: In constrast to your promise, this app has a large memory footprint!* - +
+ Q: In constrast to your promise, this app has a large memory footprint! A: This is because Native Alpha makes use of caching in the same way your browser app does, i.e., it saves web content locally on your device. Then it can be loaded faster if you visit the same page again. You can either delete cache regularly yourself or set the "Clear cache after usage" setting in the global settings if memory footprint is a concern for you. However, then websites will take a longer time to load because everything has to be loaded from net. +
-*Q: What is the minimum Android version for running Native Alpha?* - -A: Oreo (8.0). This is because older versions use a discontinued API for creating screenshots which currently is not implemented. - -*Q: I don't want to use Google Play services, is there any other way to obtain Native Alpha Plus?* +
+ Q: What is the minimum Android version for running Native Alpha? +A: Android 9 and newer are supported. +
-A: You can build the app yourself, everything is open-source including the paid features. +
+ Q: I don't want to use Google Play services, is there any other way to obtain Native Alpha Plus? +A: You can build the app yourself, everything is open-source including the paid features. Also, the GitHub release includes the Pro features. +
-## Used libraries/resources +## Notable used libraries/resources * [CircularProgressBar](https://github.com/lopspower/CircularProgressBar) * [JSoup](https://jsoup.org/) -* [AdBlock+WebView](https://github.com/adblockplus/libadblockplus-android) +* [AdblockAndroid](https://github.com/Edsuns/AdblockAndroid) * [MovableFloatingActionButton](https://stackoverflow.com/questions/46370836/android-movable-draggable-floating-action-button-fab) * [Android About Page](https://github.com/medyo/android-about-page) * [Android Databinding](https://developer.android.com/topic/libraries/data-binding) * [AboutLibraries](https://github.com/mikepenz/AboutLibraries) +* [Drag & Drop n' Swipe Recyclerview](https://github.com/ernestoyaquello/DragDropSwipeRecyclerview) For testing purposes: * [Robolectric](https://github.com/robolectric/robolectric) @@ -96,12 +118,15 @@ For testing purposes: A list of used open-source libraries can also be found inside the app ("About" section). ## Screenshots +
+ Click to see screenshots
Main Screen Add Web App Available Web App Settings Global Settings
+
## License diff --git a/app/build.gradle b/app/build.gradle index dc038ed7..27b5a6b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,30 +2,35 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'com.mikepenz.aboutlibraries.plugin' -apply plugin: 'kotlin-android-extensions' + +aboutLibraries { + excludeFields = ["generated"] +} //https://gist.github.com/shakalaca/6422811 android { - applicationVariants.all { variant -> - variant.assembleProvider.get().doLast { - delete fileTree('src/main/java/com/cylonid/nativealpha') { - include '**/__WebViewActivity_*.java' - } - delete fileTree('src/main') { - include '**/AndroidManifest.xml' - } - ant.move(file: 'src/main/AndroidManifest_original.xml', tofile:'src/main/AndroidManifest.xml') - - } + applicationVariants.all { variant -> + variant.assembleProvider.get().doLast { + delete fileTree('src/main/java/com/cylonid/nativealpha') { + include '**/__WebViewActivity_*.java' + } + delete fileTree('src/main') { + include '**/AndroidManifest.xml' + } + ant.move(file: 'src/main/AndroidManifest_original.xml', tofile:'src/main/AndroidManifest.xml') + + } + } + + dependenciesInfo { + includeInApk = false } - - compileSdkVersion 32 - buildToolsVersion "32.1.0-rc1" + compileSdk 35 splits { abi { enable true reset() - include "armeabi-v7a", "arm64-v8a" + include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" universalApk true } } @@ -34,30 +39,35 @@ android { extended { dimension "default" applicationIdSuffix ".pro" - resValue "string", "app_name_unicode", "Native α+" + resValue "string", "app_name", "Native Alpha +" + } + extendedGithub { + dimension "default" + resValue "string", "app_name", "Native Alpha" } standard { dimension "default" isDefault true - resValue "string", "app_name_unicode", "Native α" + resValue "string", "app_name", "Native Alpha" } } defaultConfig { applicationId "com.cylonid.nativealpha" - minSdkVersion 26 - targetSdkVersion 32 - versionCode 1305 - versionName "1.3.0" + minSdkVersion 28 + targetSdkVersion 35 + versionCode 1520 + versionName "1.5.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' } buildFeatures { dataBinding true + viewBinding true } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } buildTypes { release { @@ -66,7 +76,7 @@ android { } debug { applicationIdSuffix = ".debug" - resValue "string", "app_name_unicode", "_DEBUG-Nα" + resValue "string", "app_name", "_DEBUG-Native Alpha" } applicationVariants.all { // this method is use to rename your release apk only @@ -97,90 +107,83 @@ android { execution 'ANDROIDX_TEST_ORCHESTRATOR' } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } + namespace 'com.cylonid.nativealpha' + } repositories { - maven { - url 'https://gitlab.com/api/v4/projects/8817162/packages/maven' - } + google() mavenCentral() - jcenter() } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.mikhaellopez:circularprogressbar:3.0.3' + implementation 'com.mikhaellopez:circularprogressbar:3.1.0' implementation 'org.jsoup:jsoup:1.14.1' - // https://gitlab.com/eyeo/distpartners/libadblockplus-android/-/tags - implementation 'org.adblockplus:adblock-android-webview:5.0.1' implementation 'com.github.ihimanshurawat:Hasher:1.2' - implementation 'androidx.navigation:navigation-fragment:2.5.2' - implementation 'androidx.navigation:navigation-ui:2.5.2' - implementation 'com.google.code.gson:gson:2.8.9' + implementation 'androidx.navigation:navigation-fragment:2.8.9' + implementation 'androidx.navigation:navigation-ui:2.8.9' + implementation 'com.google.code.gson:gson:2.11.0' implementation 'io.github.medyo:android-about-page:2.0.0' - implementation 'androidx.webkit:webkit:1.5.0' + implementation 'androidx.webkit:webkit:1.13.0' implementation 'com.jakewharton:process-phoenix:2.1.2' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.3.1' - testImplementation 'org.json:json:20210307' - androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0' - androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestUtil 'androidx.test:orchestrator:1.4.1' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - implementation "com.mikepenz:aboutlibraries:10.0.0" - implementation "com.mikepenz:aboutlibraries-core:10.0.0" + testImplementation 'org.robolectric:robolectric:4.14.1' + testImplementation 'org.json:json:20250107' + testImplementation 'org.mockito:mockito-core:5.17.0' + testImplementation 'org.mockito:mockito-android:5.17.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0' + androidTestImplementation 'org.mockito:mockito-android:5.17.0' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1' + androidTestImplementation 'androidx.test.espresso:espresso-web:3.6.1' + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test:runner:1.6.2' + androidTestUtil 'androidx.test:orchestrator:1.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + implementation "com.mikepenz:aboutlibraries:11.6.3" + implementation "com.mikepenz:aboutlibraries-core:11.6.3" implementation 'pub.devrel:easypermissions:3.0.0' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "androidx.biometric:biometric:1.1.0" - + implementation 'com.ernestoyaquello.dragdropswiperecyclerview:drag-drop-swipe-recyclerview:1.2.0' + implementation "androidx.fragment:fragment-ktx:1.8.6" + implementation('com.github.Edsuns.AdblockAndroid:ad-filter:v0.9.1') { + exclude group: 'org.jetbrains.anko' + } + implementation 'androidx.work:work-runtime:2.10.0' kapt "com.android.databinding:compiler:3.1.4" + implementation 'com.google.android.material:material:1.12.0' } - -int NUM_OF_CONTAINERS = 8 -String placeholder = "" - -0.upto(NUM_OF_CONTAINERS-1) { int i -> - def trTask = tasks.register( "createWebViewclass${i}", Copy ) { - from 'src/main/java/com/cylonid/nativealpha/WebViewActivity.java' - into 'src/main/java/com/cylonid/nativealpha' - filter { - String line -> line.replaceAll("WebViewActivity", "__WebViewActivity_${i}") - } - rename { String fileName -> - fileName.replace("WebViewActivity.java", "__WebViewActivity_${i}.java") - } - } - preBuild.dependsOn trTask -} - def renameManifest = tasks.register("renameManifest") { ant.move(file: 'src/main/AndroidManifest.xml', tofile:'src/main/AndroidManifest_original.xml') } preBuild.dependsOn renameManifest +int NUM_OF_CONTAINERS = 8 def generateWebViewActivities = tasks.register( "extendAndroidManifest", Copy ) { + dependsOn(renameManifest) from 'src/main/AndroidManifest_original.xml' into 'src/main' + String placeholder = "" String replacement = "" for (int i = 0; i < NUM_OF_CONTAINERS; i++) { replacement += " + TaskProvider trTask = tasks.register("createWebViewclass${i}", Copy) { + from 'src/main/java/com/cylonid/nativealpha/WebViewActivity.java' + into "src/main/java/com/cylonid/nativealpha/__WebViewActivity_${i}" + dependsOn(generateWebViewActivities) + mustRunAfter(i == 0 ? [] : tasks.getByName("createWebViewclass${i-1}")) + + filter { String line -> + line.replaceAll("WebViewActivity", "__WebViewActivity_${i}") + } + + rename { String fileName -> + fileName.replace("WebViewActivity.java", "__WebViewActivity_${i}.java") + } + } + preBuild.dependsOn(trTask) +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/cylonid/nativealpha/TestUtils.java b/app/src/androidTest/java/com/cylonid/nativealpha/TestUtils.java index 302fdb1b..b532cc71 100644 --- a/app/src/androidTest/java/com/cylonid/nativealpha/TestUtils.java +++ b/app/src/androidTest/java/com/cylonid/nativealpha/TestUtils.java @@ -11,7 +11,14 @@ import androidx.test.espresso.UiController; import androidx.test.espresso.ViewAction; import androidx.test.espresso.ViewInteraction; - +import androidx.test.espresso.action.CoordinatesProvider; +import androidx.test.espresso.action.GeneralSwipeAction; +import androidx.test.espresso.action.Press; +import androidx.test.espresso.action.Swipe; +import androidx.test.espresso.matcher.ViewMatchers; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; import org.hamcrest.Matcher; import org.junit.Assert; @@ -58,6 +65,64 @@ public static void waitFor(final long ms) { } } + // Custom ViewAction to perform drag + public static ViewAction dragFromTo(final int amountInPixels) { + return new ViewAction() { + @Override + public Matcher getConstraints() { + return ViewMatchers.isDisplayed(); // Constraints to ensure the view is displayed + } + + @Override + public String getDescription() { + return "Drag from a position given by resource ID and move by the specified amount of pixels"; + } + + @Override + public void perform(androidx.test.espresso.UiController uiController, View view) { + // Start position coordinates + CoordinatesProvider startCoordinates = v -> { + int[] location = new int[2]; + v.getLocationOnScreen(location); + return new float[]{location[0] + v.getWidth() / 2f, location[1] + v.getHeight() / 2f}; + }; + + // End position coordinates (move down by a specified amount of pixels) + CoordinatesProvider endCoordinates = v -> { + int[] location = new int[2]; + v.getLocationOnScreen(location); + return new float[]{location[0] + v.getWidth() / 2f, location[1] + v.getHeight() / 2f + amountInPixels}; + }; + + // Perform swipe action + new GeneralSwipeAction(Swipe.SLOW, startCoordinates, endCoordinates, Press.FINGER) + .perform(uiController, view); + } + }; + } + + public static Matcher getElementFromMatchAtPosition(final Matcher matcher, final int position) { + return new BaseMatcher() { + int counter = 0; + @Override + public boolean matches(final Object item) { + if (matcher.matches(item)) { + if(counter == position) { + counter++; + return true; + } + counter++; + } + return false; + } + + @Override + public void describeTo(final Description description) { + description.appendText("Element at hierarchy position " + position); + } + }; + } + public static void waitForElementWithText(@IdRes int stringId) { ViewInteraction element; @@ -71,6 +136,16 @@ public static void waitForElementWithText(@IdRes int stringId) { } + public static CoordinatesProvider percentX(final float percent) { + return view -> { + int[] location = new int[2]; + view.getLocationOnScreen(location); + float x = location[0] + view.getWidth() * percent; + float y = location[1] + view.getHeight() / 2f; + return new float[]{x, y}; + }; + } + public static AppCompatActivity getCurrentActivity() { final AppCompatActivity[] activity = new AppCompatActivity[1]; onView(isRoot()).check((view, noViewFoundException) -> activity[0] = (AppCompatActivity) view.getContext()); diff --git a/app/src/androidTest/java/com/cylonid/nativealpha/UITests.java b/app/src/androidTest/java/com/cylonid/nativealpha/UITests.java index b174719d..6ca889eb 100644 --- a/app/src/androidTest/java/com/cylonid/nativealpha/UITests.java +++ b/app/src/androidTest/java/com/cylonid/nativealpha/UITests.java @@ -1,7 +1,8 @@ package com.cylonid.nativealpha; -import androidx.appcompat.app.AppCompatDelegate; import androidx.test.espresso.NoMatchingViewException; +import androidx.test.espresso.action.GeneralSwipeAction; +import androidx.test.espresso.action.ViewActions; import androidx.test.espresso.web.webdriver.Locator; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.ActivityTestRule; @@ -9,37 +10,40 @@ import com.cylonid.nativealpha.model.DataManager; import com.cylonid.nativealpha.model.WebApp; +import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; - import java.util.List; import java.util.concurrent.TimeUnit; import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; +import static androidx.test.espresso.action.Press.FINGER; +import static androidx.test.espresso.action.Swipe.FAST; +import static androidx.test.espresso.action.ViewActions.actionWithAssertions; import static androidx.test.espresso.action.ViewActions.clearText; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.scrollTo; +import static androidx.test.espresso.action.ViewActions.swipeDown; import static androidx.test.espresso.action.ViewActions.typeText; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withTagValue; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches; import static androidx.test.espresso.web.model.Atoms.getCurrentUrl; import static androidx.test.espresso.web.sugar.Web.onWebView; import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement; import static androidx.test.espresso.web.webdriver.DriverAtoms.getText; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.cylonid.nativealpha.TestUtils.dragFromTo; import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.core.StringContains.containsString; import static org.junit.Assert.assertEquals; +import android.view.View; + /** * Instrumented test, which will execute on an Android device. * @@ -57,39 +61,58 @@ public void addWebsite() { onView(withId(R.id.switchCreateShortcut)).perform(click()); onView(withId(android.R.id.button1)).perform(click()); assertEquals(DataManager.getInstance().getWebApp(0).getBaseUrl(), "https://github.com"); - onView(allOf(withTagValue(is("btnOpenWebview0")), isDisplayed())).perform(click()); + onView(allOf(withId(R.id.btnWebAppTitle), isDisplayed())).perform(click()); + } + + @Test + public void addMultipleWebsiteAndTestLoadedUrl() { + initMultipleWebsites(List.of("github.com", "orf.at")); + onView(TestUtils.getElementFromMatchAtPosition(withId(R.id.btnWebAppTitle), 1)).perform(click()); + onWebView(Matchers.allOf(withId(R.id.webview))).withNoTimeout().check(webMatches(getCurrentUrl(), containsString("orf.at"))); + } + + @Test + public void setCustomOrderOfWebapps() { + initMultipleWebsites(List.of("github.com", "orf.at")); + onView(TestUtils.getElementFromMatchAtPosition(withId(R.id.dragAnchor), 0)).perform(swipeDown()); + Matcher anchor = TestUtils.getElementFromMatchAtPosition(withId(R.id.dragAnchor), 0); + + onView(anchor) + .perform(actionWithAssertions(dragFromTo(500))); + assertEquals(DataManager.getInstance().getWebApp(0).getOrder(), 1); + } @Test public void startWebView() { - initSingleWebsite("https://twitter.com"); - onView(allOf(withTagValue(is("btnOpenWebview0")), isDisplayed())).perform(click()); + initSingleWebsite("https://github.com"); + onView(allOf(withId(R.id.btnWebAppTitle))).perform(click()); onView(withId(R.id.webview)).check(matches(isDisplayed())); } @Test(expected = NoMatchingViewException.class) public void deleteWebsite() { - initSingleWebsite("https://twitter.com"); - onView(allOf(withTagValue(is("btnDelete0")))).perform(click()); + initSingleWebsite("https://github.com"); + onView(allOf(withId(R.id.webAppListItem))).perform(ViewActions.swipeLeft()); TestUtils.alertDialogAccept(); - onView(allOf(withTagValue(is("btnDelete0")))).check(matches(not(isDisplayed()))); //Throws exception + onView(allOf(withId(R.id.webAppListItem))).check(matches(not(isDisplayed()))); //Throws exception } @Test public void changeWebAppSettings() { - initSingleWebsite("https://whatismybrowser.com/detect/are-cookies-enabled"); - onView(allOf(withTagValue(is("btnSettings0")))).perform(click()); - onView(withId(R.id.switchCookies)).perform(scrollTo()).perform(click()); + initSingleWebsite("https://whatismybrowser.com/detect/are-third-party-cookies-enabled"); + onView(withId(R.id.webAppListItem)).perform(new GeneralSwipeAction(FAST, TestUtils.percentX(0.25f), TestUtils.percentX(0.9f), FINGER)); + onView(withId(R.id.switch3PCookies)).perform(scrollTo()).perform(click()); onView(withId(R.id.btnSave)).perform(click()); - onView(allOf(withTagValue(is("btnOpenWebview0")), isDisplayed())).perform(click()); - onWebView(Matchers.allOf(withId(R.id.webview))).withNoTimeout().withElement(findElement(Locator.ID, "detected_value")).check(webMatches(getText(), containsString("No"))); + onView(allOf(withId(R.id.btnWebAppTitle), isDisplayed())).perform(click()); + onWebView(Matchers.allOf(withId(R.id.webview))).withNoTimeout().withElement(findElement(Locator.ID, "detected_value")).check(webMatches(getText(), containsString("Yes"))); } @Test public void badSSLAccept() { initSingleWebsite("https://untrusted-root.badssl.com/"); - onView(allOf(withTagValue(is("btnOpenWebview0")), isDisplayed())).perform(click()); + onView(allOf(withId(R.id.btnWebAppTitle), isDisplayed())).perform(click()); TestUtils.waitForElementWithText(R.string.load_anyway); onView(withText(R.string.load_anyway)).perform(click()); onWebView(Matchers.allOf(withId(R.id.webview))).withNoTimeout().withElement(findElement(Locator.ID, "content")).check(webMatches(getText(), containsString("untrusted-root"))); @@ -98,7 +121,7 @@ public void badSSLAccept() { @Test(expected = java.lang.RuntimeException.class) public void badSSLDismiss() { initSingleWebsite("https://untrusted-root.badssl.com/"); - onView(allOf(withTagValue(is("btnOpenWebview0")), isDisplayed())).perform(click()); + onView(allOf(withId(R.id.btnWebAppTitle), isDisplayed())).perform(click()); TestUtils.waitForElementWithText(android.R.string.cancel); onView(withText(android.R.string.cancel)).perform(click()); onWebView(Matchers.allOf(withId(R.id.webview))).withTimeout(3, TimeUnit.SECONDS).withElement(findElement(Locator.ID, "content")).check(webMatches(getText(), containsString("untrusted-root"))); @@ -107,48 +130,22 @@ public void badSSLDismiss() { @Test public void openHTTPSite() { - initSingleWebsite("http://annozone.de"); - onView(allOf(withTagValue(is("btnOpenWebview0")), isDisplayed())).perform(click()); + initSingleWebsite("http://httpforever.com/"); + onView(allOf(withId(R.id.btnWebAppTitle), isDisplayed())).perform(click()); TestUtils.waitForElementWithText(android.R.string.cancel); onView(withId(android.R.id.button2)).perform(scrollTo()).perform(click()); -// onView(isRoot()).perform(ViewActions.pressBack()); - onView(allOf(withTagValue(is("btnOpenWebview0")), isDisplayed())).perform(click()); + onView(allOf(withId(R.id.btnWebAppTitle), isDisplayed())).perform(click()); TestUtils.waitForElementWithText(android.R.string.cancel); onView(withId(android.R.id.button1)).perform(scrollTo()).perform(click()); - onWebView(Matchers.allOf(withId(R.id.webview))).withTimeout(6, TimeUnit.SECONDS).check(webMatches(getCurrentUrl(), containsString("annozone"))); + onWebView(Matchers.allOf(withId(R.id.webview))).withTimeout(6, TimeUnit.SECONDS).check(webMatches(getCurrentUrl(), containsString("httpforever.com"))); } -// @Test -// public void changeUIModes() { -// String[] ui_modes = activityTestRule.getActivity().getResources().getStringArray(R.array.ui_modes); -// TestUtils.alertDialogDismiss(); -// -// //Open settings, set to dark mode and cancel -// openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); -// onView(withText(R.string.action_settings)).perform(click()); -// onView(withId(R.id.dropDownTheme)).perform(click()); -// onView(withText(ui_modes[2])).perform(click()); -// assertEquals(AppCompatDelegate.getDefaultNightMode(), AppCompatDelegate.MODE_NIGHT_YES); -// onView(withId(R.id.btnCancel)).perform(click()); -// -// //Check that default mode is restored, change to light mode and check light mode -// assertEquals(AppCompatDelegate.getDefaultNightMode(), AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); -// openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); -// onView(withText(R.string.action_settings)).perform(click()); -// onView(withId(R.id.dropDownTheme)).perform(click()); -// onView(withText(ui_modes[1])).perform(click()); -// onView(withId(R.id.btnSave)).perform(click()); -// assertEquals(AppCompatDelegate.getDefaultNightMode(), AppCompatDelegate.MODE_NIGHT_NO); -// -// } - - private void initSingleWebsite(final String base_url) { activityTestRule.getActivity().runOnUiThread(() -> { DataManager.getInstance().addWebsite(new WebApp(base_url, DataManager.getInstance().getIncrementedID())); - activityTestRule.getActivity().addActiveWebAppsToUI(); + activityTestRule.getActivity().updateWebAppList(); }); TestUtils.acceptLicense(); @@ -161,8 +158,9 @@ private void initMultipleWebsites(final List urls) { for (String base_url : urls) { DataManager.getInstance().addWebsite(new WebApp(base_url, DataManager.getInstance().getIncrementedID())); } - activityTestRule.getActivity().addActiveWebAppsToUI(); + activityTestRule.getActivity().updateWebAppList(); }); + TestUtils.acceptLicense(); //Get rid of welcome message TestUtils.alertDialogDismiss(); } diff --git a/app/src/androidTest/kotlin/com/cylonid/nativealpha/AdblockProviderApiHelperTest.kt b/app/src/androidTest/kotlin/com/cylonid/nativealpha/AdblockProviderApiHelperTest.kt new file mode 100644 index 00000000..8b36384a --- /dev/null +++ b/app/src/androidTest/kotlin/com/cylonid/nativealpha/AdblockProviderApiHelperTest.kt @@ -0,0 +1,74 @@ +package com.cylonid.nativealpha + +import com.cylonid.nativealpha.helper.AdblockProviderApiHelper +import com.cylonid.nativealpha.model.AdblockConfig +import io.github.edsuns.adfilter.AdFilter +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class AdblockProviderApiHelperTest { + + private lateinit var adblockProviderApiHelper: AdblockProviderApiHelper + private lateinit var adFilterProvider: AdFilter + + + @Before + fun setUp() { + adFilterProvider = AdFilter.create(mock()) + adblockProviderApiHelper = AdblockProviderApiHelper(adFilterProvider) + } + + + private fun getActiveUrls(): List { + return adFilterProvider.viewModel.filters.value?.values + ?.map { it.url } ?: emptyList() + } + + @Test + fun shouldInitEmptyRuntimeConfig() { + val ourConfig = listOf( + AdblockConfig("Test", "https://someendpoint.xyzabc"), + AdblockConfig("Test2", "https://someendpoint.xyzdef") + ) + adblockProviderApiHelper.synchronizeAdblockProviderWithSettings(ourConfig) + Assert.assertEquals( + listOf("https://someendpoint.xyzabc", "https://someendpoint.xyzdef"), + getActiveUrls() + ) + } + + @Test + fun shouldAddToExistingRuntimeConfig() { + adFilterProvider.viewModel.addFilter("Test", "https://someendpoint.xyzabc") + val ourConfig = listOf( + AdblockConfig("Test", "https://someendpoint.xyzabc"), + AdblockConfig("Test2", "https://someendpoint.xyzdef") + ) + adblockProviderApiHelper.synchronizeAdblockProviderWithSettings(ourConfig) + Assert.assertEquals( + listOf("https://someendpoint.xyzabc", "https://someendpoint.xyzdef"), + getActiveUrls() + ) + } + + @Test + fun shouldRemoveDeletedRuntimeConfig() { + val ourConfig = listOf( + AdblockConfig("Test", "https://someendpoint.xyzabc"), + AdblockConfig("Test2", "https://someendpoint.xyzdef") + ) + adblockProviderApiHelper.synchronizeAdblockProviderWithSettings(ourConfig) + + adblockProviderApiHelper.synchronizeAdblockProviderWithSettings(emptyList()) + Assert.assertEquals( + emptyList(), + getActiveUrls() + ) + } + +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8cfbe950..933beb54 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ android:name="com.cylonid.nativealpha.util.App" android:allowBackup="true" android:icon="@mipmap/native_alpha" - android:label="@string/app_name_unicode" + android:label="@string/app_name" android:roundIcon="@mipmap/native_alpha" android:supportsRtl="true" android:screenOrientation="portrait" @@ -42,9 +42,10 @@ + android:theme="@style/AppTheme"> + android:theme="@style/AppTheme"> + android:theme="@style/AppTheme"> + android:theme="@style/AppTheme"> + + + @@ -22,21 +25,20 @@

Aktuelle Neuerungen

-

Version: v1.3.0

-
    -
  • Zurück-Verhalten auf div. Webseiten verbessert
  • -
  • Unterstützung für Webseiten mit Google OAuth
  • -
  • Kontextmenü: Langes Drücken öffnen Menü mit mehreren Optionen (Teilen, Vor/Zurück, Neu laden...)
  • -
  • Pinch-To-Zoom-Einstellung hinzugefügt
  • -
  • Start-URL kann nun frei gewählt werden (Experten-Einstellungen)
  • -
  • Build für x86- und x86_64-Plattformen enthalten
  • -
  • Mehrere Bugfixes und generelle Verbesserungen
  • -
-
Native Alpha Plus
+

Version: v1.5.0

+
    -
  • Biometrische Zugriffskontrolle: Für jede Web-App kann getrennt eine Zugriffskontrolle aktiviert werden (Fingerabdruck / Lockscreen-PIN als Fallback)
  • -
  • Weitere Verbesserungen des Dark Modes
  • +
  • Neue Adblock-Engine, mit der Nutzer ihre eigene Auswahl an Blocklisten hinzufügen können. Standardmäßig lädt die App die „Fanboy Ultimate List“ von https://fanboy.co.nz herunter und verwendet sie. Sie können die Quellen Ihrer Blocklisten jederzeit ändern
  • +
  • Komponenten und Design basierend auf Material Design 3
  • +
  • Aufgeräumtere Hauptansicht, weniger Schaltflächen: „Löschen“ und „Einstellungen öffnen“ sind per Wischgeste verfügbar, Web-Apps werden durch Tippen auf das Label geöffnet
  • +
  • Login mittels HTTP Auth wird unterstützt
  • +
  • Seiten mit color-scheme (z.B. wikipedia.org) werden nun automatisch gemäß dem aktivem System-Farbschema (hell/dunkel) dargestellt.
  • +
  • Mehrere Fehlerbehebungen, insbesondere betreffend die obere Systemleiste auf Geräten mit Android 15
+
+
+
+ Nachdem Sie die über GitHub bzw. IzzyOnDroid verteilte App nutzen, erhalten Sie kostenlosen Zugang zu allen Features von Native Alpha Plus. Wenn Sie die App nützlich finden, können Sie den Entwickler auf Liberapay unterstützen.

Nutzungsbedingungen - Haftungsausschluss

@@ -52,6 +54,9 @@

Nutzungsbedingungen - Haftungsausschluss

oder der Nutzung oder anderen Geschäften mit der Software ergeben.

+
+ +
diff --git a/app/src/main/assets/news/latestUpdate_en.html b/app/src/main/assets/news/latestUpdate_en.html index 98638d9d..f073f4f7 100644 --- a/app/src/main/assets/news/latestUpdate_en.html +++ b/app/src/main/assets/news/latestUpdate_en.html @@ -11,6 +11,9 @@ function hideById(id) { document.getElementById(id).style.display = 'none'; } + function showById(id) { + document.getElementById(id).style.display = 'block'; + } @@ -22,27 +25,27 @@

News

-

Version: v1.3.0

-
    -
  • Resolved unusual going back behaviour on certain websites
  • -
  • Added support for Google OAuth-enabled sites
  • -
  • Context Menu: Long-press context menu with several options (Share, going back/forward, reload...)
  • -
  • Added pinch-to-zoom setting
  • -
  • Added option to freely set start URL of Web Apps to support non-standard URLs (expert settings)
  • -
  • Build for x86 and x86_64 platform included
  • -
  • Several bugfixes and general improvements
  • -
-
Native Alpha Plus
+

Version: v1.5.0

+
    -
  • Biometric access Protection: For every Web App, you can enable access protection ( - Fingerprint + fallback to lockscreen PIN)
  • -
  • Further enhancements for Dark Mode
  • +
  • New adblock engine that allows users to add their own selection of block lists. By default, the app will download and use "Fanboy Ultimate List" from https://fanboy.co.nz. You can change your block list sources at any time
  • +
  • Material Design 3-based components and theme
  • +
  • Cleaner main screen, less buttons: "Delete" and "Open settings" actions are available via swipe, Web Apps are opened by clicking on the label
  • +
  • Login using HTTP Auth is supported
  • +
  • Websites with implemented native color-scheme (e.g., wikipedia.org) will now be displayed with respect to the active system theme (light/dark)
  • +
  • Several bugfixes, most notably regarding the top system bar on devices running Android 15 +
+
+ +
+
+ As you are using the app version distributed via GitHub or IzzyOnDroid, you get access to all Plus features free of charge. If you find Native Alpha useful, please consider supporting the developer on Liberapay.

Terms of Usage - Nonliability

This app is published under GNU General Public - License v3. The source code is freely accessible on GitHub. More information in the "About" section.

+ License v3. The source code is freely accessible on GitHub. More information in the "About" section.

The software is provided “as is”, without warranty of any kind, express or implied, including but not limited to @@ -52,6 +55,9 @@

Terms of Usage - Nonliability

dealings in the software.

+
+ +
diff --git a/app/src/main/assets/news/news.css b/app/src/main/assets/news/news.css index 42b96ba0..ec5b94b4 100644 --- a/app/src/main/assets/news/news.css +++ b/app/src/main/assets/news/news.css @@ -1,3 +1,31 @@ +:root { + --custom-red: #b71c1c; + --tonal-border-radius: 20px; + --tonal-font-weight: 500; + --tonal-font-size: 14px; + --button-max-width: 120px; + --button-margin: 15px; + --tonal-bg: #55442A; + --tonal-fg: #F8DFBB; +} + +@media (prefers-color-scheme: dark) { + :root { + --tonal-bg: #FFDAD6; + --tonal-fg: #5D3F3C; + } +} + + +body { + font-family: Arial, Helvetica, sans-serif; + user-select: none; +} + +button { + -webkit-tap-highlight-color: transparent; +} + header { display: flex; justify-content: center; @@ -12,6 +40,12 @@ header img { #wrapper { display: flex; flex-direction: row; + justify-content: center; +} + +#update { + display: flex; + flex-direction: column; } main { @@ -20,6 +54,7 @@ main { width: 80%; flex-direction: column; overflow-wrap: break-word; + text-align: center; } aside { @@ -36,10 +71,42 @@ ul { padding: 0; } +.list-wrapper { + text-align: start; + margin: 0 auto; +} + ul li::before { - content: "✔"; - color: rgb(122, 122, 122); - font-weight: bold; + content: "»"; + font-weight: 1000; + font-size: 18pt; display: inline-block; - width: 2em; + width: 1em; + color: var(--custom-red); +} + +#nonGp { + display: none; + text-align: start; +} + +.button-wrapper { + display: flex; + flex-wrap: wrap; + gap: var(--button-margin); + justify-content: flex-end +} + +.tonal-button { + background-color: var(--tonal-bg); + color: var(--tonal-fg); + border: none; + border-radius: var(--tonal-border-radius); + padding: 10px 16px; + font-size: var(--tonal-font-size); + font-weight: var(--tonal-font-weight); + cursor: pointer; + max-width: var(--button-max-width); + width: 100%; + text-align: center; } \ No newline at end of file diff --git a/app/src/main/java/com/cylonid/nativealpha/AboutActivity.java b/app/src/main/java/com/cylonid/nativealpha/AboutActivity.java deleted file mode 100644 index 53a0e443..00000000 --- a/app/src/main/java/com/cylonid/nativealpha/AboutActivity.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.cylonid.nativealpha; - - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; - -import androidx.appcompat.app.AppCompatActivity; - -import com.mikepenz.aboutlibraries.LibsBuilder; - -import java.time.Year; - -import mehdi.sakout.aboutpage.AboutPage; -import mehdi.sakout.aboutpage.Element; - -public class AboutActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - View aboutPage = new AboutPage(this) - .setDescription("Native Alpha for Android\nby cylonid © " + Year.now().getValue()) - .setImage(R.drawable.native_alpha_foreground) - .addItem(new Element().setTitle("Version " + BuildConfig.VERSION_NAME)) - .addGitHub("cylonid", "GitHub") - .addPlayStore("com.cylonid.nativealpha.pro", "Play Store") - .addWebsite("https://github.com/cylonid/NativeAlphaForAndroid/blob/110releasePreparations/privacy_policy.md", getString(R.string.privacy_policy)) - .addGroup(getString(R.string.eula_title)) - .addItem(showEULA()) - .addGroup(getString(R.string.license)) - .addItem(showLicense()) - .addItem(showOpenSourcelibs()) - .create(); - - setContentView(aboutPage); - } - - Element showEULA() { - Element license = new Element(); - license.setTitle(getString(R.string.eula_content)); - return license; - } - Element showLicense() { - Element license = new Element(); - - license.setTitle(getString(R.string.gnu_license)); - license.setOnClickListener(v -> { - String url = "https://www.gnu.org/licenses/gpl-3.0.txt"; - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse(url)); - startActivity(i); - }); - return license; - - } - Element showPayPal() { - Element license = new Element(); - - license.setTitle(getString(R.string.paypal)); - license.setOnClickListener(v -> { - String url = "https://paypal.me/cylonid"; - Intent i = new Intent(Intent.ACTION_VIEW); - i.setData(Uri.parse(url)); - startActivity(i); - }); - return license; - - } - - Element showOpenSourcelibs() { - Element os = new Element(); - os.setTitle(getString(R.string.open_source_libs)); - os.setOnClickListener(v -> { - startActivity(new LibsBuilder() - .withEdgeToEdge(true) - .withSearchEnabled(true) - .intent(this)); - }); - return os; - } - - - -// Element getCopyRightsElement() { -// Element copyRightsElement = new Element(); -// final String copyrights = String.format(getString(R.string.copy_right), Calendar.getInstance().get(Calendar.YEAR)); -// copyRightsElement.setTitle(copyrights); -// copyRightsElement.setIconDrawable(R.drawable.about_icon_copy_right); -// copyRightsElement.setAutoApplyIconTint(true); -// copyRightsElement.setIconTint(mehdi.sakout.aboutpage.R.color.about_item_icon_color); -// copyRightsElement.setIconNightTint(android.R.color.white); -// copyRightsElement.setGravity(Gravity.CENTER); -// copyRightsElement.setOnClickListener(new View.OnClickListener() { -// @Override -// public void onClick(View v) { -// Toast.makeText(AboutActivity.this, copyrights, Toast.LENGTH_SHORT).show(); -// } -// }); -// return copyRightsElement; -// } -} \ No newline at end of file diff --git a/app/src/main/java/com/cylonid/nativealpha/AboutActivity.kt b/app/src/main/java/com/cylonid/nativealpha/AboutActivity.kt new file mode 100644 index 00000000..25966b4e --- /dev/null +++ b/app/src/main/java/com/cylonid/nativealpha/AboutActivity.kt @@ -0,0 +1,154 @@ +package com.cylonid.nativealpha + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.activity.addCallback +import androidx.appcompat.app.AppCompatActivity +import com.cylonid.nativealpha.databinding.ActivityToolbarBaseBinding +import com.cylonid.nativealpha.util.ColorUtils.getColorResFromThemeAttr +import com.mikepenz.aboutlibraries.LibsBuilder +import mehdi.sakout.aboutpage.AboutPage +import mehdi.sakout.aboutpage.Element +import java.time.Year + +class AboutActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val baseBinding = ActivityToolbarBaseBinding.inflate(layoutInflater) + setContentView(baseBinding.root) + + baseBinding.activityContent.addView(generateAboutPageView()) + + val toolbar = baseBinding.toolbar.topAppBar + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.title = getString(R.string.app_info) + + onBackPressedDispatcher.addCallback(this) { + finish() + } + + toolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + + } + + private fun generateAboutPageView(): View { + val page = AboutPage(this).apply { + setDescription( + """ + Native Alpha for Android + by cylonid © ${Year.now().value} + """.trimIndent() + ) + setImage(R.drawable.native_alpha_foreground) + addItem(Element().setTitle("Version " + BuildConfig.VERSION_NAME)) + addItem(addGitHubCustom("cylonid", "GitHub")) + addPlayStore("com.cylonid.nativealpha.pro", "Play Store") + addWebsite( + "https://github.com/cylonid/NativeAlphaForAndroid/blob/dev/privacy_policy.md", + getString( + R.string.privacy_policy + ) + ) + if(BuildConfig.FLAVOR == "extendedGithub") { + addItem(showLiberaPay()) + } + addGroup(getString(R.string.eula_title)) + addItem(showEULA()) + addGroup(getString(R.string.license)) + addItem(showLicense()) + addItem(showOpenSourcelibs()) + } + + return page.create() + + } + + private fun addGitHubCustom(id: String, title: String): Element { + val gitHubElement = Element() + gitHubElement.setTitle(title) + gitHubElement.setIconDrawable(R.drawable.about_icon_github) + gitHubElement.setIconTint( + getColorResFromThemeAttr( + this, com.google.android.material.R.attr.colorOnSurface, R.color.about_github_color + ) + ) + gitHubElement.setIconNightTint(R.color.about_item_dark_text_color) + gitHubElement.setValue(id) + + val intent = Intent() + intent.setAction(Intent.ACTION_VIEW) + intent.addCategory(Intent.CATEGORY_BROWSABLE) + intent.setData(Uri.parse(String.format("https://github.com/%s", id))) + + gitHubElement.setIntent(intent) + + return gitHubElement + } + + fun showEULA(): Element { + val license = Element() + license.setTitle(getString(R.string.eula_content)) + return license + } + + fun showLicense(): Element { + val license = Element() + + license.setTitle(getString(R.string.gnu_license)) + license.setOnClickListener { + val url = "https://www.gnu.org/licenses/gpl-3.0.txt" + val i = Intent(Intent.ACTION_VIEW) + i.setData(Uri.parse(url)) + startActivity(i) + } + return license + } + + fun showLiberaPay(): Element { + val element = Element() + element.setTitle(getString(R.string.support_liberapay)) + element.setIconDrawable(R.drawable.liberapay_logo) + element.skipTint = true + + element.setOnClickListener { + val url = "https://liberapay.com/cylonid" + val i = Intent(Intent.ACTION_VIEW) + i.setData(Uri.parse(url)) + startActivity(i) + } + return element + } + + fun showPayPal(): Element { + val license = Element() + + license.setTitle(getString(R.string.paypal)) + license.setOnClickListener { + val url = "https://paypal.me/cylonid" + val i = Intent(Intent.ACTION_VIEW) + i.setData(Uri.parse(url)) + startActivity(i) + } + return license + } + + fun showOpenSourcelibs(): Element { + val os = Element() + os.setTitle(getString(R.string.open_source_libs)) + os.setOnClickListener { + startActivity( + LibsBuilder() + .withEdgeToEdge(true) + .withSearchEnabled(true) + .intent(this) + ) + } + return os + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cylonid/nativealpha/MainActivity.java b/app/src/main/java/com/cylonid/nativealpha/MainActivity.java deleted file mode 100644 index 992252f1..00000000 --- a/app/src/main/java/com/cylonid/nativealpha/MainActivity.java +++ /dev/null @@ -1,282 +0,0 @@ -package com.cylonid.nativealpha; - -import android.content.Intent; -import android.os.Bundle; -import android.text.Html; -import android.text.Spanned; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.Switch; - -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.res.ResourcesCompat; - -import com.cylonid.nativealpha.activities.NewsActivity; -import com.cylonid.nativealpha.model.DataManager; -import com.cylonid.nativealpha.model.WebApp; -import com.cylonid.nativealpha.util.Const; -import com.cylonid.nativealpha.util.EntryPointUtils; -import com.cylonid.nativealpha.util.LocaleUtils; -import com.cylonid.nativealpha.util.Utility; -import com.cylonid.nativealpha.util.WebViewLauncher; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; - -import java.util.ArrayList; - -import static android.widget.LinearLayout.HORIZONTAL; - -public class MainActivity extends AppCompatActivity { - private LinearLayout mainScreen; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(R.style.AppTheme_NoActionBar); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mainScreen = findViewById(R.id.mainScreen); - - EntryPointUtils.entryPointReached(this); - addActiveWebAppsToUI(); - - if (DataManager.getInstance().getWebsites().size() == 0) { - buildAddWebsiteDialog(getString(R.string.welcome_msg)); - } - - FloatingActionButton fab = findViewById(R.id.fab); - fab.setOnClickListener(view -> buildAddWebsiteDialog(getString(R.string.add_webapp))); - personalizeToolbar(); - - } - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - if (intent.getBooleanExtra(Const.INTENT_BACKUP_RESTORED, false)) { - - mainScreen.removeAllViews(); - addActiveWebAppsToUI(); - - buildImportSuccessDialog(); - intent.putExtra(Const.INTENT_BACKUP_RESTORED, false); - intent.putExtra(Const.INTENT_REFRESH_NEW_THEME, false); - } - if (intent.getBooleanExtra(Const.INTENT_WEBAPP_CHANGED, false)) { - mainScreen.removeAllViews(); - addActiveWebAppsToUI(); - intent.putExtra(Const.INTENT_WEBAPP_CHANGED, false); - } - } - private void personalizeToolbar() { - Toolbar toolbar = findViewById(R.id.toolbar); - toolbar.setLogo(R.mipmap.native_alpha_white); - @StringRes int appName = !BuildConfig.FLAVOR.equals("extended") ? R.string.app_name : R.string.app_name_plus; - toolbar.setTitle(appName); - setSupportActionBar(toolbar); - } - - private void buildImportSuccessDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - - String message = getString(R.string.import_success_dialog_txt2) + "\n\n" + getString(R.string.import_success_dialog_txt3); - - builder.setMessage(message); - builder.setCancelable(false); - builder.setTitle(getString(R.string.import_success, DataManager.getInstance().getActiveWebsitesCount())); - builder.setPositiveButton(getString(android.R.string.yes), (dialog, id) -> { - - ArrayList webapps = DataManager.getInstance().getActiveWebsites(); - - for (int i = webapps.size() - 1; i >= 0; i--) { - WebApp webapp = webapps.get(i); - boolean last_webapp = i == webapps.size() - 1; - Spanned msg = Html.fromHtml(getString(R.string.restore_shortcut, webapp.getTitle()), Html.FROM_HTML_MODE_COMPACT); - final AlertDialog addition_dialog = new AlertDialog.Builder(this) - .setMessage(msg) - .setPositiveButton(android.R.string.yes, (dialog1, which) -> { - ShortcutDialogFragment frag = ShortcutDialogFragment.newInstance(webapp); - frag.show(getSupportFragmentManager(), "SCFetcher-" + webapp.getID()); - }) - .setNegativeButton(android.R.string.no, (dialog1, which) -> { - }) - .create(); - - addition_dialog.show(); - - } - - }); - builder.setNegativeButton(getString(android.R.string.no), (dialog, id) -> { }); - builder.create().show(); - } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - if (id == R.id.action_settings) { - Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - return true; - } - if (id == R.id.action_about) { - Intent intent = new Intent(this, AboutActivity.class); - startActivity(intent); - return true; - - } - - return super.onOptionsItemSelected(item); - } - - @Override - public void onBackPressed() { - moveTaskToBack(true); - } - - private ImageButton generateImageButton(String name, int resourceID, int webappID, LinearLayout ll_row) { - int row_height = (int) getResources().getDimension(R.dimen.line_height); - int transparent_color = ResourcesCompat.getColor(getResources(), R.color.transparent, null); - - ImageButton btn = new ImageButton(this); - btn.setTag(name + webappID); - btn.setBackgroundColor(transparent_color); - btn.setImageResource(resourceID); - LinearLayout.LayoutParams layout_action_buttons = new LinearLayout.LayoutParams(0, row_height, 1); - btn.setLayoutParams(layout_action_buttons); - ll_row.addView(btn); - - return btn; - } - private void addRow(final WebApp webapp) { - int row_height = (int) getResources().getDimension(R.dimen.line_height); - int transparent_color = ResourcesCompat.getColor(getResources(), R.color.transparent, null); - - LinearLayout ll_row = new LinearLayout(this); - ll_row.setOrientation(HORIZONTAL); - LinearLayout.LayoutParams layout_row = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, row_height); - ll_row.setLayoutParams(layout_row); - - Button btn_title = new Button(this); - btn_title.setBackgroundColor(transparent_color); - btn_title.setText(webapp.getTitle()); - btn_title.setMaxLines(1); - btn_title.setEllipsize(TextUtils.TruncateAt.END); - LinearLayout.LayoutParams layout_title = new LinearLayout.LayoutParams(0, row_height, 4); - btn_title.setLayoutParams(layout_title); - ll_row.addView(btn_title); - - ImageButton btn_open_webview = generateImageButton("btnOpenWebview", R.drawable.ic_baseline_open_in_browser_24, webapp.getID(), ll_row); - btn_open_webview.setOnClickListener(v -> openWebView(webapp)); - - ImageButton btn_settings = generateImageButton("btnSettings", R.drawable.ic_settings_black_24dp, webapp.getID(), ll_row); - btn_settings.setOnClickListener(v -> { - Intent intent = new Intent(MainActivity.this, WebAppSettingsActivity.class); - intent.putExtra(Const.INTENT_WEBAPPID, webapp.getID()); - intent.setAction(Intent.ACTION_VIEW); - startActivity(intent); - - }); - - ImageButton btn_delete = generateImageButton("btnDelete", R.drawable.ic_delete_black_24dp, webapp.getID(), ll_row); - btn_delete.setOnClickListener(v -> buildDeleteItemDialog(webapp.getID())); - - mainScreen.addView(ll_row); - - } - - - private void buildAddWebsiteDialog(String title) { - final View inflated_view = getLayoutInflater().inflate(R.layout.add_website_dialogue, null); - final EditText url = inflated_view.findViewById(R.id.websiteUrl); - final Switch create_shortcut = inflated_view.findViewById(R.id.switchCreateShortcut); - - final AlertDialog dialog = new AlertDialog.Builder(MainActivity.this) - .setView(inflated_view) - .setTitle(title) - .setPositiveButton(android.R.string.ok, null) //Set to null. We override the onclick - .setNegativeButton(android.R.string.cancel, null) - .create(); - - dialog.setOnShowListener(dialogInterface -> { - - Button positive = dialog.getButton(AlertDialog.BUTTON_POSITIVE); - url.requestFocus(); - positive.setOnClickListener(view -> { - String str_url = url.getText().toString().trim(); - if(str_url.equals("")) { - Utility.showInfoSnackbar(this, getString(R.string.no_url_entered), Snackbar.LENGTH_SHORT); - } else { - if (!(str_url.startsWith("https://")) && !(str_url.startsWith("http://"))) { - str_url = "https://" + str_url; - } - WebApp new_site = new WebApp(str_url, DataManager.getInstance().getIncrementedID()); - new_site.applySettingsForNewWebApp(); - DataManager.getInstance().addWebsite(new_site); - - addRow(new_site); - dialog.dismiss(); - if (create_shortcut.isChecked()) { - ShortcutDialogFragment frag = ShortcutDialogFragment.newInstance(new_site); - frag.show(getSupportFragmentManager(), "SCFetcher-" + new_site.getID()); - } - } - }); - }); - dialog.show(); - } - - private void buildDeleteItemDialog(final int ID) { - - AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); - builder.setMessage(getString(R.string.delete_question)); - builder.setPositiveButton(getString(android.R.string.yes), (dialog, id) -> { - WebApp webapp = DataManager.getInstance().getWebApp(ID); - if (webapp != null) { - webapp.markInactive(); - DataManager.getInstance().saveWebAppData(); - } - mainScreen.removeAllViews(); - addActiveWebAppsToUI(); - - - }); - builder.setNegativeButton(getString(android.R.string.no), (dialog, id) -> dialog.cancel()); - AlertDialog dialog = builder.create(); - dialog.show(); - } - - public void addActiveWebAppsToUI() { - for (WebApp d : DataManager.getInstance().getWebsites()) { - if (d.isActiveEntry()) - addRow(d); - } - } - - private void openWebView(WebApp webapp) { - WebViewLauncher.startWebView(webapp, MainActivity.this); - } - - -} - - diff --git a/app/src/main/java/com/cylonid/nativealpha/MainActivity.kt b/app/src/main/java/com/cylonid/nativealpha/MainActivity.kt new file mode 100644 index 00000000..1292629d --- /dev/null +++ b/app/src/main/java/com/cylonid/nativealpha/MainActivity.kt @@ -0,0 +1,190 @@ +package com.cylonid.nativealpha + +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.text.Html +import android.text.TextWatcher +import android.view.Menu +import android.view.MenuItem +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import com.cylonid.nativealpha.databinding.AddWebsiteDialogueBinding +import com.cylonid.nativealpha.fragments.webapplist.WebAppListFragment +import com.cylonid.nativealpha.helper.AdblockLifecycleHelper +import com.cylonid.nativealpha.model.DataManager +import com.cylonid.nativealpha.model.WebApp +import com.cylonid.nativealpha.util.Const +import com.cylonid.nativealpha.util.EntryPointUtils.entryPointReached +import com.google.android.material.floatingactionbutton.FloatingActionButton +import io.github.edsuns.adfilter.AdFilter + + +class MainActivity : AppCompatActivity() { + private lateinit var webAppListFragment: WebAppListFragment + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(R.style.AppTheme) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + webAppListFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container_view) as WebAppListFragment + entryPointReached(this) + + if (DataManager.getInstance().websites.size == 0) { + buildAddWebsiteDialog(getString(R.string.welcome_msg)) + } + + val fab = findViewById(R.id.fab) + fab.setOnClickListener { buildAddWebsiteDialog(getString(R.string.add_webapp)) } + personalizeToolbar() + + AdblockLifecycleHelper(this).trySyncOperation({ AdFilter.create(applicationContext) }) + + } + + override fun onResume() { + super.onResume() + DataManager.getInstance().loadAppData(); + updateWebAppList() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + if (intent.getBooleanExtra(Const.INTENT_BACKUP_RESTORED, false)) { + updateWebAppList() + + buildImportSuccessDialog() + intent.putExtra(Const.INTENT_BACKUP_RESTORED, false) + intent.putExtra(Const.INTENT_REFRESH_NEW_THEME, false) + } + if (intent.getBooleanExtra(Const.INTENT_WEBAPP_CHANGED, false)) { + updateWebAppList() + intent.putExtra(Const.INTENT_WEBAPP_CHANGED, false) + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun updateWebAppList() { + webAppListFragment.updateWebAppList() + } + + private fun personalizeToolbar() { + val toolbar = findViewById(R.id.toolbar) + @StringRes val appName = + if (BuildConfig.FLAVOR == "extended") R.string.app_name_plus else R.string.app_name + toolbar.setTitle(appName) + setSupportActionBar(toolbar) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + val id = item.itemId + + if (id == R.id.action_settings) { + val intent = Intent(this, SettingsActivity::class.java) + startActivity(intent) + return true + } + if (id == R.id.action_about) { + val intent = Intent(this, AboutActivity::class.java) + startActivity(intent) + return true + } + + return super.onOptionsItemSelected(item) + } + + private fun buildImportSuccessDialog() { + val message = """ + ${getString(R.string.import_success_dialog_txt2)} + + ${getString(R.string.import_success_dialog_txt3)} + """.trimIndent() + + AlertDialog.Builder(this).setMessage(message) + .setCancelable(false) + .setTitle( + getString( + R.string.import_success, + DataManager.getInstance().activeWebsitesCount + ) + ) + .setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> + val webapps = DataManager.getInstance().activeWebsites + for (i in webapps.indices.reversed()) { + val webapp = webapps[i] + val msg = Html.fromHtml( + getString(R.string.restore_shortcut, webapp.title), + Html.FROM_HTML_MODE_COMPACT + ) + AlertDialog.Builder(this) + .setMessage(msg) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + val frag = ShortcutDialogFragment.newInstance(webapp) + frag.show(supportFragmentManager, "SCFetcher-" + webapp.ID) + } + .setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> } + .create() + .show() + + } + } + .setNegativeButton(getString(R.string.cancel)) { _: DialogInterface?, _: Int -> } + .create().show() + } + + private fun buildAddWebsiteDialog(title: String) { + val localBinding = AddWebsiteDialogueBinding.inflate(layoutInflater) + val dialog = AlertDialog.Builder(this@MainActivity) + .setView(localBinding.root) + .setTitle(title) + .setPositiveButton(R.string.ok) { _: DialogInterface, _: Int -> + val url = localBinding.websiteUrl.text.toString().trim() + val urlWithProtocol = + if (url.startsWith("https://") || url.startsWith("http://")) url else "https://$url" + val newSite = WebApp( + urlWithProtocol, + DataManager.getInstance().incrementedID, + DataManager.getInstance().incrementedOrder + ) + newSite.applySettingsForNewWebApp() + DataManager.getInstance().addWebsite(newSite) + + updateWebAppList() + if (localBinding.switchCreateShortcut.isChecked) { + val frag = ShortcutDialogFragment.newInstance(newSite) + frag.show(supportFragmentManager, "SCFetcher-" + newSite.ID) + } + } + .setNegativeButton(R.string.cancel, null) + .create() + + dialog.show() + val okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + okButton.isEnabled = false + localBinding.websiteUrl.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + okButton.isEnabled = !s.isNullOrBlank() + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + } +} + + diff --git a/app/src/main/java/com/cylonid/nativealpha/SettingsActivity.java b/app/src/main/java/com/cylonid/nativealpha/SettingsActivity.java deleted file mode 100644 index 6ebcaa1d..00000000 --- a/app/src/main/java/com/cylonid/nativealpha/SettingsActivity.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.cylonid.nativealpha; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.widget.Button; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.databinding.DataBindingUtil; - -import com.cylonid.nativealpha.databinding.GlobalSettingsBinding; -import com.cylonid.nativealpha.model.DataManager; -import com.cylonid.nativealpha.model.GlobalSettings; -import com.cylonid.nativealpha.util.Const; -import com.cylonid.nativealpha.util.Utility; -import com.google.android.material.snackbar.Snackbar; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import static com.cylonid.nativealpha.util.Const.CODE_OPEN_FILE; -import static com.cylonid.nativealpha.util.Const.CODE_WRITE_FILE; - - -public class SettingsActivity extends AppCompatActivity { - - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if(requestCode == CODE_WRITE_FILE && resultCode == RESULT_OK) { - Uri uri = data.getData(); - - DataManager.getInstance().saveGlobalSettings(); //Needed to write legacy settings to new XML - - if (!DataManager.getInstance().saveSharedPreferencesToFile(uri)) { - Utility.showInfoSnackbar(this, getString(R.string.export_failed), Snackbar.LENGTH_LONG); - } else { - Utility.showInfoSnackbar(this, getString(R.string.export_success), Snackbar.LENGTH_SHORT); - } - } - if (requestCode == CODE_OPEN_FILE && resultCode == RESULT_OK) { - Uri uri = data.getData(); - - if (!DataManager.getInstance().loadSharedPreferencesFromFile(uri)) { - Utility.showInfoSnackbar(this, getString(R.string.import_failed), Snackbar.LENGTH_LONG); - } else { - Intent i = new Intent(SettingsActivity.this, MainActivity.class); - DataManager.getInstance().loadAppData(); - i.putExtra(Const.INTENT_BACKUP_RESTORED, true); - finish(); - startActivity(i); - } - } - } - - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GlobalSettingsBinding binding = DataBindingUtil.setContentView(this, R.layout.global_settings); - GlobalSettings settings = DataManager.getInstance().getSettings(); - final GlobalSettings modified_settings = new GlobalSettings(settings); - binding.setSettings(modified_settings); - - Button btnSave = findViewById(R.id.btnSave); - Button btnCancel = findViewById(R.id.btnCancel); - Button btnExport = findViewById(R.id.btnExportSettings); - Button btnImport = findViewById(R.id.btnImportSettings); - Button btnGlobalWebApp = findViewById(R.id.btnGlobalWebApp); - - btnGlobalWebApp.setOnClickListener(v -> { - Intent intent = new Intent(SettingsActivity.this, WebAppSettingsActivity.class); - intent.putExtra(Const.INTENT_WEBAPPID, settings.getGlobalWebApp().getID()); - intent.setAction(Intent.ACTION_VIEW); - startActivity(intent); - }); - - - btnExport.setOnClickListener(v -> { - - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT).addCategory(Intent.CATEGORY_OPENABLE).setType("*/*"); - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()); - String currentDateTime = sdf.format(new Date()); - intent.putExtra(Intent.EXTRA_TITLE, "NativeAlpha_" + currentDateTime); - try { - startActivityForResult(intent, CODE_WRITE_FILE); - } catch (android.content.ActivityNotFoundException e) { - Utility.showInfoSnackbar(SettingsActivity.this, getString(R.string.no_filemanager), Snackbar.LENGTH_LONG); - e.printStackTrace(); - } - - }); - - btnImport.setOnClickListener(v -> { - Intent intent = new Intent().setType("*/*").setAction(Intent.ACTION_GET_CONTENT); - try { - startActivityForResult(Intent.createChooser(intent, "Select a file"), CODE_OPEN_FILE); - } catch (android.content.ActivityNotFoundException e) { - Utility.showInfoSnackbar(SettingsActivity.this, getString(R.string.no_filemanager), Snackbar.LENGTH_LONG); - e.printStackTrace(); - } - - }); - - btnSave.setOnClickListener(v -> { - DataManager.getInstance().setSettings(modified_settings); - onBackPressed(); - }); - - btnCancel.setOnClickListener(v -> { - onBackPressed(); - }); - } -} diff --git a/app/src/main/java/com/cylonid/nativealpha/SettingsActivity.kt b/app/src/main/java/com/cylonid/nativealpha/SettingsActivity.kt new file mode 100644 index 00000000..82534ebc --- /dev/null +++ b/app/src/main/java/com/cylonid/nativealpha/SettingsActivity.kt @@ -0,0 +1,153 @@ +package com.cylonid.nativealpha + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.webkit.CookieManager +import android.webkit.WebStorage +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import com.cylonid.nativealpha.activities.AdblockConfigActivity +import com.cylonid.nativealpha.activities.ToolbarBaseActivity +import com.cylonid.nativealpha.databinding.GlobalSettingsBinding +import com.cylonid.nativealpha.model.DataManager +import com.cylonid.nativealpha.model.GlobalSettings +import com.cylonid.nativealpha.util.Const +import com.cylonid.nativealpha.util.NotificationUtils +import com.cylonid.nativealpha.util.Utility +import com.google.android.material.snackbar.Snackbar +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class SettingsActivity : ToolbarBaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setToolbarTitle(getString(R.string.global_settings)) + + val settings = DataManager.getInstance().settings + val modified_settings = settings.copy() + binding.settings = modified_settings + binding.btnAdblockConfig.setOnClickListener { v: View? -> + val intent = Intent( + this@SettingsActivity, + AdblockConfigActivity::class.java + ) + intent.setAction(Intent.ACTION_VIEW) + startActivity(intent) + } + + binding.btnGlobalWebApp.setOnClickListener { v: View? -> + val intent = Intent( + this@SettingsActivity, + WebAppSettingsActivity::class.java + ) + intent.putExtra( + Const.INTENT_WEBAPPID, + settings.globalWebApp.ID + ) + intent.setAction(Intent.ACTION_VIEW) + startActivity(intent) + } + + + binding.btnExportSettings.setOnClickListener { v: View? -> + val intent = + Intent(Intent.ACTION_CREATE_DOCUMENT).addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*") + val sdf = + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) + val currentDateTime = sdf.format(Date()) + intent.putExtra(Intent.EXTRA_TITLE, "NativeAlpha_$currentDateTime") + try { + startActivityForResult(intent, Const.CODE_WRITE_FILE) + } catch (e: ActivityNotFoundException) { + NotificationUtils.showInfoSnackbar( + this@SettingsActivity, + getString(R.string.no_filemanager), + Snackbar.LENGTH_LONG + ) + e.printStackTrace() + } + } + + binding.btnImportSettings.setOnClickListener { v: View? -> + val intent = Intent().setType("*/*").setAction(Intent.ACTION_GET_CONTENT) + try { + startActivityForResult( + Intent.createChooser(intent, "Select a file"), + Const.CODE_OPEN_FILE + ) + } catch (e: ActivityNotFoundException) { + NotificationUtils.showInfoSnackbar( + this@SettingsActivity, + getString(R.string.no_filemanager), + Snackbar.LENGTH_LONG + ) + e.printStackTrace() + } + } + + binding.btnSave.setOnClickListener { + DataManager.getInstance().settings = modified_settings + finish() + } + + binding.btnCancel.setOnClickListener { + finish() + } + } + + override fun inflateBinding(layoutInflater: LayoutInflater): GlobalSettingsBinding { + return GlobalSettingsBinding.inflate(layoutInflater) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == Const.CODE_WRITE_FILE && resultCode == RESULT_OK) { + val uri = data?.data + + DataManager.getInstance() + .saveGlobalSettings() //Needed to write legacy settings to new XML + + if (!DataManager.getInstance().saveSharedPreferencesToFile(uri)) { + NotificationUtils.showInfoSnackbar( + this, + getString(R.string.export_failed), + Snackbar.LENGTH_LONG + ) + } else { + NotificationUtils.showInfoSnackbar( + this, + getString(R.string.export_success), + Snackbar.LENGTH_SHORT + ) + } + } + if (requestCode == Const.CODE_OPEN_FILE && resultCode == RESULT_OK) { + val uri = data?.data + + if (!DataManager.getInstance().loadSharedPreferencesFromFile(uri)) { + NotificationUtils.showInfoSnackbar( + this, + getString(R.string.import_failed), + Snackbar.LENGTH_LONG + ) + } else { + val i = Intent(this@SettingsActivity, MainActivity::class.java) + + WebStorage.getInstance().deleteAllData() + CookieManager.getInstance().removeAllCookies(null) + + DataManager.getInstance().loadAppData() + i.putExtra(Const.INTENT_BACKUP_RESTORED, true) + finish() + startActivity(i) + } + } + } +} diff --git a/app/src/main/java/com/cylonid/nativealpha/ShortcutDialogFragment.java b/app/src/main/java/com/cylonid/nativealpha/ShortcutDialogFragment.java index cacd9a96..c0a9eac9 100644 --- a/app/src/main/java/com/cylonid/nativealpha/ShortcutDialogFragment.java +++ b/app/src/main/java/com/cylonid/nativealpha/ShortcutDialogFragment.java @@ -19,7 +19,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; @@ -29,7 +28,8 @@ import com.cylonid.nativealpha.model.WebApp; import com.cylonid.nativealpha.util.App; import com.cylonid.nativealpha.util.Const; -import com.cylonid.nativealpha.util.Utility; +import com.cylonid.nativealpha.util.NotificationUtils; +import com.cylonid.nativealpha.util.ShortcutIconUtils; import com.cylonid.nativealpha.util.WebViewLauncher; import com.google.android.material.snackbar.Snackbar; import com.mikhaellopez.circularprogressbar.CircularProgressBar; @@ -54,6 +54,17 @@ import static androidx.appcompat.app.AppCompatActivity.RESULT_OK; import static com.cylonid.nativealpha.util.Const.CODE_OPEN_FILE; +enum IconFetchResult { + FAVICON(0), + TITLE(1), + NEW_BASEURL(2); + + public final int index; + IconFetchResult(int index) { + this.index = index; + } + +} public class ShortcutDialogFragment extends DialogFragment { @@ -98,7 +109,7 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d } catch(IOException e) { - Utility.showToast(requireActivity(), getString(R.string.icon_not_found), Toast.LENGTH_SHORT); + NotificationUtils.showToast(requireActivity(), getString(R.string.icon_not_found), Toast.LENGTH_SHORT); e.printStackTrace(); } } @@ -116,7 +127,6 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { .setPositiveButton(android.R.string.ok, (dialog1, which) -> { addShortcutToHomeScreen(bitmap); dismiss(); - }) .setNegativeButton(android.R.string.cancel, (dialog1, which) -> { dismiss(); @@ -140,7 +150,7 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { try { startActivityForResult(Intent.createChooser(intent, "Select an icon"), CODE_OPEN_FILE); } catch (android.content.ActivityNotFoundException e) { - Utility.showInfoSnackbar((AppCompatActivity) requireActivity(), getString(R.string.no_filemanager), Snackbar.LENGTH_LONG); + NotificationUtils.showInfoSnackbar(requireActivity(), getString(R.string.no_filemanager), Snackbar.LENGTH_LONG); e.printStackTrace(); } }); @@ -171,11 +181,9 @@ private TreeMap buildIconMap() { String host_part = base_url.replace("http://", "").replace("https://", "").replace("www.", ""); //No suitable icon - if (host_part.startsWith("facebook.")) - found_icons.put(325, "https://static.xx.fbcdn.net/rsrc.php/v3/y3/r/UrYT8B96uSq.png"); if (host_part.startsWith("amazon.")) - found_icons.put(300, "https://s3.amazonaws.com/prod-widgetSource/in-shop/pub/images/amzn_favicon_blk.png"); + found_icons.put(300, "https://upload.wikimedia.org/wikipedia/commons/d/de/Amazon_icon.png"); if (host_part.startsWith("paypal.")) found_icons.put(196, "https://www.paypalobjects.com/webstatic/icon/pp196.png"); @@ -191,17 +199,11 @@ private TreeMap buildIconMap() { if (host_part.startsWith("oebb.at")) found_icons.put(Integer.MAX_VALUE, "https://www.oebb.at/.resources/pv-2017/themes/images/favicons/android-chrome-192x192.png"); - //Wrong path in PWA manifest - if (host_part.startsWith("explosm.net")) - found_icons.put(Integer.MAX_VALUE, "https://files.explosm.net/img/favicons/site/android-chrome-192x192.png"); //Path in PWA manifest is HTTP if (host_part.startsWith("oe3.orf.at")) found_icons.put(Integer.MAX_VALUE, "https://tubestatic.orf.at/mojo/1_3/storyserver//tube/common/images/apple-icons/oe3.png"); - //Non-existing path - if (host_part.startsWith("darfichrein.de")) - found_icons.put(Integer.MAX_VALUE, "https://c.darfichrein.de/assets/img/logo1.png"); return found_icons; } @@ -235,12 +237,12 @@ public String[] fetchWebappData() { JSONObject json = new JSONObject(data); try { - result[Const.RESULT_IDX_TITLE] = json.getString("name"); + result[IconFetchResult.TITLE.index] = json.getString("name"); String start_url = json.getString("start_url"); if (!start_url.isEmpty()) { URL base_url = new URL(mf.absUrl("href")); URL fl_url = new URL(base_url, start_url); - result[Const.RESULT_IDX_NEW_BASEURL] = fl_url.toString(); + result[IconFetchResult.NEW_BASEURL.index] = fl_url.toString(); } } catch (JSONException e) { e.printStackTrace(); @@ -251,7 +253,7 @@ public String[] fetchWebappData() { for (int i = 0; i < manifest_icons.length(); i++) { String icon_href = manifest_icons.getJSONObject(i).getString("src"); String sizes = manifest_icons.getJSONObject(i).getString("sizes"); - Integer width = Utility.getWidthFromIcon(sizes); + Integer width = ShortcutIconUtils.getWidthFromIcon(sizes); URL base_url = new URL(mf.absUrl("href")); URL full_url = new URL(base_url, icon_href); found_icons.put(width, full_url.toString()); @@ -265,7 +267,7 @@ public String[] fetchWebappData() { Elements html_title = doc.select("title"); if (!html_title.isEmpty()) - result[Const.RESULT_IDX_TITLE] = html_title.first().text(); + result[IconFetchResult.TITLE.index] = html_title.first().text(); Elements icons = doc.select("link[rel=icon]"); icons.addAll(doc.select("link[rel=shortcut icon]")); @@ -281,7 +283,7 @@ public String[] fetchWebappData() { String icon_href = icon.absUrl("href"); String sizes = icon.attr("sizes"); if (!sizes.equals("")) { - Integer width = Utility.getWidthFromIcon(sizes); + Integer width = ShortcutIconUtils.getWidthFromIcon(sizes); found_icons.put(width, icon_href); } else found_icons.put(1, icon_href); @@ -295,7 +297,7 @@ public String[] fetchWebappData() { if (!found_icons.isEmpty()) { Map.Entry best_fit = found_icons.lastEntry(); - result[Const.RESULT_IDX_FAVICON] = best_fit.getValue(); + result[IconFetchResult.FAVICON.index] = best_fit.getValue(); } @@ -306,13 +308,13 @@ private void startFaviconFetching() { faviconFetcherThread = new Thread(() -> { String[] webappdata = fetchWebappData(); - bitmap = loadBitmap(webappdata[Const.RESULT_IDX_FAVICON]); + bitmap = loadBitmap(webappdata[IconFetchResult.FAVICON.index]); if (isAdded()) { requireActivity().runOnUiThread(() -> { applyNewBitmapToDialog(); - setShortcutTitle(webappdata[Const.RESULT_IDX_TITLE]); - applyNewBaseUrl(webappdata[Const.RESULT_IDX_NEW_BASEURL]); + setShortcutTitle(webappdata[IconFetchResult.TITLE.index]); + applyNewBaseUrl(webappdata[IconFetchResult.NEW_BASEURL.index]); }); } @@ -360,7 +362,7 @@ private void addShortcutToHomeScreen(Bitmap bitmap) { if(!scManager.getPinnedShortcuts().stream().anyMatch(s -> s.getId().equals(newScId))) { ShortcutManagerCompat.requestPinShortcut(requireActivity(), pinShortcutInfo, null); } else { - Utility.showToast(requireActivity(), getString(R.string.shortcut_already_exists)); + NotificationUtils.showToast(requireActivity(), getString(R.string.shortcut_already_exists)); } } @@ -368,14 +370,18 @@ private void addShortcutToHomeScreen(Bitmap bitmap) { private void prepareFailedUI() { showFailedMessage(); - uiTitle.setText(webapp.getTitle()); + if(webapp.getTitle() != null && !webapp.getTitle().equals("")) { + uiTitle.setText(webapp.getTitle()); + } + uiTitle.requestFocus(); uiProgressBar.setVisibility(View.GONE); uiFavicon.setVisibility(View.VISIBLE); } private void showFailedMessage() { - Utility.showToast(requireActivity(), getString(R.string.icon_fetch_failed_line1, webapp.getTitle()) + getString(R.string.icon_fetch_failed_line2) + getString(R.string.icon_fetch_failed_line3)); + String title = webapp != null && webapp.getTitle() != null ? webapp.getTitle() : ""; + NotificationUtils.showToast(requireActivity(), getString(R.string.icon_fetch_failed_line1, title) + getString(R.string.icon_fetch_failed_line2) + getString(R.string.icon_fetch_failed_line3)); } private void setShortcutTitle(String shortcut_title) { diff --git a/app/src/main/java/com/cylonid/nativealpha/WebAppSettingsActivity.java b/app/src/main/java/com/cylonid/nativealpha/WebAppSettingsActivity.java deleted file mode 100644 index 13b997e5..00000000 --- a/app/src/main/java/com/cylonid/nativealpha/WebAppSettingsActivity.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.cylonid.nativealpha; - -import android.annotation.SuppressLint; -import android.app.ActivityManager; -import android.app.TimePickerDialog; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.text.Html; -import android.text.method.LinkMovementMethod; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.databinding.DataBindingUtil; - -import com.cylonid.nativealpha.databinding.WebappSettingsBinding; -import com.cylonid.nativealpha.model.DataManager; -import com.cylonid.nativealpha.model.WebApp; -import com.cylonid.nativealpha.util.App; -import com.cylonid.nativealpha.util.Const; -import com.cylonid.nativealpha.util.Utility; - -import java.util.Calendar; - -public class WebAppSettingsActivity extends AppCompatActivity { - - int webappID = -1; - WebApp webapp; - boolean isGlobalWebApp; - - @SuppressLint({"SetJavaScriptEnabled", "ClickableViewAccessibility"}) - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - WebappSettingsBinding binding = DataBindingUtil.setContentView(this, R.layout.webapp_settings); - TextView txt = findViewById(R.id.txthintUserAgent); - txt.setText(Html.fromHtml(getString(R.string.hint_user_agent), Html.FROM_HTML_MODE_LEGACY)); - txt.setMovementMethod(LinkMovementMethod.getInstance()); - - webappID = getIntent().getIntExtra(Const.INTENT_WEBAPPID, -1); - Utility.Assert(webappID != -1, "WebApp ID could not be retrieved."); - isGlobalWebApp = webappID == DataManager.getInstance().getSettings().getGlobalWebApp().getID(); - - final View inflated_view = binding.getRoot(); - - if (isGlobalWebApp) { - webapp = DataManager.getInstance().getSettings().getGlobalWebApp(); - prepareGlobalWebAppScreen(); - } - else - webapp = DataManager.getInstance().getWebAppIgnoringGlobalOverride(webappID, true); - - if (webapp == null) { - finish(); - } - else { - final WebApp modified_webapp = new WebApp(webapp); - binding.setWebapp(modified_webapp); - binding.setActivity(WebAppSettingsActivity.this); - - final Button btnCreateShortcut = inflated_view.findViewById(R.id.btnRecreateShortcut); - - btnCreateShortcut.setOnClickListener(view -> { - ShortcutDialogFragment frag = ShortcutDialogFragment.newInstance(webapp); - frag.show(getSupportFragmentManager(), "SCFetcher-" + webapp.getID()); - - }); - Button btnSave = findViewById(R.id.btnSave); - Button btnCancel = findViewById(R.id.btnCancel); - - btnSave.setOnClickListener(v -> { - ActivityManager activityManager = - (ActivityManager) App.getAppContext().getSystemService(Context.ACTIVITY_SERVICE); - - //Global web app => close all webview activities, save to global settings - if (isGlobalWebApp) { - for (ActivityManager.AppTask task : activityManager.getAppTasks()) { - int id = task.getTaskInfo().baseIntent.getIntExtra(Const.INTENT_WEBAPPID, -1); - if (id != -1) - task.finishAndRemoveTask(); - } - for (ActivityManager.RunningAppProcessInfo processInfo : activityManager.getRunningAppProcesses()) { - if (processInfo.processName.contains("web_sandbox")) { - android.os.Process.killProcess(processInfo.pid); - } - } - DataManager.getInstance().getSettings().setGlobalWebApp(modified_webapp); - DataManager.getInstance().saveGlobalSettings(); - } - //Normal web app => only close that specific web app, save to webapp arraylist - else { - for (ActivityManager.AppTask task : activityManager.getAppTasks()) { - int id = task.getTaskInfo().baseIntent.getIntExtra(Const.INTENT_WEBAPPID, -1); - if (id == webappID) - task.finishAndRemoveTask(); - } - for (ActivityManager.RunningAppProcessInfo processInfo : activityManager.getRunningAppProcesses()) { - if (processInfo.processName.contains("web_sandbox_" + modified_webapp.getContainerId())) { - android.os.Process.killProcess(processInfo.pid); - } - } - DataManager.getInstance().replaceWebApp(modified_webapp); - } - - Intent i = new Intent(WebAppSettingsActivity.this, MainActivity.class); - i.putExtra(Const.INTENT_WEBAPP_CHANGED, true); - finish(); - startActivity(i); - }); - - btnCancel.setOnClickListener(v -> finish()); - EditText txtBeginDarkMode = inflated_view.findViewById(R.id.textDarkModeBegin); - EditText txtEndDarkMode = inflated_view.findViewById(R.id.textDarkModeEnd); - - txtBeginDarkMode.setOnClickListener(view -> showTimePicker(txtBeginDarkMode)); - txtEndDarkMode.setOnClickListener(view -> showTimePicker(txtEndDarkMode)); - - webapp.onSwitchExpertSettingsChanged(inflated_view.findViewById(R.id.switchExpertSettings), webapp.isShowExpertSettings()); - webapp.onSwitchOverrideGlobalSettingsChanged(findViewById(R.id.switchOverrideGlobal), webapp.isOverrideGlobalSettings()); - setPlusSettings(inflated_view); - } - } - - private void setPlusSettings(View v) { - LinearLayout secDarkMode = v.findViewById(R.id.sectionDarkmode); - LinearLayout secSandbox= v.findViewById(R.id.sectionSandbox); - LinearLayout secKiosk = v.findViewById(R.id.sectionKioskMode); - LinearLayout secAccessRestriction = v.findViewById(R.id.sectionAccessRestriction); - if (!BuildConfig.FLAVOR.equals("extended")) { - secDarkMode.setVisibility(View.GONE); - secSandbox.setVisibility(View.GONE); - secKiosk.setVisibility(View.GONE); - secAccessRestriction.setVisibility(View.GONE); - } - } - - - private void showTimePicker(EditText txtField) { - Calendar c = Utility.convertStringToCalendar(txtField.getText().toString()); - TimePickerDialog timePickerDialog = new TimePickerDialog(WebAppSettingsActivity.this, R.style.CustomDatePickerDialog, (timePicker, selectedHour, selectedMinute) -> { - Calendar datetime = Calendar.getInstance(); - datetime.set(Calendar.HOUR_OF_DAY, selectedHour); - datetime.set(Calendar.MINUTE, selectedMinute); - txtField.setText(Utility.getHourMinFormat().format(datetime.getTime())); - }, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), true); - timePickerDialog.show(); - - } - - private void prepareGlobalWebAppScreen() { - findViewById(R.id.btnRecreateShortcut).setVisibility(View.GONE); - findViewById(R.id.labelWebAppName).setVisibility(View.GONE); - findViewById(R.id.txtWebAppName).setVisibility(View.GONE); - findViewById(R.id.switchOverrideGlobal).setVisibility(View.GONE); - findViewById(R.id.sectionSSL).setVisibility(View.GONE); - findViewById(R.id.sectionSandbox).setVisibility(View.GONE); - findViewById(R.id.labelTitle).setVisibility(View.GONE); - findViewById(R.id.labelEditableBaseUrl).setVisibility(View.GONE); - findViewById(R.id.textBaseUrl).setVisibility(View.GONE); - - TextView page_title = findViewById(R.id.labelPageTitle); - page_title.setText(getString(R.string.global_web_app_settings)); - - } -} - - diff --git a/app/src/main/java/com/cylonid/nativealpha/WebAppSettingsActivity.kt b/app/src/main/java/com/cylonid/nativealpha/WebAppSettingsActivity.kt new file mode 100644 index 00000000..fd1a15ea --- /dev/null +++ b/app/src/main/java/com/cylonid/nativealpha/WebAppSettingsActivity.kt @@ -0,0 +1,184 @@ +package com.cylonid.nativealpha + +import android.annotation.SuppressLint +import android.app.ActivityManager +import android.app.TimePickerDialog +import android.content.Intent +import android.os.Bundle +import android.os.Process +import android.text.Html +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.widget.EditText +import android.widget.TimePicker +import com.cylonid.nativealpha.activities.ToolbarBaseActivity +import com.cylonid.nativealpha.databinding.WebappSettingsBinding +import com.cylonid.nativealpha.model.DataManager +import com.cylonid.nativealpha.model.WebApp +import com.cylonid.nativealpha.util.Const +import com.cylonid.nativealpha.util.DateUtils.convertStringToCalendar +import com.cylonid.nativealpha.util.DateUtils.getHourMinFormat +import com.cylonid.nativealpha.util.ProcessUtils.closeAllWebAppsAndProcesses +import com.cylonid.nativealpha.util.Utility +import java.util.Calendar + +class WebAppSettingsActivity : ToolbarBaseActivity() { + var webappID: Int = -1 + var webapp: WebApp? = null + private var isGlobalWebApp: Boolean = false + + @SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setToolbarTitle(getString(R.string.web_app_settings)) + + webappID = intent.getIntExtra(Const.INTENT_WEBAPPID, -1) + Utility.Assert(webappID != -1, "WebApp ID could not be retrieved.") + isGlobalWebApp = webappID == DataManager.getInstance().settings.globalWebApp.ID + + if (isGlobalWebApp) { + webapp = DataManager.getInstance().settings.globalWebApp + prepareGlobalWebAppScreen() + } else webapp = DataManager.getInstance().getWebAppIgnoringGlobalOverride(webappID, true) + + if (webapp == null) { + finish() + return + } + val modifiedWebapp = WebApp(webapp!!) + binding.webapp = modifiedWebapp + binding.activity = this@WebAppSettingsActivity + + setupSaveAndCancel(modifiedWebapp) + setupDarkModeElements() + setupPlusSettings() + setupDesktopUserAgentHint() + setupShortcutButton() + setupSwitchListeners(webapp!!) + + } + + override fun inflateBinding(layoutInflater: LayoutInflater): WebappSettingsBinding { + return WebappSettingsBinding.inflate(layoutInflater) + } + + private fun setupSwitchListeners(webapp: WebApp) { + webapp.onSwitchExpertSettingsChanged( + binding.switchExpertSettings, + webapp.isShowExpertSettings + ) + webapp.onSwitchOverrideGlobalSettingsChanged( + binding.switchOverrideGlobal, + webapp.isOverrideGlobalSettings + ) + } + + private fun setupSaveAndCancel(modifiedWebapp: WebApp) { + binding.btnSave.setOnClickListener { + val activityManager = + getSystemService(ACTIVITY_SERVICE) as ActivityManager + // Global web app => close all webview activities, save to global settings + if (isGlobalWebApp) { + closeAllWebAppsAndProcesses( + activityManager + ) + DataManager.getInstance().settings.globalWebApp = modifiedWebapp + DataManager.getInstance().saveGlobalSettings() + } else { + for (task in activityManager.appTasks) { + val id = task.taskInfo.baseIntent.getIntExtra( + Const.INTENT_WEBAPPID, + -1 + ) + if (id == webappID) task.finishAndRemoveTask() + } + for (processInfo in activityManager.runningAppProcesses) { + if (processInfo.processName.contains("web_sandbox_" + modifiedWebapp.containerId)) { + Process.killProcess(processInfo.pid) + } + } + DataManager.getInstance().replaceWebApp(modifiedWebapp) + } + + val i = Intent(this@WebAppSettingsActivity, MainActivity::class.java) + i.putExtra(Const.INTENT_WEBAPP_CHANGED, true) + finish() + startActivity(i) + } + + binding.btnCancel.setOnClickListener { finish() } + } + + private fun setupDarkModeElements() { + val txtBeginDarkMode = binding.textDarkModeBegin + val txtEndDarkMode = binding.textDarkModeEnd + + txtBeginDarkMode.setOnClickListener { + showTimePicker( + txtBeginDarkMode + ) + } + txtEndDarkMode.setOnClickListener { + showTimePicker( + txtEndDarkMode + ) + } + + } + + private fun setupDesktopUserAgentHint() { + val txt = binding.txthintUserAgent + txt.text = Html.fromHtml(getString(R.string.hint_user_agent), Html.FROM_HTML_MODE_LEGACY) + txt.movementMethod = LinkMovementMethod.getInstance() + } + + private fun setupShortcutButton() { + binding.btnRecreateShortcut.setOnClickListener { + val frag = ShortcutDialogFragment.newInstance(webapp) + frag.show(supportFragmentManager, "SCFetcher-" + webapp!!.ID) + } + } + + private fun setupPlusSettings() { + if (!BuildConfig.FLAVOR.contains("extended")) { + binding.sectionDarkmode.visibility = View.GONE + binding.sectionSandbox.visibility = View.GONE + binding.sectionKioskMode.visibility = View.GONE + binding.sectionAccessRestriction.visibility = View.GONE + } + } + + private fun showTimePicker(txtField: EditText) { + val c = convertStringToCalendar(txtField.text.toString()) + val timePickerDialog = TimePickerDialog( + this@WebAppSettingsActivity, R.style.AppTheme, + { timePicker: TimePicker?, selectedHour: Int, selectedMinute: Int -> + val datetime = Calendar.getInstance() + datetime[Calendar.HOUR_OF_DAY] = selectedHour + datetime[Calendar.MINUTE] = selectedMinute + txtField.setText(getHourMinFormat().format(datetime.time)) + }, c!![Calendar.HOUR_OF_DAY], c[Calendar.MINUTE], true + ) + timePickerDialog.show() + } + + private fun prepareGlobalWebAppScreen() { + binding.btnRecreateShortcut.visibility = View.GONE + binding.labelWebAppName.visibility = View.GONE + binding.txtWebAppName.visibility = View.GONE + binding.switchOverrideGlobal.visibility = View.GONE + binding.sectionSSL.visibility = View.GONE + binding.sectionSandbox.visibility = View.GONE + binding.labelTitle.visibility = View.GONE + binding.labelEditableBaseUrl.visibility = View.GONE + binding.textBaseUrl.visibility = View.GONE + + binding.globalSettingsInfoText.visibility = View.VISIBLE + + setToolbarTitle(getString(R.string.global_web_app_settings)) + } +} + + diff --git a/app/src/main/java/com/cylonid/nativealpha/WebViewActivity.java b/app/src/main/java/com/cylonid/nativealpha/WebViewActivity.java index 759bba52..ad2aa915 100644 --- a/app/src/main/java/com/cylonid/nativealpha/WebViewActivity.java +++ b/app/src/main/java/com/cylonid/nativealpha/WebViewActivity.java @@ -6,13 +6,11 @@ import android.app.DownloadManager; import android.content.ClipData; import android.content.ClipboardManager; -import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.net.http.SslError; import android.os.Build; @@ -23,8 +21,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.util.Log; -import android.view.Gravity; -import android.view.Menu; +import android.view.LayoutInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; @@ -33,55 +30,65 @@ import android.view.WindowManager; import android.webkit.CookieManager; import android.webkit.GeolocationPermissions; +import android.webkit.HttpAuthHandler; import android.webkit.PermissionRequest; import android.webkit.SslErrorHandler; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; -import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.FrameLayout; import android.widget.PopupMenu; import android.widget.ProgressBar; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; import androidx.core.app.ActivityCompat; import androidx.core.app.ShareCompat; import androidx.core.content.ContextCompat; -import androidx.core.view.MenuCompat; import androidx.webkit.WebSettingsCompat; import androidx.webkit.WebViewFeature; +import com.cylonid.nativealpha.databinding.DialogHttpAuthBinding; +import com.cylonid.nativealpha.helper.AdblockLifecycleHelper; +import com.cylonid.nativealpha.helper.AdblockProviderApiHelper; import com.cylonid.nativealpha.helper.BiometricPromptHelper; import com.cylonid.nativealpha.helper.IconPopupMenuHelper; +import com.cylonid.nativealpha.model.AdblockConfig; import com.cylonid.nativealpha.model.DataManager; import com.cylonid.nativealpha.model.SandboxManager; import com.cylonid.nativealpha.model.WebApp; import com.cylonid.nativealpha.util.Const; +import com.cylonid.nativealpha.util.DateUtils; import com.cylonid.nativealpha.util.EntryPointUtils; import com.cylonid.nativealpha.util.LocaleUtils; +import com.cylonid.nativealpha.util.NotificationUtils; import com.cylonid.nativealpha.util.Utility; import com.cylonid.nativealpha.util.WebViewLauncher; +import com.google.android.material.color.MaterialColors; import com.google.android.material.snackbar.Snackbar; -import com.jakewharton.processphoenix.ProcessPhoenix; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; +import io.github.edsuns.adfilter.AdFilter; +import io.github.edsuns.adfilter.Filter; import pub.devrel.easypermissions.EasyPermissions; + import static com.cylonid.nativealpha.util.Const.CODE_OPEN_FILE; public class WebViewActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks { @@ -107,12 +114,21 @@ public class WebViewActivity extends AppCompatActivity implements EasyPermission private boolean fallbackToDefaultLongClickBehaviour = false; private PopupMenu mPopupMenu = null; + private AdFilter adFilter; + + private AdblockProviderApiHelper adblockProviderApiHelper; + private AdblockLifecycleHelper adblockLifecycleHelper; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + adblockLifecycleHelper = new AdblockLifecycleHelper(this); + adblockLifecycleHelper.trySyncOperation(() -> adFilter = AdFilter.Companion.get(getApplicationContext())); + + adblockProviderApiHelper = new AdblockProviderApiHelper(adFilter); webappID = getIntent().getIntExtra(Const.INTENT_WEBAPPID, -1); EntryPointUtils.entryPointReached(this); - webapp = DataManager.getInstance().getWebApp(webappID); if (webapp == null) { // Toast is shown in getWebApp method @@ -128,29 +144,28 @@ protected void onCreate(Bundle savedInstanceState) { @SuppressLint("ClickableViewAccessibility") private void setupWebView() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - String processName = Application.getProcessName(); - String packageName = this.getPackageName(); + String processName = Application.getProcessName(); + String packageName = this.getPackageName(); + + boolean hasSandboxing = SandboxManager.getInstance() != null; + // Sandboxed Web App is openend in main process using an old shortcut + if (packageName.equals(processName) && webapp.isUseContainer() && hasSandboxing) { + WebViewLauncher.startWebViewInNewProcess(webapp, this); + } - // Sandboxed Web App is openend in main process using an old shortcut - if(packageName.equals(processName) && webapp.isUseContainer()) { + if (!packageName.equals(processName) && hasSandboxing) { + if (SandboxManager.getInstance().isSandboxUsedByAnotherApp(webapp)) { + SandboxManager.getInstance().unregisterWebAppFromSandbox(webapp.getContainerId()); WebViewLauncher.startWebViewInNewProcess(webapp, this); } - - if (!packageName.equals(processName) && SandboxManager.getInstance() != null) { - if (SandboxManager.getInstance().isSandboxUsedByAnotherApp(webapp)) { - SandboxManager.getInstance().unregisterWebAppFromSandbox(webapp.getContainerId()); - WebViewLauncher.startWebViewInNewProcess(webapp, this); - } - try { - SandboxManager.getInstance().registerWebAppToSandbox(webapp); - WebView.setDataDirectorySuffix(webapp.getContainerId() + webapp.getAlphanumericBaseUrl() + "_" + webapp.getID()); - } catch (IllegalStateException e) { - e.printStackTrace(); - } + try { + SandboxManager.getInstance().registerWebAppToSandbox(webapp); + WebView.setDataDirectorySuffix(webapp.getContainerId() + webapp.getAlphanumericBaseUrl() + "_" + webapp.getID()); + } catch (IllegalStateException e) { + e.printStackTrace(); } - } + setContentView(R.layout.full_webview); if(webapp.isKeepAwake()) { @@ -162,26 +177,43 @@ private void setupWebView() { wv = findViewById(R.id.webview); progressBar = findViewById(R.id.progressBar); - if (webapp.isUseAdblock()) { + List adblockConfigs = DataManager.getInstance().getSettings().getGlobalWebApp().getAdBlockSettings(); + if (webapp.isUseAdblock() && !adblockConfigs.isEmpty()) { wv.setVisibility(View.GONE); wv = findViewById(R.id.adblockwebview); wv.setVisibility(View.VISIBLE); + + adFilter.setupWebView(wv); + adblockLifecycleHelper.beforeAdblockOperation(() -> adblockProviderApiHelper.synchronizeAdblockProviderWithSettings(adblockConfigs)); + + adFilter.getViewModel().getOnDirty().observe(this, none -> wv.clearCache(false) + ); + + adFilter.getViewModel().getEnabledFilterCount().observe(this, count -> { + if (count == adblockConfigs.size()) { + adblockLifecycleHelper.afterAdblockOperation(); + } + }); } + String fieldName = Stream.of(WebViewActivity.class.getDeclaredFields()).filter(f -> f.getType() == WebView.class).findFirst().orElseThrow(null).getName(); String uaString = wv.getSettings().getUserAgentString().replace("; " + fieldName, ""); wv.getSettings().setUserAgentString(uaString); if (webapp.isUseCustomUserAgent()) { - wv.getSettings().setUserAgentString(webapp.getUserAgent().replace("\0", "").replace("\n", "").replace("\r", "")); + if(webapp.getUserAgent() != null && !webapp.getUserAgent().equals("")) { + wv.getSettings().setUserAgentString(webapp.getUserAgent().replace("\0", "").replace("\n", "").replace("\r", "")); + } } if (webapp.isShowFullscreen()) { this.hideSystemBars(); - } else { + } else if(DataManager.getInstance().getSettings().getAlwaysShowSoftwareButtons()) { this.showSystemBars(); } wv.setWebViewClient(new CustomBrowser()); wv.getSettings().setSafeBrowsingEnabled(false); wv.getSettings().setDomStorageEnabled(true); + wv.getSettings().setDatabaseEnabled(true); wv.getSettings().setAllowFileAccess(true); wv.getSettings().setBlockNetworkLoads(false); // wv.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); @@ -217,6 +249,7 @@ private void setupWebView() { loadURL(wv, url); wv.setWebChromeClient(new CustomWebChromeClient()); wv.setOnLongClickListener(view -> { + if(webapp.getAlwaysUseFallbackContextMenu()) return false; if(fallbackToDefaultLongClickBehaviour) { fallbackToDefaultLongClickBehaviour = false; return false; @@ -225,6 +258,7 @@ private void setupWebView() { return true; }); + wv.setDownloadListener((dl_url, userAgent, contentDisposition, mimeType, contentLength) -> { if (mimeType.equals("application/pdf")) { @@ -232,39 +266,55 @@ private void setupWebView() { i.setData(Uri.parse(dl_url)); startActivity(i); } else { - DownloadManager.Request request = new DownloadManager.Request( - Uri.parse(dl_url)); - String file_name = Utility.getFileNameFromDownload(dl_url, contentDisposition, mimeType); - - request.setMimeType(mimeType); - request.addRequestHeader("cookie", CookieManager.getInstance().getCookie(dl_url)); - request.addRequestHeader("User-Agent", userAgent); - request.setTitle(file_name); - request.allowScanningByMediaScanner(); - request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - request.setDestinationInExternalPublicDir( - Environment.DIRECTORY_DOWNLOADS, file_name); - - DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - String[] perms = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}; - if (!EasyPermissions.hasPermissions(WebViewActivity.this, perms)) { - dl_request = request; - EasyPermissions.requestPermissions(WebViewActivity.this, getString(R.string.permission_storage_rationale), Const.PERMISSION_RC_STORAGE, perms); - } else { - if (dm != null) { - dm.enqueue(request); - Utility.showInfoSnackbar(this, getString(R.string.file_download), Snackbar.LENGTH_SHORT); + if(dl_url != null && !dl_url.equals("")) { + if(dl_url.startsWith("blob:")) { + dl_url = dl_url.replace("blob:", ""); + try { + dl_url = URLDecoder.decode(dl_url, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); } } - } - //No storage permission needed for Android 10+ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - if (dm != null) { - dm.enqueue(request); - Utility.showInfoSnackbar(this, getString(R.string.file_download), Snackbar.LENGTH_SHORT); + DownloadManager.Request request = null; + try { + request = new DownloadManager.Request( + Uri.parse(dl_url)); } + catch(Exception e) { + NotificationUtils.showInfoSnackbar(this, getString(R.string.file_download), Snackbar.LENGTH_SHORT); + } + String file_name = Utility.getFileNameFromDownload(dl_url, contentDisposition, mimeType); + + request.setMimeType(mimeType); + request.addRequestHeader("cookie", CookieManager.getInstance().getCookie(dl_url)); + request.addRequestHeader("User-Agent", userAgent); + request.setTitle(file_name); + request.allowScanningByMediaScanner(); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + request.setDestinationInExternalPublicDir( + Environment.DIRECTORY_DOWNLOADS, file_name); + + DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + String[] perms = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}; + if (!EasyPermissions.hasPermissions(WebViewActivity.this, perms)) { + dl_request = request; + EasyPermissions.requestPermissions(WebViewActivity.this, getString(R.string.permission_storage_rationale), Const.PERMISSION_RC_STORAGE, perms); + } else { + if (dm != null) { + dm.enqueue(request); + NotificationUtils.showInfoSnackbar(this, getString(R.string.file_download), Snackbar.LENGTH_SHORT); + } + } + } + //No storage permission needed for Android 10+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (dm != null) { + dm.enqueue(request); + NotificationUtils.showInfoSnackbar(this, getString(R.string.file_download), Snackbar.LENGTH_SHORT); + } + } } } @@ -332,30 +382,50 @@ public boolean onTouch(View v, MotionEvent event) { }); } + @SuppressLint("RequiresFeature") private void setDarkModeIfNeeded() { + if (!BuildConfig.FLAVOR.contains("extended")) { + return; + } + boolean needsForcedDarkMode = webapp.isUseTimespanDarkMode() && - Utility.isInInterval(Utility.convertStringToCalendar(webapp.getTimespanDarkModeBegin()), Calendar.getInstance(), Utility.convertStringToCalendar(webapp.getTimespanDarkModeEnd())) + DateUtils.isInInterval(DateUtils.convertStringToCalendar(webapp.getTimespanDarkModeBegin()), Calendar.getInstance(), DateUtils.convertStringToCalendar(webapp.getTimespanDarkModeEnd())) || (!webapp.isUseTimespanDarkMode() && webapp.isForceDarkMode()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - if (Utility.isNightMode(this) || needsForcedDarkMode) { - wv.getSettings().setForceDark(WebSettings.FORCE_DARK_ON); - } else { - wv.getSettings().setForceDark(WebSettings.FORCE_DARK_OFF); - } + boolean isForceDarkSupported = WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK); + boolean isForceDarkStrategySupported = WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY); + boolean isAlgorithmicDarkeningSupported = WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING); if (needsForcedDarkMode) { - if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + wv.setBackgroundColor(Color.BLACK); + wv.setForceDarkAllowed(true); + getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES); + if (isForceDarkSupported) { + WebSettingsCompat.setForceDark(wv.getSettings(), WebSettingsCompat.FORCE_DARK_ON); + } + if (isForceDarkStrategySupported) { WebSettingsCompat.setForceDarkStrategy(wv.getSettings(), WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING); } - wv.setBackgroundColor(Color.BLACK); + if (isAlgorithmicDarkeningSupported) { + WebSettingsCompat.setAlgorithmicDarkeningAllowed(wv.getSettings(), true); + } } else { - if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + wv.setBackgroundColor(Color.WHITE); + + if (isForceDarkSupported) { + WebSettingsCompat.setForceDark(wv.getSettings(), WebSettingsCompat.FORCE_DARK_OFF); + } + if (isForceDarkStrategySupported) { WebSettingsCompat.setForceDarkStrategy(wv.getSettings(), WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY); } - wv.setBackgroundColor(Color.WHITE); + if (isAlgorithmicDarkeningSupported) { + WebSettingsCompat.setAlgorithmicDarkeningAllowed(wv.getSettings(), false); + } } } + } @SuppressLint("NonConstantResourceId") @@ -364,13 +434,30 @@ private void showWebViewPopupMenu() { mPopupMenu = IconPopupMenuHelper.getMenu(center, R.menu.wv_context_menu, WebViewActivity.this); String currentUrl = wv.getUrl(); - String title = currentUrl.length() < 32 ? currentUrl : currentUrl.substring(0, 32) + "…"; - SpannableString spanString = new SpannableString(title); - spanString.setSpan(new ForegroundColorSpan(Color.BLACK), 0, spanString.length(), 0); //fix the color to white - spanString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, spanString.length(), 0); - mPopupMenu.getMenu().getItem(0).setTitle(spanString); - if(wv.canGoForward()) mPopupMenu.getMenu().getItem(2).setVisible(true); + String title = ""; + if (currentUrl != null) { + title = currentUrl.length() < 32 ? currentUrl : currentUrl.substring(0, 32) + "…"; + } + SpannableString spanStringWebAppTitle = new SpannableString(title); + + // The item is disabled because it has no click action, but we want to override the disabled style (text color) + int colorOnSurface = MaterialColors.getColor(center, R.attr.colorOnSurface, Color.BLACK); + ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(colorOnSurface); + spanStringWebAppTitle.setSpan(foregroundColorSpan, 0, spanStringWebAppTitle.length(), 0); + spanStringWebAppTitle.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, spanStringWebAppTitle.length(), 0); + mPopupMenu.getMenu().getItem(0).setTitle(spanStringWebAppTitle); + + for (int i = 0; i < mPopupMenu.getMenu().size(); i++) { + MenuItem item = mPopupMenu.getMenu().getItem(i); + SpannableString spanString = new SpannableString(item.getTitle()); + spanString.setSpan(foregroundColorSpan, 0, spanString.length(),0); + item.setTitle(spanString); + } + if(wv.canGoForward()) mPopupMenu.getMenu().getItem(2).setVisible(true); + if(BuildConfig.DEBUG) { + mPopupMenu.getMenu().getItem(6).setVisible(true); + } mPopupMenu.setOnMenuItemClickListener(menuItem -> { switch(menuItem.getItemId()) { case R.id.cmItemForward: @@ -397,13 +484,21 @@ private void showWebViewPopupMenu() { case R.id.cmItemCloseWebApp: finishAndRemoveTask(); return true; - case R.id.cmSelectText: + case R.id.cmFallbackContextmenuTemp: fallbackToDefaultLongClickBehaviour = true; return true; case R.id.cmMainMenu: Intent intent = new Intent(this, MainActivity.class); startActivity(intent); return true; + case R.id.cmShowAdblockProviders: + StringBuilder message = new StringBuilder(); + for(Map.Entry entry : Objects.requireNonNull(AdFilter.Companion.get().getViewModel().getFilters().getValue()).entrySet()) { + Filter filter = entry.getValue(); + message.append(filter.getUrl()).append(" has downloaded: ").append(filter.hasDownloaded()).append("\n\n"); + } + NotificationUtils.showToast(this, message.toString()); + return true; } return false; @@ -469,7 +564,7 @@ protected void onResume() { protected void onPause() { super.onPause(); - wv.evaluateJavascript("document.querySelectorAll('audio').forEach(x => x.pause());", null); + wv.evaluateJavascript("document.querySelectorAll('audio').forEach(x => x.pause());document.querySelectorAll('video').forEach(x => x.pause());", null); wv.onPause(); wv.pauseTimers(); if(mPopupMenu != null) mPopupMenu.dismiss(); @@ -498,6 +593,7 @@ public WebView getWebView() { private Map initCustomHeaders(boolean save_data) { Map extraHeaders = new HashMap<>(); extraHeaders.put("DNT", "1"); + extraHeaders.put("X-REQUESTED-WITH", ""); if (save_data) extraHeaders.put("Save-Data", "on"); return Collections.unmodifiableMap(extraHeaders); @@ -598,7 +694,7 @@ public void onPermissionsGranted(int requestCode, @NonNull List list) { DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); if (dm != null) { dm.enqueue(dl_request); - Utility.showInfoSnackbar(this, getString(R.string.file_download), Snackbar.LENGTH_SHORT); + NotificationUtils.showInfoSnackbar(this, getString(R.string.file_download), Snackbar.LENGTH_SHORT); } dl_request = null; @@ -683,7 +779,7 @@ public boolean onShowFileChooser( Intent intent = fileChooserParams.createIntent(); startActivityForResult(intent, CODE_OPEN_FILE); } catch (Exception e) { - Utility.showInfoSnackbar(WebViewActivity.this, getString(R.string.no_filemanager), Snackbar.LENGTH_LONG); + NotificationUtils.showInfoSnackbar(WebViewActivity.this, getString(R.string.no_filemanager), Snackbar.LENGTH_LONG); e.printStackTrace(); } return true; @@ -769,8 +865,32 @@ public void onGeolocationPermissionsShowPrompt(final String origin, } } + private void showHttpAuthDialog(final HttpAuthHandler handler, String host, String realm) { + DialogHttpAuthBinding localBinding = DialogHttpAuthBinding.inflate(LayoutInflater.from(this)); + new AlertDialog.Builder(this) + .setView(localBinding.getRoot()) + .setTitle(getString(R.string.http_auth_title)) + .setMessage(getString(R.string.enter_http_auth_credentials, realm, host)) + .setPositiveButton(getString(R.string.ok), (dialog, whichButton) -> { + String username = localBinding.username.getText().toString(); + String password = localBinding.password.getText().toString(); + + handler.proceed(username, password); + + }) + .setNegativeButton(getString(R.string.cancel), (dialog, whichButton) -> handler.cancel()) + .show(); + } + private class CustomBrowser extends WebViewClient { + private AdFilter adFilter = AdFilter.Companion.get(); + + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { + showHttpAuthDialog(handler, host, realm); + } + @Override public void onPageFinished(WebView view, String url) { if(url.equals("about:blank")) { @@ -781,16 +901,28 @@ public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); } + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + adFilter.performScript(view, url); + super.onPageStarted(view, url, favicon); + } + @Nullable @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { if(urlOnFirstPageload.equals("")) urlOnFirstPageload = request.getUrl().toString(); + + if(webapp.isUseAdblock()) { + return (adFilter.shouldIntercept(view, request)).getResourceResponse(); + } if (webapp.isBlockThirdPartyRequests()) { Uri uri = request.getUrl(); Uri webapp_uri = Uri.parse(webapp.getBaseUrl()); - if (!uri.getHost().endsWith(webapp_uri.getHost())) { - return null; + if(uri.getHost() != null) { + if (!uri.getHost().endsWith(webapp_uri.getHost())) { + return new WebResourceResponse("text/plain", "utf-8", null); + } } } return super.shouldInterceptRequest(view, request); @@ -840,8 +972,14 @@ public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslE @Override public void onLoadResource(WebView view, String url) { super.onLoadResource(view, url); - if (DataManager.getInstance().getWebApp(webappID).isRequestDesktop()) - view.evaluateJavascript("document.querySelector('meta[name=\"viewport\"]').setAttribute('content', 'width=1024px, initial-scale=' + (document.documentElement.clientWidth / 1024));", null); + + if (DataManager.getInstance().getWebApp(webappID).isRequestDesktop()) + view.evaluateJavascript(""" + var needsForcedWidth = document.documentElement.clientWidth < 1200; + if(needsForcedWidth) { + document.querySelector('meta[name=\"viewport\"]').setAttribute('content', 'width=1200px, initial-scale=' + (document.documentElement.clientWidth / 1200)); + } + """, null); view.evaluateJavascript("document.addEventListener( \"visibilitychange\" , (event) => { event.stopImmediatePropagation(); } );", null); } diff --git a/app/src/main/java/com/cylonid/nativealpha/model/DataManager.java b/app/src/main/java/com/cylonid/nativealpha/model/DataManager.java index 9ef0ad26..0d7ff6ad 100644 --- a/app/src/main/java/com/cylonid/nativealpha/model/DataManager.java +++ b/app/src/main/java/com/cylonid/nativealpha/model/DataManager.java @@ -9,8 +9,12 @@ import android.widget.Toast; import com.cylonid.nativealpha.R; +import com.cylonid.nativealpha.model.deserializer.GlobalSettingsDeserializer; +import com.cylonid.nativealpha.model.deserializer.WebAppDeserializer; import com.cylonid.nativealpha.util.App; +import com.cylonid.nativealpha.util.Const; import com.cylonid.nativealpha.util.InvalidChecksumException; +import com.cylonid.nativealpha.util.ShortcutIconUtils; import com.cylonid.nativealpha.util.Utility; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -24,6 +28,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; +import java.util.Comparator; import java.util.Map; import java.util.TreeMap; @@ -38,6 +43,8 @@ public class DataManager { private static final String SHARED_PREF_KEY = "WEBSITEDATA"; private static final String GENERAL_INFO = "com.cylonid.nativealpha.GENERAL_INFO"; public static final String EULA_ACCEPTED = "eulaAccepted"; + + public static final String ADBLOCK_CRASH = "adblockCrash"; public static final String LAST_SHOWN_UPDATE = "lastShownUpdate"; public static final String DATA_FORMAT = "dataFormat"; @@ -101,22 +108,20 @@ public void saveWebAppData() { editor.apply(); } - public void setDataFormat(int dataFormat) { - getGeneralInfo().edit().putInt(DATA_FORMAT, dataFormat).apply(); - } - public int getDataFormat() { - return getGeneralInfo().getInt(DATA_FORMAT, LEGACY_DATA_FORMAT); - } public boolean getEulaData() { return getGeneralInfo().getBoolean(EULA_ACCEPTED, false); } + public boolean getHasAdblockCrashed() { return getGeneralInfo().getBoolean(ADBLOCK_CRASH, false);} + public int getLastShownUpdate() { return getGeneralInfo().getInt(LAST_SHOWN_UPDATE, 0); } + public void setHasAdblockCrashed(boolean newValue) { getGeneralInfo().edit().putBoolean(ADBLOCK_CRASH, newValue).apply(); } + public void setEulaData(boolean newValue) { getGeneralInfo().edit().putBoolean(EULA_ACCEPTED, newValue).apply(); } @@ -143,7 +148,7 @@ private void checkIfWebAppIdsCollide(ArrayList oldWebApps, ArrayList() {}.getType()); - } - } - else loadGlobalSettingsLegacy(); + } + //Global settings + if (appdata.contains(shared_pref_globalsettings)) { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(WebApp.class, new WebAppDeserializer()); + gsonBuilder.registerTypeAdapter(GlobalSettings.class, new GlobalSettingsDeserializer()); + Gson gson = gsonBuilder.create(); + String json = appdata.getString(shared_pref_globalsettings, ""); + int oldDataFormat = DataVersionConverter.getDataFormat(json); + String currentDataFormattedJson = this.checkDataFormat(oldDataFormat, json); + settings = gson.fromJson(currentDataFormattedJson, GlobalSettings.class); + assertGlobalWebappData(); + if(oldDataFormat != DataVersionConverter.getDataFormat(currentDataFormattedJson)) this.saveGlobalSettings(); + } + } public void loadGlobalSettingsLegacy() { @@ -208,30 +217,19 @@ public void saveGlobalSettings() { editor.apply(); } - -// public void initDummyData() -// { -// loadAppData(); -// WebApp d1 = new WebApp("orf.at"); -// WebApp d2 = new WebApp("diepresse.com"); -// WebApp d3 = new WebApp("oebb.at"); -// -// addWebsite(d1); -// addWebsite(d2); -// addWebsite(d3); -// -// } - public void addWebsite(WebApp new_site) { websites.add(new_site); - Utility.Assert(new_site.getBaseUrl().equals(websites.get(new_site.getID()).getBaseUrl()), "WebApp ID and array position out of sync."); saveWebAppData(); } public int getIncrementedID() { - max_assigned_ID++; - return max_assigned_ID; + return getWebsites().size(); } + + public int getIncrementedOrder() { + return getActiveWebsitesCount() + 1; + } + public ArrayList getWebsites() { Utility.Assert(websites != null, "Websites not loaded"); return websites; @@ -239,10 +237,12 @@ public ArrayList getWebsites() { public ArrayList getActiveWebsites() { ArrayList active_webapps = new ArrayList<>(); + for (WebApp webapp : websites) { if (webapp.isActiveEntry()) active_webapps.add(webapp); } + active_webapps.sort(Comparator.comparingInt(WebApp::getOrder)); return active_webapps; } @@ -349,7 +349,6 @@ private String checkDataFormat(int dataFormat, String jsonInput) { switch(dataFormat) { case LEGACY_DATA_FORMAT: String convertedInput = DataVersionConverter.convertToDataFormat(jsonInput, DataVersionConverter.getLegacyTo1300Map()); - this.setDataFormat(1300); return convertedInput; default: case 1300: // Current data format => corresponding to app release version @@ -379,19 +378,16 @@ public WebApp getPredecessor(int i) { } while (!websites.get(neighbor).isActiveEntry()); return websites.get(neighbor); + } -// if (i != (websites.size() - 1)) { -// return websites.get(i + 1); -// } -// else -// return websites.get(0); - -// -// if (i != 0) { -// return websites.get(i - 1); -// } -// else -// return websites.get(websites.size() - 1); + private void assertGlobalWebappData() { + boolean override = settings.getGlobalWebApp().isOverrideGlobalSettings(); + int container = settings.getGlobalWebApp().getContainerId(); + if(!override || container != Const.NO_CONTAINER) { + settings.getGlobalWebApp().setOverrideGlobalSettings(true); + settings.getGlobalWebApp().setContainerId(Const.NO_CONTAINER); + this.saveGlobalSettings(); + } } diff --git a/app/src/main/java/com/cylonid/nativealpha/model/DataVersionConverter.java b/app/src/main/java/com/cylonid/nativealpha/model/DataVersionConverter.java index 95b3c07f..0348a08c 100644 --- a/app/src/main/java/com/cylonid/nativealpha/model/DataVersionConverter.java +++ b/app/src/main/java/com/cylonid/nativealpha/model/DataVersionConverter.java @@ -15,8 +15,8 @@ public static String convertToDataFormat(String input, Map map) } public static int getDataFormat(String input) { - if(input.contains(DataVersionConverter.formatAsJsonKey("base_url"))) return 1000; - if(input.contains(DataVersionConverter.formatAsJsonKey("baseUrl"))) return 1300; + if(input.contains(DataVersionConverter.formatAsJsonKey("allow_js"))) return 1000; + if(input.contains(DataVersionConverter.formatAsJsonKey("isAllowJs"))) return 1300; return 0; } diff --git a/app/src/main/java/com/cylonid/nativealpha/model/GlobalSettings.java b/app/src/main/java/com/cylonid/nativealpha/model/GlobalSettings.java deleted file mode 100644 index 92cbbf30..00000000 --- a/app/src/main/java/com/cylonid/nativealpha/model/GlobalSettings.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.cylonid.nativealpha.model; - -public class GlobalSettings { - - private boolean clear_cache; - private boolean clear_cookies; - private boolean two_finger_multitouch; - private boolean three_finger_multitouch; - private boolean show_progressbar; - private boolean multitouch_reload; - private int theme_id; - private WebApp global_web_app; - - public GlobalSettings(GlobalSettings other) { - this.clear_cache = other.clear_cache; - this.clear_cookies = other.clear_cookies; - this.two_finger_multitouch = other.two_finger_multitouch; - this.three_finger_multitouch = other.three_finger_multitouch; - this.theme_id = other.theme_id; - this.multitouch_reload = other.multitouch_reload; - this.show_progressbar = other.show_progressbar; - this.global_web_app = other.global_web_app; - } - - public GlobalSettings() { - clear_cache = false; - clear_cookies = false; - two_finger_multitouch = true; - three_finger_multitouch = false; - multitouch_reload = true; - theme_id = 0; - show_progressbar = false; - global_web_app = new WebApp("about:blank", Integer.MAX_VALUE); - } - - public boolean isTwoFingerMultitouch() { - return two_finger_multitouch; - } - - public void setTwoFingerMultitouch(boolean twoFingerMultitouch) { - this.two_finger_multitouch = twoFingerMultitouch; - } - - public boolean isThreeFingerMultitouch() { - return three_finger_multitouch; - } - - public void setThreeFingerMultitouch(boolean threeFingerMultitouch) { - this.three_finger_multitouch = threeFingerMultitouch; - } - - public boolean isClearCache() { - return clear_cache; - } - - public void setClearCache(boolean clearCache) { - this.clear_cache = clearCache; - } - - public void setClearCookies(boolean clear_cookies) { - this.clear_cookies = clear_cookies; - } - - public int getThemeId() { - return theme_id; - } - - public void setThemeId(int theme_id) { - this.theme_id = theme_id; - } - - public boolean isMultitouchReload() { - return multitouch_reload; - } - - public void setMultitouchReload(boolean multitouch_reload) { - this.multitouch_reload = multitouch_reload; - } - - public boolean isShowProgressbar() { - return show_progressbar; - } - - public void setShowProgressbar(boolean show_progressbar) { - this.show_progressbar = show_progressbar; - } - - public WebApp getGlobalWebApp() { - return global_web_app; - } - - public void setGlobalWebApp(WebApp globalWebApp) { - this.global_web_app = globalWebApp; - } - - -} diff --git a/app/src/main/java/com/cylonid/nativealpha/model/GlobalSettings.kt b/app/src/main/java/com/cylonid/nativealpha/model/GlobalSettings.kt new file mode 100644 index 00000000..1b3f0c2f --- /dev/null +++ b/app/src/main/java/com/cylonid/nativealpha/model/GlobalSettings.kt @@ -0,0 +1,21 @@ +package com.cylonid.nativealpha.model + +import com.cylonid.nativealpha.util.Const + + +data class GlobalSettings( + var isClearCache: Boolean = false, + var isTwoFingerMultitouch: Boolean = true, + var isThreeFingerMultitouch: Boolean = false, + var isShowProgressbar: Boolean = false, + var isMultitouchReload: Boolean = true, + var themeId: Int = 0, + var globalWebApp: WebApp = WebApp("about:blank", Int.MAX_VALUE, Const.getDefaultAdBlockConfig()), + var alwaysShowSoftwareButtons: Boolean = false, + var clear_cookies: Boolean = false +) { + + fun setClearCookies(clear_cookies: Boolean) { + this.clear_cookies = clear_cookies + } +} diff --git a/app/src/main/java/com/cylonid/nativealpha/model/GlobalSettingsInstanceCreator.java b/app/src/main/java/com/cylonid/nativealpha/model/GlobalSettingsInstanceCreator.java deleted file mode 100644 index 42473c5a..00000000 --- a/app/src/main/java/com/cylonid/nativealpha/model/GlobalSettingsInstanceCreator.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.cylonid.nativealpha.model; - -import com.google.gson.InstanceCreator; - -import java.lang.reflect.Type; - -public class GlobalSettingsInstanceCreator implements InstanceCreator -{ - @Override - public GlobalSettings createInstance(Type type) - { - return new GlobalSettings(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/cylonid/nativealpha/model/SandboxManager.java b/app/src/main/java/com/cylonid/nativealpha/model/SandboxManager.java index b9d57a2f..b9a2305c 100644 --- a/app/src/main/java/com/cylonid/nativealpha/model/SandboxManager.java +++ b/app/src/main/java/com/cylonid/nativealpha/model/SandboxManager.java @@ -21,7 +21,7 @@ private SandboxManager() } public static SandboxManager getInstance() { - if (BuildConfig.FLAVOR.equals("extended")) { + if (BuildConfig.FLAVOR.contains("extended")) { instance = instance == null ? new SandboxManager() : instance; return instance; } diff --git a/app/src/main/java/com/cylonid/nativealpha/util/App.java b/app/src/main/java/com/cylonid/nativealpha/util/App.java index e8e1ee55..a548a6e3 100644 --- a/app/src/main/java/com/cylonid/nativealpha/util/App.java +++ b/app/src/main/java/com/cylonid/nativealpha/util/App.java @@ -4,6 +4,9 @@ import android.app.Application; import android.content.Context; +import androidx.work.Configuration; +import androidx.work.WorkManager; + public class App extends Application { @SuppressLint("StaticFieldLeak") //We are using app context which is never deleted during runtime, so this is not a leak per se. @@ -12,7 +15,13 @@ public class App extends Application { public void onCreate() { super.onCreate(); + App.context = getApplicationContext(); + if(!WorkManager.isInitialized()) { + WorkManager.initialize(this, new Configuration.Builder().build()); + } + + } public static Context getAppContext() { diff --git a/app/src/main/java/com/cylonid/nativealpha/util/Const.java b/app/src/main/java/com/cylonid/nativealpha/util/Const.java index a9f3d772..4b41a68d 100644 --- a/app/src/main/java/com/cylonid/nativealpha/util/Const.java +++ b/app/src/main/java/com/cylonid/nativealpha/util/Const.java @@ -1,7 +1,11 @@ package com.cylonid.nativealpha.util; +import com.cylonid.nativealpha.model.AdblockConfig; + +import java.util.ArrayList; + public class Const { - public static final String DESKTOP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0"; + public static final String DESKTOP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0"; public static final String INTENT_WEBAPPID = "webappID"; public static final String INTENT_BACKUP_RESTORED = "backup_restored"; @@ -10,10 +14,6 @@ public class Const { public static final int NO_CONTAINER = -1; - public static final int RESULT_IDX_FAVICON = 0; - public static final int RESULT_IDX_TITLE = 1; - public static final int RESULT_IDX_NEW_BASEURL = 2; - public static final int PERMISSION_RC_LOCATION = 123; public static final int PERMISSION_RC_STORAGE = 132; public static final int PERMISSION_CAMERA = 100; @@ -23,6 +23,12 @@ public class Const { public static final int CODE_WRITE_FILE = 4096; public static final int FAVICON_MIN_WIDTH = 96; + + public static ArrayList getDefaultAdBlockConfig() { + ArrayList list = new ArrayList<>(); + list.add(new AdblockConfig("Fanboy Ultimate List", "https://fanboy.co.nz/r/fanboy-ultimate.txt")); + return list; + } } diff --git a/app/src/main/java/com/cylonid/nativealpha/util/Utility.java b/app/src/main/java/com/cylonid/nativealpha/util/Utility.java index 4c087e68..73c0b618 100644 --- a/app/src/main/java/com/cylonid/nativealpha/util/Utility.java +++ b/app/src/main/java/com/cylonid/nativealpha/util/Utility.java @@ -1,89 +1,13 @@ package com.cylonid.nativealpha.util; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ActivityManager; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.content.res.Configuration; -import android.graphics.Color; -import android.net.Uri; -import android.os.Build; -import android.util.Log; -import android.util.TypedValue; -import android.view.Gravity; + import android.view.View; import android.view.ViewGroup; import android.webkit.URLUtil; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.AttrRes; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.res.ResourcesCompat; - -import com.cylonid.nativealpha.BuildConfig; -import com.cylonid.nativealpha.R; -import com.cylonid.nativealpha.WebViewActivity; -import com.cylonid.nativealpha.model.DataManager; -import com.cylonid.nativealpha.model.WebApp; -import com.google.android.material.snackbar.Snackbar; - -import java.io.File; -import java.io.FileWriter; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Calendar; -import java.util.List; -import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class Utility { - private static final String TAG = "XXX"; - - public static void killWebSandbox(int id) { - ActivityManager activityManager = - (ActivityManager) App.getAppContext().getSystemService(Context.ACTIVITY_SERVICE); - - for (ActivityManager.RunningAppProcessInfo processInfo : activityManager.getRunningAppProcesses()) { - if (processInfo.processName.contains("web_sandbox_" + id)) { - android.os.Process.killProcess(processInfo.pid); - } - } - } - - public static void deleteShortcuts(List removableWebAppIds) { - ShortcutManager manager = App.getAppContext().getSystemService(ShortcutManager.class); - for (ShortcutInfo info : manager.getPinnedShortcuts()) { - int id = info.getIntent().getIntExtra(Const.INTENT_WEBAPPID, -1); - if (removableWebAppIds.contains(id)) { - manager.disableShortcuts(Arrays.asList(info.getId()), App.getAppContext().getString(R.string.webapp_already_deleted)); - } - } - } - - public static void showToast(Activity a, String text) { - showToast(a, text, Toast.LENGTH_LONG); - } - - public static void showToast(Activity a, String text, int toastDisplayDuration) { - Toast toast = Toast.makeText(a, text, toastDisplayDuration); - toast.setGravity(Gravity.TOP, 0, 100); - toast.show(); - } public static void setViewAndChildrenEnabled(View view, boolean enabled) { @@ -104,56 +28,6 @@ public static void setViewAndChildrenEnabled(View view, boolean enabled) { } } - public static Long getTimeInSeconds() - { - return System.currentTimeMillis() / 1000; - } - - @SuppressLint("SimpleDateFormat") - public static SimpleDateFormat getHourMinFormat() { - return new SimpleDateFormat("HH:mm"); - } - @SuppressLint("SimpleDateFormat") - public static SimpleDateFormat getDayHourMinuteSecondsFormat() { - return new SimpleDateFormat( "EEE, d MMM yyyy HH:mm:ss Z"); - } - - - - public static Calendar convertStringToCalendar(String str) { - Calendar c = Calendar.getInstance(); - try { - c.setTime(Objects.requireNonNull(getHourMinFormat().parse(str))); - } catch (Exception e) { - e.printStackTrace(); - } - return c; - } - - public static boolean isInInterval(Calendar low, Calendar time, Calendar high) { - //Bring timestamp with day_current + HH:mm => day_unixZero + HH:mm by parsing it again... - Calendar middle = Calendar.getInstance(); - try { - middle.setTime(Objects.requireNonNull(getHourMinFormat().parse(Utility.getHourMinFormat().format(time.getTime())))); - } catch (ParseException e) { - e.printStackTrace(); - } - - //CASE: If the end of our timespan is after midnight, add one day to the end date to get a proper span. - if (high.before(low)) { - high.add(Calendar.DATE, 1); - if (middle.before(low)) { - middle.add(Calendar.DATE, 1); - } - } - return middle.after(low) && middle.before(high); - } -// System.out.println("Low: " + Utility.getDayHourMinuteSecondsFormat().format(low.getTime())); -// System.out.println("Middle: " + Utility.getDayHourMinuteSecondsFormat().format(middle.getTime())); -// System.out.println("High: " + Utility.getDayHourMinuteSecondsFormat().format(high.getTime())); -// System.out.println("Is Before high: " + (middle.before(high))); -// System.out.println("Is after low: " + (middle.after(low))); - public static void Assert(boolean condition, String message) { @@ -162,88 +36,6 @@ public static void Assert(boolean condition, String message) { } } - public static Integer getWidthFromIcon(String size_string) { - int x_index = size_string.indexOf("x"); - if (x_index == -1) - x_index = size_string.indexOf("×"); - - if (x_index == -1) - return 1; - String width = size_string.substring(0, x_index); - - return Integer.parseInt(width); - } - - - public static void applyUITheme() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - int id = DataManager.getInstance().getSettings().getThemeId(); - switch (id) { - case 0: - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - break; - case 1: - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); - break; - case 2: - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); - break; - } - } - } - - public static boolean isNightMode(Context context) { - int nightModeFlags = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - return nightModeFlags == Configuration.UI_MODE_NIGHT_YES; - } - - public static void writeFileOnInternalStorage(Context mcoContext, String sFileName, String sBody){ - - try { - File gpxfile = new File(mcoContext.getExternalFilesDir(null), sFileName); - FileWriter writer = new FileWriter(gpxfile); - writer.append(sBody); - writer.flush(); - writer.close(); - } catch (Exception e){ - e.printStackTrace(); - } - } - - public static void showInfoSnackbar(AppCompatActivity activity, String msg, int duration) { - - Snackbar snackbar = Snackbar.make(activity.findViewById(android.R.id.content), msg, duration); - - snackbar.setAction(App.getAppContext().getString(android.R.string.ok), v -> snackbar.dismiss()); - - View snackBarView = snackbar.getView(); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - - params.setMargins(0, 30, 0, 20); - - - snackBarView.setLayoutParams(params); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - snackBarView.setForceDarkAllowed(false); - - TextView tv = snackBarView.findViewById(com.google.android.material.R.id.snackbar_text); - tv.setMaxLines(10); - snackbar.setBackgroundTint(ResourcesCompat.getColor(App.getAppContext().getResources(), R.color.snackbar_background, null)); - snackbar.setTextColor(Color.BLACK); - snackbar.show(); - - } - - public static boolean URLEqual(String left, String right) { - if (left == null || right == null) - return false; - String stripped_left = left.replace("/", "").replace("www.", ""); - String stripped_right = right.replace("/", "").replace("www.", ""); - return stripped_left.equals(stripped_right); - } - public static String getFileNameFromDownload(String url, String content_disposition, String mime_type) { String file_name = null; if (content_disposition != null && !content_disposition.equals("")) { @@ -258,28 +50,4 @@ public static String getFileNameFromDownload(String url, String content_disposit return file_name; } - @ColorInt - public static int getThemeColor - ( - @NonNull final Context context, - @AttrRes final int attributeColor - ) - { - final TypedValue value = new TypedValue(); - context.getTheme ().resolveAttribute (attributeColor, value, true); - return value.data; - } - - public static String getProcessName(Context context) { - if (context == null) return null; - ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) { - if (processInfo.pid == android.os.Process.myPid()) { - return processInfo.processName; - } - } - return null; - } - - } diff --git a/app/src/main/java/com/cylonid/nativealpha/util/WebViewLauncher.kt b/app/src/main/java/com/cylonid/nativealpha/util/WebViewLauncher.kt index c8105365..ec0bc83c 100644 --- a/app/src/main/java/com/cylonid/nativealpha/util/WebViewLauncher.kt +++ b/app/src/main/java/com/cylonid/nativealpha/util/WebViewLauncher.kt @@ -1,5 +1,6 @@ package com.cylonid.nativealpha.util +import android.app.Activity import android.content.Context import com.cylonid.nativealpha.model.WebApp import androidx.appcompat.app.AppCompatActivity @@ -16,7 +17,7 @@ object WebViewLauncher { try { c.startActivity(createWebViewIntent(webapp, c)) } catch (e: NullPointerException) { - Utility.showInfoSnackbar( + NotificationUtils.showInfoSnackbar( c as AppCompatActivity, c.getString(R.string.webview_activity_launch_failed), Snackbar.LENGTH_LONG @@ -26,13 +27,13 @@ object WebViewLauncher { } @JvmStatic - fun startWebViewInNewProcess(webapp: WebApp, c: Context) { + fun startWebViewInNewProcess(webapp: WebApp, a: Activity) { try { - ProcessPhoenix.triggerRebirth(c, createWebViewIntent(webapp, c)) + ProcessPhoenix.triggerRebirth(a, createWebViewIntent(webapp, a)) } catch (e: NullPointerException) { - Utility.showInfoSnackbar( - c as AppCompatActivity, - c.getString(R.string.webview_activity_launch_failed), + NotificationUtils.showInfoSnackbar( + a, + a.getString(R.string.webview_activity_launch_failed), Snackbar.LENGTH_LONG ) e.printStackTrace() diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/activities/AdblockConfigActivity.kt b/app/src/main/kotlin/com/cylonid/nativealpha/activities/AdblockConfigActivity.kt new file mode 100644 index 00000000..fdcfc249 --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/activities/AdblockConfigActivity.kt @@ -0,0 +1,98 @@ +package com.cylonid.nativealpha.activities + +import android.app.ActivityManager +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.cylonid.nativealpha.R +import com.cylonid.nativealpha.databinding.AdblockConfigActivityBinding +import com.cylonid.nativealpha.databinding.AddAdblockConfigDialogBinding +import com.cylonid.nativealpha.fragments.adblocklist.AdblockListFragment +import com.cylonid.nativealpha.model.AdblockConfig +import com.cylonid.nativealpha.model.DataManager +import com.cylonid.nativealpha.util.Const +import com.cylonid.nativealpha.util.NotificationUtils +import com.cylonid.nativealpha.util.ProcessUtils + + +class AdblockConfigActivity : ToolbarBaseActivity() { + private lateinit var adblockListFragment: AdblockListFragment + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding.adblockFab.setOnClickListener { showAddAdblockDialog() } + + binding.btnRestoreDefault.setOnClickListener { + DataManager.getInstance().apply { + settings.globalWebApp.adBlockSettings = Const.getDefaultAdBlockConfig() + saveGlobalSettings() + } + ProcessUtils.closeAllWebAppsAndProcesses(getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) + updateAdblockList() + } + + setToolbarTitle(getString(R.string.adblock_config)) + + adblockListFragment = + supportFragmentManager.findFragmentById(R.id.adblock_fragment_container_view) as AdblockListFragment + } + + override fun inflateBinding(layoutInflater: LayoutInflater): AdblockConfigActivityBinding { + return AdblockConfigActivityBinding.inflate(layoutInflater) + } + + private fun updateAdblockList() { + adblockListFragment.updateAdblockList() + } + + private fun showAddAdblockDialog() { + val localBinding = AddAdblockConfigDialogBinding.inflate(layoutInflater) + val dialog = AlertDialog.Builder(this) + .setView(localBinding.root) + .setTitle(getString(R.string.add_a_new_adblock_provider)) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> + + val url = localBinding.addAdblockUrl.text.toString().trim() + + val formattedUrl = + if (url.startsWith("https://") || url.startsWith("http://")) url else "https://$url" + + val urlAlreadyExists = DataManager.getInstance().settings.globalWebApp.adBlockSettings.any { it.value == formattedUrl} + if(urlAlreadyExists) { + NotificationUtils.showToast(this, getString(R.string.entry_already_exists)) + return@setPositiveButton + } + + DataManager.getInstance().apply { + val label = + if (localBinding.addAdblockLabel.text.isNotEmpty()) localBinding.addAdblockLabel.text.toString() else url + settings.globalWebApp.adBlockSettings += AdblockConfig(label, formattedUrl) + saveGlobalSettings() + } + updateAdblockList() + } + .setNegativeButton(android.R.string.cancel, null) + .create() + dialog.show() + + val okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + okButton.isEnabled = false + localBinding.addAdblockUrl.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + okButton.isEnabled = !s.isNullOrBlank() + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + } +} + + diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/activities/NewsActivity.kt b/app/src/main/kotlin/com/cylonid/nativealpha/activities/NewsActivity.kt index d58b7f08..3f645301 100644 --- a/app/src/main/kotlin/com/cylonid/nativealpha/activities/NewsActivity.kt +++ b/app/src/main/kotlin/com/cylonid/nativealpha/activities/NewsActivity.kt @@ -1,138 +1,78 @@ package com.cylonid.nativealpha.activities; +import android.annotation.SuppressLint import android.content.Intent -import android.graphics.drawable.Drawable import android.os.Bundle -import android.util.Log import android.view.MotionEvent import android.view.View -import android.view.ViewTreeObserver -import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.webkit.JavascriptInterface +import android.webkit.WebChromeClient import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import android.widget.Toast -import androidx.annotation.ColorInt + +import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import com.cylonid.nativealpha.BuildConfig -import com.cylonid.nativealpha.R +import com.cylonid.nativealpha.databinding.NewsActivityBinding import com.cylonid.nativealpha.model.DataManager import com.cylonid.nativealpha.util.LocaleUtils -import com.cylonid.nativealpha.util.Utility -import kotlinx.android.synthetic.main.news_activity.* -import kotlin.properties.Delegates - - -class NewsActivity : AppCompatActivity(), View.OnTouchListener, ViewTreeObserver.OnScrollChangedListener { - - var btnDefaultBackgroundColor: Drawable? = null - var btnDefaultTextColor: Int = android.R.color.black - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.news_activity) - - initializeUI() - setButtonState() - } - override fun onBackPressed() {} +class NewsActivity : AppCompatActivity(), View.OnTouchListener { - private fun disableAcceptButton() { - btnNewsConfirm.isActivated = false - btnDefaultTextColor = btnNewsConfirm.currentTextColor - btnDefaultBackgroundColor = btnNewsConfirm.background + private lateinit var binding: NewsActivityBinding - btnNewsConfirm.setBackgroundColor( - ContextCompat.getColor( - baseContext, - R.color.disabled_background_color - ) - ) - btnNewsConfirm.setTextColor( - ContextCompat.getColor( - baseContext, - R.color.disabled_text_color - ) - ) - btnNewsConfirm.setOnClickListener { - Utility.showToast( - this, - getString(R.string.scroll_to_bottom), - Toast.LENGTH_SHORT - ) + inner class WebAppInterface { + @JavascriptInterface + fun onOkButtonPressed() { + DataManager.getInstance().eulaData = true + DataManager.getInstance().lastShownUpdate = BuildConfig.VERSION_CODE + finish() } } - private fun enableAcceptButton() { - btnNewsConfirm.isActivated = true - btnNewsConfirm.setTextColor(btnDefaultTextColor) - btnNewsConfirm.background = btnDefaultBackgroundColor + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = NewsActivityBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) - btnNewsConfirm.setOnClickListener { confirm() } - } + binding.newsContent.settings.javaScriptEnabled = true + binding.newsContent.isLongClickable = false + binding.newsContent.webChromeClient = WebChromeClient() - private fun initializeUI() { + onBackPressedDispatcher.addCallback(this) {} setText() - btnNewsConfirm.setOnClickListener { - confirm() - } - } - - private fun setButtonState() { - val vto: ViewTreeObserver = news_scrollchild.viewTreeObserver - vto.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { - override fun onGlobalLayout() { - val height: Int = news_scrollchild.measuredHeight - if (height > 0) { - news_scrollchild.viewTreeObserver.removeOnGlobalLayoutListener(this) - if (news_scrollview.canScrollVertically(1) || news_scrollview.canScrollVertically(-1)) { - news_scrollview.setOnTouchListener(this@NewsActivity) - news_scrollview.viewTreeObserver.addOnScrollChangedListener(this@NewsActivity) - disableAcceptButton() - } - } - } - }) } private fun setText() { val fileId = intent.extras?.getString("text") ?: "latestUpdate" - news_content.loadUrl("file:///android_asset/news/" + fileId + "_" + LocaleUtils.fileEnding +".html") - if(DataManager.getInstance().eulaData) { - btnNewsConfirm.isEnabled = true - news_content.settings.javaScriptEnabled = true - news_content.webViewClient = NewsWebViewClient() - } - } + binding.newsContent.loadUrl("file:///android_asset/news/" + fileId + "_" + LocaleUtils.fileEnding + ".html") + binding.newsContent.addJavascriptInterface(WebAppInterface(), "NAlpha") + val hideEula = DataManager.getInstance().eulaData; - private fun confirm() { - DataManager.getInstance().eulaData = true - DataManager.getInstance().lastShownUpdate = BuildConfig.VERSION_CODE - finish() + binding.newsContent.webViewClient = NewsWebViewClient( + hideEula = hideEula, + showLiberaPay = BuildConfig.FLAVOR == "extendedGithub" + ) } override fun onTouch(p0: View?, p1: MotionEvent?): Boolean { return false } - override fun onScrollChanged() { - val view = news_scrollview.getChildAt(news_scrollview.childCount - 1) - val bottomDetector: Int = view.bottom - (news_scrollview.height + news_scrollview.scrollY) - - if (bottomDetector < 30) { - enableAcceptButton() - } - } } -private class NewsWebViewClient : WebViewClient() { +private class NewsWebViewClient(val hideEula: Boolean, val showLiberaPay: Boolean) : + WebViewClient() { override fun onPageFinished(view: WebView, url: String) { - view.evaluateJavascript("hideById('eula')", null) - view.settings.javaScriptEnabled = false + if (hideEula) view.evaluateJavascript("hideById('eula')", null) + if (showLiberaPay) { + view.evaluateJavascript("showById('nonGp')", null) + } } override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/activities/ToolbarBaseActivity.kt b/app/src/main/kotlin/com/cylonid/nativealpha/activities/ToolbarBaseActivity.kt new file mode 100644 index 00000000..2c7d2f3b --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/activities/ToolbarBaseActivity.kt @@ -0,0 +1,48 @@ +package com.cylonid.nativealpha.activities + +import android.os.Bundle +import android.view.LayoutInflater +import androidx.activity.addCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.viewbinding.ViewBinding +import com.cylonid.nativealpha.databinding.ActivityToolbarBaseBinding + +abstract class ToolbarBaseActivity : AppCompatActivity() { + + private lateinit var _binding: VB + protected val binding get() = _binding + + private var onNavigationClickListener: (() -> Unit)? = null + + abstract fun inflateBinding(layoutInflater: LayoutInflater): VB + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val baseBinding = ActivityToolbarBaseBinding.inflate(layoutInflater) + setContentView(baseBinding.root) + + _binding = inflateBinding(layoutInflater) + baseBinding.activityContent.addView(_binding.root) + + val toolbar = baseBinding.toolbar.topAppBar + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + onBackPressedDispatcher.addCallback(this) { + finish() + } + + toolbar.setNavigationOnClickListener { + onNavigationClickListener?.invoke() ?: onBackPressedDispatcher.onBackPressed() + } + } + + fun setToolbarTitle(title: String) { + supportActionBar?.title = title + } + + fun setNavigationClickListener(listener: () -> Unit) { + onNavigationClickListener = listener + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/fragments/adblocklist/AdblockListAdapter.kt b/app/src/main/kotlin/com/cylonid/nativealpha/fragments/adblocklist/AdblockListAdapter.kt new file mode 100644 index 00000000..6dc147b8 --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/fragments/adblocklist/AdblockListAdapter.kt @@ -0,0 +1,39 @@ +package com.cylonid.nativealpha.fragments.adblocklist + +import android.app.Activity + +import android.view.View +import android.widget.TextView +import androidx.annotation.StringRes +import com.cylonid.nativealpha.R +import com.cylonid.nativealpha.model.AdblockConfig +import com.cylonid.nativealpha.model.DataManager +import com.ernestoyaquello.dragdropswiperecyclerview.DragDropSwipeAdapter + +class AdblockListAdapter(dataSet: List) + : DragDropSwipeAdapter(dataSet) { + + class ViewHolder(webAppLayout: View) : DragDropSwipeAdapter.ViewHolder(webAppLayout) { + + val titleView: TextView = itemView.findViewById(R.id.adblock_title) + val subtitleView: TextView = itemView.findViewById(R.id.adblock_subtitle) + } + + override fun getViewHolder(itemView: View) = ViewHolder(itemView) + override fun onBindViewHolder(item: AdblockConfig, viewHolder: ViewHolder, position: Int) { + viewHolder.titleView.text = item.label + viewHolder.subtitleView.text = item.value + + } + + override fun canBeDragged(item: AdblockConfig, viewHolder: ViewHolder, position: Int): Boolean { + return false + } + + fun updateAdblockList() { + dataSet = DataManager.getInstance().settings.globalWebApp.adBlockSettings + } + + override fun getViewToTouchToStartDraggingItem(item: AdblockConfig, viewHolder: ViewHolder, position: Int) = viewHolder.titleView + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/fragments/adblocklist/AdblockListFragment.kt b/app/src/main/kotlin/com/cylonid/nativealpha/fragments/adblocklist/AdblockListFragment.kt new file mode 100644 index 00000000..63044169 --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/fragments/adblocklist/AdblockListFragment.kt @@ -0,0 +1,89 @@ +package com.cylonid.nativealpha.fragments.adblocklist + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.LinearLayoutManager +import com.cylonid.nativealpha.R +import com.cylonid.nativealpha.model.AdblockConfig +import com.cylonid.nativealpha.model.DataManager +import com.ernestoyaquello.dragdropswiperecyclerview.DragDropSwipeRecyclerView +import com.ernestoyaquello.dragdropswiperecyclerview.listener.OnItemSwipeListener +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.snackbar.Snackbar + +class AdblockListFragment : Fragment(R.layout.fragment_adblock_list) { + private lateinit var adapter: AdblockListAdapter + private lateinit var list: DragDropSwipeRecyclerView + + private var fab: FloatingActionButton? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val globalWebApp = DataManager.getInstance().settings.globalWebApp + adapter = AdblockListAdapter(globalWebApp.adBlockSettings) + + fab = requireActivity().findViewById(R.id.adblock_fab) + checkFabEnabledStateIfNecessary() + + list = view.findViewById(R.id.adblock_list) + list.layoutManager = LinearLayoutManager(requiredActivity()) + list.adapter = adapter + list.swipeListener = onItemSwipeListener + list.orientation = + DragDropSwipeRecyclerView.ListOrientation.VERTICAL_LIST_WITH_VERTICAL_DRAGGING + list.disableSwipeDirection(DragDropSwipeRecyclerView.ListOrientation.DirectionFlag.RIGHT) + + } + + + fun updateAdblockList() { + adapter.updateAdblockList() + checkFabEnabledStateIfNecessary() + } + + private fun checkFabEnabledStateIfNecessary() { + if(fab != null && adapter.itemCount >= 8) { + fab?.isEnabled = false + } else { + fab?.isEnabled = true + } + } + + private fun requiredActivity(): FragmentActivity { + return requireNotNull(activity) { "AdblockListFragment is not attached to an activity." } + } + + private val onItemSwipeListener = object : OnItemSwipeListener { + + override fun onItemSwiped( + position: Int, + direction: OnItemSwipeListener.SwipeDirection, + item: AdblockConfig + ): Boolean { + + DataManager.getInstance().apply { + settings.globalWebApp.adBlockSettings.removeAt(position) + saveGlobalSettings() + } + updateAdblockList() + checkFabEnabledStateIfNecessary() + + val itemSwipedSnackBar = + view?.let { Snackbar.make(it, getString(R.string.x_was_removed, item.label), Snackbar.LENGTH_SHORT) } + itemSwipedSnackBar?.setAction(getString(R.string.undo).uppercase()) { + DataManager.getInstance().apply { + settings.globalWebApp.adBlockSettings.add(position, item) + saveGlobalSettings() + } + updateAdblockList() + } + itemSwipedSnackBar?.show() + + return true + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/fragments/webapplist/WebAppListAdapter.kt b/app/src/main/kotlin/com/cylonid/nativealpha/fragments/webapplist/WebAppListAdapter.kt new file mode 100644 index 00000000..e257a52d --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/fragments/webapplist/WebAppListAdapter.kt @@ -0,0 +1,55 @@ +package com.cylonid.nativealpha.fragments.webapplist + +import android.app.Activity +import android.content.DialogInterface +import android.content.Intent +import android.transition.Visibility +import android.view.View +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat.startActivity +import com.cylonid.nativealpha.R +import com.cylonid.nativealpha.WebAppSettingsActivity +import com.cylonid.nativealpha.model.DataManager +import com.cylonid.nativealpha.model.WebApp +import com.cylonid.nativealpha.util.Const +import com.cylonid.nativealpha.util.WebViewLauncher.startWebView +import com.ernestoyaquello.dragdropswiperecyclerview.DragDropSwipeAdapter +import com.google.android.material.internal.VisibilityAwareImageButton +import java.util.ArrayList + +class WebAppListAdapter(dataSet: List = emptyList(), private val activityOfFragment: Activity) + : DragDropSwipeAdapter(dataSet) { + + class ViewHolder(webAppLayout: View) : DragDropSwipeAdapter.ViewHolder(webAppLayout) { + val dragAnchor : ImageView = itemView.findViewById(R.id.dragAnchor) + val titleView: TextView = itemView.findViewById(R.id.btnWebAppTitle) + + } + + override fun getViewHolder(itemView: View) = ViewHolder(itemView) + override fun onBindViewHolder(item: WebApp, viewHolder: ViewHolder, position: Int) { + viewHolder.titleView.text = item.title + viewHolder.titleView.setOnClickListener { + openWebView( + item + ) + } + } + + fun updateWebAppList() { + dataSet = DataManager.getInstance().activeWebsites + } + + + private fun openWebView(webapp: WebApp) { + startWebView(webapp, activityOfFragment) + } + + + override fun getViewToTouchToStartDraggingItem(item: WebApp, viewHolder: ViewHolder, position: Int) = viewHolder.dragAnchor + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/fragments/webapplist/WebAppListFragment.kt b/app/src/main/kotlin/com/cylonid/nativealpha/fragments/webapplist/WebAppListFragment.kt new file mode 100644 index 00000000..f2b74f6d --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/fragments/webapplist/WebAppListFragment.kt @@ -0,0 +1,104 @@ +package com.cylonid.nativealpha.fragments.webapplist + +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.LinearLayoutManager +import com.cylonid.nativealpha.R +import com.cylonid.nativealpha.WebAppSettingsActivity +import com.cylonid.nativealpha.model.AdblockConfig +import com.cylonid.nativealpha.model.DataManager +import com.cylonid.nativealpha.model.WebApp +import com.cylonid.nativealpha.util.Const +import com.ernestoyaquello.dragdropswiperecyclerview.DragDropSwipeRecyclerView +import com.ernestoyaquello.dragdropswiperecyclerview.listener.OnItemDragListener +import com.ernestoyaquello.dragdropswiperecyclerview.listener.OnItemSwipeListener +import com.google.android.material.snackbar.Snackbar + +class WebAppListFragment : Fragment(R.layout.fragment_web_app_list) { + private lateinit var adapter: WebAppListAdapter + + private lateinit var list: DragDropSwipeRecyclerView + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + adapter = WebAppListAdapter(DataManager.getInstance().activeWebsites, requiredActivity()) + + list = view.findViewById(R.id.web_app_list) + list.layoutManager = LinearLayoutManager(requiredActivity()) + list.adapter = adapter + list.orientation = DragDropSwipeRecyclerView.ListOrientation.VERTICAL_LIST_WITH_VERTICAL_DRAGGING + list.dragListener = onItemDragListener + list.swipeListener = onItemSwipeListener + } + + fun updateWebAppList() { + adapter.updateWebAppList() + } + + private fun requiredActivity(): FragmentActivity { + return requireNotNull(activity) { "WebAppListFragment is not attached to an activity." } + } + + private val onItemSwipeListener = object : OnItemSwipeListener { + override fun onItemSwiped( + position: Int, + direction: OnItemSwipeListener.SwipeDirection, + item: WebApp + ): Boolean { + if(direction == OnItemSwipeListener.SwipeDirection.RIGHT_TO_LEFT) { + item.markInactive(requiredActivity()) + saveCurrentDisplayedOrderOfWebAppsToDisk() + + val itemSwipedSnackBar = + view?.let { Snackbar.make(it, getString(R.string.x_was_removed, item.title), Snackbar.LENGTH_SHORT) } + itemSwipedSnackBar?.setAction(getString(R.string.undo).uppercase()) { + item.isActiveEntry = true + DataManager.getInstance().saveWebAppData() + updateWebAppList() + } + itemSwipedSnackBar?.show() + } + if(direction == OnItemSwipeListener.SwipeDirection.LEFT_TO_RIGHT) { + val intent = Intent( + activity, + WebAppSettingsActivity::class.java + ) + intent.putExtra(Const.INTENT_WEBAPPID, item.ID) + intent.setAction(Intent.ACTION_VIEW) + context?.let { ContextCompat.startActivity(it, intent, null) } + return true + } + return false + } + } + + private val onItemDragListener = object : OnItemDragListener { + + override fun onItemDropped(initialPosition: Int, finalPosition: Int, item: WebApp) { + saveCurrentDisplayedOrderOfWebAppsToDisk() + + } + + override fun onItemDragged(previousPosition: Int, newPosition: Int, item: WebApp) { + } + } + + private fun saveCurrentDisplayedOrderOfWebAppsToDisk() { + for ((i, webapp) in adapter.dataSet.withIndex()) { + // Do not use "i" as index here, since adapter.dataSet includes only active website. + // The DataManager's websites array contains both active and inactive websites. + DataManager.getInstance().websites[webapp.ID].order = i + } + DataManager.getInstance().saveWebAppData() + } + companion object { + fun newInstance() = WebAppListFragment() + } +} diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/helper/AdblockLifecycleHelper.kt b/app/src/main/kotlin/com/cylonid/nativealpha/helper/AdblockLifecycleHelper.kt new file mode 100644 index 00000000..5d52ea2c --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/helper/AdblockLifecycleHelper.kt @@ -0,0 +1,51 @@ +package com.cylonid.nativealpha.helper + + +import android.app.Activity +import android.content.DialogInterface +import androidx.appcompat.app.AlertDialog +import com.cylonid.nativealpha.R +import com.cylonid.nativealpha.model.DataManager + +class AdblockLifecycleHelper(private val activity: Activity) { + + fun trySyncOperation(callback: AdblockLifecycleCallback) { + beforeAdblockOperation(callback) + afterAdblockOperation() + } + + fun beforeAdblockOperation(callback: AdblockLifecycleCallback) { + if(DataManager.getInstance().hasAdblockCrashed) { + showWarningDialog() + DataManager.getInstance().hasAdblockCrashed = false + return + } + DataManager.getInstance().hasAdblockCrashed = true + callback.execute() + } + + fun afterAdblockOperation() { + DataManager.getInstance().hasAdblockCrashed = false + } + + private fun showWarningDialog() { + AlertDialog.Builder(activity) + .setMessage(activity.getString(R.string.adblock_warning_text)) + .setCancelable(false) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(activity.getString(R.string.warning)) + .setPositiveButton(activity.getString(R.string.ok)) { _: DialogInterface?, _: Int -> + DataManager.getInstance().apply { + settings.globalWebApp.adBlockSettings.clear() + saveGlobalSettings() + } + } + .setNegativeButton(activity.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> } + .create().show() + } + + fun interface AdblockLifecycleCallback { + fun execute() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/helper/AdblockProviderApiHelper.kt b/app/src/main/kotlin/com/cylonid/nativealpha/helper/AdblockProviderApiHelper.kt new file mode 100644 index 00000000..9dfdc449 --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/helper/AdblockProviderApiHelper.kt @@ -0,0 +1,37 @@ +package com.cylonid.nativealpha.helper + +import com.cylonid.nativealpha.model.AdblockConfig +import com.cylonid.nativealpha.util.DateUtils +import io.github.edsuns.adfilter.AdFilter +import io.github.edsuns.adfilter.Filter + +internal class AdblockProviderApiHelper(private val adFilterProvider: AdFilter) { + + fun synchronizeAdblockProviderWithSettings(settings: List) { + val map = transformToMapWithUrlKey(adFilterProvider.viewModel.filters.value ?: emptyMap()) + for (config: AdblockConfig in settings) { + var setFilter = map[config.value] + if (setFilter == null) { + setFilter = adFilterProvider.viewModel.addFilter(config.label, config.value) + adFilterProvider.viewModel.download(setFilter.id) + } + if (DateUtils.isOlderThanDays(setFilter.updateTime, 10)) { + adFilterProvider.viewModel.download(setFilter.id) + } + } + for ((_, filter) in map) { + val existingConfig = settings.find { it.value == filter.url } + if(existingConfig == null) { + adFilterProvider.viewModel.removeFilter(filter.id) + } + } + } + + private fun transformToMapWithUrlKey(originalMap: Map): Map { + val urlBasedMap: HashMap = HashMap() + for ((_, value) in originalMap) { + urlBasedMap[value.url] = value + } + return urlBasedMap + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/helper/BiometricPromptHelper.kt b/app/src/main/kotlin/com/cylonid/nativealpha/helper/BiometricPromptHelper.kt index 4b6fed6d..cd766466 100644 --- a/app/src/main/kotlin/com/cylonid/nativealpha/helper/BiometricPromptHelper.kt +++ b/app/src/main/kotlin/com/cylonid/nativealpha/helper/BiometricPromptHelper.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.FragmentActivity import androidx.core.content.ContextCompat import androidx.biometric.BiometricPrompt.PromptInfo import com.cylonid.nativealpha.R +import com.cylonid.nativealpha.util.NotificationUtils import com.cylonid.nativealpha.util.Utility import com.google.android.material.snackbar.Snackbar @@ -49,10 +50,10 @@ internal class BiometricPromptHelper(private val activity: FragmentActivity) { isSupported = true } BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { - Utility.showInfoSnackbar(activity as AppCompatActivity?, activity.getString(R.string.no_biometric_keys_enrolled), Snackbar.LENGTH_LONG); + NotificationUtils.showInfoSnackbar(activity, activity.getString(R.string.no_biometric_keys_enrolled), Snackbar.LENGTH_LONG); } else -> { - Utility.showInfoSnackbar(activity as AppCompatActivity?, activity.getString(R.string.no_biometric_devices), Snackbar.LENGTH_LONG); + NotificationUtils.showInfoSnackbar(activity, activity.getString(R.string.no_biometric_devices), Snackbar.LENGTH_LONG); } } return isSupported diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/helper/IconPopupMenuHelper.kt b/app/src/main/kotlin/com/cylonid/nativealpha/helper/IconPopupMenuHelper.kt index e07bf966..c4a488ee 100644 --- a/app/src/main/kotlin/com/cylonid/nativealpha/helper/IconPopupMenuHelper.kt +++ b/app/src/main/kotlin/com/cylonid/nativealpha/helper/IconPopupMenuHelper.kt @@ -1,6 +1,7 @@ package com.cylonid.nativealpha.helper import android.content.Context +import android.os.Build import android.view.Gravity import android.view.View import android.widget.PopupMenu @@ -11,7 +12,9 @@ object IconPopupMenuHelper { @JvmStatic fun getMenu(v: View, @MenuRes menuRes: Int, c: Context): PopupMenu { val popup = PopupMenu(c, v, Gravity.END) - popup.setForceShowIcon(true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + popup.setForceShowIcon(true) + } popup.menuInflater.inflate(menuRes, popup.menu) return popup diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/model/WebApp.kt b/app/src/main/kotlin/com/cylonid/nativealpha/model/WebApp.kt index d43684c6..5e9072e2 100644 --- a/app/src/main/kotlin/com/cylonid/nativealpha/model/WebApp.kt +++ b/app/src/main/kotlin/com/cylonid/nativealpha/model/WebApp.kt @@ -1,18 +1,21 @@ package com.cylonid.nativealpha.model +import android.app.Activity import android.view.View import android.widget.* +import androidx.appcompat.widget.SwitchCompat import androidx.fragment.app.FragmentActivity import com.cylonid.nativealpha.R import com.cylonid.nativealpha.WebAppSettingsActivity import com.cylonid.nativealpha.helper.BiometricPromptHelper import com.cylonid.nativealpha.util.Const +import com.cylonid.nativealpha.util.ShortcutIconUtils import com.cylonid.nativealpha.util.Utility import java.util.* -class WebApp { - val ID: Int - var baseUrl: String +data class AdblockConfig(val label: String, val value: String) + +data class WebApp(var baseUrl: String, val ID: Int) { var title: String var isActiveEntry = true var isOverrideGlobalSettings = true @@ -51,24 +54,33 @@ class WebApp { var isEnableZooming = false var isBiometricProtection = false var isAllowMediaPlaybackInBackground = false + var order = 0 + var alwaysUseFallbackContextMenu = false + var adBlockSettings = mutableListOf() - constructor(url: String, id: Int) { - title = url.replace("http://", "").replace("https://", "").replace("www.", "") - baseUrl = url - this.ID = id + init { + title = baseUrl.replace("http://", "").replace("https://", "").replace("www.", "") initDefaultSettings() } - constructor(other: WebApp) { + constructor(baseUrl: String, ID: Int, order: Int): this(baseUrl, ID) { + this.order = order + } + + constructor(baseUrl: String, ID: Int, adBlockSettings: MutableList): this(baseUrl, ID) { + this.adBlockSettings = adBlockSettings + } + + constructor(other: WebApp) : this(other.baseUrl, other.ID) { title = other.title - ID = other.ID - baseUrl = other.baseUrl isOverrideGlobalSettings = other.isOverrideGlobalSettings containerId = other.containerId isUseContainer = other.isUseContainer copySettings(other) } + + //This part of the copy ctor should be callable independently from actual object construction to copy values of the global web app template fun copySettings(other: WebApp) { isOpenUrlExternal = other.isOpenUrlExternal @@ -104,6 +116,9 @@ class WebApp { isEnableZooming = other.isEnableZooming isBiometricProtection = other.isBiometricProtection isAllowMediaPlaybackInBackground = other.isAllowMediaPlaybackInBackground + order = other.order + alwaysUseFallbackContextMenu = other.alwaysUseFallbackContextMenu + adBlockSettings = other.adBlockSettings } private fun initDefaultSettings() { @@ -121,9 +136,12 @@ class WebApp { isOverrideGlobalSettings = false } - fun markInactive() { + fun markInactive(activity: Activity) { isActiveEntry = false - Utility.deleteShortcuts(Arrays.asList(ID)) + ShortcutIconUtils.deleteShortcuts( + listOf(ID), + activity + ) } @@ -131,18 +149,18 @@ class WebApp { get() = baseUrl.replace("\\P{Alnum}".toRegex(), "").replace("https", "").replace("http", "").replace("www", "") fun onSwitchCookiesChanged(mSwitch: CompoundButton, isChecked: Boolean) { - val switchThirdPCookies = mSwitch.rootView.findViewById(R.id.switch3PCookies) + val switchThirdPCookies = mSwitch.rootView.findViewById(R.id.switch3PCookies) if (isChecked) switchThirdPCookies.isEnabled = true else { switchThirdPCookies.isEnabled = false switchThirdPCookies.isChecked = false } } - private fun disableSwitchBiometricAccessChangeListener(switchBiometricAccess: Switch) { + private fun disableSwitchBiometricAccessChangeListener(switchBiometricAccess: SwitchCompat) { switchBiometricAccess.setOnCheckedChangeListener(null) } - private fun enableSwitchBiometricAccessChangeListener(switchBiometricAccess: Switch, + private fun enableSwitchBiometricAccessChangeListener(switchBiometricAccess: SwitchCompat, activity: WebAppSettingsActivity) { switchBiometricAccess.setOnCheckedChangeListener { switch, checked -> onSwitchBiometricAccessChanged( @@ -154,7 +172,7 @@ class WebApp { } private fun setSwitchBiometricAccessSilently(newValue: Boolean, - switchBiometricAccess: Switch, + switchBiometricAccess: SwitchCompat, activity: WebAppSettingsActivity) { disableSwitchBiometricAccessChangeListener(switchBiometricAccess) switchBiometricAccess.isChecked = newValue @@ -167,7 +185,7 @@ class WebApp { activity: WebAppSettingsActivity ) { val switchBiometricAccess = - mSwitch.rootView.findViewById(R.id.switchBiometricAccess) + mSwitch.rootView.findViewById(R.id.switchBiometricAccess) // reset to value before user toggled, actual setting of value is done by prompt success callback setSwitchBiometricAccessSilently(!switchBiometricAccess.isChecked, switchBiometricAccess, activity) @@ -191,8 +209,8 @@ class WebApp { } fun onSwitchJsChanged(mSwitch: CompoundButton, isChecked: Boolean) { - val switchDesktopVersion = mSwitch.rootView.findViewById(R.id.switchDesktopSite) - val switchAdblock = mSwitch.rootView.findViewById(R.id.switchAdblock) + val switchDesktopVersion = mSwitch.rootView.findViewById(R.id.switchDesktopSite) + val switchAdblock = mSwitch.rootView.findViewById(R.id.switchAdblock) if (isChecked) { switchDesktopVersion.isEnabled = true switchAdblock.isEnabled = true @@ -205,7 +223,7 @@ class WebApp { } fun onSwitchForceDarkChanged(mSwitch: CompoundButton, isChecked: Boolean) { - val switchLimit = mSwitch.rootView.findViewById(R.id.switchTimeSpanDarkMode) + val switchLimit = mSwitch.rootView.findViewById(R.id.switchTimeSpanDarkMode) val txtBegin = mSwitch.rootView.findViewById(R.id.textDarkModeBegin) val txtEnd = mSwitch.rootView.findViewById(R.id.textDarkModeEnd) if (isChecked) { @@ -240,7 +258,7 @@ class WebApp { fun onSwitchUserAgentChanged(mSwitch: CompoundButton, isChecked: Boolean) { val txt = mSwitch.rootView.findViewById(R.id.textUserAgent) - val switchDesktopVersion = mSwitch.rootView.findViewById(R.id.switchDesktopSite) + val switchDesktopVersion = mSwitch.rootView.findViewById(R.id.switchDesktopSite) if (isChecked) { switchDesktopVersion.isChecked = false switchDesktopVersion.isEnabled = false diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/model/WebAppInstanceCreator.kt b/app/src/main/kotlin/com/cylonid/nativealpha/model/WebAppInstanceCreator.kt deleted file mode 100644 index 7db5849a..00000000 --- a/app/src/main/kotlin/com/cylonid/nativealpha/model/WebAppInstanceCreator.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.cylonid.nativealpha.model - -import com.google.gson.InstanceCreator -import java.lang.reflect.Type - -class WebAppInstanceCreator : InstanceCreator { - override fun createInstance(type: Type): WebApp { - return WebApp("", Int.MAX_VALUE) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/model/deserializer/GlobalSettingsDeserializer.kt b/app/src/main/kotlin/com/cylonid/nativealpha/model/deserializer/GlobalSettingsDeserializer.kt new file mode 100644 index 00000000..c2194cfc --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/model/deserializer/GlobalSettingsDeserializer.kt @@ -0,0 +1,33 @@ +package com.cylonid.nativealpha.model.deserializer + +import android.util.Log +import com.cylonid.nativealpha.model.GlobalSettings +import com.cylonid.nativealpha.model.WebApp +import com.cylonid.nativealpha.util.Const +import com.google.gson.Gson +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import java.lang.NullPointerException +import java.lang.reflect.Type + +class GlobalSettingsDeserializer : JsonDeserializer { + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): GlobalSettings { + try { + val obj = json.asJsonObject + val globalWebApp = + context.deserialize(obj.get("globalWebApp"), WebApp::class.java) + val settings = Gson().fromJson(obj, GlobalSettings::class.java) + settings.globalWebApp = globalWebApp + + return settings + } + catch(e: NullPointerException) { + return GlobalSettings() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/model/deserializer/WebAppDeserializer.kt b/app/src/main/kotlin/com/cylonid/nativealpha/model/deserializer/WebAppDeserializer.kt new file mode 100644 index 00000000..a939d2ab --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/model/deserializer/WebAppDeserializer.kt @@ -0,0 +1,56 @@ +package com.cylonid.nativealpha.model.deserializer + +import com.cylonid.nativealpha.model.AdblockConfig +import com.cylonid.nativealpha.model.WebApp +import com.cylonid.nativealpha.util.Const +import com.google.gson.Gson +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import java.lang.reflect.Type + +class WebAppDeserializer : JsonDeserializer { + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): WebApp { + val obj = json.asJsonObject + val webapp = Gson().fromJson(obj, WebApp::class.java) + patchDataVersion1500(webapp, obj) + return webapp + } + + /** + * With release v1.5.0 (code 1500), we added the array "adBlockSettings" to WebApp. + If the value in JSON string is null, we set an empty array for usual web apps and the default adblock provider for the global web app. + */ + private fun patchDataVersion1500(webapp: WebApp, obj: JsonObject) { + var adblockKeyPresent = false + + val parsedAdblockSettings = + if (obj.has("adBlockSettings") && obj.get("adBlockSettings").isJsonArray) { + val array = obj.getAsJsonArray("adBlockSettings") + adblockKeyPresent = true + array.mapNotNull { item -> + try { + val configObj = item.asJsonObject + val label = configObj.get("label")?.asString ?: return@mapNotNull null + val value = configObj.get("value")?.asString ?: return@mapNotNull null + AdblockConfig(label, value) + } catch (e: Exception) { + null // Skip malformed entries + } + } + } else { + emptyList() + } + + webapp.adBlockSettings = parsedAdblockSettings.toMutableList() + if (webapp.ID == Int.MAX_VALUE && !adblockKeyPresent) { + webapp.adBlockSettings = Const.getDefaultAdBlockConfig() + } + webapp.adBlockSettings = webapp.adBlockSettings.take(8).toMutableList() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/util/ColorUtils.kt b/app/src/main/kotlin/com/cylonid/nativealpha/util/ColorUtils.kt new file mode 100644 index 00000000..a37bcf14 --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/util/ColorUtils.kt @@ -0,0 +1,27 @@ +package com.cylonid.nativealpha.util + +import android.content.Context +import android.util.TypedValue +import androidx.annotation.AttrRes +import androidx.annotation.ColorRes + +object ColorUtils { + + @JvmStatic + @ColorRes + fun getColorResFromThemeAttr(context: Context, @AttrRes resId: Int, @ColorRes fallback: Int): Int { + val typedValue = TypedValue() + val theme = context.theme + var colorResId = fallback + + val success = theme.resolveAttribute( + resId, + typedValue, + true + ) + if (success) { + colorResId = typedValue.resourceId + } + return colorResId + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/util/DateUtils.kt b/app/src/main/kotlin/com/cylonid/nativealpha/util/DateUtils.kt new file mode 100644 index 00000000..224d646c --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/util/DateUtils.kt @@ -0,0 +1,65 @@ +package com.cylonid.nativealpha.util + +import android.annotation.SuppressLint +import java.text.SimpleDateFormat +import java.util.Calendar + +import java.util.Locale +import java.util.Objects.requireNonNull + +object DateUtils { + + @JvmStatic + fun getTimeInSeconds(): Long { + return System.currentTimeMillis() / 1000 + } + + @JvmStatic + @SuppressLint("SimpleDateFormat") + fun getHourMinFormat(): SimpleDateFormat { + return SimpleDateFormat("HH:mm", Locale.getDefault()) + } + + @JvmStatic + @SuppressLint("SimpleDateFormat") + fun getDayHourMinuteSecondsFormat(): SimpleDateFormat { + return SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.getDefault()) + } + + @JvmStatic + fun convertStringToCalendar(str: String?): Calendar? { + if (str.isNullOrBlank()) return null + return try { + val parsedDate = getHourMinFormat().parse(str) + Calendar.getInstance().also { it.time = parsedDate!! } + } catch (e: Exception) { + null + } + } + + @JvmStatic + fun isInInterval(low: Calendar, time: Calendar, high: Calendar): Boolean { + // Bring timestamp with day_current + HH:mm => day_unixZero + HH:mm by parsing it again... + val middle = Calendar.getInstance() + middle.time = requireNonNull( + getHourMinFormat().parse( + getHourMinFormat().format(time.time) + ) + ) + + // CASE: If the end of our timespan is after midnight, add one day to the end date to get a proper span. + if (high.before(low)) { + high.add(Calendar.DATE, 1) + if (middle.before(low)) { + middle.add(Calendar.DATE, 1) + } + } + return middle.after(low) && middle.before(high) + } + + @JvmStatic + fun isOlderThanDays(timestamp: Long, days: Int, targetTime: Long = System.currentTimeMillis()): Boolean { + val daysInMillis = days * 24L * 60 * 60 * 1000 + return (targetTime - timestamp) > daysInMillis + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/util/EntryPointUtils.kt b/app/src/main/kotlin/com/cylonid/nativealpha/util/EntryPointUtils.kt index b14926fb..760cb7db 100644 --- a/app/src/main/kotlin/com/cylonid/nativealpha/util/EntryPointUtils.kt +++ b/app/src/main/kotlin/com/cylonid/nativealpha/util/EntryPointUtils.kt @@ -9,7 +9,7 @@ import com.cylonid.nativealpha.activities.NewsActivity object EntryPointUtils { @JvmStatic fun entryPointReached(a: Activity) { - if (DataManager.getInstance().lastShownUpdate != BuildConfig.VERSION_CODE) { + if (kotlin.math.abs(DataManager.getInstance().lastShownUpdate - BuildConfig.VERSION_CODE) > 50 ) { a.startActivity(Intent(a, NewsActivity::class.java)) } DataManager.getInstance().loadAppData() diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/util/NotificationUtils.kt b/app/src/main/kotlin/com/cylonid/nativealpha/util/NotificationUtils.kt new file mode 100644 index 00000000..be04d834 --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/util/NotificationUtils.kt @@ -0,0 +1,42 @@ +package com.cylonid.nativealpha.util + +import android.app.Activity +import android.view.Gravity +import android.widget.TextView +import android.widget.Toast +import com.cylonid.nativealpha.R +import com.google.android.material.snackbar.Snackbar + +object NotificationUtils { + + @JvmStatic + fun showToast(a: Activity, text: String) { + showToast(a, text, Toast.LENGTH_LONG) + } + + @JvmStatic + fun showToast(a: Activity, text: String, toastDisplayDuration: Int) { + val toast = Toast.makeText(a, text, toastDisplayDuration) + toast.setGravity(Gravity.TOP, 0, 100) + toast.show() + } + + @JvmStatic + fun showInfoSnackbar(activity: Activity, msg: String, duration: Int) { + val snackbar = Snackbar.make( + activity.findViewById(android.R.id.content), + msg, duration + ) + + snackbar.setAction( + activity.getString(R.string.ok) + ) { snackbar.dismiss() } + + + val tv = snackbar.view.findViewById(com.google.android.material.R.id.snackbar_text).findViewById(com.google.android.material.R.id.snackbar_text) + tv.maxLines = 10 + snackbar.show() + } + + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/util/ProcessUtils.kt b/app/src/main/kotlin/com/cylonid/nativealpha/util/ProcessUtils.kt new file mode 100644 index 00000000..7f5bdb7d --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/util/ProcessUtils.kt @@ -0,0 +1,28 @@ +package com.cylonid.nativealpha.util + +import android.app.ActivityManager +import android.os.Process + +object ProcessUtils { + @JvmStatic + fun closeAllWebAppsAndProcesses(activityManager: ActivityManager) { + for (task in activityManager.appTasks) { + val id = task.taskInfo.baseIntent.getIntExtra(Const.INTENT_WEBAPPID, -1) + if (id != -1) task.finishAndRemoveTask() + } + for (processInfo in activityManager.runningAppProcesses) { + if (processInfo.processName.contains("web_sandbox")) { + Process.killProcess(processInfo.pid) + } + } + } + + + fun killWebSandbox(id: Int, activityManager: ActivityManager) { + for (processInfo in activityManager.runningAppProcesses) { + if (processInfo.processName.contains("web_sandbox_$id")) { + Process.killProcess(processInfo.pid) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/cylonid/nativealpha/util/ShortcutIconUtils.kt b/app/src/main/kotlin/com/cylonid/nativealpha/util/ShortcutIconUtils.kt new file mode 100644 index 00000000..5264af79 --- /dev/null +++ b/app/src/main/kotlin/com/cylonid/nativealpha/util/ShortcutIconUtils.kt @@ -0,0 +1,37 @@ +package com.cylonid.nativealpha.util + +import android.content.Context +import android.content.pm.ShortcutManager +import com.cylonid.nativealpha.R + + +object ShortcutIconUtils { + @JvmStatic + fun deleteShortcuts(removableWebAppIds: List, context: Context) { + val manager = context.getSystemService( + ShortcutManager::class.java + ) + for (info in manager.pinnedShortcuts) { + val id = info.intent!! + .getIntExtra(Const.INTENT_WEBAPPID, -1) + if (removableWebAppIds.contains(id)) { + manager.disableShortcuts( + listOf(info.id), + context.getString(R.string.webapp_already_deleted) + ) + } + } + } + + @JvmStatic + fun getWidthFromIcon(sizeString: String): Int { + var xIndex = sizeString.indexOf("x") + if (xIndex == -1) xIndex = sizeString.indexOf("×") + if (xIndex == -1) xIndex = sizeString.indexOf("*") + + if (xIndex == -1) return 1 + val width = sizeString.substring(0, xIndex) + + return width.toInt() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_drag_indicator_24.xml b/app/src/main/res/drawable/baseline_drag_indicator_24.xml new file mode 100644 index 00000000..edd016e0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_drag_indicator_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_menu_open_24.xml b/app/src/main/res/drawable/baseline_menu_open_24.xml new file mode 100644 index 00000000..c19daa90 --- /dev/null +++ b/app/src/main/res/drawable/baseline_menu_open_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/launch_screen.xml b/app/src/main/res/drawable/launch_screen.xml index 70e2b721..cacd1a67 100644 --- a/app/src/main/res/drawable/launch_screen.xml +++ b/app/src/main/res/drawable/launch_screen.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/main/res/drawable/liberapay_logo.xml b/app/src/main/res/drawable/liberapay_logo.xml new file mode 100644 index 00000000..ec9df19c --- /dev/null +++ b/app/src/main/res/drawable/liberapay_logo.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/web_app_list_separator.xml b/app/src/main/res/drawable/web_app_list_separator.xml new file mode 100644 index 00000000..b204c05e --- /dev/null +++ b/app/src/main/res/drawable/web_app_list_separator.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f3aa3fbe..b04ab3e3 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -8,15 +8,15 @@ + android:layout_height="wrap_content"> + android:background="@color/colorSignature" /> diff --git a/app/src/main/res/layout/activity_toolbar_base.xml b/app/src/main/res/layout/activity_toolbar_base.xml new file mode 100644 index 00000000..51af1390 --- /dev/null +++ b/app/src/main/res/layout/activity_toolbar_base.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adblock_config_activity.xml b/app/src/main/res/layout/adblock_config_activity.xml new file mode 100644 index 00000000..977da4a4 --- /dev/null +++ b/app/src/main/res/layout/adblock_config_activity.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + +