mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
Merge pull request #57 from jiongxuan/feat/platform-android-offline
Major Android Update: Introducing Offline Support and Resolving CORS Issues
This commit is contained in:
commit
f6b9d1f306
26 changed files with 919 additions and 257 deletions
95
.gitignore
vendored
95
.gitignore
vendored
|
|
@ -1,17 +1,92 @@
|
|||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches/build_file_checksums.ser
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/deploymentTargetSelector.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/caches/deviceStreaming.xml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
.idea/deploymentTargetSelector.xml
|
||||
.idea/deploymentTargetDropDown.xml
|
||||
.idea/caches/deviceStreaming.xml
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-android-plugins
|
||||
|
||||
# Copied web assets
|
||||
app/src/main/assets/public
|
||||
|
||||
# Generated Config files
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
|
||||
# Platform and build
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
|
||||
/app/release
|
||||
/app/fdroid/release
|
||||
|
|
|
|||
3
.idea/gradle.xml
generated
3
.idea/gradle.xml
generated
|
|
@ -10,10 +10,11 @@
|
|||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/../node_modules/@capacitor/android/capacitor" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
|
|
|||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.7.20" />
|
||||
<option name="version" value="1.9.10" />
|
||||
</component>
|
||||
</project>
|
||||
74
README.md
74
README.md
|
|
@ -4,66 +4,50 @@ Android App for Super Productivity (https://super-productivity.com/).
|
|||
|
||||
I am not an Android developer, so help would be very welcome!!
|
||||
|
||||
## Building locally
|
||||
## New Connectivity-Free Mode is Here!
|
||||
|
||||
*This feature was added on September 7, 2024. See [Pull Request #52](https://github.com/johannesjo/super-productivity-android/pull/52).*
|
||||
*This feature was added on October 7, 2024. See [Pull Request #57](https://github.com/johannesjo/super-productivity-android/pull/57).*
|
||||
|
||||
To build this app locally, you can configure the URL that the web view loads by modifying the `app_config.properties` file. This allows you to easily switch between a local development server, the production server, or a self-hosted server without changing the source code directly.
|
||||
You can now use the core features of the app without an internet connection, offering a smoother and more reliable experience. We've made several key updates to enhance usability:
|
||||
|
||||
> **IMPORTANT**: The `app_config.properties` file is intended for LOCAL MODIFICATIONS ONLY.
|
||||
> **DO NOT COMMIT** this file unless you are absolutely sure of what you are doing.
|
||||
- **Connectivity-Free Mode Support**: Enjoy uninterrupted access to the app's main features without needing a network connection. You can still sync with WebDAV, Dropbox, or choose to work entirely offline without any network access.
|
||||
- **Online-Only Mode (Compatibility Mode)**: For users who prefer or need the traditional experience, the app still supports the original mode, which requires an internet connection for functionality.
|
||||
- **CORS Issues Resolved**: Fixed cross-origin resource sharing (CORS) problems, especially for WebDAV sync, ensuring secure and smooth synchronization with local or hosted resources.
|
||||
- **Enhanced Security**: Strengthened data protection to keep your information secure, even when offline.
|
||||
- **Seamless Upgrade**: Existing users can continue using the app in Online-Only Mode (Compatibility Mode) without any disruptions, while new users can immediately enjoy the benefits of Connectivity-Free Mode. Future updates will also include a smooth migration plan for everyone.
|
||||
|
||||
### Configuration Options
|
||||
Update now to enjoy these exciting new features and improvements!
|
||||
|
||||
1. **Use Production URL**:
|
||||
- By default, the app points to the production URL `https://app.super-productivity.com`.
|
||||
- To use this, ensure the `SERVICE_IS_LOCAL` setting in `app_config.properties` is set to `false`.
|
||||
## Launch Modes
|
||||
|
||||
2. **Use Local Development Server**:
|
||||
- If you're running the [super productivity](https://github.com/johannesjo/super-productivity) app locally, you can point the web view to your local server.
|
||||
- Set the `SERVICE_IS_LOCAL` setting in `app_config.properties` to `true`.
|
||||
- Start the local web app using the following command:
|
||||
```bash
|
||||
ng serve --disable-host-check --host 0.0.0.0 --port 4200 --live-reload --watch
|
||||
```
|
||||
- This makes the web app accessible from the Android Studio emulator at `http://10.0.2.2:4200`. The URL should also work in the emulator's Chrome browser.
|
||||
The app supports two launch modes:
|
||||
|
||||
3. **Use a Self-Hosted Server**: `NEW FEATURE`
|
||||
- If you prefer to self-host the web app, you can configure the app to point to your own server.
|
||||
- Set the `SERVICE_IS_LOCAL` setting to `false` and update the `SERVICE_HOST` and `SERVICE_PROTOCOL` values in the `app_config.properties` file to point to your self-hosted environment.
|
||||
1. **Connectivity-Free Mode** (Recommended) – Use the app without an internet connection.
|
||||
2. **Online-Only Mode (Compatibility Mode)** – Requires an internet connection to connect to production, local development, or self-hosted servers.
|
||||
|
||||
### How to Modify the URL
|
||||
### Configuring Launch Mode
|
||||
|
||||
You can edit the URL that the web view loads by modifying the `app_config.properties` file located in the project's root directory. This allows you to easily switch between a production server, a local development server, or a self-hosted server. Here's how the relevant settings work:
|
||||
To configure the launch mode, adjust the `LAUNCH_MODE` setting in the `app_config.properties` file:
|
||||
|
||||
- `SERVICE_IS_LOCAL`:
|
||||
- Set to `true` to load the web app from your local development server (`http://10.0.2.2:4200`).
|
||||
- Set to `false` to load the production web app (`https://app.super-productivity.com`) or your self-hosted server.
|
||||
- **0**: Default behavior (read from SharedPreferences)
|
||||
- **1**: Force Online-Only Mode (Compatibility Mode)
|
||||
- **2**: Force Connectivity-Free Mode (Recommended)
|
||||
|
||||
- `SERVICE_HOST`:
|
||||
- Defines the server's address.
|
||||
- If `SERVICE_IS_LOCAL` is `true`, this value is ignored, and the app uses `10.0.2.2:4200` instead.
|
||||
- If `SERVICE_IS_LOCAL` is `false`, this value determines the server the app will connect to, making it possible to connect to a self-hosted server.
|
||||
**Recommendation**: Set `LAUNCH_MODE` to `2` for Connectivity-Free Mode.
|
||||
|
||||
- `SERVICE_PROTOCOL`:
|
||||
- Defines the protocol used (`http` or `https`).
|
||||
- When `SERVICE_IS_LOCAL` is `true`, the default is `http`.
|
||||
- When `SERVICE_IS_LOCAL` is `false`, the app uses the protocol specified in this property, which can be set for self-hosted environments.
|
||||
### How to Adjust `LAUNCH_MODE`
|
||||
|
||||
### Example `app_config.properties` file:
|
||||
1. Locate the `app_config.properties` file in the project's root directory.
|
||||
2. Open the file in a text editor.
|
||||
3. Find the `LAUNCH_MODE` setting and set it to your desired mode (`0`, `1`, or `2`).
|
||||
|
||||
```properties
|
||||
# Use 'true' to point to the local server, 'false' to use the production server or self-hosted server
|
||||
SERVICE_IS_LOCAL=true
|
||||
|
||||
# The server address (ignored if SERVICE_IS_LOCAL is true)
|
||||
# Set this to your self-hosted server address if SERVICE_IS_LOCAL is false
|
||||
SERVICE_HOST=app.super-productivity.com:1234
|
||||
|
||||
# The protocol to use (http or https)
|
||||
SERVICE_PROTOCOL=https
|
||||
LAUNCH_MODE=2
|
||||
```
|
||||
|
||||
By configuring these properties, you can seamlessly switch between local development, production, and self-hosted environments without making direct changes to your Kotlin source files, improving your development workflow and offering flexibility in deployment.
|
||||
**Important**: The `app_config.properties` file is intended for local modifications only. **DO NOT COMMIT** this file unless you are absolutely sure of what you are doing.
|
||||
|
||||
You can edit the properties in the `app_config.properties` file [here](https://github.com/johannesjo/super-productivity-android/blob/master/app/app_config.properties).
|
||||
### Detailed Configuration Guides
|
||||
|
||||
- **[Connectivity-Free Mode Documentation (Recommended)](./README_OFFLINE.md)**: Step-by-step guide to setting up and building the app in Connectivity-Free Mode.
|
||||
- **[Online-Only Mode (Compatibility) Documentation](./README_ONLINE.md)**: Step-by-step guide to setting up and building the app in Online-Only Mode.
|
||||
80
README_OFFLINE.md
Normal file
80
README_OFFLINE.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Connectivity-Free Mode Configuration
|
||||
|
||||
**Connectivity-Free Mode** allows you to use the Super Productivity Android app without an internet connection. This mode is recommended for users who prefer local usage.
|
||||
|
||||
## Setting Launch Mode to Connectivity-Free
|
||||
|
||||
To enable Connectivity-Free Mode, set the `LAUNCH_MODE` to `0` (Default for new installation) or `2` in the `app_config.properties` file.
|
||||
|
||||
For users performing a **new installation**, setting `LAUNCH_MODE` to `2` ensures that the app starts in Connectivity-Free Mode by default. This avoids any attempts to connect to online services, providing a seamless offline experience from the outset.
|
||||
|
||||
**Important**: If you set `LAUNCH_MODE` to `0`, the app will use the default behavior, which may attempt to read from SharedPreferences and connect to online services if available. To maintain a purely offline experience, always set `LAUNCH_MODE` to `2` for new installations.
|
||||
|
||||
## Building and Running super-productivity-android Locally
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
To set up the project, clone the `super-productivity` repository instead of directly cloning the `super-productivity-android` repository. This ensures that all submodules, including the Android project, are properly initialized.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/super-productivity/super-productivity.git
|
||||
cd super-productivity
|
||||
git submodule init
|
||||
git submodule update
|
||||
```
|
||||
|
||||
### 2. Compile the Node.js Project
|
||||
|
||||
Ensure you have Node.js and npm installed. Navigate to the root directory of the `super-productivity` project and install the necessary dependencies.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Compile the Android Project
|
||||
|
||||
From the root directory, compile the Android project using the following commands:
|
||||
|
||||
- **For Testing Builds:**
|
||||
|
||||
```bash
|
||||
npm run dist:android
|
||||
```
|
||||
|
||||
- **For Production Builds:**
|
||||
|
||||
```bash
|
||||
npm run dist:android:prod
|
||||
```
|
||||
|
||||
### 4. Installation
|
||||
|
||||
You can install the compiled Android application using either Android Studio or npm scripts.
|
||||
|
||||
- **Using Android Studio:**
|
||||
|
||||
1. Open Android Studio.
|
||||
2. Select `Open an existing project`.
|
||||
3. Navigate to the `android` directory within the cloned repository.
|
||||
4. Follow the prompts to build and run the application on your device or emulator.
|
||||
|
||||
- **Using NPM Scripts:**
|
||||
|
||||
- **For Testing Installation:**
|
||||
|
||||
```bash
|
||||
npm run install:android
|
||||
```
|
||||
|
||||
- **For Production Installation:**
|
||||
|
||||
```bash
|
||||
npm run install:android:prod
|
||||
```
|
||||
|
||||
## Additional Notes
|
||||
|
||||
- **Local Modifications**: The `app_config.properties` file is intended for local modifications only. **DO NOT COMMIT** this file unless you are absolutely sure of the changes.
|
||||
- **No Additional Configuration**: Connectivity-Free Mode does not require further configuration beyond setting the `LAUNCH_MODE` to `0` or `2`.
|
||||
|
||||
For more information, refer to the [main README](./README.md).
|
||||
93
README_ONLINE.md
Normal file
93
README_ONLINE.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Online-Only Mode (Compatibility Mode) Configuration
|
||||
|
||||
**Online-Only Mode (Compatibility Mode)** allows the Super Productivity Android app to connect to the production server, a local development server, or a self-hosted server. This mode requires an internet connection and is compatible with various server setups.
|
||||
|
||||
**Note**: While Online-Only Mode offers connectivity to production, local development, or self-hosted servers, it is highly recommended to use the latest **Connectivity-Free Mode** for a more stable and reliable experience. Connectivity-Free Mode allows you to use the app without an internet connection, ensuring uninterrupted productivity, enhanced privacy, and reduced latency.
|
||||
|
||||
For more information, refer to the **[Connectivity-Free Mode Documentation (Recommended)](./README_OFFLINE.md)**.
|
||||
|
||||
If you require online features or need to connect to specific servers, proceed with the Online-Only Mode configuration below.
|
||||
|
||||
## Setting Launch Mode to Online
|
||||
|
||||
To enable Online-Only Mode, set the `LAUNCH_MODE` to `1` or `0` in the `app_config.properties` file.
|
||||
|
||||
- **1**: Force Online-Only Mode (Compatibility Mode)
|
||||
- **0**: Default behavior (read from SharedPreferences)
|
||||
|
||||
**Recommendation**: Set `LAUNCH_MODE` to `0` for default behavior. The app will use the default behavior, which may attempt to read from SharedPreferences and connect to online services if available.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
1. **Launch Mode (`LAUNCH_MODE`)**
|
||||
|
||||
```properties
|
||||
LAUNCH_MODE=1
|
||||
```
|
||||
|
||||
- **0**: Default behavior (read from SharedPreferences)
|
||||
- **1**: Force Online-Only Mode (compatible mode)
|
||||
- **2**: Force Connectivity-Free Mode (for offline configuration)
|
||||
|
||||
2. **Use Production URL**
|
||||
|
||||
- **Condition**: Applicable when `LAUNCH_MODE` is set to `1`, or set to `0` and the user has upgraded from a previous version.
|
||||
- **Default**: `https://app.super-productivity.com`
|
||||
- **Configuration**: Ensure `ONLINE_SERVICE_IS_LOCAL` is set to `false`.
|
||||
|
||||
```properties
|
||||
ONLINE_SERVICE_IS_LOCAL=false
|
||||
```
|
||||
|
||||
3. **Use Local Development Server**
|
||||
|
||||
- **Condition**: Applicable when `LAUNCH_MODE` is set to `1`, or set to `0` and the user has upgraded from a previous version.
|
||||
- **Configuration**: Set `ONLINE_SERVICE_IS_LOCAL` to `true` and start the local server.
|
||||
|
||||
```properties
|
||||
ONLINE_SERVICE_IS_LOCAL=true
|
||||
```
|
||||
|
||||
- **Start Local Server**
|
||||
|
||||
```bash
|
||||
ng serve --disable-host-check --host 0.0.0.0 --port 4200 --live-reload --watch
|
||||
```
|
||||
|
||||
- **Access URL**: `http://10.0.2.2:4200` (accessible from the Android Studio emulator and emulator's Chrome browser).
|
||||
|
||||
4. **Use a Self-Hosted Server**
|
||||
|
||||
- **Condition**: Applicable when `LAUNCH_MODE` is set to `1`, or set to `0` and the user has upgraded from a previous version.
|
||||
- **Configuration**: Set `ONLINE_SERVICE_IS_LOCAL` to `false` and update `ONLINE_SERVICE_HOST` and `ONLINE_SERVICE_PROTOCOL`.
|
||||
|
||||
```properties
|
||||
ONLINE_SERVICE_IS_LOCAL=false
|
||||
ONLINE_SERVICE_HOST=your.server.address
|
||||
ONLINE_SERVICE_PROTOCOL=https
|
||||
```
|
||||
|
||||
## How to Modify the URL
|
||||
|
||||
You can edit the URL that the web view loads by modifying the `app_config.properties` file located in the project's root directory. This allows you to easily switch between a production server, a local development server, or a self-hosted server.
|
||||
|
||||
### Relevant Settings
|
||||
|
||||
- **`LAUNCH_MODE`**:
|
||||
- `0`: Default behavior (read from SharedPreferences)
|
||||
- `1`: Force Online-Only Mode
|
||||
- `2`: Force Connectivity-Free Mode
|
||||
|
||||
- **When `LAUNCH_MODE` is `1` or `0` (with upgrade)**:
|
||||
- **`ONLINE_SERVICE_IS_LOCAL`**:
|
||||
- `true`: Load from local development server (`http://10.0.2.2:4200`).
|
||||
- `false`: Load from production or self-hosted server.
|
||||
- **`ONLINE_SERVICE_HOST`**:
|
||||
- Defines the server's address.
|
||||
- **`ONLINE_SERVICE_PROTOCOL`**:
|
||||
- `http` or `https`.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Local Modifications**: The `app_config.properties` file is intended for local modifications only. **DO NOT COMMIT** this file unless you are absolutely sure of the changes.
|
||||
- **Switching Servers**: By configuring these properties, you can seamlessly switch between default, online, and offline launch behaviors without making direct changes to your Kotlin source files, improving your development workflow and offering flexibility in deployment.
|
||||
|
|
@ -1,21 +1,28 @@
|
|||
# IMPORTANT: The app_config.properties file is intended for LOCAL MODIFICATIONS ONLY.
|
||||
# DO NOT COMMIT this file unless you are absolutely sure of what you are doing.
|
||||
|
||||
# SERVICE_IS_LOCAL determines if the app should use a local server during development.
|
||||
# LAUNCH_MODE determines the launch behavior.
|
||||
# - 0: Default behavior (read from SharedPreferences)
|
||||
# - 1: Force Online-Only Mode (Compatibility Mode)
|
||||
# - 2: Force Connectivity-Free Mode (all new offline mode)
|
||||
# Default: 0
|
||||
LAUNCH_MODE=0
|
||||
|
||||
# ONLINE_SERVICE_IS_LOCAL determines if the app should use a local server during development.
|
||||
# - true: The app connects to a local server, typically used for development and testing.
|
||||
# - false: The app connects to a remote server, useful for staging or production environments.
|
||||
# Default: true
|
||||
SERVICE_IS_LOCAL=false
|
||||
ONLINE_SERVICE_IS_LOCAL=false
|
||||
|
||||
# SERVICE_HOST specifies the server's address. Set this to your [self-hosted] server address
|
||||
# - When SERVICE_IS_LOCAL is true, this value is ignored in favor of "10.0.2.2:4200" (Android emulator's localhost).
|
||||
# - When SERVICE_IS_LOCAL is false or in release builds, this value determines the server the app will connect to.
|
||||
# ONLINE_SERVICE_HOST specifies the server's address. Set this to your [self-hosted] server address
|
||||
# - When ONLINE_SERVICE_IS_LOCAL is true, this value is ignored in favor of "10.0.2.2:4200" (Android emulator's localhost).
|
||||
# - When ONLINE_SERVICE_IS_LOCAL is false or in release builds, this value determines the server the app will connect to.
|
||||
# Default: app.super-productivity.com
|
||||
SERVICE_HOST=app.super-productivity.com
|
||||
ONLINE_SERVICE_HOST=app.super-productivity.com
|
||||
|
||||
# SERVICE_PROTOCOL defines the protocol used to communicate with the server.
|
||||
# ONLINE_SERVICE_PROTOCOL defines the protocol used to communicate with the server.
|
||||
# - http: Used typically in local development or non-secure environments.
|
||||
# - https: Used in secure, production environments.
|
||||
# - In debug builds, when SERVICE_IS_LOCAL is true, the default is http. Otherwise, this value is used.
|
||||
# - In debug builds, when ONLINE_SERVICE_IS_LOCAL is true, the default is http. Otherwise, this value is used.
|
||||
# Default: https
|
||||
SERVICE_PROTOCOL=https
|
||||
ONLINE_SERVICE_PROTOCOL=https
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ android {
|
|||
minSdkVersion 24
|
||||
targetSdkVersion 34
|
||||
compileSdk 34
|
||||
versionCode 23
|
||||
versionName "23.0"
|
||||
versionCode 30
|
||||
versionName "30.10.0.11"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
manifestPlaceholders = [
|
||||
hostName : "app.super-productivity.com",
|
||||
|
|
@ -77,13 +77,19 @@ dependencies {
|
|||
implementation "com.google.androidbrowserhelper:androidbrowserhelper:2.4.0"
|
||||
implementation "com.google.code.gson:gson:2.10"
|
||||
implementation "androidx.core:core-ktx:1.9.0"
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.6'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation "com.anggrayudi:storage:1.5.4"
|
||||
|
||||
playImplementation "com.google.android.gms:play-services-auth:20.4.0"
|
||||
|
||||
implementation project(':capacitor-android')
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
|
||||
androidTestImplementation "androidx.test:rules:1.5.0"
|
||||
androidTestImplementation "com.android.support.test:runner:1.0.2"
|
||||
androidTestImplementation "com.android.support.test.espresso:espresso-core:3.0.2"
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
|
|
|||
19
app/capacitor.build.gradle
Normal file
19
app/capacitor.build.gradle
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (hasProperty('postBuildExtras')) {
|
||||
postBuildExtras()
|
||||
}
|
||||
|
|
@ -19,19 +19,23 @@ ext.loadAppConfig = {
|
|||
|
||||
// Determine the build configuration fields based on the build type (Debug or Release)
|
||||
ext.getBuildConfigFields = { appConfig, isDebugBuild ->
|
||||
// SERVICE_IS_LOCAL remains as a string for comparison
|
||||
def serviceIsLocal = appConfig.getProperty("SERVICE_IS_LOCAL", "true")
|
||||
// LAUNCH_MODE determines the launch behavior
|
||||
def launchMode = appConfig.getProperty("LAUNCH_MODE", "0")
|
||||
|
||||
// ONLINE_SERVICE_IS_LOCAL remains as a string for comparison
|
||||
def serviceIsLocal = appConfig.getProperty("ONLINE_SERVICE_IS_LOCAL", "true")
|
||||
|
||||
// Set the service host based on the build type and whether it's running locally
|
||||
def serviceHost = (serviceIsLocal == "true" && isDebugBuild) ? "10.0.2.2:4200" : appConfig.getProperty("SERVICE_HOST", "app.super-productivity.com")
|
||||
def serviceHost = (serviceIsLocal == "true" && isDebugBuild) ? "10.0.2.2:4200" : appConfig.getProperty("ONLINE_SERVICE_HOST", "app.super-productivity.com")
|
||||
|
||||
// Set the protocol based on the build type and whether it's running locally
|
||||
def serviceProtocol = (serviceIsLocal == "true" && isDebugBuild) ? "http" : appConfig.getProperty("SERVICE_PROTOCOL", "https")
|
||||
def serviceProtocol = (serviceIsLocal == "true" && isDebugBuild) ? "http" : appConfig.getProperty("ONLINE_SERVICE_PROTOCOL", "https")
|
||||
|
||||
// Return the configuration fields as a map
|
||||
return [
|
||||
SERVICE_IS_LOCAL: "\"${serviceIsLocal}\"", // Keeping SERVICE_IS_LOCAL as a string
|
||||
SERVICE_HOST: "\"${serviceHost}\"",
|
||||
SERVICE_PROTOCOL: "\"${serviceProtocol}\""
|
||||
LAUNCH_MODE: "\"${launchMode}\"",
|
||||
ONLINE_SERVICE_IS_LOCAL: "\"${serviceIsLocal}\"",
|
||||
ONLINE_SERVICE_HOST: "\"${serviceHost}\"",
|
||||
ONLINE_SERVICE_PROTOCOL: "\"${serviceProtocol}\""
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
android:theme="@style/AppTheme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<!-- App in WebFrame -->
|
||||
<!-- Set FullscreenActivity as the default launcher -->
|
||||
<activity
|
||||
android:name=".FullscreenActivity"
|
||||
android:allowTaskReparenting="true"
|
||||
|
|
@ -45,5 +45,19 @@
|
|||
<action android:name="com.superproductivity.superproductivity.PAUSE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- All new Super-Productivity main activity, based on Capacitor to support offline use -->
|
||||
<activity
|
||||
android:name=".CapacitorMainActivity"
|
||||
android:allowTaskReparenting="true"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:label="@string/title_activity_fullscreen"
|
||||
android:launchMode="singleTask"
|
||||
android:lockTaskMode="never"
|
||||
android:persistableMode="persistNever"
|
||||
android:theme="@style/FullscreenTheme"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
package com.superproductivity.superproductivity
|
||||
|
||||
import android.app.Application
|
||||
import android.webkit.WebView
|
||||
import com.superproductivity.superproductivity.app.AppLifecycleObserver
|
||||
import com.superproductivity.superproductivity.app.KeyValStore
|
||||
|
||||
class App : Application() {
|
||||
|
||||
|
|
@ -13,4 +14,11 @@ class App : Application() {
|
|||
val keyValStore: KeyValStore by lazy {
|
||||
KeyValStore(this)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Initialize AppLifecycleObserver at app startup
|
||||
AppLifecycleObserver.getInstance()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
package com.superproductivity.superproductivity
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.webkit.ServiceWorkerClient
|
||||
import android.webkit.ServiceWorkerController
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.getcapacitor.BridgeActivity
|
||||
import com.getcapacitor.BridgeWebViewClient
|
||||
import com.superproductivity.superproductivity.webview.JavaScriptInterface
|
||||
import com.superproductivity.superproductivity.webview.WebHelper
|
||||
import com.superproductivity.superproductivity.webview.WebViewRequestHandler
|
||||
|
||||
/**
|
||||
* All new Super-Productivity main activity, based on Capacitor to support offline use of the entire application
|
||||
*/
|
||||
class CapacitorMainActivity : BridgeActivity() {
|
||||
private lateinit var javaScriptInterface: JavaScriptInterface
|
||||
|
||||
private var webViewRequestHandler = WebViewRequestHandler(this, "localhost")
|
||||
private val storageHelper =
|
||||
SimpleStorageHelper(this) // for scoped storage permission management on Android 10+
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Register Plugin
|
||||
// TODO: The changes to the compatible logic are too complex, so they will not be added for now
|
||||
// (separate branch, there will be opportunities to add it later)
|
||||
|
||||
// DEBUG ONLY
|
||||
if (BuildConfig.DEBUG) {
|
||||
Toast.makeText(this, "DEBUG: Offline Mode", Toast.LENGTH_SHORT).show()
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
|
||||
// Hide the action bar
|
||||
supportActionBar?.hide()
|
||||
|
||||
// Initialize JavaScriptInterface
|
||||
javaScriptInterface = JavaScriptInterface(this, bridge.webView, storageHelper)
|
||||
|
||||
// Initialize WebView
|
||||
WebHelper().setupView(bridge.webView, false)
|
||||
|
||||
// Inject JavaScriptInterface into Capacitor's WebView
|
||||
bridge.webView.addJavascriptInterface(
|
||||
javaScriptInterface,
|
||||
WINDOW_INTERFACE_PROPERTY
|
||||
)
|
||||
if (BuildConfig.FLAVOR.equals("fdroid")) {
|
||||
bridge.webView.addJavascriptInterface(
|
||||
javaScriptInterface,
|
||||
WINDOW_PROPERTY_F_DROID
|
||||
)
|
||||
// not ready in time, that's why we create a second JS interface just to fill the prop
|
||||
// callJavaScriptFunction("window.$WINDOW_PROPERTY_F_DROID=true")
|
||||
}
|
||||
|
||||
// Set custom SP WebViewClient & ServiceWorkerController
|
||||
// No need to set up WebChromeClient, as most of the processes have been implemented in Bridge
|
||||
bridge.webViewClient = object : BridgeWebViewClient(bridge) {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
return webViewRequestHandler.handleUrlLoading(view, url)
|
||||
}
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): WebResourceResponse? {
|
||||
val interceptedResponse = webViewRequestHandler.interceptWebRequest(request)
|
||||
return interceptedResponse ?: super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
}
|
||||
val swController = ServiceWorkerController.getInstance()
|
||||
swController.setServiceWorkerClient(
|
||||
object : ServiceWorkerClient() {
|
||||
override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
|
||||
return bridge.webViewClient.shouldInterceptRequest(bridge.webView, request)
|
||||
}
|
||||
})
|
||||
|
||||
// Register OnBackPressedCallback to handle back button press
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
Log.v("TW", "onBackPressed ${bridge.webView.canGoBack()}")
|
||||
if (bridge.webView.canGoBack()) {
|
||||
bridge.webView.goBack()
|
||||
} else {
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle keyboard visibility changes
|
||||
val rootView = findViewById<View>(android.R.id.content)
|
||||
rootView.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val rect = Rect()
|
||||
rootView.getWindowVisibleDisplayFrame(rect)
|
||||
val screenHeight = rootView.rootView.height
|
||||
|
||||
val keypadHeight = screenHeight - rect.bottom
|
||||
if (keypadHeight > screenHeight * 0.15) {
|
||||
// keyboard is opened
|
||||
callJSInterfaceFunctionIfExists("next", "isKeyboardShown$", "true")
|
||||
} else {
|
||||
// keyboard is closed
|
||||
callJSInterfaceFunctionIfExists("next", "isKeyboardShown$", "false")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
// Save scoped storage permission on Android 10+
|
||||
storageHelper.onSaveInstanceState(outState)
|
||||
bridge.webView.saveState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
// Restore scoped storage permission on Android 10+
|
||||
storageHelper.onRestoreInstanceState(savedInstanceState)
|
||||
bridge.webView.restoreState(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.v("TW", "CapacitorFullscreenActivity: onPause")
|
||||
callJSInterfaceFunctionIfExists("next", "onPause$")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.v("TW", "CapacitorFullscreenActivity: onResume")
|
||||
callJSInterfaceFunctionIfExists("next", "onResume$")
|
||||
}
|
||||
|
||||
private fun callJSInterfaceFunctionIfExists(fnName: String, objectPath: String, fnParam: String = "") {
|
||||
val fnFullName = "window.${FullscreenActivity.WINDOW_INTERFACE_PROPERTY}.$objectPath.$fnName"
|
||||
val fullObjectPath = "window.${FullscreenActivity.WINDOW_INTERFACE_PROPERTY}.$objectPath"
|
||||
javaScriptInterface.callJavaScriptFunction("if($fullObjectPath && $fnFullName)$fnFullName($fnParam)")
|
||||
}
|
||||
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (data != null) {
|
||||
javaScriptInterface.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WINDOW_INTERFACE_PROPERTY: String = "SUPAndroid"
|
||||
const val WINDOW_PROPERTY_F_DROID: String = "SUPFDroid"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package com.superproductivity.superproductivity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
|
|
@ -20,12 +20,11 @@ import android.widget.FrameLayout
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.ByteArrayInputStream
|
||||
import com.superproductivity.superproductivity.app.LaunchDecider
|
||||
import com.superproductivity.superproductivity.webview.JavaScriptInterface
|
||||
import com.superproductivity.superproductivity.webview.WebHelper
|
||||
import com.superproductivity.superproductivity.webview.WebViewRequestHandler
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -36,18 +35,28 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
private lateinit var javaScriptInterface: JavaScriptInterface
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var wvContainer: FrameLayout
|
||||
var isInForeground: Boolean = false
|
||||
private var webViewRequestHandler = WebViewRequestHandler(this, BuildConfig.ONLINE_SERVICE_HOST)
|
||||
val storageHelper =
|
||||
SimpleStorageHelper(this) // for scoped storage permission management on Android 10+
|
||||
val appUrl =
|
||||
// if (BuildConfig.DEBUG) "https://test-app.super-productivity.com" else "https://app.super-productivity.com"
|
||||
"${BuildConfig.SERVICE_PROTOCOL}://${BuildConfig.SERVICE_HOST}"
|
||||
"${BuildConfig.ONLINE_SERVICE_PROTOCOL}://${BuildConfig.ONLINE_SERVICE_HOST}"
|
||||
|
||||
@Suppress("ReplaceCallWithBinaryOperator")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Log.v("TW", "FullScreenActivity: onCreate")
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Determines which launch mode to use. (Online-old or Offline-new)
|
||||
val launchDecider = LaunchDecider(this)
|
||||
if (launchDecider.shouldSwitchToNewActivity()) {
|
||||
// Switch to CapacitorMainActivity
|
||||
val intent = intent.setComponent(ComponentName(this, CapacitorMainActivity::class.java))
|
||||
startActivity(intent)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
initWebView()
|
||||
|
||||
// FOR TESTING HTML INPUTS QUICKLY
|
||||
|
|
@ -106,14 +115,12 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
isInForeground = false
|
||||
Log.v("TW", "FullScreenActivity: onPause")
|
||||
callJSInterfaceFunctionIfExists("next", "onPause$")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
isInForeground = true
|
||||
Log.v("TW", "FullScreenActivity: onResume")
|
||||
callJSInterfaceFunctionIfExists("next", "onResume$")
|
||||
}
|
||||
|
|
@ -140,7 +147,7 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
|
||||
webView.loadUrl(appUrl)
|
||||
supportActionBar?.hide()
|
||||
javaScriptInterface = JavaScriptInterface(this)
|
||||
javaScriptInterface = JavaScriptInterface(this, webView, storageHelper)
|
||||
webView.addJavascriptInterface(javaScriptInterface, WINDOW_INTERFACE_PROPERTY)
|
||||
if (BuildConfig.FLAVOR.equals("fdroid")) {
|
||||
webView.addJavascriptInterface(javaScriptInterface, WINDOW_PROPERTY_F_DROID)
|
||||
|
|
@ -152,34 +159,21 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
swController.setServiceWorkerClient(@RequiresApi(Build.VERSION_CODES.N)
|
||||
object : ServiceWorkerClient() {
|
||||
override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
|
||||
return interceptRequest(request)
|
||||
return webViewRequestHandler.interceptWebRequest(request)
|
||||
}
|
||||
})
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
Log.v("TW", url)
|
||||
return if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
if (url.contains("super-productivity.com") || url.contains("localhost") || url.contains(
|
||||
BuildConfig.SERVICE_HOST
|
||||
)
|
||||
) {
|
||||
false
|
||||
} else {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
return webViewRequestHandler.handleUrlLoading(view, url)
|
||||
}
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): WebResourceResponse? {
|
||||
return interceptRequest(request)
|
||||
return webViewRequestHandler.interceptWebRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,11 +224,7 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
private fun callJSInterfaceFunctionIfExists(fnName: String, objectPath: String, fnParam: String = "") {
|
||||
val fnFullName = "window.$WINDOW_INTERFACE_PROPERTY.$objectPath.$fnName"
|
||||
val fullObjectPath = "window.$WINDOW_INTERFACE_PROPERTY.$objectPath"
|
||||
callJavaScriptFunction("if($fullObjectPath && $fnFullName)$fnFullName($fnParam)")
|
||||
}
|
||||
|
||||
fun callJavaScriptFunction(script: String) {
|
||||
webView.post { webView.evaluateJavascript(script) { } }
|
||||
javaScriptInterface.callJavaScriptFunction("if($fullObjectPath && $fnFullName)$fnFullName($fnParam)")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
|
|
@ -248,7 +238,10 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
wvContainer.removeView(webView)
|
||||
// Ensure wvContainer is initialized before removing the view
|
||||
if (::wvContainer.isInitialized) {
|
||||
wvContainer.removeView(webView)
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
|
@ -268,134 +261,4 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||
//storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
|
||||
private fun interceptRequest(request: WebResourceRequest?): WebResourceResponse? {
|
||||
if (request == null || request.isForMainFrame) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (request.url?.path?.contains("assets/icons/favicon") == true) {
|
||||
try {
|
||||
return WebResourceResponse("image/png", null, null)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
if (request.url.toString().contains(BuildConfig.SERVICE_HOST)) {
|
||||
return null
|
||||
}
|
||||
|
||||
Log.v(
|
||||
"TW",
|
||||
"interceptRequest mf:${request?.isForMainFrame.toString()} ${request.method} ${request?.url}"
|
||||
)
|
||||
|
||||
// since we currently don't have a way to also post the body, we only handle GET, HEAD and OPTIONS requests
|
||||
// see https://github.com/KonstantinSchubert/request_data_webviewclient for a possible solution
|
||||
if (request.method.uppercase() != "GET" && request.method.uppercase() != "OPTIONS" && request.method.uppercase() !="HEAD") {
|
||||
return null
|
||||
}
|
||||
|
||||
// remove user agent header in the hopes that we're treated better by the remotes :D
|
||||
val keysToRemove =
|
||||
request.requestHeaders.keys.filter { it.equals("User-Agent", ignoreCase = true) }
|
||||
for (key in keysToRemove) {
|
||||
request.requestHeaders.remove(key)
|
||||
}
|
||||
|
||||
val client = OkHttpClient()
|
||||
val newRequestBuilder = Request.Builder()
|
||||
.url(request.url.toString())
|
||||
.method(request.method, null)
|
||||
|
||||
// Add each header from the original request to the new request
|
||||
for ((key, value) in request.requestHeaders) {
|
||||
newRequestBuilder.addHeader(key, value)
|
||||
}
|
||||
val newRequest = newRequestBuilder.build()
|
||||
|
||||
// currently we can't handle POST requests since everything
|
||||
if (request.method.uppercase() == "OPTIONS") {
|
||||
Log.v("TW", "OPTIONS request triggered")
|
||||
client.newCall(newRequest).execute().use { response ->
|
||||
Log.v(
|
||||
"TW",
|
||||
"OPTIONS original response: ${response.code} ${response.message} ${response.body?.string()}"
|
||||
)
|
||||
if (response.code != 200) {
|
||||
Log.v("TW", "OPTIONS overwrite")
|
||||
return OptionsAllowResponse.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle HEAD requests
|
||||
if (request.method.uppercase() == "HEAD") {
|
||||
Log.v("TW", "HEAD request triggered")
|
||||
client.newCall(newRequest).execute().use { response ->
|
||||
Log.v("TW", "HEAD response ${response.code} ${response.message}")
|
||||
val responseHeaders = response.headers.names()
|
||||
.associateWith { response.headers(it)?.joinToString() }
|
||||
.toMutableMap()
|
||||
|
||||
val keysToRemoveI = responseHeaders.keys.filter {
|
||||
it.equals("Access-Control-Allow-Origin", ignoreCase = true)
|
||||
}
|
||||
for (key in keysToRemoveI) {
|
||||
responseHeaders.remove(key)
|
||||
}
|
||||
responseHeaders["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
val contentType = response.header("Content-Type", "text/plain")
|
||||
val contentEncoding = response.header("Content-Encoding", "utf-8")
|
||||
val reasonPhrase = response.message.ifEmpty { "OK" }
|
||||
return WebResourceResponse(
|
||||
contentType,
|
||||
contentEncoding,
|
||||
response.code,
|
||||
reasonPhrase,
|
||||
responseHeaders,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Log.v("TW", "exec request ${request.url}")
|
||||
client.newCall(newRequest).execute().use { response ->
|
||||
Log.v("TW", "response ${response.code} ${response.message}")
|
||||
val responseHeaders = response.headers.names()
|
||||
.associateWith { response.headers(it)?.joinToString() }
|
||||
.toMutableMap()
|
||||
|
||||
val keysToRemoveI =
|
||||
responseHeaders.keys.filter {
|
||||
it.equals(
|
||||
"Access-Control-Allow-Origin",
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
for (key in keysToRemoveI) {
|
||||
responseHeaders.remove(key)
|
||||
}
|
||||
responseHeaders["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
val contentType = response.header("Content-Type", "text/plain")
|
||||
val contentEncoding = response.header("Content-Encoding", "utf-8")
|
||||
val inputStream = ByteArrayInputStream(response.body?.bytes())
|
||||
val reasonPhrase =
|
||||
response.message.ifEmpty { "OK" } // provide a default value if the message is null or empty
|
||||
return WebResourceResponse(
|
||||
contentType,
|
||||
contentEncoding,
|
||||
response.code,
|
||||
reasonPhrase,
|
||||
responseHeaders,
|
||||
inputStream
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package com.superproductivity.superproductivity.app
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
|
||||
// AppLifecycleObserver as a Singleton
|
||||
class AppLifecycleObserver private constructor() : DefaultLifecycleObserver {
|
||||
|
||||
private var _isInForeground: Boolean = false
|
||||
|
||||
val isInForeground: Boolean
|
||||
get() = _isInForeground
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
_isInForeground = true
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
_isInForeground = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var instance: AppLifecycleObserver? = null
|
||||
|
||||
fun getInstance(): AppLifecycleObserver {
|
||||
if (instance == null) {
|
||||
instance = AppLifecycleObserver()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(instance!!)
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.superproductivity.superproductivity
|
||||
package com.superproductivity.superproductivity.app
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
|
|
@ -6,6 +6,7 @@ import android.database.DatabaseUtils
|
|||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.util.Log
|
||||
import com.superproductivity.superproductivity.App
|
||||
|
||||
class KeyValStore(private val context: Context) :
|
||||
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.superproductivity.superproductivity.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.superproductivity.superproductivity.BuildConfig
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Determines which launch mode to use. (Online-old or Offline-new)
|
||||
*/
|
||||
class LaunchDecider(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
// Enum values represented as integers
|
||||
private const val LAUNCH_MODE_KEY = "launch_mode"
|
||||
private const val MODE_DEFAULT = 0 // Need to determine
|
||||
private const val MODE_ONLINE = 1 // Use FullscreenActivity (old activity)
|
||||
private const val MODE_OFFLINE = 2 // Use CapacitorMainActivity (new activity)
|
||||
}
|
||||
|
||||
private val sharedPrefs: SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
/**
|
||||
* Determines which launch mode to use.
|
||||
* If the mode is MODE_DEFAULT, it will perform checks to decide between MODE_ONLINE and MODE_OFFLINE.
|
||||
* If LAUNCH_MODE is set to 1 or 2, it will force the corresponding mode.
|
||||
* The result is saved in SharedPreferences for future launches.
|
||||
*/
|
||||
private fun getLaunchMode(): Int {
|
||||
val launchMode = BuildConfig.LAUNCH_MODE.toIntOrNull() ?: 0
|
||||
return when (launchMode) {
|
||||
1 -> MODE_ONLINE
|
||||
2 -> MODE_OFFLINE
|
||||
else -> {
|
||||
val currentMode = sharedPrefs.getInt(LAUNCH_MODE_KEY, MODE_DEFAULT)
|
||||
if (currentMode != MODE_DEFAULT) {
|
||||
// If mode is already determined, return it
|
||||
currentMode
|
||||
} else {
|
||||
// Mode is MODE_DEFAULT, need to determine
|
||||
val newMode = determineLaunchMode()
|
||||
// Save the new mode for future launches
|
||||
sharedPrefs.edit().putInt(LAUNCH_MODE_KEY, newMode).apply()
|
||||
newMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to use MODE_ONLINE or MODE_OFFLINE.
|
||||
* Logic:
|
||||
* - If firstInstallTime == lastUpdateTime, it's a new installation -> MODE_OFFLINE
|
||||
* - If firstInstallTime < lastUpdateTime, it's an upgrade:
|
||||
* - If databases/SupKeyValStore file exists, user has data -> MODE_ONLINE
|
||||
* - If not, user might not have data -> MODE_OFFLINE
|
||||
*/
|
||||
private fun determineLaunchMode(): Int {
|
||||
val packageManager = context.packageManager
|
||||
val packageName = context.packageName
|
||||
try {
|
||||
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
||||
val firstInstallTime = packageInfo.firstInstallTime
|
||||
val lastUpdateTime = packageInfo.lastUpdateTime
|
||||
|
||||
return if (firstInstallTime == lastUpdateTime) {
|
||||
// New installation
|
||||
MODE_OFFLINE
|
||||
} else {
|
||||
// Upgrade installation
|
||||
if (hasLegacyData()) {
|
||||
// User has existing data, use online mode
|
||||
MODE_ONLINE
|
||||
} else {
|
||||
// No existing data, use offline mode
|
||||
MODE_OFFLINE
|
||||
}
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
// In case of error, default to online mode to be safe
|
||||
return MODE_ONLINE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the legacy data file exists.
|
||||
* Returns true if databases/SupKeyValStore file exists.
|
||||
*/
|
||||
private fun hasLegacyData(): Boolean {
|
||||
val dataDir = context.filesDir.parentFile
|
||||
val dbFile = File(dataDir, "databases/SupKeyValStore")
|
||||
return dbFile.exists()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if we should switch to the new activity.
|
||||
* Returns true if MODE_OFFLINE (value 2), indicating we should switch to CapacitorMainActivity.
|
||||
*/
|
||||
fun shouldSwitchToNewActivity(): Boolean {
|
||||
return getLaunchMode() == MODE_OFFLINE
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.superproductivity.superproductivity
|
||||
package com.superproductivity.superproductivity.app
|
||||
|
||||
data class SpTask(
|
||||
val id: String,
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.superproductivity.superproductivity
|
||||
package com.superproductivity.superproductivity.webview
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
|
|
@ -13,11 +14,17 @@ import android.os.Build
|
|||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.anggrayudi.storage.file.*
|
||||
import com.superproductivity.superproductivity.App
|
||||
import com.superproductivity.superproductivity.app.AppLifecycleObserver
|
||||
import com.superproductivity.superproductivity.FullscreenActivity
|
||||
import com.superproductivity.superproductivity.FullscreenActivity.Companion.WINDOW_INTERFACE_PROPERTY
|
||||
import com.superproductivity.superproductivity.R
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedOutputStream
|
||||
|
|
@ -37,7 +44,9 @@ import java.util.Locale
|
|||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
class JavaScriptInterface(
|
||||
private val activity: FullscreenActivity,
|
||||
private val activity: Activity,
|
||||
private val webView: WebView,
|
||||
private val storageHelper: SimpleStorageHelper
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -47,7 +56,7 @@ class JavaScriptInterface(
|
|||
// Additional callback for scoped storage permission management on Android 10+
|
||||
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||
Log.d("SuperProductivity", "onActivityResult")
|
||||
activity.storageHelper.storage.onActivityResult(requestCode, resultCode, data)
|
||||
storageHelper.storage.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
|
|
@ -133,7 +142,7 @@ class JavaScriptInterface(
|
|||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun showNotificationIfAppIsNotOpen(title: String, body: String) {
|
||||
if (!activity.isInForeground) {
|
||||
if (!AppLifecycleObserver.getInstance().isInForeground) {
|
||||
showNotification(title, body)
|
||||
}
|
||||
}
|
||||
|
|
@ -512,7 +521,7 @@ class JavaScriptInterface(
|
|||
// Note that SimpleStorage takes care of all the gritty technical details, including whether the user must pick a root path BEFORE selecting the folder they want to store in, everything is explained to the user
|
||||
Log.d("SuperProductivity", "Before SimpleStorageHelper callback func def")
|
||||
// Register a callback with SimpleStorage when a folder is picked
|
||||
activity.storageHelper.onFolderSelected =
|
||||
storageHelper.onFolderSelected =
|
||||
{ requestCode, root -> // could also use simpleStorageHelper.onStorageAccessGranted()
|
||||
Log.d("SuperProductivity", "Success Folder Pick! Now saving...")
|
||||
// Get absolute path to folder
|
||||
|
|
@ -528,7 +537,7 @@ class JavaScriptInterface(
|
|||
// Open folder picker via SimpleStorage, this will request the necessary scoped storage permission
|
||||
// Note that even though we get permissions, we need to only write DocumentFile files, not MediaStore files, because the latter are not meant to be reopened in the future so we can lose permission at anytime once they are written once, see: https://github.com/anggrayudi/SimpleStorage/issues/103
|
||||
Log.d("SuperProductivity", "Get Storage Access permission")
|
||||
activity.storageHelper.openFolderPicker(
|
||||
storageHelper.openFolderPicker(
|
||||
// We could also use simpleStorageHelper.requestStorageAccess()
|
||||
initialPath = FileFullPath(
|
||||
activity,
|
||||
|
|
@ -541,9 +550,9 @@ class JavaScriptInterface(
|
|||
)
|
||||
}
|
||||
|
||||
protected fun callJavaScriptFunction(script: String) {
|
||||
activity.callJavaScriptFunction(script)
|
||||
}
|
||||
fun callJavaScriptFunction(script: String) {
|
||||
webView.post { webView.evaluateJavascript(script) { } }
|
||||
}
|
||||
|
||||
companion object {
|
||||
// TODO rename to WINDOW_PROPERTY
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.superproductivity.superproductivity
|
||||
package com.superproductivity.superproductivity.webview
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.util.Log
|
||||
import android.webkit.WebResourceResponse
|
||||
import java.text.SimpleDateFormat
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.superproductivity.superproductivity
|
||||
package com.superproductivity.superproductivity.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
|
|
@ -11,10 +11,16 @@ import android.widget.LinearLayout
|
|||
class WebHelper {
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun instanceView(context: Context) : WebView {
|
||||
fun instanceView(context: Context, modifyUA: Boolean = true) : WebView {
|
||||
val wv = WebView(context)
|
||||
wv.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT)
|
||||
|
||||
return setupView(wv, modifyUA)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun setupView(wv: WebView, modifyUA: Boolean) : WebView {
|
||||
wv.setLayerType(View.LAYER_TYPE_HARDWARE, null)
|
||||
wv.isFocusableInTouchMode = true
|
||||
|
||||
|
|
@ -37,7 +43,13 @@ class WebHelper {
|
|||
// @see https://stackoverflow.com/questions/45863004/how-some-apps-are-able-to-perform-google-login-successfully-in-android-webview
|
||||
// Force links and redirects to open in the WebView instead of in a browser
|
||||
wSettings.javaScriptCanOpenWindowsAutomatically = true
|
||||
wSettings.userAgentString = "Mozilla/5.0 (Linux Android 5.0 SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36"
|
||||
if (modifyUA) {
|
||||
wSettings.userAgentString =
|
||||
"Mozilla/5.0 (Linux Android 5.0 SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36"
|
||||
} else {
|
||||
// IMPORTANT: Do not remove or modify "; wv" in the User-Agent string.
|
||||
// Removing "; wv" prevents Service Workers from registering and functioning correctly in Capacitor.
|
||||
}
|
||||
return wv
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package com.superproductivity.superproductivity.webview
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.util.Log
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
/**
|
||||
* Strip the original WebViewClient logic to ensure that both types share the same logic
|
||||
*/
|
||||
class WebViewRequestHandler(private val activity: Activity, private val serviceHost: String){
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
fun handleUrlLoading(view: WebView, url: String): Boolean {
|
||||
Log.v("TW", url)
|
||||
return if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
if (url.contains("super-productivity.com") || url.contains("localhost") || url.contains(
|
||||
serviceHost
|
||||
)
|
||||
) {
|
||||
false
|
||||
} else {
|
||||
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun interceptWebRequest(request: WebResourceRequest?): WebResourceResponse? {
|
||||
if (request == null || request.isForMainFrame) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (request.url?.path?.contains("assets/icons/favicon") == true) {
|
||||
try {
|
||||
return WebResourceResponse("image/png", null, null)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
if (request.url.toString().contains(serviceHost)) {
|
||||
return null
|
||||
}
|
||||
|
||||
Log.v(
|
||||
"TW",
|
||||
"interceptRequest mf:${request?.isForMainFrame.toString()} ${request.method} ${request?.url}"
|
||||
)
|
||||
|
||||
// since we currently don't have a way to also post the body, we only handle GET, HEAD and OPTIONS requests
|
||||
// see https://github.com/KonstantinSchubert/request_data_webviewclient for a possible solution
|
||||
if (request.method.uppercase() != "GET" && request.method.uppercase() != "OPTIONS" && request.method.uppercase() !="HEAD") {
|
||||
return null
|
||||
}
|
||||
|
||||
// remove user agent header in the hopes that we're treated better by the remotes :D
|
||||
val keysToRemove =
|
||||
request.requestHeaders.keys.filter { it.equals("User-Agent", ignoreCase = true) }
|
||||
for (key in keysToRemove) {
|
||||
request.requestHeaders.remove(key)
|
||||
}
|
||||
|
||||
val client = OkHttpClient()
|
||||
val newRequestBuilder = Request.Builder()
|
||||
.url(request.url.toString())
|
||||
.method(request.method, null)
|
||||
|
||||
// Add each header from the original request to the new request
|
||||
for ((key, value) in request.requestHeaders) {
|
||||
newRequestBuilder.addHeader(key, value)
|
||||
}
|
||||
val newRequest = newRequestBuilder.build()
|
||||
|
||||
// currently we can't handle POST requests since everything
|
||||
if (request.method.uppercase() == "OPTIONS") {
|
||||
Log.v("TW", "OPTIONS request triggered")
|
||||
client.newCall(newRequest).execute().use { response ->
|
||||
Log.v(
|
||||
"TW",
|
||||
"OPTIONS original response: ${response.code} ${response.message} ${response.body?.string()}"
|
||||
)
|
||||
if (response.code != 200) {
|
||||
Log.v("TW", "OPTIONS overwrite")
|
||||
return OptionsAllowResponse.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle HEAD requests
|
||||
if (request.method.uppercase() == "HEAD") {
|
||||
Log.v("TW", "HEAD request triggered")
|
||||
client.newCall(newRequest).execute().use { response ->
|
||||
Log.v("TW", "HEAD response ${response.code} ${response.message}")
|
||||
val responseHeaders = response.headers.names()
|
||||
.associateWith { response.headers(it)?.joinToString() }
|
||||
.toMutableMap()
|
||||
|
||||
val keysToRemoveI = responseHeaders.keys.filter {
|
||||
it.equals("Access-Control-Allow-Origin", ignoreCase = true)
|
||||
}
|
||||
for (key in keysToRemoveI) {
|
||||
responseHeaders.remove(key)
|
||||
}
|
||||
responseHeaders["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
val contentType = response.header("Content-Type", "text/plain")
|
||||
val contentEncoding = response.header("Content-Encoding", "utf-8")
|
||||
val reasonPhrase = response.message.ifEmpty { "OK" }
|
||||
return WebResourceResponse(
|
||||
contentType,
|
||||
contentEncoding,
|
||||
response.code,
|
||||
reasonPhrase,
|
||||
responseHeaders,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Log.v("TW", "exec request ${request.url}")
|
||||
client.newCall(newRequest).execute().use { response ->
|
||||
Log.v("TW", "response ${response.code} ${response.message}")
|
||||
val responseHeaders = response.headers.names()
|
||||
.associateWith { response.headers(it)?.joinToString() }
|
||||
.toMutableMap()
|
||||
|
||||
val keysToRemoveI =
|
||||
responseHeaders.keys.filter {
|
||||
it.equals(
|
||||
"Access-Control-Allow-Origin",
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
for (key in keysToRemoveI) {
|
||||
responseHeaders.remove(key)
|
||||
}
|
||||
responseHeaders["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
val contentType = response.header("Content-Type", "text/plain")
|
||||
val contentEncoding = response.header("Content-Encoding", "utf-8")
|
||||
val inputStream = ByteArrayInputStream(response.body?.bytes())
|
||||
val reasonPhrase =
|
||||
response.message.ifEmpty { "OK" } // provide a default value if the message is null or empty
|
||||
return WebResourceResponse(
|
||||
contentType,
|
||||
contentEncoding,
|
||||
response.code,
|
||||
reasonPhrase,
|
||||
responseHeaders,
|
||||
inputStream
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
buildscript {
|
||||
|
||||
ext.kotlin_version = "1.9.10"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
|
@ -7,7 +8,7 @@ buildscript {
|
|||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.6.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
3
capacitor.settings.gradle
Normal file
3
capacitor.settings.gradle
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
10
fastlane/metadata/android/en-US/changelogs/30.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/30.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
New Connectivity-Free Mode is Here!
|
||||
|
||||
You can now use the core features of the app without an internet connection, offering a smoother and more reliable experience. We've made several key updates to enhance usability:
|
||||
|
||||
· Connectivity-Free Mode Support: Enjoy uninterrupted access to the app's main features without needing a network connection. You can still sync with WebDAV, Dropbox, or choose to work entirely offline without any network access.
|
||||
· Seamless Transition: Existing users will continue using the app in Online-Only Mode (Compatibility Mode) with no disruptions, while new users can immediately benefit from the Connectivity-Free Mode. Future updates will include a smooth migration plan for everyone.
|
||||
· CORS Issues Resolved: Fixed cross-origin resource sharing (CORS) problems, especially for WebDAV sync, ensuring secure and smooth synchronization with local or hosted resources.
|
||||
· Enhanced Security: Strengthened data protection to keep your information secure, even when offline.
|
||||
|
||||
Update now to enjoy these exciting new features and improvements!
|
||||
|
|
@ -1 +1,2 @@
|
|||
include ':app'
|
||||
apply from: 'capacitor.settings.gradle'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue