____MERGING SUPER PRODUCTIVITY ANDROID ____

This commit is contained in:
Johannes Millan 2024-10-11 14:11:23 +02:00
commit 5224beb00f
125 changed files with 3261 additions and 38 deletions

@ -1 +0,0 @@
Subproject commit 85f0f5a2a46b113123da76f90e2b75d178d81fc3

94
android/.gitignore vendored Normal file
View 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
View 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
View 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>

View 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
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
/build

View 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
View 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'

View 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
View 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}\""
]
}

View file

@ -0,0 +1,3 @@
webToken=XXXXX
clientIdDebug=XXX
clientIdProd=XXXX

21
android/app/proguard-rules.pro vendored Normal file
View 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

View file

@ -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());
}
}

View 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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -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()
}
}

View file

@ -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"
}
}

View file

@ -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)
}
}

View file

@ -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!!
}
}
}

View file

@ -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)")
}
}

View file

@ -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
}
}

View file

@ -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
)

View file

@ -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."
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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
)
}
}
}

View 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>

View 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>

View 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>

View 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="M8,5v14l11,-7z"/>
</group>
</vector>

View 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>

View 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="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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 195 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 247 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 247 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 254 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 333 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 441 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 B

After

Width:  |  Height:  |  Size: 116 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 B

After

Width:  |  Height:  |  Size: 126 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 B

After

Width:  |  Height:  |  Size: 126 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 B

After

Width:  |  Height:  |  Size: 246 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 316 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 400 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 400 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 336 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 524 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 333 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 B

After

Width:  |  Height:  |  Size: 598 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 B

After

Width:  |  Height:  |  Size: 598 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 B

After

Width:  |  Height:  |  Size: 439 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 B

After

Width:  |  Height:  |  Size: 795 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 917 B

Before After
Before After

View 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>

View 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>

View 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>

View 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>

View 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"/>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View 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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0B77D2</color>
</resources>

View 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>

View 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>

View 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>

View 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>

View 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" />

Some files were not shown because too many files have changed in this diff Show more