diff options
77 files changed, 2250 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..6f466ad --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +CleverSync
\ No newline at end of file diff --git a/.idea/codeStyles b/.idea/codeStyles new file mode 100644 index 0000000..320a4bc --- /dev/null +++ b/.idea/codeStyles @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectCodeStyleConfiguration"> + <code_scheme name="Project" version="173"> + <JetCodeStyleSettings> + <option name="PACKAGES_TO_USE_STAR_IMPORTS"> + <value> + <package name="java.util" alias="false" withSubpackages="false" /> + <package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" /> + <package name="io.ktor" alias="false" withSubpackages="true" /> + </value> + </option> + <option name="PACKAGES_IMPORT_LAYOUT"> + <value> + <package name="" alias="false" withSubpackages="true" /> + <package name="java" alias="false" withSubpackages="true" /> + <package name="javax" alias="false" withSubpackages="true" /> + <package name="kotlin" alias="false" withSubpackages="true" /> + <package name="" alias="true" withSubpackages="true" /> + </value> + </option> + </JetCodeStyleSettings> + <codeStyleSettings language="XML"> + <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> + </code_scheme> + </component> +</project>
\ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="CompilerConfiguration"> + <bytecodeTargetLevel target="17" /> + </component> +</project>
\ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="deploymentTargetSelector"> + <selectionStates> + <SelectionState runConfigName="app"> + <option name="selectionMode" value="DROPDOWN" /> + </SelectionState> + </selectionStates> + </component> +</project>
\ No newline at end of file diff --git a/.idea/detekt.xml b/.idea/detekt.xml new file mode 100644 index 0000000..a8dfb5a --- /dev/null +++ b/.idea/detekt.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="DetektPluginSettings"> + <option name="configurationFiles"> + <list> + <option value="$PROJECT_DIR$/detekt.yaml" /> + </list> + </option> + <option name="enableDetekt" value="true" /> + <option name="enableForProjectResult" value="Accepted" /> + <option name="redirectChannels" value="true" /> + </component> +</project>
\ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ +<?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" /> + </set> + </option> + <option name="resolveExternalAnnotations" value="false" /> + </GradleProjectSettings> + </option> + </component> +</project>
\ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..6d0ee1c --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="KotlinJpsPluginSettings"> + <option name="version" value="2.0.0" /> + </component> +</project>
\ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -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>
\ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0ad17cb --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ExternalStorageConfigurationManager" enabled="true" /> + <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>
\ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..0d3a1fb --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,263 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="direct_access_persist.xml"> + <option name="deviceSelectionList"> + <list> + <PersistentDeviceSelectionData> + <option name="api" value="27" /> + <option name="brand" value="DOCOMO" /> + <option name="codename" value="F01L" /> + <option name="id" value="F01L" /> + <option name="manufacturer" value="FUJITSU" /> + <option name="name" value="F-01L" /> + <option name="screenDensity" value="360" /> + <option name="screenX" value="720" /> + <option name="screenY" value="1280" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="28" /> + <option name="brand" value="DOCOMO" /> + <option name="codename" value="SH-01L" /> + <option name="id" value="SH-01L" /> + <option name="manufacturer" value="SHARP" /> + <option name="name" value="AQUOS sense2 SH-01L" /> + <option name="screenDensity" value="480" /> + <option name="screenX" value="1080" /> + <option name="screenY" value="2160" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="31" /> + <option name="brand" value="samsung" /> + <option name="codename" value="a51" /> + <option name="id" value="a51" /> + <option name="manufacturer" value="Samsung" /> + <option name="name" value="Galaxy A51" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="1080" /> + <option name="screenY" value="2400" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="34" /> + <option name="brand" value="google" /> + <option name="codename" value="akita" /> + <option name="id" value="akita" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel 8a" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="1080" /> + <option name="screenY" value="2400" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="33" /> + <option name="brand" value="samsung" /> + <option name="codename" value="b0q" /> + <option name="id" value="b0q" /> + <option name="manufacturer" value="Samsung" /> + <option name="name" value="Galaxy S22 Ultra" /> + <option name="screenDensity" value="600" /> + <option name="screenX" value="1440" /> + <option name="screenY" value="3088" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="32" /> + <option name="brand" value="google" /> + <option name="codename" value="bluejay" /> + <option name="id" value="bluejay" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel 6a" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="1080" /> + <option name="screenY" value="2400" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="29" /> + <option name="brand" value="samsung" /> + <option name="codename" value="crownqlteue" /> + <option name="id" value="crownqlteue" /> + <option name="manufacturer" value="Samsung" /> + <option name="name" value="Galaxy Note9" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="2220" /> + <option name="screenY" value="1080" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="34" /> + <option name="brand" value="samsung" /> + <option name="codename" value="dm3q" /> + <option name="id" value="dm3q" /> + <option name="manufacturer" value="Samsung" /> + <option name="name" value="Galaxy S23 Ultra" /> + <option name="screenDensity" value="600" /> + <option name="screenX" value="1440" /> + <option name="screenY" value="3088" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="33" /> + <option name="brand" value="google" /> + <option name="codename" value="felix" /> + <option name="id" value="felix" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel Fold" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="2208" /> + <option name="screenY" value="1840" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="33" /> + <option name="brand" value="google" /> + <option name="codename" value="felix_camera" /> + <option name="id" value="felix_camera" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel Fold (Camera-enabled)" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="2208" /> + <option name="screenY" value="1840" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="33" /> + <option name="brand" value="samsung" /> + <option name="codename" value="gts8uwifi" /> + <option name="id" value="gts8uwifi" /> + <option name="manufacturer" value="Samsung" /> + <option name="name" value="Galaxy Tab S8 Ultra" /> + <option name="screenDensity" value="320" /> + <option name="screenX" value="1848" /> + <option name="screenY" value="2960" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="34" /> + <option name="brand" value="google" /> + <option name="codename" value="husky" /> + <option name="id" value="husky" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel 8 Pro" /> + <option name="screenDensity" value="390" /> + <option name="screenX" value="1008" /> + <option name="screenY" value="2244" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="30" /> + <option name="brand" value="motorola" /> + <option name="codename" value="java" /> + <option name="id" value="java" /> + <option name="manufacturer" value="Motorola" /> + <option name="name" value="G20" /> + <option name="screenDensity" value="280" /> + <option name="screenX" value="720" /> + <option name="screenY" value="1600" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="33" /> + <option name="brand" value="google" /> + <option name="codename" value="lynx" /> + <option name="id" value="lynx" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel 7a" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="1080" /> + <option name="screenY" value="2400" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="31" /> + <option name="brand" value="google" /> + <option name="codename" value="oriole" /> + <option name="id" value="oriole" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel 6" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="1080" /> + <option name="screenY" value="2400" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="33" /> + <option name="brand" value="google" /> + <option name="codename" value="panther" /> + <option name="id" value="panther" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel 7" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="1080" /> + <option name="screenY" value="2400" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="31" /> + <option name="brand" value="samsung" /> + <option name="codename" value="q2q" /> + <option name="id" value="q2q" /> + <option name="manufacturer" value="Samsung" /> + <option name="name" value="Galaxy Z Fold3" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="1768" /> + <option name="screenY" value="2208" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="34" /> + <option name="brand" value="samsung" /> + <option name="codename" value="q5q" /> + <option name="id" value="q5q" /> + <option name="manufacturer" value="Samsung" /> + <option name="name" value="Galaxy Z Fold5" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="1812" /> + <option name="screenY" value="2176" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="30" /> + <option name="brand" value="google" /> + <option name="codename" value="r11" /> + <option name="id" value="r11" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel Watch" /> + <option name="screenDensity" value="320" /> + <option name="screenX" value="384" /> + <option name="screenY" value="384" /> + <option name="type" value="WEAR_OS" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="30" /> + <option name="brand" value="google" /> + <option name="codename" value="redfin" /> + <option name="id" value="redfin" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel 5" /> + <option name="screenDensity" value="440" /> + <option name="screenX" value="1080" /> + <option name="screenY" value="2340" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="34" /> + <option name="brand" value="google" /> + <option name="codename" value="shiba" /> + <option name="id" value="shiba" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel 8" /> + <option name="screenDensity" value="420" /> + <option name="screenX" value="1080" /> + <option name="screenY" value="2400" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="33" /> + <option name="brand" value="google" /> + <option name="codename" value="tangorpro" /> + <option name="id" value="tangorpro" /> + <option name="manufacturer" value="Google" /> + <option name="name" value="Pixel Tablet" /> + <option name="screenDensity" value="320" /> + <option name="screenX" value="1600" /> + <option name="screenY" value="2560" /> + </PersistentDeviceSelectionData> + <PersistentDeviceSelectionData> + <option name="api" value="29" /> + <option name="brand" value="samsung" /> + <option name="codename" value="x1q" /> + <option name="id" value="x1q" /> + <option name="manufacturer" value="Samsung" /> + <option name="name" value="Galaxy S20" /> + <option name="screenDensity" value="480" /> + <option name="screenX" value="1440" /> + <option name="screenY" value="3200" /> + </PersistentDeviceSelectionData> + </list> + </option> + </component> +</project>
\ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + </component> +</project>
\ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build
\ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..f28bc23 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) +} + +android { + namespace = "org.the_jk.cleversync" + compileSdk = 34 + + defaultConfig { + applicationId = "org.the_jk.cleversync" + minSdk = 29 + targetSdk = 34 + versionCode = 1 + versionName = "0.1" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.livedata) + implementation(libs.androidx.livedata.ktx) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.material) + testImplementation(libs.junit) + testImplementation(libs.robolectric) + testImplementation(libs.truth) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..25042b4 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,7 @@ +# 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 diff --git a/app/src/androidTest/java/org/the_jk/cleversync/ExampleInstrumentedTest.kt b/app/src/androidTest/java/org/the_jk/cleversync/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..388e356 --- /dev/null +++ b/app/src/androidTest/java/org/the_jk/cleversync/ExampleInstrumentedTest.kt @@ -0,0 +1,20 @@ +package org.the_jk.cleversync + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = + InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.the_jk.cleversync", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d6fb563 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + <application + android:allowBackup="true" + android:dataExtractionRules="@xml/data_extraction_rules" + android:fullBackupContent="@xml/backup_rules" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/Theme.CleverSync" + tools:targetApi="31"> + <activity + android:name=".MainActivity" + android:exported="true" + android:theme="@style/Theme.CleverSync"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> + +</manifest> diff --git a/app/src/main/java/org/the_jk/cleversync/FirstFragment.kt b/app/src/main/java/org/the_jk/cleversync/FirstFragment.kt new file mode 100644 index 0000000..fa73bf6 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/FirstFragment.kt @@ -0,0 +1,35 @@ +package org.the_jk.cleversync + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import org.the_jk.cleversync.databinding.FragmentFirstBinding + +class FirstFragment : Fragment() { + private var _binding: FragmentFirstBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentFirstBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonFirst.setOnClickListener { + findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/LiveDataUtils.kt b/app/src/main/java/org/the_jk/cleversync/LiveDataUtils.kt new file mode 100644 index 0000000..7f6ab1f --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/LiveDataUtils.kt @@ -0,0 +1,14 @@ +package org.the_jk.cleversync + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +fun <T> LiveData<T>.safeValue(): T? { + if (this.hasActiveObservers()) + return value + var ret: T? = null + val observer = Observer<T> { value -> ret = value } + this.observeForever(observer) + this.removeObserver(observer) + return ret +} diff --git a/app/src/main/java/org/the_jk/cleversync/MainActivity.kt b/app/src/main/java/org/the_jk/cleversync/MainActivity.kt new file mode 100644 index 0000000..96fd167 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/MainActivity.kt @@ -0,0 +1,60 @@ +package org.the_jk.cleversync + +import android.os.Bundle +import com.google.android.material.snackbar.Snackbar +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import android.view.Menu +import android.view.MenuItem +import org.the_jk.cleversync.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + val navController = + findNavController(R.id.nav_host_fragment_content_main) + appBarConfiguration = AppBarConfiguration(navController.graph) + setupActionBarWithNavController(navController, appBarConfiguration) + + binding.fab.setOnClickListener { view -> + Snackbar.make( + view, + "Replace with your own action", + Snackbar.LENGTH_LONG + ) + .setAction("Action", null) + .setAnchorView(R.id.fab).show() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_settings -> true + else -> super.onOptionsItemSelected(item) + } + } + + override fun onSupportNavigateUp(): Boolean { + val navController = + findNavController(R.id.nav_host_fragment_content_main) + return navController.navigateUp(appBarConfiguration) + || super.onSupportNavigateUp() + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/SecondFragment.kt b/app/src/main/java/org/the_jk/cleversync/SecondFragment.kt new file mode 100644 index 0000000..b105450 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/SecondFragment.kt @@ -0,0 +1,35 @@ +package org.the_jk.cleversync + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import org.the_jk.cleversync.databinding.FragmentSecondBinding + +class SecondFragment : Fragment() { + private var _binding: FragmentSecondBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSecondBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonSecond.setOnClickListener { + findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/Directory.kt b/app/src/main/java/org/the_jk/cleversync/io/Directory.kt new file mode 100644 index 0000000..2273f3e --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/Directory.kt @@ -0,0 +1,15 @@ +package org.the_jk.cleversync.io + +import androidx.lifecycle.LiveData + +interface Directory { + val name: String + + fun list(): Content + + data class Content( + val directories: LiveData<List<Directory>>, + val files: LiveData<List<File>>, + val links: LiveData<List<Link>>, + ) +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/File.kt b/app/src/main/java/org/the_jk/cleversync/io/File.kt new file mode 100644 index 0000000..b5333eb --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/File.kt @@ -0,0 +1,12 @@ +package org.the_jk.cleversync.io + +import java.io.InputStream +import java.time.Instant + +interface File { + val name: String + val size: ULong + val lastModified: Instant + + fun open(): InputStream +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/Link.kt b/app/src/main/java/org/the_jk/cleversync/io/Link.kt new file mode 100644 index 0000000..3ecf5e6 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/Link.kt @@ -0,0 +1,13 @@ +package org.the_jk.cleversync.io + +interface Link { + val name: String + + fun resolve(): LinkTarget + + sealed class LinkTarget + + class DirectoryTarget(val directory: Directory): LinkTarget() + class FileTarget(val file: File): LinkTarget() + data object NoTarget: LinkTarget() +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/ModifiableDirectory.kt b/app/src/main/java/org/the_jk/cleversync/io/ModifiableDirectory.kt new file mode 100644 index 0000000..43efa8f --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableDirectory.kt @@ -0,0 +1,21 @@ +package org.the_jk.cleversync.io + +import androidx.lifecycle.LiveData + +interface ModifiableDirectory : Directory { + fun modifiableList(): Content + + fun createDirectory(name: String): ModifiableDirectory + fun createFile(name: String): ModifiableFile + fun createLink(name: String, target: String): ModifiableLink + + fun removeDirectory(name: String): Boolean + fun removeFile(name: String): Boolean + fun removeLink(name: String): Boolean + + data class Content( + val directories: LiveData<List<ModifiableDirectory>>, + val files: LiveData<List<ModifiableFile>>, + val links: LiveData<List<ModifiableLink>>, + ) +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/ModifiableFile.kt b/app/src/main/java/org/the_jk/cleversync/io/ModifiableFile.kt new file mode 100644 index 0000000..8675dae --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableFile.kt @@ -0,0 +1,7 @@ +package org.the_jk.cleversync.io + +import java.io.OutputStream + +interface ModifiableFile : File { + fun write(): OutputStream +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/ModifiableLink.kt b/app/src/main/java/org/the_jk/cleversync/io/ModifiableLink.kt new file mode 100644 index 0000000..a20bb6a --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableLink.kt @@ -0,0 +1,15 @@ +package org.the_jk.cleversync.io + +interface ModifiableLink : Link { + fun modifiableResolve(): ModifiableLinkTarget + + fun target(directory: Directory) = target(directory.name) + fun target(file: File) = target(file.name) + fun target(name: String) + + sealed class ModifiableLinkTarget + + class ModifiableDirectoryTarget(val directory: ModifiableDirectory): ModifiableLinkTarget() + class ModifiableFileTarget(val file: ModifiableFile): ModifiableLinkTarget() + data object ModifiableNoTarget: ModifiableLinkTarget() +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/ModifiableTree.kt b/app/src/main/java/org/the_jk/cleversync/io/ModifiableTree.kt new file mode 100644 index 0000000..383360d --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/ModifiableTree.kt @@ -0,0 +1,3 @@ +package org.the_jk.cleversync.io + +interface ModifiableTree : Tree, ModifiableDirectory diff --git a/app/src/main/java/org/the_jk/cleversync/io/Tree.kt b/app/src/main/java/org/the_jk/cleversync/io/Tree.kt new file mode 100644 index 0000000..b6f2d54 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/Tree.kt @@ -0,0 +1,7 @@ +package org.the_jk.cleversync.io + +import android.content.res.Resources + +interface Tree : Directory { + fun description(resources: Resources): CharSequence +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/TreeFactory.kt b/app/src/main/java/org/the_jk/cleversync/io/TreeFactory.kt new file mode 100644 index 0000000..d7c22f5 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/TreeFactory.kt @@ -0,0 +1,12 @@ +package org.the_jk.cleversync.io + +import org.the_jk.cleversync.io.impl.PathTree +import java.nio.file.Path + +object TreeFactory { + fun localModifiableTree(root: Path): ModifiableTree { + return PathTree(root) + } + + fun localTree(root: Path): Tree = localModifiableTree(root) +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/impl/PathDirectory.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathDirectory.kt new file mode 100644 index 0000000..fab4dcc --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathDirectory.kt @@ -0,0 +1,161 @@ +package org.the_jk.cleversync.io.impl + +import androidx.annotation.AnyThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.map +import org.the_jk.cleversync.io.Directory +import org.the_jk.cleversync.io.ModifiableDirectory +import org.the_jk.cleversync.io.ModifiableFile +import org.the_jk.cleversync.io.ModifiableLink +import java.nio.file.LinkOption +import java.nio.file.NoSuchFileException +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.createDirectory +import kotlin.io.path.createSymbolicLinkPointingTo +import kotlin.io.path.deleteIfExists +import kotlin.io.path.deleteRecursively +import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile +import kotlin.io.path.isSymbolicLink +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name + +@OptIn(ExperimentalPathApi::class) +internal open class PathDirectory( + private val path: Path, + private val pathWatcher: PathWatcher, +) : ModifiableDirectory { + private val watcher: DirectoryWatcher by lazy { + DirectoryWatcher() + } + + private val modifiableContent: ModifiableDirectory.Content by lazy { + val base = watcher.content + ModifiableDirectory.Content( + base.map { entries -> + entries.filterIsInstance<Entry.Directory>().map { entry -> + PathDirectory(entry.path, pathWatcher) + } + }, + base.map { entries -> + entries.filterIsInstance<Entry.File>().map { entry -> + PathFile(entry.path) + } + }, + base.map { entries -> + entries.filterIsInstance<Entry.Link>().map { entry -> + PathLink(entry.path, pathWatcher) + } + }, + ) + } + + private val content: Directory.Content by lazy { + val base = modifiableContent + Directory.Content( + base.directories.map { it }, + base.files.map { it }, + base.links.map { it }, + ) + } + + override fun modifiableList() = modifiableContent + + override fun createDirectory(name: String): ModifiableDirectory { + val path = path.resolve(name) + return PathDirectory(path.createDirectory(), pathWatcher) + } + + override fun createFile(name: String): ModifiableFile { + val path = path.resolve(name) + return PathFile(path) + } + + override fun createLink(name: String, target: String): ModifiableLink { + val path = path.resolve(name) + return PathLink(path.createSymbolicLinkPointingTo(path.resolve(target)), pathWatcher) + } + + override fun removeDirectory(name: String): Boolean { + val path = path.resolve(name) + return if (path.isDirectory(LinkOption.NOFOLLOW_LINKS)) { + path.deleteRecursively() + true + } else false + } + + override fun removeFile(name: String): Boolean { + val path = path.resolve(name) + return path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && path.deleteIfExists() + } + + override fun removeLink(name: String): Boolean { + val path = path.resolve(name) + return path.isSymbolicLink() && path.deleteIfExists() + } + + override val name: String + get() = path.name + + override fun list() = content + + override fun equals(other: Any?) = other is PathDirectory && other.path == path + override fun hashCode() = path.hashCode() + + private sealed class Entry(val path: Path) { + class Directory(path: Path) : Entry(path) + class File(path: Path) : Entry(path) + class Link(path: Path) : Entry(path) + } + + private inner class DirectoryWatcher : PathWatcher.Delegate { + val content: LiveData<List<Entry>> + get() = _content + + @AnyThread + override fun update(added: List<Path>, removed: List<Path>) { + try { + _content.postValue(mapEntries(path.listDirectoryEntries())) + } catch (ignored: NoSuchFileException) { + } + } + + private val _content = object : MutableLiveData<List<Entry>>() { + override fun onActive() { + setup() + } + + override fun onInactive() { + clear() + } + } + + private fun setup() { + val entries = path.listDirectoryEntries() + pathWatcher.add(path, this) + _content.value = mapEntries(entries) + } + + private fun clear() { + pathWatcher.remove(path) + } + } + + companion object { + private fun mapEntries(entries: List<Path>): List<Entry> { + return entries.mapNotNull { + if (it.isDirectory(LinkOption.NOFOLLOW_LINKS)) { + Entry.Directory(it) + } else if (it.isRegularFile(LinkOption.NOFOLLOW_LINKS)) { + Entry.File(it) + } else if (it.isSymbolicLink()) { + Entry.Link(it) + } else { + null + } + } + } + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/impl/PathFile.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathFile.kt new file mode 100644 index 0000000..d8ca900 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathFile.kt @@ -0,0 +1,65 @@ +package org.the_jk.cleversync.io.impl + +import org.the_jk.cleversync.io.ModifiableFile +import java.io.InputStream +import java.io.OutputStream +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.nio.file.StandardOpenOption +import java.time.Instant +import kotlin.io.path.exists +import kotlin.io.path.fileSize +import kotlin.io.path.getLastModifiedTime +import kotlin.io.path.inputStream +import kotlin.io.path.name +import kotlin.io.path.outputStream + +internal class PathFile(private val path: Path) : ModifiableFile { + override fun write(): OutputStream { + // If file doesn't exist, write to it directly. + if (!path.exists(LinkOption.NOFOLLOW_LINKS)) + return path.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) + + // Otherwise, write to temp file, only overwriting when done. + val tmp = path.parent.resolve(".#" + path.name) + val os = tmp.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) + return object : OutputStream() { + override fun write(value: Int) { + os.write(value) + } + + override fun write(b: ByteArray) { + os.write(b) + } + + override fun write(b: ByteArray, off: Int, len: Int) { + os.write(b, off, len) + } + + override fun flush() { + os.flush() + } + + override fun close() { + os.close() + Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE) + } + } + } + + override val name: String + get() = path.name + override val size: ULong + get() = path.fileSize().toULong() + override val lastModified: Instant + get() = path.getLastModifiedTime().toInstant() + + override fun open(): InputStream { + return path.inputStream(StandardOpenOption.READ) + } + + override fun equals(other: Any?) = other is PathFile && other.path == path + override fun hashCode() = path.hashCode() +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/impl/PathLink.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathLink.kt new file mode 100644 index 0000000..5d11228 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathLink.kt @@ -0,0 +1,49 @@ +package org.the_jk.cleversync.io.impl + +import org.the_jk.cleversync.io.Link +import org.the_jk.cleversync.io.ModifiableLink +import java.nio.file.Path +import kotlin.io.path.createSymbolicLinkPointingTo +import kotlin.io.path.deleteIfExists +import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile +import kotlin.io.path.name +import kotlin.io.path.readSymbolicLink + +internal class PathLink( + private val path: Path, + private val pathWatcher: PathWatcher, +) : ModifiableLink { + override fun modifiableResolve(): ModifiableLink.ModifiableLinkTarget { + val target = path.readSymbolicLink() + return if (target.isDirectory()) { + ModifiableLink.ModifiableDirectoryTarget(PathDirectory(target, pathWatcher)) + } else if (target.isRegularFile()) { + ModifiableLink.ModifiableFileTarget(PathFile(target)) + } else { + ModifiableLink.ModifiableNoTarget + } + } + + override fun target(name: String) { + path.deleteIfExists() + path.createSymbolicLinkPointingTo(path.resolve(name)) + } + + override val name: String + get() = path.name + + override fun equals(other: Any?) = other is PathLink && other.path == path + override fun hashCode() = path.hashCode() + + override fun resolve(): Link.LinkTarget { + val target = path.readSymbolicLink() + return if (target.isDirectory()) { + Link.DirectoryTarget(PathDirectory(target, pathWatcher)) + } else if (target.isRegularFile()) { + Link.FileTarget(PathFile(target)) + } else { + Link.NoTarget + } + } +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/impl/PathTree.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathTree.kt new file mode 100644 index 0000000..a8a74c5 --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathTree.kt @@ -0,0 +1,10 @@ +package org.the_jk.cleversync.io.impl + +import android.content.res.Resources +import org.the_jk.cleversync.R +import org.the_jk.cleversync.io.ModifiableTree +import java.nio.file.Path + +internal class PathTree(root: Path) : PathDirectory(root, PathWatcher()), ModifiableTree { + override fun description(resources: Resources) = resources.getString(R.string.local_directory) +} diff --git a/app/src/main/java/org/the_jk/cleversync/io/impl/PathWatcher.kt b/app/src/main/java/org/the_jk/cleversync/io/impl/PathWatcher.kt new file mode 100644 index 0000000..945019a --- /dev/null +++ b/app/src/main/java/org/the_jk/cleversync/io/impl/PathWatcher.kt @@ -0,0 +1,82 @@ +package org.the_jk.cleversync.io.impl + +import androidx.annotation.GuardedBy +import androidx.annotation.WorkerThread +import java.nio.file.ClosedWatchServiceException +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds +import java.nio.file.WatchKey +import java.nio.file.WatchService +import java.util.concurrent.Executors + +internal class PathWatcher { + private val executor = Executors.newSingleThreadExecutor() + private var service: WatchService? = null + private val keys = mutableMapOf<Path, WatchKey>() + private val lock = Object() + @GuardedBy("lock") + private val delegates = mutableMapOf<WatchKey, Delegate>() + + fun add(path: Path, delegate: Delegate) { + if (keys.isEmpty()) { + val service = FileSystems.getDefault().newWatchService() + executor.execute{ runner(service) } + this.service = service + } + val key = path.register(service!!, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE) + delegates[key] = delegate + keys[path] = key + } + + fun remove(path: Path) { + val key = keys.remove(path) ?: return + key.cancel() + synchronized(lock) { + delegates.remove(key) + } + if (keys.isEmpty()) { + service?.close() + service = null + } + } + + interface Delegate { + fun update(added: List<Path>, removed: List<Path>) + } + + @WorkerThread + private fun runner(service: WatchService) { + while (true) { + val key: WatchKey + try { + key = service.take() + } catch (ignored: InterruptedException) { + return + } catch (ignored: ClosedWatchServiceException) { + return + } + + val added = mutableListOf<Path>() + val removed = mutableListOf<Path>() + var overflow = false + + for (event in key.pollEvents()) { + when (event.kind()) { + StandardWatchEventKinds.OVERFLOW -> overflow = true + StandardWatchEventKinds.ENTRY_CREATE -> added.add(event.context() as Path) + StandardWatchEventKinds.ENTRY_DELETE -> removed.add(event.context() as Path) + } + } + + if (overflow || added.isNotEmpty() || removed.isNotEmpty()) { + val delegate = synchronized(lock) { + delegates[key] + } + delegate?.update(added, removed) + } + + key.reset() + } + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..1e4408c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportHeight="108" + android:viewportWidth="108"> + <path + android:fillColor="#3DDC84" + 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> diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..14780bb --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + android:width="108dp" + android:height="108dp" + android:viewportHeight="108" + android:viewportWidth="108"> + <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> + <aapt:attr name="android:fillColor"> + <gradient + android:endX="85.84757" + android:endY="92.4963" + android:startX="42.9492" + android:startY="49.59793" + android:type="linear"> + <item + android:color="#44000000" + android:offset="0.0" /> + <item + android:color="#00000000" + android:offset="1.0" /> + </gradient> + </aapt:attr> + </path> + <path + android:fillColor="#FFFFFF" + android:fillType="nonZero" + android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" + android:strokeColor="#00000000" + android:strokeWidth="1" /> +</vector> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b214f0f --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true" + tools:context=".MainActivity"> + + <com.google.android.material.appbar.AppBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" /> + + </com.google.android.material.appbar.AppBarLayout> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/fab" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_marginBottom="16dp" + android:layout_marginEnd="@dimen/fab_margin" + app:srcCompat="@android:drawable/ic_dialog_email" /> + + <include layout="@layout/content_main" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..041049e --- /dev/null +++ b/app/src/main/res/layout/content_main.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <fragment + android:id="@+id/nav_host_fragment_content_main" + android:name="androidx.navigation.fragment.NavHostFragment" + android:layout_width="0dp" + android:layout_height="0dp" + app:defaultNavHost="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:navGraph="@navigation/nav_graph" /> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_first.xml b/app/src/main/res/layout/fragment_first.xml new file mode 100644 index 0000000..a3a474c --- /dev/null +++ b/app/src/main/res/layout/fragment_first.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".FirstFragment"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp"> + + <Button + android:id="@+id/button_first" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/next" + app:layout_constraintBottom_toTopOf="@id/textview_first" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/textview_first" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/lorem_ipsum" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/button_first" /> + </androidx.constraintlayout.widget.ConstraintLayout> +</androidx.core.widget.NestedScrollView> diff --git a/app/src/main/res/layout/fragment_second.xml b/app/src/main/res/layout/fragment_second.xml new file mode 100644 index 0000000..cc64afc --- /dev/null +++ b/app/src/main/res/layout/fragment_second.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".SecondFragment"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp"> + + <Button + android:id="@+id/button_second" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/previous" + app:layout_constraintBottom_toTopOf="@id/textview_second" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/textview_second" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/lorem_ipsum" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/button_second" /> + </androidx.constraintlayout.widget.ConstraintLayout> +</androidx.core.widget.NestedScrollView> diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000..6aac919 --- /dev/null +++ b/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,10 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + tools:context="org.the_jk.cleversync.MainActivity"> + <item + android:id="@+id/action_settings" + android:orderInCategory="100" + android:title="@string/action_settings" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..b3e26b4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> + <monochrome android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon> diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..b3e26b4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> + <monochrome android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon> diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differnew file mode 100644 index 0000000..aa7d642 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Binary files differnew file mode 100644 index 0000000..9126ae3 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..9c92ead --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<navigation xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/nav_graph" + app:startDestination="@id/FirstFragment"> + + <fragment + android:id="@+id/FirstFragment" + android:name="org.the_jk.cleversync.FirstFragment" + android:label="@string/first_fragment_label" + tools:layout="@layout/fragment_first"> + + <action + android:id="@+id/action_FirstFragment_to_SecondFragment" + app:destination="@id/SecondFragment" /> + </fragment> + <fragment + android:id="@+id/SecondFragment" + android:name="org.the_jk.cleversync.SecondFragment" + android:label="@string/second_fragment_label" + tools:layout="@layout/fragment_second"> + + <action + android:id="@+id/action_SecondFragment_to_FirstFragment" + app:destination="@id/FirstFragment" /> + </fragment> +</navigation> diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 0000000..ec4deb8 --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="fab_margin">48dp</dimen> +</resources> diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..e6c567a --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ +<resources xmlns:tools="http://schemas.android.com/tools"> + <!-- Base application theme. --> + <style name="Base.Theme.CleverSync" parent="Theme.Material3.DayNight.NoActionBar"> + <!-- Customize your dark theme here. --> + <!-- <item name="colorPrimary">@color/my_dark_primary</item> --> + </style> +</resources> diff --git a/app/src/main/res/values-v23/themes.xml b/app/src/main/res/values-v23/themes.xml new file mode 100644 index 0000000..5d974d6 --- /dev/null +++ b/app/src/main/res/values-v23/themes.xml @@ -0,0 +1,10 @@ +<resources xmlns:tools="http://schemas.android.com/tools"> + + <style name="Theme.CleverSync" parent="Base.Theme.CleverSync"> + <!-- Transparent system bars for edge-to-edge. --> + <item name="android:navigationBarColor">@android:color/transparent + </item> + <item name="android:statusBarColor">@android:color/transparent</item> + <item name="android:windowLightStatusBar">?attr/isLightTheme</item> + </style> +</resources> diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/app/src/main/res/values-w1240dp/dimens.xml new file mode 100644 index 0000000..2ecead2 --- /dev/null +++ b/app/src/main/res/values-w1240dp/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="fab_margin">200dp</dimen> +</resources> diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml new file mode 100644 index 0000000..ec4deb8 --- /dev/null +++ b/app/src/main/res/values-w600dp/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="fab_margin">48dp</dimen> +</resources> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..768b058 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="black">#FF000000</color> + <color name="white">#FFFFFFFF</color> +</resources> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..59a0b0c --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="fab_margin">16dp</dimen> +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..723c6e1 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,55 @@ +<resources> + <string name="app_name">CleverSync</string> + <string name="action_settings">Settings</string> + <!-- Strings used for fragments for navigation --> + <string name="first_fragment_label">First Fragment</string> + <string name="second_fragment_label">Second Fragment</string> + <string name="next">Next</string> + <string name="previous">Previous</string> + + <string name="lorem_ipsum"> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in + scelerisque sem. Mauris volutpat, dolor id interdum ullamcorper, risus + dolor egestas lectus, sit amet mattis purus dui nec risus. Maecenas non + sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad litora + torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit + eleifend diam, vel rutrum tellus vulputate quis. Aliquam eget libero + aliquet, imperdiet nisl a, ornare ex. Sed rhoncus est ut libero porta + lobortis. Fusce in dictum tellus.\n\n + Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id + magna felis. Vivamus egestas, est a condimentum egestas, turpis nisl + iaculis ipsum, in dictum tellus dolor sed neque. Morbi tellus erat, + dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada fames ac + ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies + urna vitae, molestie nibh. Phasellus at commodo eros, non aliquet metus. + Sed maximus nisl nec dolor bibendum, vel congue leo egestas.\n\n + Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi + odio, condimentum sit amet auctor at, mollis non turpis. Nullam pretium + libero vestibulum, finibus orci vel, molestie quam. Fusce blandit + tincidunt nulla, quis sollicitudin libero facilisis et. Integer interdum + nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, + dictum at lacinia sit amet, tristique id quam. Cras eu consequat dui. + Suspendisse sodales nunc ligula, in lobortis sem porta sed. Integer id + ultrices magna, in luctus elit. Sed a pellentesque est.\n\n + Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam + a venenatis nibh. Morbi laoreet, tortor sed facilisis varius, nibh orci + rhoncus nulla, id elementum leo dui non lorem. Nam mollis ipsum quis + auctor varius. Quisque elementum eu libero sed commodo. In eros nisl, + imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex + nunc, quis imperdiet eros placerat ac. Duis finibus orci et est auctor + tincidunt. Sed non viverra ipsum. Nunc quis augue egestas, cursus lorem + at, molestie sem. Morbi a consectetur ipsum, a placerat diam. Etiam + vulputate dignissim convallis. Integer faucibus mauris sit amet finibus + convallis.\n\n + Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus + et netus et malesuada fames ac turpis egestas. In volutpat arcu ut felis + sagittis, in finibus massa gravida. Pellentesque id tellus orci. Integer + dictum, lorem sed efficitur ullamcorper, libero justo consectetur ipsum, + in mollis nisl ex sed nisl. Donec maximus ullamcorper sodales. Praesent + bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus + libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum + enim a justo luctus vestibulum. Fusce dictum libero quis erat maximus, + vitae volutpat diam dignissim. + </string> + <string name="local_directory">Local directory</string> +</resources> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..6c0b175 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ +<resources xmlns:tools="http://schemas.android.com/tools"> + <!-- Base application theme. --> + <style name="Base.Theme.CleverSync" parent="Theme.Material3.DayNight.NoActionBar"> + <!-- Customize your light theme here. --> + <!-- <item name="colorPrimary">@color/my_light_primary</item> --> + </style> + + <style name="Theme.CleverSync" parent="Base.Theme.CleverSync" /> +</resources> diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..10d94d3 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<full-backup-content> +<!-- + <include domain="sharedpref" path="."/> + <exclude domain="sharedpref" path="device.xml"/> +--> +</full-backup-content> diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..4f7fc1d --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<data-extraction-rules> + <cloud-backup> + <!-- TODO: Use <include> and <exclude> to control what is backed up. + <include .../> + <exclude .../> + --> + </cloud-backup> + <device-transfer> + <!-- + <include .../> + <exclude .../> + --> + </device-transfer> +</data-extraction-rules> diff --git a/app/src/test/java/org/the_jk/cleversync/LocalTreeTest.kt b/app/src/test/java/org/the_jk/cleversync/LocalTreeTest.kt new file mode 100644 index 0000000..e3f70d4 --- /dev/null +++ b/app/src/test/java/org/the_jk/cleversync/LocalTreeTest.kt @@ -0,0 +1,90 @@ +package org.the_jk.cleversync + +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowLooper +import org.the_jk.cleversync.io.Directory +import org.the_jk.cleversync.io.ModifiableTree +import org.the_jk.cleversync.io.TreeFactory + +@Config(manifest=Config.NONE) +@RunWith(RobolectricTestRunner::class) +class LocalTreeTest { + @get:Rule + val folder = TemporaryFolder() + + private lateinit var tree: ModifiableTree + + @Before + fun setUp() { + tree = TreeFactory.localModifiableTree(folder.root.toPath()) + } + + @Test + fun empty() { + val content = tree.list() + assertThat(content.directories.safeValue()).isEmpty() + assertThat(content.files.safeValue()).isEmpty() + assertThat(content.links.safeValue()).isEmpty() + } + + @Test + fun createDirectory() { + val foo = tree.createDirectory("foo") + assertThat(foo.name).isEqualTo("foo") + val fooContent = foo.list() + assertThat(fooContent.directories.safeValue()).isEmpty() + assertThat(fooContent.files.safeValue()).isEmpty() + assertThat(fooContent.links.safeValue()).isEmpty() + val content = tree.list() + assertThat(content.directories.safeValue()).contains(foo) + assertThat(content.files.safeValue()).isEmpty() + assertThat(content.links.safeValue()).isEmpty() + } + + @Test + fun observeCreateDirectory() { + val content = tree.list() + var dir: Directory? = null + content.directories.observeForever { list -> + if (list.size == 1) dir = list[0] + } + tree.createDirectory("foo") + while (dir == null) { + ShadowLooper.idleMainLooper() + } + assertThat(dir?.name).isEqualTo("foo") + } + + @Test + fun createFile() { + val foo = tree.createFile("foo") + // Files are not created until you write to them. + assertThat(tree.list().files.safeValue()).isEmpty() + foo.write().use { os -> + os.write(byteArrayOf(1, 2, 3, 4)) + } + assertThat(tree.list().files.safeValue()).contains(foo) + assertThat(foo.size).isEqualTo(4.toULong()) + } + + @Test + fun overwriteFile() { + val foo = tree.createFile("foo") + foo.write().use { os -> + os.write(byteArrayOf(1, 2, 3, 4)) + } + foo.write().use { os -> + os.write(byteArrayOf(1)) + assertThat(foo.size).isEqualTo(4.toULong()) + } + assertThat(foo.size).isEqualTo(1.toULong()) + assertThat(tree.list().files.safeValue()).hasSize(1) + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..435b1fa --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.jetbrains.kotlin.android) apply false +} diff --git a/detekt.yaml b/detekt.yaml new file mode 100644 index 0000000..6b5d389 --- /dev/null +++ b/detekt.yaml @@ -0,0 +1,9 @@ +config: + validation: true + warningsAsErrors: false + excludes: '' + +naming: + PackageNaming: + packagePattern: '[a-z]+(\.[a-z][_A-Za-z0-9]*)*' + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..27ad9bf --- /dev/null +++ b/gradle.properties @@ -0,0 +1,27 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official + +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..e04d556 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,33 @@ +[versions] +agp = "8.5.0" +appcompat = "1.7.0" +constraintlayout = "2.1.4" +coreKtx = "1.13.1" +espressoCore = "3.6.1" +junit = "4.13.2" +junitVersion = "1.2.1" +kotlin = "2.0.0" +livedata = "2.8.3" +material = "1.12.0" +navigationFragmentKtx = "2.7.7" +navigationUiKtx = "2.7.7" + +[libraries] +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata", version.ref = "livedata" } +androidx-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "livedata" } +androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } +androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +robolectric = { group = "org.robolectric", name = "robolectric", version = "4.13" } +truth = { group = "com.google.truth", name = "truth", version = "1.4.3" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 0000000..e644113 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..09523c0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..7101f8e --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..0a813b0 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "CleverSync" +include(":app") |
