____MERGING SUPER PRODUCTIVITY ANDROID ____
1
android
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 85f0f5a2a46b113123da76f90e2b75d178d81fc3
|
||||
94
android/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# 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
|
||||
.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
|
||||
|
||||
/app/release
|
||||
/app/fdroid/release
|
||||
/app/play/release
|
||||
/app/src/play/java/com/superproductivity/superproductivity/GoogleClientId.java
|
||||
5
android/.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# GitHub Copilot persisted chat sessions
|
||||
/copilot/chatSessions
|
||||
123
android/.idea/codeStyles/Project.xml
generated
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
android/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
6
android/.idea/compiler.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
10
android/.idea/deploymentTargetDropDown.xml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="app">
|
||||
<State />
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
||||
18
android/.idea/deploymentTargetSelector.xml
generated
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-08-17T08:22:51.115862664Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/johannes/.android/avd/Pixel_4_API_29.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
20
android/.idea/gradle.xml
generated
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<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>
|
||||
30
android/.idea/jarRepositories.xml
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://jitpack.io" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
6
android/.idea/kotlinc.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.10" />
|
||||
</component>
|
||||
</project>
|
||||
10
android/.idea/migrations.xml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
8
android/.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
6
android/.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
4
android/ALWAYS_TAG_RELEASES_FOR_FDROID
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
@see
|
||||
https://gitlab.com/fdroid/rfp/-/issues/1523#note_1310237299
|
||||
https://gitlab.com/-/snippets/1895688
|
||||
21
android/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Johannes Millan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
53
android/README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# super-productivity-android
|
||||
|
||||
Android App for Super Productivity (https://super-productivity.com/).
|
||||
|
||||
I am not an Android developer, so help would be very welcome!!
|
||||
|
||||
## New Connectivity-Free Mode is Here!
|
||||
|
||||
_This feature was added on October 7, 2024. See [Pull Request #57](https://github.com/johannesjo/super-productivity-android/pull/57)._
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
||||
Update now to enjoy these exciting new features and improvements!
|
||||
|
||||
## Launch Modes
|
||||
|
||||
The app supports two launch modes:
|
||||
|
||||
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.
|
||||
|
||||
### Configuring Launch Mode
|
||||
|
||||
To configure the launch mode, adjust the `LAUNCH_MODE` setting in the `app_config.properties` file:
|
||||
|
||||
- **0**: Default behavior (read from SharedPreferences)
|
||||
- **1**: Force Online-Only Mode (Compatibility Mode)
|
||||
- **2**: Force Connectivity-Free Mode (Recommended)
|
||||
|
||||
**Recommendation**: Set `LAUNCH_MODE` to `2` for Connectivity-Free Mode.
|
||||
|
||||
### How to Adjust `LAUNCH_MODE`
|
||||
|
||||
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
|
||||
LAUNCH_MODE=2
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
### 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
android/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).
|
||||
94
android/README_ONLINE.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# 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
android/app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
28
android/app/app_config.properties
Normal file
|
|
@ -0,0 +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.
|
||||
|
||||
# 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
|
||||
ONLINE_SERVICE_IS_LOCAL=false
|
||||
|
||||
# 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
|
||||
ONLINE_SERVICE_HOST=app.super-productivity.com
|
||||
|
||||
# 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 ONLINE_SERVICE_IS_LOCAL is true, the default is http. Otherwise, this value is used.
|
||||
# Default: https
|
||||
ONLINE_SERVICE_PROTOCOL=https
|
||||
95
android/app/build.gradle
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
plugins {
|
||||
|
||||
id "com.android.application"
|
||||
id "org.jetbrains.kotlin.android"
|
||||
}
|
||||
|
||||
// Apply the external configuration script
|
||||
apply from: "config.gradle"
|
||||
|
||||
// Load the application configuration properties
|
||||
def appConfig = loadAppConfig()
|
||||
|
||||
// Retrieve the build configuration fields for Debug and Release builds
|
||||
def debugConfigFields = getBuildConfigFields(appConfig, true)
|
||||
def releaseConfigFields = getBuildConfigFields(appConfig, false)
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId "com.superproductivity.superproductivity"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 34
|
||||
compileSdk 34
|
||||
versionCode 30
|
||||
versionName "30.10.0.11"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
manifestPlaceholders = [
|
||||
hostName : "app.super-productivity.com",
|
||||
defaultUrl : "https://app.super-productivity.com",
|
||||
launcherName : "Super Productivity",
|
||||
assetStatements: '[{ "relation": ["delegate_permission/common.handle_all_urls"], ' +
|
||||
'"target": {"namespace": "web", "site": "https://app.super-productivity.com"}}]'
|
||||
]
|
||||
}
|
||||
|
||||
// Configure the build types (Debug and Release)
|
||||
buildTypes {
|
||||
debug {
|
||||
// Define each field in BuildConfig for the Debug build
|
||||
debugConfigFields.each { key, value ->
|
||||
buildConfigField "String", key, value.toString()
|
||||
}
|
||||
}
|
||||
release {
|
||||
// Define each field in BuildConfig for the Release build
|
||||
releaseConfigFields.each { key, value ->
|
||||
buildConfigField "String", key, value.toString()
|
||||
}
|
||||
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
flavorDimensions "version"
|
||||
productFlavors {
|
||||
play {
|
||||
dimension "version"
|
||||
Properties properties = new Properties()
|
||||
file("google.properties").withInputStream {
|
||||
properties.load(it)
|
||||
}
|
||||
buildConfigField "String", "CLIENT_ID_WEB", "\"" + properties.webToken + "\""
|
||||
}
|
||||
fdroid {
|
||||
dimension "version"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
namespace "com.superproductivity.superproductivity"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
|
||||
implementation "androidx.appcompat:appcompat:1.5.1"
|
||||
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
android/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()
|
||||
}
|
||||
41
android/app/config.gradle
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// config.gradle
|
||||
|
||||
// Load properties from the app_config.properties file
|
||||
ext.loadAppConfig = {
|
||||
// Create a Properties object to hold the app configuration
|
||||
def appConfig = new Properties()
|
||||
|
||||
// Locate the app_config.properties file in the project root
|
||||
def appConfigFile = file("app_config.properties")
|
||||
|
||||
// If the properties file exists, load its contents
|
||||
if (appConfigFile.exists()) {
|
||||
appConfig.load(new FileInputStream(appConfigFile))
|
||||
}
|
||||
|
||||
// Return the loaded properties
|
||||
return appConfig
|
||||
}
|
||||
|
||||
// Determine the build configuration fields based on the build type (Debug or Release)
|
||||
ext.getBuildConfigFields = { appConfig, isDebugBuild ->
|
||||
// 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("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("ONLINE_SERVICE_PROTOCOL", "https")
|
||||
|
||||
// Return the configuration fields as a map
|
||||
return [
|
||||
LAUNCH_MODE: "\"${launchMode}\"",
|
||||
ONLINE_SERVICE_IS_LOCAL: "\"${serviceIsLocal}\"",
|
||||
ONLINE_SERVICE_HOST: "\"${serviceHost}\"",
|
||||
ONLINE_SERVICE_PROTOCOL: "\"${serviceProtocol}\""
|
||||
]
|
||||
}
|
||||
3
android/app/google.properties
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
webToken=XXXXX
|
||||
clientIdDebug=XXX
|
||||
clientIdProd=XXXX
|
||||
21
android/app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.superproductivity.superproductivity;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertEquals("com.superproductivity.superproductivity", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
63
android/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<!-- Set FullscreenActivity as the default launcher -->
|
||||
<activity
|
||||
android:name=".FullscreenActivity"
|
||||
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">
|
||||
|
||||
<!-- This intent-filter adds the Trusted Web Activity to the Android Launcher -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
<action android:name="com.superproductivity.superproductivity.DONE" />
|
||||
<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>
|
||||
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,24 @@
|
|||
package com.superproductivity.superproductivity
|
||||
|
||||
import android.app.Application
|
||||
import com.superproductivity.superproductivity.app.AppLifecycleObserver
|
||||
import com.superproductivity.superproductivity.app.KeyValStore
|
||||
|
||||
class App : Application() {
|
||||
|
||||
// NOTE using the web view like this causes all html5 inputs not to work
|
||||
// val wv: WebView by lazy {
|
||||
// WebHelper().instanceView(this)
|
||||
// }
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
package com.superproductivity.superproductivity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.webkit.JsResult
|
||||
import android.webkit.ServiceWorkerClient
|
||||
import android.webkit.ServiceWorkerController
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.superproductivity.superproductivity.app.LaunchDecider
|
||||
import com.superproductivity.superproductivity.webview.JavaScriptInterface
|
||||
import com.superproductivity.superproductivity.webview.WebHelper
|
||||
import com.superproductivity.superproductivity.webview.WebViewRequestHandler
|
||||
|
||||
|
||||
/**
|
||||
* An example full-screen activity that shows and hides the system UI (i.e.
|
||||
* status bar and navigation/system bar) with user interaction.
|
||||
*/
|
||||
class FullscreenActivity : AppCompatActivity() {
|
||||
private lateinit var javaScriptInterface: JavaScriptInterface
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var wvContainer: FrameLayout
|
||||
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.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
|
||||
//// webView = (application as App).wv
|
||||
// webView = WebHelper().instanceView(this)
|
||||
//// webView = WebView(this)
|
||||
// val data = "<html><body><h1>TEST</h1><h2>aa</h2><input type = 'color' value='#ae1234'>"
|
||||
// webView.settings.javaScriptEnabled = true
|
||||
// webView.loadData(data, "text/html; charset=utf-8", "UTF-8")
|
||||
// webView.loadDataWithBaseURL(null, data, "text/html", "UTF-8", null)
|
||||
|
||||
|
||||
setContentView(R.layout.activity_fullscreen)
|
||||
wvContainer = findViewById(R.id.webview_container)
|
||||
wvContainer.addView(webView)
|
||||
if (savedInstanceState != null) {
|
||||
webView.restoreState(savedInstanceState)
|
||||
} else {
|
||||
webView.loadUrl(appUrl)
|
||||
}
|
||||
|
||||
|
||||
val rootView = findViewById<View>(android.R.id.content)
|
||||
rootView.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val rect = Rect()
|
||||
rootView.getWindowVisibleDisplayFrame(rect)
|
||||
val screenHeight = rootView.rootView.height
|
||||
|
||||
// rect.bottom is the position above soft keypad or device button.
|
||||
// if keypad is shown, the rect.bottom is smaller than the screen height.
|
||||
val keypadHeight = screenHeight - rect.bottom
|
||||
// 0.15 ratio is perhaps enough to determine keypad height.
|
||||
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)
|
||||
webView.saveState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
// Restore scoped storage permission on Android 10+
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
storageHelper.onRestoreInstanceState(savedInstanceState)
|
||||
webView.restoreState(savedInstanceState);
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.v("TW", "FullScreenActivity: onPause")
|
||||
callJSInterfaceFunctionIfExists("next", "onPause$")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.v("TW", "FullScreenActivity: onResume")
|
||||
callJSInterfaceFunctionIfExists("next", "onResume$")
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
Log.v("TW", "FullScreenActivity: onNewIntent")
|
||||
val action = intent.getStringExtra("action")
|
||||
Log.v("TW", "FullScreenActivity: action $action")
|
||||
if (action == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun initWebView() {
|
||||
webView = WebHelper().instanceView(this)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Toast.makeText(this, "DEBUG: $appUrl", Toast.LENGTH_SHORT).show()
|
||||
// webView.clearCache(true)
|
||||
// webView.clearHistory()
|
||||
WebView.setWebContentsDebuggingEnabled(true); // necessary to enable chrome://inspect of webviews on physical remote Android devices, but not for AVD emulator, as the latter automatically enables debug build features
|
||||
}
|
||||
|
||||
webView.loadUrl(appUrl)
|
||||
supportActionBar?.hide()
|
||||
javaScriptInterface = JavaScriptInterface(this, webView, storageHelper)
|
||||
webView.addJavascriptInterface(javaScriptInterface, WINDOW_INTERFACE_PROPERTY)
|
||||
if (BuildConfig.FLAVOR.equals("fdroid")) {
|
||||
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")
|
||||
}
|
||||
|
||||
val swController = ServiceWorkerController.getInstance()
|
||||
swController.setServiceWorkerClient(@RequiresApi(Build.VERSION_CODES.N)
|
||||
object : ServiceWorkerClient() {
|
||||
override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
|
||||
return webViewRequestHandler.interceptWebRequest(request)
|
||||
}
|
||||
})
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
@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? {
|
||||
return webViewRequestHandler.interceptWebRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun onJsAlert(
|
||||
view: WebView,
|
||||
url: String,
|
||||
message: String,
|
||||
result: JsResult
|
||||
): Boolean {
|
||||
Log.v("TW", "onJsAlert")
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(this@FullscreenActivity)
|
||||
builder.setMessage(message)
|
||||
.setNeutralButton("OK") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
result.cancel()
|
||||
return super.onJsAlert(view, url, message, result)
|
||||
}
|
||||
|
||||
override fun onJsConfirm(
|
||||
view: WebView,
|
||||
url: String,
|
||||
message: String,
|
||||
result: JsResult
|
||||
): Boolean {
|
||||
AlertDialog.Builder(this@FullscreenActivity)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> result.confirm() }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> result.cancel() }
|
||||
.create()
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
private fun callJSInterfaceFunctionIfExists(fnName: String, objectPath: String, fnParam: String = "") {
|
||||
val fnFullName = "window.$WINDOW_INTERFACE_PROPERTY.$objectPath.$fnName"
|
||||
val fullObjectPath = "window.$WINDOW_INTERFACE_PROPERTY.$objectPath"
|
||||
javaScriptInterface.callJavaScriptFunction("if($fullObjectPath && $fnFullName)$fnFullName($fnParam)")
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
Log.v("TW", "onBackPressed ${webView.canGoBack().toString()}")
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
// Ensure wvContainer is initialized before removing the view
|
||||
if (::wvContainer.isInitialized) {
|
||||
wvContainer.removeView(webView)
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WINDOW_INTERFACE_PROPERTY: String = "SUPAndroid"
|
||||
const val WINDOW_PROPERTY_F_DROID: String = "SUPFDroid"
|
||||
}
|
||||
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
// Restore scoped storage permission on Android 10+
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||
//storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
|
@ -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!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.superproductivity.superproductivity.app
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
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) {
|
||||
|
||||
// private static final String CREATE_TABLE = "CREATE TABLE supKeyValStore(TEXT PRIMARY KEY,VALUE TEXT,KEY_CREATED_AT DATETIME)"
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase?) {
|
||||
Log.v(TAG, "onCreate")
|
||||
db?.execSQL(CREATE_TABLE)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase?, p1: Int, p2: Int) {
|
||||
Log.v(TAG, "onUpgrade")
|
||||
db?.execSQL("DROP TABLE IF EXISTS $DATABASE_TABLE")
|
||||
onCreate(db)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter method. Sets a (key, value) pair in sqlite3 db.
|
||||
*
|
||||
* @param key The URL or some other unique id for data can be used
|
||||
* @param value String data to be saved
|
||||
* @return rowid of the insertion row
|
||||
*/
|
||||
@Synchronized
|
||||
fun set(key: String, value: String?): Long {
|
||||
val newKey = DatabaseUtils.sqlEscapeString(key)
|
||||
Log.v(TAG, "setting db value: $newKey")
|
||||
val dbHelper = (context.applicationContext as App).keyValStore
|
||||
val db = dbHelper.writableDatabase
|
||||
var row = 0L
|
||||
if (db != null) {
|
||||
val values = ContentValues()
|
||||
values.put(KEY, newKey)
|
||||
values.put(VALUE, value)
|
||||
values.put(KEY_CREATED_AT, "time('now')")
|
||||
row = db.replace(DATABASE_TABLE, null, values)
|
||||
Log.v(TAG, "save db value size: " + value?.length)
|
||||
db.close()
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key The URL or some other unique id for data can be used
|
||||
* @param defaultValue value to be returned in case something goes wrong or no data is found
|
||||
* @return value stored in DB if present, defaultValue otherwise.
|
||||
*/
|
||||
@Synchronized
|
||||
fun get(key: String, defaultValue: String): String {
|
||||
val newKey = DatabaseUtils.sqlEscapeString(key)
|
||||
Log.v(TAG, "getting db value: $newKey")
|
||||
val dbHelper = (context.applicationContext as App).keyValStore
|
||||
var value = defaultValue
|
||||
dbHelper.readableDatabase?.let { database ->
|
||||
database.query(
|
||||
DATABASE_TABLE, arrayOf(VALUE), "$KEY=?", arrayOf(newKey), null, null, null
|
||||
)?.let { cursor ->
|
||||
if (cursor.moveToNext()) {
|
||||
value = cursor.getString(cursor.getColumnIndexOrThrow(VALUE))
|
||||
}
|
||||
Log.v(TAG, "get db value size:" + value.length)
|
||||
cursor.close()
|
||||
}
|
||||
database.close()
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun clearAll(context: Context) {
|
||||
val dbHelper = (context.applicationContext as App).keyValStore
|
||||
val db = dbHelper.writableDatabase
|
||||
if (db != null) {
|
||||
db.delete(DATABASE_TABLE, null, null)
|
||||
Log.v(TAG, "cleared db ")
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DATABASE_TABLE: String = "supKeyValStore"
|
||||
private const val DATABASE_VERSION: Int = 1
|
||||
private const val KEY: String = "KEY"
|
||||
private const val DATABASE_NAME: String = "SupKeyValStore"
|
||||
private const val VALUE: String = "VALUE"
|
||||
private const val KEY_CREATED_AT: String = "KEY_CREATED_AT"
|
||||
private const val TAG: String = "SupKeyValStore"
|
||||
private const val CREATE_TABLE =
|
||||
("CREATE TABLE $DATABASE_TABLE($KEY TEXT PRIMARY KEY,$VALUE TEXT,$KEY_CREATED_AT DATETIME)")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.superproductivity.superproductivity.app
|
||||
|
||||
data class SpTask(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val category: String,
|
||||
val categoryHtml: String,
|
||||
val isDone: Boolean
|
||||
)
|
||||
|
|
@ -0,0 +1,561 @@
|
|||
package com.superproductivity.superproductivity.webview
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.BitmapFactory
|
||||
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
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.Writer
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.cert.CertPathValidatorException
|
||||
import java.util.Locale
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
class JavaScriptInterface(
|
||||
private val activity: Activity,
|
||||
private val webView: WebView,
|
||||
private val storageHelper: SimpleStorageHelper
|
||||
) {
|
||||
|
||||
/**
|
||||
* Instantiate the interface and set the context
|
||||
*/
|
||||
open fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
|
||||
// Additional callback for scoped storage permission management on Android 10+
|
||||
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||
Log.d("SuperProductivity", "onActivityResult")
|
||||
storageHelper.storage.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun showToast(toast: String) {
|
||||
Toast.makeText(activity, toast, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun updateTaskData(str: String) {
|
||||
Log.w("TW", "JavascriptInterface: updateTaskData")
|
||||
// val intent = Intent(activity.applicationContext, TaskListWidget::class.java)
|
||||
// intent.action = TaskListWidget.LIST_CHANGED
|
||||
// intent.putExtra("taskJson", str)
|
||||
// (activity.application as App).dataHolder.data = str
|
||||
// activity.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
// TODO remove for good after a while
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun updatePermanentNotification(title: String, message: String, progress: Int) {
|
||||
Log.w("TW", "JavascriptInterface: REMOVED updateNotificationWidget")
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
open fun triggerGetGoogleToken() {
|
||||
// NOTE: empty here, and only filled for google build flavor
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
// LEGACY
|
||||
fun saveToDbNew(requestId: String, key: String, value: String) {
|
||||
(activity.application as App).keyValStore.set(key, value)
|
||||
callJavaScriptFunction(FN_PREFIX + "saveToDbCallback('" + requestId + "')")
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
// LEGACY
|
||||
fun loadFromDbNew(requestId: String, key: String) {
|
||||
val r = (activity.application as App).keyValStore.get(key, "")
|
||||
// NOTE: ' are important as otherwise the json messes up
|
||||
callJavaScriptFunction(FN_PREFIX + "loadFromDbCallback('" + requestId + "', '" + key + "', '" + r + "')")
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun removeFromDb(requestId: String, key: String) {
|
||||
(activity.application as App).keyValStore.set(key, null)
|
||||
callJavaScriptFunction(FN_PREFIX + "removeFromDbCallback('" + requestId + "')")
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun clearDb(requestId: String) {
|
||||
(activity.application as App).keyValStore.clearAll(activity)
|
||||
callJavaScriptFunction(FN_PREFIX + "clearDbCallback('" + requestId + "')")
|
||||
}
|
||||
|
||||
|
||||
// TODO: legacy remove in future version, but no the next release
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun saveToDb(key: String, value: String) {
|
||||
(activity.application as App).keyValStore.set(key, value)
|
||||
callJavaScriptFunction("window.saveToDbCallback()")
|
||||
}
|
||||
|
||||
// TODO: legacy remove in future version, but no the next release
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun loadFromDb(key: String) {
|
||||
val r = (activity.application as App).keyValStore.get(key, "")
|
||||
// NOTE: ' are important as otherwise the json messes up
|
||||
callJavaScriptFunction("window.loadFromDbCallback('$key', '$r')")
|
||||
}
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun showNotificationIfAppIsNotOpen(title: String, body: String) {
|
||||
if (!AppLifecycleObserver.getInstance().isInForeground) {
|
||||
showNotification(title, body)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun showNotification(title: String, body: String) {
|
||||
Log.d("TW", "title $title")
|
||||
Log.d("TW", "body $body")
|
||||
|
||||
val mBuilder: NotificationCompat.Builder = NotificationCompat.Builder(
|
||||
activity.applicationContext, "SUP_CHANNEL_ID"
|
||||
)
|
||||
mBuilder.build().flags = mBuilder.build().flags or Notification.FLAG_AUTO_CANCEL
|
||||
|
||||
val ii = Intent(activity.applicationContext, FullscreenActivity::class.java)
|
||||
val pendingIntentFlags: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(activity, 0, ii, pendingIntentFlags)
|
||||
|
||||
// Title
|
||||
mBuilder.setContentTitle(title)
|
||||
val bigText: NotificationCompat.BigTextStyle = NotificationCompat.BigTextStyle()
|
||||
bigText.setBigContentTitle(title)
|
||||
|
||||
// Body
|
||||
if (body.isNotEmpty() && body.trim() != "undefined") {
|
||||
mBuilder.setContentText(body)
|
||||
bigText.bigText(body)
|
||||
}
|
||||
|
||||
mBuilder.setStyle(bigText)
|
||||
mBuilder.setContentIntent(pendingIntent)
|
||||
mBuilder.setSmallIcon(R.mipmap.ic_launcher)
|
||||
mBuilder.setLargeIcon(
|
||||
BitmapFactory.decodeResource(
|
||||
activity.resources, R.mipmap.ic_launcher
|
||||
)
|
||||
)
|
||||
mBuilder.setSmallIcon(R.drawable.ic_stat_sp)
|
||||
mBuilder.priority = Notification.PRIORITY_MAX
|
||||
mBuilder.setAutoCancel(true)
|
||||
|
||||
val mNotificationManager =
|
||||
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channelId = "SUP_CHANNEL_ID"
|
||||
val channel = NotificationChannel(
|
||||
channelId, "Super Productivity", NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
mNotificationManager.createNotificationChannel(channel)
|
||||
mBuilder.setChannelId(channelId)
|
||||
}
|
||||
mNotificationManager.notify(0, mBuilder.build())
|
||||
}
|
||||
|
||||
private fun readFully(inputStream: InputStream): ByteArray {
|
||||
val buffer = ByteArrayOutputStream()
|
||||
var nRead: Int
|
||||
val data = ByteArray(16384)
|
||||
nRead = inputStream.read(data, 0, data.size)
|
||||
while (nRead != -1) {
|
||||
buffer.write(data, 0, nRead)
|
||||
nRead = inputStream.read(data, 0, data.size)
|
||||
}
|
||||
return buffer.toByteArray()
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun makeHttpRequest(
|
||||
requestId: String,
|
||||
urlString: String,
|
||||
method: String,
|
||||
data: String,
|
||||
username: String,
|
||||
password: String,
|
||||
readResponse: String
|
||||
) {
|
||||
Log.d("TW", "$requestId $urlString $method $data $username $readResponse")
|
||||
var status: Int
|
||||
var statusText: String
|
||||
var resultData = ""
|
||||
val headers = JSONObject()
|
||||
val doInput = readResponse.toBoolean()
|
||||
try {
|
||||
val url = URL(urlString)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
if (username.isNotEmpty() && password.isNotEmpty()) {
|
||||
val auth = "$username:$password"
|
||||
val encodedAuth = Base64.encodeToString(auth.toByteArray(), Base64.NO_WRAP)
|
||||
connection.setRequestProperty(/* key = */ "Authorization", /* value = */
|
||||
"Basic $encodedAuth"
|
||||
)
|
||||
}
|
||||
connection.requestMethod = method
|
||||
connection.setRequestProperty("Content-Type", "application/octet-stream")
|
||||
connection.doInput = doInput
|
||||
if (data.isNotEmpty()) {
|
||||
connection.doOutput = true
|
||||
val bytes = data.toByteArray()
|
||||
connection.setFixedLengthStreamingMode(bytes.size)
|
||||
val out = BufferedOutputStream(connection.outputStream)
|
||||
out.write(bytes)
|
||||
out.flush()
|
||||
out.close()
|
||||
}
|
||||
connection.headerFields.entries.forEach { entry ->
|
||||
val output = entry.value.joinToString(separator = ", ")
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.2
|
||||
try {
|
||||
if (entry.key != null)
|
||||
headers.put(entry.key.lowercase(Locale.ROOT), output)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
status = connection.responseCode
|
||||
statusText = connection.responseMessage
|
||||
val out: ByteArray
|
||||
if (status in 200..299 && doInput) {
|
||||
val inputStream = connection.inputStream
|
||||
out = readFully(inputStream)
|
||||
inputStream.close()
|
||||
} else {
|
||||
out = ByteArray(0)
|
||||
}
|
||||
connection.disconnect()
|
||||
resultData = String(out, StandardCharsets.UTF_8)
|
||||
} catch (e: MalformedURLException) {
|
||||
e.printStackTrace()
|
||||
status = -1
|
||||
statusText = "Malformed URL"
|
||||
} catch (e: SSLHandshakeException) {
|
||||
e.printStackTrace()
|
||||
var cause = e.cause
|
||||
while (cause != null && cause !is CertPathValidatorException) {
|
||||
cause = cause.cause
|
||||
}
|
||||
if (cause != null) {
|
||||
val validationException = cause as CertPathValidatorException
|
||||
val message = StringBuilder("Failed trust path:")
|
||||
validationException.certPath.certificates.forEach { certificate ->
|
||||
message.append("\n")
|
||||
message.append(certificate.toString())
|
||||
}
|
||||
Log.e("TW", message.toString())
|
||||
}
|
||||
status = -2
|
||||
statusText = "SSL Handshake Error"
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
status = -3
|
||||
statusText = "Network Error"
|
||||
} catch (e: ClassCastException) {
|
||||
e.printStackTrace()
|
||||
status = -4
|
||||
statusText = "Unsupported Protocol"
|
||||
}
|
||||
val result = JSONObject()
|
||||
try {
|
||||
result.put("data", resultData)
|
||||
result.put("status", status)
|
||||
result.put("headers", headers)
|
||||
result.put("statusText", statusText)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
Log.d("TW", "$requestId: $result")
|
||||
callJavaScriptFunction(FN_PREFIX + "makeHttpRequestCallback('" + requestId + "', " + result + ")")
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun getFileRev(filePath: String): String {
|
||||
Log.d("SuperProductivity", "getFileRev")
|
||||
// Get folder path
|
||||
val sp = activity.getPreferences(Context.MODE_PRIVATE)
|
||||
val folderPath = sp.getString("filesyncFolder", "") ?: ""
|
||||
// Build fullFilePath from folder path and filepath
|
||||
val fullFilePath = "$folderPath/$filePath"
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Scoped storage permission management for Android 10+
|
||||
// Load file
|
||||
val file = DocumentFileCompat.fromFullPath(activity, fullFilePath, requiresWriteAccess=false)
|
||||
// Get last modified date
|
||||
val lastModif = file?.lastModified().toString()
|
||||
Log.d("SuperProductivity", "getFileRev lastModified: $lastModif")
|
||||
lastModif
|
||||
} else {
|
||||
val file = File(fullFilePath)
|
||||
// Get last modified date
|
||||
val lastModif = file.lastModified().toString()
|
||||
Log.d("SuperProductivity", "getFileRev lastModified: $lastModif")
|
||||
lastModif
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun readFile(filePath: String): String {
|
||||
// Read a file, most likely the filesync database
|
||||
Log.d("SuperProductivity", "readFile")
|
||||
|
||||
// Get folder path
|
||||
val sp = activity.getPreferences(Context.MODE_PRIVATE)
|
||||
val folderPath = sp.getString("filesyncFolder", "") ?: ""
|
||||
// Build fullFilePath from folder path and filepath
|
||||
val fullFilePath = "$folderPath/$filePath"
|
||||
Log.d(
|
||||
"SuperProductivity",
|
||||
"readFile: trying to read from fullFilePath: " + fullFilePath
|
||||
)
|
||||
// Open file in read only mode and an InputStream
|
||||
// Make a reader pointing to the input file
|
||||
val reader =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Scoped storage permission management for Android 10+
|
||||
val file = DocumentFileCompat.fromFullPath(
|
||||
activity,
|
||||
fullFilePath,
|
||||
requiresWriteAccess = false
|
||||
)
|
||||
file?.openInputStream(activity)?.reader()
|
||||
} else {
|
||||
// Older versions of Android <= 9 don't need scoped storage management
|
||||
try {
|
||||
BufferedReader(FileReader(fullFilePath))
|
||||
} catch (e: Exception) {
|
||||
// File does not exist, that's normal if it's the first time, we simply return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Use a StringBuilder to rebuild the input file's content but replace the line returns with current OS's line returns
|
||||
val sb: String =
|
||||
if (reader == null) {
|
||||
Log.d("SuperProductivity", "readFile warning: tried to open file, but file does not exist or we do not have permission! This may be normal if file does not exist yet, it will be created when some tasks will be added.")
|
||||
""
|
||||
} else {
|
||||
// Read input file
|
||||
try {
|
||||
reader.readText()
|
||||
} catch (e: Exception) {
|
||||
Log.d("SuperProductivity", "readFile error: " + e.stackTraceToString())
|
||||
// Return an empty string if there is an error (maybe file does not exist yet)
|
||||
""
|
||||
} finally {
|
||||
reader.close()
|
||||
}
|
||||
}
|
||||
// Return file's content
|
||||
return sb
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun writeFile(filePath: String, data: String) {
|
||||
Log.d("SuperProductivity", "writeFile: trying to save to filePath: " + filePath)
|
||||
// Get folder path
|
||||
val sp = activity.getPreferences(Context.MODE_PRIVATE)
|
||||
val folderPath = sp.getString("filesyncFolder", "") ?: ""
|
||||
// Build fullFilePath from folder path and filepath
|
||||
val fullFilePath = "$folderPath/$filePath"
|
||||
Log.d("SuperProductivity", "writeFile: trying to save to fullFilePath: " + fullFilePath)
|
||||
// Scoped storage permission management for Android 10+, but also works for Android < 10
|
||||
// Open file with write access, using SimpleStorage helper wrapper DocumentFileCompat
|
||||
var file = DocumentFileCompat.fromFullPath(activity, fullFilePath, requiresWriteAccess=true, considerRawFile=true)
|
||||
if ((file == null) || (!file.exists())) { // if file does not exist, we create it
|
||||
Log.d("SuperProductivity", "writeFile: file does not exist, try to create it")
|
||||
val folder = DocumentFileCompat.fromFullPath(activity, folderPath, requiresWriteAccess=true)
|
||||
Log.d("SuperProductivity", "writeFile: do we have access to parentFolder? " + folder.toString())
|
||||
file = folder!!.makeFile(activity, filePath, mode=CreateMode.REPLACE) // do NOT specify a mimeType, otherwise Android will force a file extension
|
||||
}
|
||||
Log.d("SuperProductivity", "writeFile: erase file content by recreating it")
|
||||
file = file?.recreateFile(activity) // erase content first by recreating file. For some reason, DocumentFileCompat.fromFullPath(requiresWriteAccess=true) and openOutputStream(append=false) only open the file in append mode, so we need to recreate the file to truncate its content first
|
||||
// Open a writer to an OutputStream to the file without append mode (so we write from the start of the file)
|
||||
Log.d("SuperProductivity", "writeFile: try to openOutputStream")
|
||||
val writer: Writer =file?.openOutputStream(activity, append=false)!!.writer()
|
||||
try {
|
||||
Log.d("SuperProductivity", "writeFile: try to write data into file: $data")
|
||||
writer.write(data)
|
||||
Log.d("SuperProductivity", "writeFile: write apparently successful!")
|
||||
} catch (e: Exception) {
|
||||
Log.d("SuperProductivity", "writeFile error: " + e.stackTraceToString())
|
||||
} finally {
|
||||
writer.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun allowedFolderPath(): String {
|
||||
val grantedPaths = DocumentFileCompat.getAccessibleAbsolutePaths(activity)
|
||||
Log.d("SuperProductivity", "allowedFolderPath grantedPaths: " + grantedPaths.toString())
|
||||
val sp = activity.getPreferences(Context.MODE_PRIVATE)
|
||||
val folderPath = sp.getString("filesyncFolder", "") ?: ""
|
||||
Log.d("SuperProductivity", "allowedFolderPath filesyncFolder: $folderPath")
|
||||
|
||||
val pathGranted: Boolean =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// If scoped storage, check if stored path is in the list of granted path, if true, then return it, else return an empty string
|
||||
if (grantedPaths.isNullOrEmpty() || folderPath.isEmpty()) {
|
||||
// list of granted paths is empty, then we have no permission
|
||||
false
|
||||
} else {
|
||||
// otherwise we loop through each path in the granted paths list, and check if the currently selected folderPath is a subfolder of a granted path
|
||||
val vpaths: List<String> = grantedPaths.values.toList().flatten()
|
||||
Log.d("SuperProductivity", "allowedFolderPath flattened values: $vpaths")
|
||||
var innerCond: Boolean = false
|
||||
for (p in vpaths) {
|
||||
if (folderPath.contains(p)) { // granted path is always a root path and hence a parent path to a user selected folderPath
|
||||
innerCond = true
|
||||
break
|
||||
}
|
||||
}
|
||||
innerCond
|
||||
}
|
||||
} else {
|
||||
// For older versions of Android, check if we have access to any folder
|
||||
val permissionRead = ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
val permissionWrite = ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
|
||||
(permissionRead == PackageManager.PERMISSION_GRANTED) && (permissionWrite == PackageManager.PERMISSION_GRANTED)
|
||||
}
|
||||
Log.d("SuperProductivity", "allowedFolderPath folderPath.isNotEmpty(): ${folderPath.isNotEmpty()} pathGranted: ${pathGranted.toString()}")
|
||||
return if (folderPath.isNotEmpty() && pathGranted) {
|
||||
folderPath
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun isGrantedFilePermission(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val grantedPaths = DocumentFileCompat.getAccessibleAbsolutePaths(activity)
|
||||
Log.d("SuperProductivity", "isGrantedFilePermission grantedPaths: " + grantedPaths.toString())
|
||||
/*
|
||||
val sp = activity.getPreferences(Context.MODE_PRIVATE)
|
||||
val folderPath = sp.getString("filesyncFolder", "") ?: ""
|
||||
Log.d("SuperProductivity", "isGrantedFilePermission filesyncFolder: $folderPath")
|
||||
*/
|
||||
grantedPaths.isNotEmpty()
|
||||
} else {
|
||||
val result = ContextCompat.checkSelfPermission(
|
||||
activity, Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
val result1 = ContextCompat.checkSelfPermission(
|
||||
activity, Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
result == PackageManager.PERMISSION_GRANTED && result1 == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JavascriptInterface
|
||||
fun grantFilePermission(requestId: String) {
|
||||
// For Android < 10, ask for permission to access the whole storage
|
||||
/* DEPRECATED: if we use this to get the permissions, then we need to use another folder picker than SimpleStorage, because otherwise SimpleStorage also asks for permissions, but Android does not accept asking for two different set of permissions in a single call: "Can reqeust only one set of permissions at a time"
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity, arrayOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
), 1
|
||||
)
|
||||
}
|
||||
*/
|
||||
// For Android >= 10, use scoped storage via SimpleStorage library to get the permission to write files in a folder
|
||||
// For Android < 10, SimpleStorage serves as a simple folder path picker, so that we still save where the user want look for a database file
|
||||
// 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
|
||||
storageHelper.onFolderSelected =
|
||||
{ requestCode, root -> // could also use simpleStorageHelper.onStorageAccessGranted()
|
||||
Log.d("SuperProductivity", "Success Folder Pick! Now saving...")
|
||||
// Get absolute path to folder
|
||||
val fpath = root.getAbsolutePath(activity)
|
||||
// Open preferences to save folder to path
|
||||
val sp = activity.getPreferences(Context.MODE_PRIVATE)
|
||||
sp.edit().putString("filesyncFolder", fpath).apply()
|
||||
// Once permissions are granted, callback web application to continue execution
|
||||
callJavaScriptFunction(
|
||||
FN_PREFIX + "grantFilePermissionCallBack('" + requestId + "')"
|
||||
)
|
||||
}
|
||||
// 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")
|
||||
storageHelper.openFolderPicker(
|
||||
// We could also use simpleStorageHelper.requestStorageAccess()
|
||||
initialPath = FileFullPath(
|
||||
activity,
|
||||
StorageId.PRIMARY,
|
||||
"SupProd"
|
||||
), // SimpleStorage.externalStoragePath if we want to default to sdcard
|
||||
// to force pick a specific folder and none others, use these arguments for simpleStorageHelper.requestStorageAccess():
|
||||
//expectedStorageType = StorageType.EXTERNAL,
|
||||
//expectedBasePath = "SupProd"
|
||||
)
|
||||
}
|
||||
|
||||
fun callJavaScriptFunction(script: String) {
|
||||
webView.post { webView.evaluateJavascript(script) { } }
|
||||
}
|
||||
|
||||
companion object {
|
||||
// TODO rename to WINDOW_PROPERTY
|
||||
const val FN_PREFIX: String = "window.$WINDOW_INTERFACE_PROPERTY."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.superproductivity.superproductivity.webview
|
||||
|
||||
import android.util.Log
|
||||
import android.webkit.WebResourceResponse
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class OptionsAllowResponse {
|
||||
companion object {
|
||||
private val formatter = SimpleDateFormat("E, dd MMM yyyy kk:mm:ss", Locale.US)
|
||||
|
||||
fun build(): WebResourceResponse {
|
||||
Log.v("TW", "OptionsAllowResponse: OPTIONS build ")
|
||||
val date = Date()
|
||||
val dateString = formatter.format(date)
|
||||
|
||||
val headers = mapOf(
|
||||
"Connection" to "close",
|
||||
"Content-Type" to "text/plain",
|
||||
"Date" to "$dateString GMT",
|
||||
"Access-Control-Allow-Origin" to "*",
|
||||
"Access-Control-Allow-Methods" to "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS",
|
||||
"Access-Control-Max-Age" to "7200",
|
||||
"Access-Control-Allow-Credentials" to "true",
|
||||
"Access-Control-Allow-Headers" to "accept, authorization, Content-Type",
|
||||
// "Via" to "1.1 vegur"
|
||||
)
|
||||
|
||||
return WebResourceResponse("text/plain", "UTF-8", 200, "OK", headers, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.superproductivity.superproductivity.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.LinearLayout
|
||||
|
||||
|
||||
class WebHelper {
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
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
|
||||
|
||||
// additional web view settings
|
||||
val wSettings = wv.settings
|
||||
|
||||
wSettings.javaScriptEnabled = true
|
||||
wSettings.loadsImagesAutomatically = true
|
||||
wSettings.domStorageEnabled = true
|
||||
wSettings.loadWithOverviewMode = true
|
||||
wSettings.databaseEnabled = true
|
||||
wSettings.allowFileAccess = true
|
||||
wSettings.setGeolocationEnabled(true)
|
||||
wSettings.mediaPlaybackRequiresUserGesture = false
|
||||
wSettings.javaScriptCanOpenWindowsAutomatically = true
|
||||
wSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
wSettings.allowUniversalAccessFromFileURLs = true
|
||||
wSettings.allowContentAccess = true
|
||||
// allow google login
|
||||
// @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
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
android/app/src/main/res/drawable-anydpi-v24/ic_add.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group android:scaleX="1.0132159"
|
||||
android:scaleY="1.0132159"
|
||||
android:translateX="-0.1585903"
|
||||
android:translateY="-0.1585903">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</group>
|
||||
</vector>
|
||||
15
android/app/src/main/res/drawable-anydpi-v24/ic_done.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group android:scaleX="1.0132159"
|
||||
android:scaleY="1.0132159"
|
||||
android:translateX="-0.1585903"
|
||||
android:translateY="-0.1585903">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
|
||||
</group>
|
||||
</vector>
|
||||
15
android/app/src/main/res/drawable-anydpi-v24/ic_pause.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group android:scaleX="1.0132159"
|
||||
android:scaleY="1.0132159"
|
||||
android:translateX="-0.1585903"
|
||||
android:translateY="-0.1585903">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group android:scaleX="1.0132159"
|
||||
android:scaleY="1.0132159"
|
||||
android:translateX="-0.1585903"
|
||||
android:translateY="-0.1585903">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8,5v14l11,-7z"/>
|
||||
</group>
|
||||
</vector>
|
||||
20
android/app/src/main/res/drawable-anydpi-v24/ic_stat_sp.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group android:scaleX="0.27037618"
|
||||
android:scaleY="0.27037618"
|
||||
android:translateX="3.3479624"
|
||||
android:translateY="3.3479624">
|
||||
<path
|
||||
android:pathData="M0.6554,34.5828 L32.4523,63.8553 63.7915,0.9436 30.2226,47.722Z"
|
||||
android:strokeLineJoin="bevel"
|
||||
android:strokeWidth="1.46761858"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeLineCap="butt"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<group android:scaleX="1.0132159"
|
||||
android:scaleY="1.0132159"
|
||||
android:translateX="-0.1585903"
|
||||
android:translateY="-0.1585903">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
|
||||
</group>
|
||||
</vector>
|
||||
BIN
android/app/src/main/res/drawable-hdpi/ic_add.png
Normal file
|
Before Width: | Height: | Size: 195 B After Width: | Height: | Size: 195 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_done.png
Normal file
|
Before Width: | Height: | Size: 247 B After Width: | Height: | Size: 247 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_pause.png
Normal file
|
Before Width: | Height: | Size: 247 B After Width: | Height: | Size: 247 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_stat_play.png
Normal file
|
Before Width: | Height: | Size: 254 B After Width: | Height: | Size: 254 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_stat_sp.png
Normal file
|
Before Width: | Height: | Size: 333 B After Width: | Height: | Size: 333 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_stat_sync.png
Normal file
|
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 441 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_add.png
Normal file
|
Before Width: | Height: | Size: 116 B After Width: | Height: | Size: 116 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_done.png
Normal file
|
Before Width: | Height: | Size: 126 B After Width: | Height: | Size: 126 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_pause.png
Normal file
|
Before Width: | Height: | Size: 126 B After Width: | Height: | Size: 126 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_stat_play.png
Normal file
|
Before Width: | Height: | Size: 201 B After Width: | Height: | Size: 201 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_stat_sp.png
Normal file
|
Before Width: | Height: | Size: 246 B After Width: | Height: | Size: 246 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_stat_sync.png
Normal file
|
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/ic_add.png
Normal file
|
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 181 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_done.png
Normal file
|
Before Width: | Height: | Size: 400 B After Width: | Height: | Size: 400 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_pause.png
Normal file
|
Before Width: | Height: | Size: 400 B After Width: | Height: | Size: 400 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_stat_play.png
Normal file
|
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_stat_sp.png
Normal file
|
Before Width: | Height: | Size: 524 B After Width: | Height: | Size: 524 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_stat_sync.png
Normal file
|
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 603 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_add.png
Normal file
|
Before Width: | Height: | Size: 333 B After Width: | Height: | Size: 333 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_done.png
Normal file
|
Before Width: | Height: | Size: 598 B After Width: | Height: | Size: 598 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_pause.png
Normal file
|
Before Width: | Height: | Size: 598 B After Width: | Height: | Size: 598 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_stat_play.png
Normal file
|
Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 439 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_stat_sp.png
Normal file
|
Before Width: | Height: | Size: 795 B After Width: | Height: | Size: 795 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_stat_sync.png
Normal file
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 917 B |
74
android/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path android:fillColor="#008577"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
19
android/app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group android:scaleX="0.91125"
|
||||
android:scaleY="0.91125"
|
||||
android:translateX="24.84"
|
||||
android:translateY="24.84">
|
||||
<path
|
||||
android:pathData="M0.6554,34.5828 L32.4523,63.8553 63.7915,0.9436 30.2226,47.722Z"
|
||||
android:strokeLineJoin="bevel"
|
||||
android:strokeWidth="1.46761858"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeLineCap="butt"/>
|
||||
</group>
|
||||
</vector>
|
||||
14
android/app/src/main/res/drawable/ic_sp.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M0.6554,34.5828 L32.4523,63.8553 63.7915,0.9436 30.2226,47.722Z"
|
||||
android:strokeLineJoin="bevel"
|
||||
android:strokeWidth="1.46761858"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
||||
6
android/app/src/main/res/drawable/list_item_selector.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- <item android:drawable="@drawable/item_selected" android:state_pressed="true" />-->
|
||||
<!-- <item android:drawable="@drawable/item_focused" android:state_focused="true" />-->
|
||||
<!-- <item android:drawable="@drawable/item_normal" />-->
|
||||
</selector>
|
||||
6
android/app/src/main/res/layout/activity_fullscreen.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/webview_container"/>
|
||||
52
android/app/src/main/res/layout/row_layout.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dip">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="0dp"
|
||||
android:minWidth="0dp"
|
||||
android:text="" />
|
||||
|
||||
|
||||
<!-- <ImageView-->
|
||||
<!-- android:id="@+id/icon"-->
|
||||
<!-- android:layout_width="30dp"-->
|
||||
<!-- android:layout_height="30dp"-->
|
||||
<!-- android:layout_alignParentTop="true"-->
|
||||
<!-- android:layout_alignParentBottom="true"-->
|
||||
<!-- android:layout_marginEnd="6dip"-->
|
||||
<!-- android:contentDescription="TODO"-->
|
||||
<!-- android:src="@mipmap/ic_launcher" />-->
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="28dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="0dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/secondLine"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="14dip"
|
||||
android:maxLines="1"
|
||||
android:padding="0dp"
|
||||
android:textSize="11sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/firstLine"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
45
android/app/src/main/res/layout/task_list_widget.xml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<!--<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- android:background="#09C"-->
|
||||
<!-- android:padding="@dimen/widget_margin">-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="@dimen/widget_margin">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/task_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="0dp"
|
||||
android:background="@color/white"
|
||||
android:padding="0dp" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/empty_task_list_text"
|
||||
android:textColor="#F8F8F8"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.AppCompat.Button.Colored"
|
||||
android:id="@+id/add_task_btn"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:drawableBottom="@drawable/ic_add" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
10
android/app/src/main/res/values-v14/dimens.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!--
|
||||
Refer to App Widget Documentation for margin information
|
||||
http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
|
||||
-->
|
||||
<dimen name="widget_margin">0dp</dimen>
|
||||
|
||||
</resources>
|
||||
12
android/app/src/main/res/values/attrs.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<resources>
|
||||
|
||||
<!-- Declare custom theme attributes that allow changing which styles are
|
||||
used for button bars depending on the API level.
|
||||
?android:attr/buttonBarStyle is new as of API 11 so this is
|
||||
necessary to support previous API levels. -->
|
||||
<declare-styleable name="ButtonBarContainerTheme">
|
||||
<attr name="metaButtonBarStyle" format="reference" />
|
||||
<attr name="metaButtonBarButtonStyle" format="reference" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
12
android/app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="primary">#0B77D2</color>
|
||||
<color name="primaryDark">#0D47A1</color>
|
||||
<color name="accent">#D81B60</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="emphasizedText">#111</color>
|
||||
<color name="mutedText">#999</color>
|
||||
<color name="statusBar">#FF1E1E1E</color>
|
||||
|
||||
<color name="black_overlay">#66000000</color>
|
||||
</resources>
|
||||
10
android/app/src/main/res/values/dimens.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!--
|
||||
Refer to App Widget Documentation for margin information
|
||||
http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
|
||||
-->
|
||||
<dimen name="widget_margin">8dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#0B77D2</color>
|
||||
</resources>
|
||||
15
android/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<resources>
|
||||
<string name="app_name">Super Productivity</string>
|
||||
<string name="asset_statements">
|
||||
[{
|
||||
\"relation\": [\"delegate_permission/common.handle_all_urls\"],
|
||||
\"target\": {
|
||||
\"namespace\": \"web\",
|
||||
\"site\": \"https://app.super-productivity.com\"}
|
||||
}]
|
||||
</string>
|
||||
<string name="title_activity_fullscreen">SupProd</string>
|
||||
<string name="widget_no_data">Please tap here to load data</string>
|
||||
<string name="empty_task_list_text">Tap to refresh task list</string>
|
||||
<string name="service_background">Service is running background</string>
|
||||
</resources>
|
||||
39
android/app/src/main/res/values/styles.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primaryDark</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="android:statusBarColor">@color/statusBar</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.LauncherActivity" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowAnimationStyle">@null</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
|
||||
<style name="FullscreenTheme" parent="AppTheme">
|
||||
<item name="android:actionBarStyle">@style/FullscreenActionBarStyle</item>
|
||||
<item name="android:windowActionBarOverlay">true</item>
|
||||
<item name="android:windowBackground">@null</item>
|
||||
<item name="metaButtonBarStyle">?android:attr/buttonBarStyle</item>
|
||||
<item name="metaButtonBarButtonStyle">?android:attr/buttonBarButtonStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="FullscreenActionBarStyle" parent="Widget.AppCompat.ActionBar">
|
||||
<item name="android:background">@color/black_overlay</item>
|
||||
</style>
|
||||
|
||||
<style name="ListView" parent="@android:style/Widget.ListView">
|
||||
<item name="android:background">@color/white</item>
|
||||
<item name="android:cacheColorHint">@android:color/transparent</item>
|
||||
<item name="android:divider">@android:color/transparent</item>
|
||||
<item name="android:dividerHeight">0dp</item>
|
||||
<item name="android:listSelector">@drawable/list_item_selector</item>
|
||||
</style>
|
||||
</resources>
|
||||
36
android/app/src/main/res/xml/data_extraction_rules.xml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!--
|
||||
TODO: Use <include> and <exclude> to control what is backed up.
|
||||
The domain can be file, database, sharedpref, external or root.
|
||||
Examples:
|
||||
|
||||
<include domain="file" path="file_to_include"/>
|
||||
<exclude domain="file" path="file_to_exclude"/>
|
||||
<include domain="file" path="include_folder"/>
|
||||
<exclude domain="file" path="include_folder/file_to_exclude"/>
|
||||
<exclude domain="file" path="exclude_folder"/>
|
||||
<include domain="file" path="exclude_folder/file_to_include"/>
|
||||
|
||||
<include domain="sharedpref" path="include_shared_pref1.xml"/>
|
||||
<include domain="database" path="db_name/file_to_include"/>
|
||||
<exclude domain="database" path="db_name/include_folder/file_to_exclude"/>
|
||||
<include domain="external" path="file_to_include"/>
|
||||
<exclude domain="external" path="file_to_exclude"/>
|
||||
<include domain="root" path="file_to_include"/>
|
||||
<exclude domain="root" path="file_to_exclude"/>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
15
android/app/src/main/res/xml/network_security_config.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
<base-config cleartextTrafficPermitted="true"
|
||||
tools:ignore="InsecureBaseConfiguration">
|
||||
<trust-anchors>
|
||||
<!-- Trust user added CAs while debuggable only -->
|
||||
<certificates src="user"
|
||||
tools:ignore="AcceptsUserCertificates" />
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
10
android/app/src/main/res/xml/task_list_widget_info.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialKeyguardLayout="@layout/task_list_widget"
|
||||
android:initialLayout="@layout/task_list_widget"
|
||||
android:minWidth="110dp"
|
||||
android:minHeight="40dp"
|
||||
android:previewImage="@drawable/example_appwidget_preview"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="86400000"
|
||||
android:widgetCategory="home_screen|keyguard" />
|
||||