├── .deepsource.toml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml └── workflows │ ├── ci_build.yaml │ ├── crowdin-download.yml │ ├── crowdin-upload.yml │ └── translation-validation.yaml ├── .gitignore ├── .idea └── checkstyle-idea.xml ├── FUNDING.md ├── LICENSE ├── PRIVACY.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── oldRelease │ └── .gitignore ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── fmsys │ │ └── snapdrop │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── closeDialogs.js │ │ └── init.js │ ├── ic_about-playstore.png │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── fmsys │ │ │ └── snapdrop │ │ │ ├── AboutLibrariesListener.java │ │ │ ├── DarkModeSetting.java │ │ │ ├── FileBrowserActivity.java │ │ │ ├── FloatingTextActivity.java │ │ │ ├── JavaScriptInterface.java │ │ │ ├── MainActivity.java │ │ │ ├── ObservableProperty.java │ │ │ ├── OnboardingActivity.java │ │ │ ├── OnboardingFragment1.java │ │ │ ├── OnboardingFragment2.java │ │ │ ├── OnboardingFragment3.java │ │ │ ├── OnboardingFragmentPermission.java │ │ │ ├── OnboardingViewModel.java │ │ │ ├── OpenUrlActivity.java │ │ │ ├── PrefixEditText.java │ │ │ ├── QuickTileService.java │ │ │ ├── SettingsActivity.java │ │ │ ├── SettingsFragment.java │ │ │ ├── SnapdropApplication.java │ │ │ ├── WebsiteLocalizer.java │ │ │ └── utils │ │ │ ├── ClipboardUtils.java │ │ │ ├── Link.java │ │ │ ├── LiveCallback.java │ │ │ ├── LogUtils.java │ │ │ ├── NetworkUtils.java │ │ │ ├── Nullable.java │ │ │ ├── ShareUtils.java │ │ │ ├── StateHandler.java │ │ │ ├── ViewUtils.java │ │ │ └── ZipUtils.java │ └── res │ │ ├── drawable-xxxhdpi │ │ └── ic_launcher_actionbar.png │ │ ├── drawable │ │ ├── ic_download.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_open_url.xml │ │ ├── ic_settings.xml │ │ ├── ic_snapdrop.xml │ │ ├── network_off.xml │ │ ├── onboarding_folder_open.xml │ │ ├── onboarding_sample_devices.png │ │ ├── phonelink_off.xml │ │ ├── pref_about.xml │ │ ├── pref_baseurl.xml │ │ ├── pref_community.xml │ │ ├── pref_connectivity_card.xml │ │ ├── pref_debug.xml │ │ ├── pref_floatingtext.xml │ │ ├── pref_license.xml │ │ ├── pref_localization.xml │ │ ├── pref_location_metadata.xml │ │ ├── pref_name.xml │ │ ├── pref_notifications.xml │ │ ├── pref_savelocation.xml │ │ ├── pref_screen.xml │ │ ├── pref_theme.xml │ │ ├── snackbar.xml │ │ ├── snackbar_larger_margin.xml │ │ ├── snapdrop_anim.xml │ │ └── tv_banner.png │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_onboarding.xml │ │ ├── activity_settings.xml │ │ ├── debug_logs_dialog.xml │ │ ├── edit_text_dialog.xml │ │ ├── fragment_onboarding_1.xml │ │ ├── fragment_onboarding_2.xml │ │ ├── fragment_onboarding_3.xml │ │ ├── fragment_onboarding_permission.xml │ │ ├── preference_switch.xml │ │ ├── progress_dialog.xml │ │ └── servercard.xml │ │ ├── menu │ │ └── actionbar.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-ar-rSA │ │ └── strings.xml │ │ ├── values-bg-rBG │ │ └── strings.xml │ │ ├── values-ca-rES │ │ └── strings.xml │ │ ├── values-cs-rCZ │ │ └── strings.xml │ │ ├── values-de-rDE │ │ └── strings.xml │ │ ├── values-el-rGR │ │ └── strings.xml │ │ ├── values-es-rES │ │ └── strings.xml │ │ ├── values-fr-rFR │ │ └── strings.xml │ │ ├── values-hi-rIN │ │ └── strings.xml │ │ ├── values-hr-rHR │ │ └── strings.xml │ │ ├── values-hu-rHU │ │ └── strings.xml │ │ ├── values-in-rID │ │ └── strings.xml │ │ ├── values-it-rIT │ │ └── strings.xml │ │ ├── values-iw-rIL │ │ └── strings.xml │ │ ├── values-ja-rJP │ │ └── strings.xml │ │ ├── values-ko-rKR │ │ └── strings.xml │ │ ├── values-mk-rMK │ │ └── strings.xml │ │ ├── values-ne-rNP │ │ └── strings.xml │ │ ├── values-night │ │ └── colors_dark.xml │ │ ├── values-nl-rNL │ │ └── strings.xml │ │ ├── values-no-rNO │ │ └── strings.xml │ │ ├── values-pl-rPL │ │ └── strings.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ ├── values-pt-rPT │ │ └── strings.xml │ │ ├── values-ro-rRO │ │ └── strings.xml │ │ ├── values-ru-rRU │ │ └── strings.xml │ │ ├── values-sk-rSK │ │ └── strings.xml │ │ ├── values-sl-rSI │ │ └── strings.xml │ │ ├── values-sv-rSE │ │ └── strings.xml │ │ ├── values-tr-rTR │ │ └── strings.xml │ │ ├── values-uk-rUA │ │ └── strings.xml │ │ ├── values-v23 │ │ └── styles.xml │ │ ├── values-v27 │ │ └── styles.xml │ │ ├── values-vi-rVN │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── preference_keys.xml │ │ ├── strings.xml │ │ ├── strings_not_translatable.xml │ │ └── styles.xml │ │ └── xml │ │ ├── network_security_config.xml │ │ ├── preferences.xml │ │ ├── provider_paths.xml │ │ └── shortcuts.xml │ └── test │ └── java │ └── com │ └── fmsys │ └── snapdrop │ └── ExampleUnitTest.java ├── build.gradle ├── checkstyle.xml ├── crowdin.yml ├── fastlane └── metadata │ └── android │ ├── ar-SA │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── bg-BG │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ca-ES │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── cs-CZ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── de-DE │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── el-GR │ ├── short_description.txt │ └── title.txt │ ├── en-US │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── featureGraphic_new.png │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ ├── Screenshots.pptx │ │ │ ├── crossplatform_graphic.png │ │ │ └── raw │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 2021-11-23 18_50_59-featureGraphic.png (PNG-Grafik, 1280 × 640 Pixel) - Skaliert (96%).png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── Screenshot_20210311_145814.png │ │ │ ├── Screenshot_20210311_145814_framed.png │ │ │ ├── Screenshot_20210311_205218.png │ │ │ ├── Screenshot_20210311_205218_framed.png │ │ │ ├── Screenshot_20210311_205647.png │ │ │ ├── Screenshot_20210311_205647_framed.png │ │ │ ├── Screenshot_20210311_205718.png │ │ │ ├── Screenshot_20210311_205718_framed.png │ │ │ ├── Screenshot_20210311_205840.png │ │ │ ├── Screenshot_20210311_205840_framed.png │ │ │ ├── Screenshot_20211123_185301_framed.png │ │ │ ├── Screenshot_20211123_185411_framed.png │ │ │ ├── Screenshot_20220125_105528.png │ │ │ ├── Screenshot_20220125_105528_framed.png │ │ │ ├── Screenshot_20220125_110311.png │ │ │ ├── Screenshot_20220125_110311_framed.png │ │ │ ├── Screenshot_20220125_110653.png │ │ │ ├── Screenshot_20220125_110653_framed.png │ │ │ ├── Screenshot_20220125_110748.png │ │ │ ├── Screenshot_20220125_110748_framed.png │ │ │ ├── Screenshot_20220125_110853.png │ │ │ ├── Screenshot_20220125_110853_framed.png │ │ │ ├── Screenshot_20220125_111023.png │ │ │ ├── Screenshot_20220125_111023_framed.png │ │ │ ├── Screenshot_20220129_213116.png │ │ │ ├── Screenshot_20220129_213116_framed.png │ │ │ ├── Screenshot_20220129_213116_framed_dimmed.png │ │ │ ├── crossplatform_graphic.pdn │ │ │ ├── favicon.png │ │ │ └── icon_with_text.png │ ├── short_description.txt │ └── title.txt │ ├── es-ES │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── fr-FR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── he-IL │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── hi-IN │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── hr-HR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── hu-HU │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── id-ID │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── it-IT │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ja-JP │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ko-KR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── mk-MK │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ne-NP │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── nl-NL │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── no-NO │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── pl-PL │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── pt-BR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── pt-PT │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ro-RO │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ru-RU │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── sk-SK │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── sl-SI │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── sv-SE │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── tr-TR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── uk-UA │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── vi-VN │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── zh-CN │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ └── zh-TW │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── suppressions.xml └── translation-validation-bot ├── Dockerfile ├── action.yaml ├── requirements.txt └── script.py /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "java" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = 8 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.buymeacoffee.com/snapdropandroid 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report for the "Snapdrop for Android" app. 3 | labels: bug 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Welcome! Thanks for taking the time to submit a bug report. 9 | 10 | - type: input 11 | id: version 12 | attributes: 13 | label: App version 14 | description: What version of "Snapdrop for Android" are you using? The version can be found as the last entery inside the Settings of the app. 15 | validations: 16 | required: true 17 | 18 | - type: dropdown 19 | id: android_version 20 | attributes: 21 | label: Android version 22 | multiple: false 23 | description: What version of Android are you running? This can usually be found in your device's settings in the "About" section. 24 | options: 25 | - newer 26 | - 15 27 | - 14 28 | - 13 29 | - 12 30 | - 11 31 | - 10 32 | - 9 33 | - 8.1 34 | - 8.0 35 | - 7.1 36 | - 7.0 37 | - 6 38 | - 5.1 39 | - 5.0 40 | - I don't know 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: bug_description 46 | attributes: 47 | label: Describe the bug 48 | description: Write a clear and concise description of the bug. 49 | placeholder: What went wrong? 50 | validations: 51 | required: true 52 | 53 | - type: textarea 54 | id: bug_steps 55 | attributes: 56 | label: Steps to reproduce the bug 57 | description: | 58 | A clear and concise description of what the bug is. Bugs that are not reproducible cannot be investigated. 59 | placeholder: | 60 | Steps to reproduce the bug (e.g.): 61 | 1. Go to '...' 62 | 2. Click on '...' 63 | 3. Scroll down to '...' 64 | 4. See error 65 | validations: 66 | required: true 67 | 68 | - type: textarea 69 | id: bug_crash 70 | attributes: 71 | label: Stacktrace 72 | description: | 73 | Is this bug a crash? If yes, please attach a log... 74 | placeholder: | 75 | You can create a log via `Settings > Debug Log` 76 | validations: 77 | required: false 78 | 79 | - type: textarea 80 | id: screenshots 81 | attributes: 82 | label: Screenshots and additional context 83 | description: | 84 | If applicable, add screenshots to help explain your problem. You can also use this section to write any further context about the problem here. 85 | placeholder: | 86 | Drag the screenshot files here to attach them. 87 | validations: 88 | required: false 89 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Snapdrop.net Repository 4 | url: https://github.com/RobinLinus/snapdrop 5 | about: If you have encountered a general https://snapdrop.net issue, please open a ticket in their repository. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for the "Snapdrop for Android" app. 3 | labels: "feature request" 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Welcome! Thanks for taking the time to submit a feature request. 9 | 10 | - type: textarea 11 | id: feature_description 12 | attributes: 13 | label: Describe the feature 14 | description: Write a clear and concise description of what feature you are missing and why. Please also write the solution you'd like to see. 15 | placeholder: | 16 | E.g.: I'm always frustrated when [...] 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: feature_alternatives 22 | attributes: 23 | label: Describe alternatives you've considered 24 | description: A clear and concise description of any alternative solutions or features you've considered. 25 | placeholder: Are there any alternatives? 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: feature_additional_context 31 | attributes: 32 | label: Additional context 33 | description: | 34 | If applicable, add screenshots to help explain your problem. You can also use this section to write any further context about the problem here. 35 | placeholder: | 36 | Drag the screenshot files here to attach them. 37 | validations: 38 | required: false 39 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/ci_build.yaml: -------------------------------------------------------------------------------- 1 | name: APK Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout the code 15 | uses: actions/checkout@v2 16 | - name: Setup Java JDK 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: 'temurin' 20 | java-version: '17' 21 | - name: Build the app 22 | run: | 23 | chmod +x gradlew 24 | ./gradlew build 25 | - name: Upload APK 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: debug-apk 29 | path: app/build/outputs/apk/debug/*.apk 30 | if-no-files-found: error 31 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-download.yml: -------------------------------------------------------------------------------- 1 | name: Crowdin Download Action 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | crowdin: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Synchronize with Crowdin 20 | uses: crowdin/github-action@v2 21 | with: 22 | upload_sources: false 23 | upload_translations: false 24 | download_translations: true 25 | localization_branch_name: l10n_crowdin_translations 26 | 27 | create_pull_request: true 28 | pull_request_title: 'New Crowdin translations' 29 | pull_request_body: 'New Crowdin pull request with translations' 30 | pull_request_base_branch_name: 'master' 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.CROWDINBOT_TOKEN }} 33 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 34 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-upload.yml: -------------------------------------------------------------------------------- 1 | name: Crowdin Upload Action 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | crowdin: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Crowdin push 15 | uses: crowdin/github-action@v2 16 | with: 17 | upload_sources: true 18 | upload_translations: false 19 | download_translations: false 20 | env: 21 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 22 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/translation-validation.yaml: -------------------------------------------------------------------------------- 1 | name: Generate reverse translations 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | translate: 7 | if: ${{ github.head_ref == ' l10n_crowdin_translations' }} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | - name: Get Pull Request Number 13 | id: pr 14 | run: echo "pull_request_number=$(gh pr view l10n_master --json number -q .number || echo "")" >> $GITHUB_OUTPUT 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | - name: Translate 18 | id: translate 19 | # we can give directory path that contains action.yaml or repo address in username/repository_name@version format 20 | # in our case it would be nashmaniac/create-issue-action@v1.0 where v1.0 is the version of action 21 | # for now we will give directory path. The directory must contain action.yaml 22 | uses: ./translation-validation-bot/ 23 | # pass user input as arguments 24 | with: 25 | pr: ${{ steps.pr.outputs.pull_request_number }} 26 | # Temporarily disable console output, as it can't handle <"> characters correctly 27 | #- name: Display translations 28 | # run: | 29 | # echo "${{ steps.translate.outputs.content }}" 30 | - name: Update PR description 31 | uses: riskledger/update-pr-description@v2 32 | with: 33 | body: ${{ steps.translate.outputs.content }} 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/caches 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | /.idea/navEditor.xml 8 | /.idea/assetWizardSettings.xml 9 | .DS_Store 10 | /build 11 | /captures 12 | .externalNativeBuild 13 | .cxx 14 | 15 | # ignore IDEA settings 16 | .idea/* 17 | # but have a common checkstyle configuration 18 | !.idea/checkstyle-idea.xml 19 | *.iml 20 | *.lnk 21 | -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8.26 5 | JavaOnlyWithTests 6 | true 7 | true 8 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /FUNDING.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Support PairDrop for Android

4 | 5 |

This Android App is one of my hobby projects. If you want to support the app, you are more than welcomed to participate by submitting code contributions.

6 |

If you relay on PairDrop in your daily life, you are welcome to support the community via a donation. This app causes quite some server load, so it's always good to get some server costs sponsored.

7 | 8 |
9 | 10 |

Support the PairDrop Community

11 |

➡️ read how to donate to https://pairdrop.net

12 | 13 |
14 | 15 |

Buy me a coffee

16 |

If you want to support the developer of this Android app, you are welcome to by me a coffe or hot chocolate :)

17 |

18 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # PairDrop for Android Privacy Policy 2 | 3 | "PairDrop for Android" is an open-source project designed to provide an easy-to-use, cross-platform file-sharing solution that integrates well with the Android™ system. 4 | 5 | Unlike standalone services, our app does not have its own backend or servers. Instead, it relies entirely on web-based services selected by the user within the app (e.g. https://pairdrop.net/ by default). Because of this, the privacy and security of your data depend on the specific website you choose to use. We strongly recommend that users review the privacy policy of the selected web service before using it. 6 | 7 | ## Server Connections 8 | 9 | To enable device discovery on your local network, the app connects to the chosen web-based service. For example, if you use https://pairdrop.net/, your public IP address is temporarily processed to create a shared "room" for all devices in the same network. Any information handled by the web service is subject to its own privacy policy. 10 | 11 | ## User Rights 12 | 13 | Since we do not operate our own servers or store any user data, we cannot provide options to delete your information. If you want to stop using the app, simply uninstall it from your device. Any files received through the app will remain on your device until you manually delete them. 14 | 15 | ## Permissions 16 | 17 | The app requires the following permissions: 18 | 19 | - **Storage Access:** Necessary for sending and receiving files on some Android versions. We do not process your files beyond what is explicitly selected for sharing. 20 | - **WiFi State Access:** Used only locally to check network conditions and is not transmitted outside your device. 21 | 22 | ## Support Email 23 | 24 | For support inquiries, you can contact us at: **snapdrop-android[at]googlegroups.com** 25 | 26 | Emails sent to this address will be forwarded to our core developers. We do not share your emails with third parties. Emails are not automatically deleted, but you can request removal by contacting us. 27 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release -------------------------------------------------------------------------------- /app/oldRelease/.gitignore: -------------------------------------------------------------------------------- 1 | *.apk -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/fmsys/snapdrop/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | import androidx.test.platform.app.InstrumentationRegistry; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | 20 | /** 21 | 22 | // InstrumentedTests are currently not executed in our CI 23 | // If it should be necessary in the future, following GitHub Actions workflow might help 24 | 25 | jobs: 26 | test: 27 | runs-on: macOS-latest 28 | steps: 29 | - name: checkout 30 | uses: actions/checkout@v1 31 | with: 32 | fetch-depth: 1 33 | 34 | - name: run tests 35 | uses: reactivecircus/android-emulator-runner@v1 36 | with: 37 | api-level: 29 38 | script: ./gradlew connectedCheck 39 | */ 40 | 41 | 42 | @Test 43 | public void useAppContext() { 44 | // Context of the app under test. 45 | final Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 46 | assertEquals("com.example.snapdrop", appContext.getPackageName()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/assets/closeDialogs.js: -------------------------------------------------------------------------------- 1 | document.querySelectorAll('[close]').forEach(el => { 2 | if (el.innerText == "CLOSE" || el.innerText == "IGNORE" || el.innerText == "CANCEL"){ 3 | el.click(); 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /app/src/main/ic_about-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fm-sys/snapdrop-android/537f43b1cd35c45739b337231808b4750f54007b/app/src/main/ic_about-playstore.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fm-sys/snapdrop-android/537f43b1cd35c45739b337231808b4750f54007b/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/AboutLibrariesListener.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.mikepenz.aboutlibraries.LibsConfiguration; 8 | 9 | /** 10 | * Default listener implementing all methods we do not really need 11 | */ 12 | public abstract class AboutLibrariesListener implements LibsConfiguration.LibsListener { 13 | @Override 14 | public boolean onLibraryAuthorClicked(final @NonNull View view, final @NonNull com.mikepenz.aboutlibraries.entity.Library library) { 15 | return false; 16 | } 17 | 18 | @Override 19 | public boolean onLibraryContentClicked(final @NonNull View view, final @NonNull com.mikepenz.aboutlibraries.entity.Library library) { 20 | return false; 21 | } 22 | 23 | @Override 24 | public boolean onLibraryBottomClicked(final @NonNull View view, final @NonNull com.mikepenz.aboutlibraries.entity.Library library) { 25 | return false; 26 | } 27 | 28 | @Override 29 | public boolean onLibraryAuthorLongClicked(final @NonNull View view, final @NonNull com.mikepenz.aboutlibraries.entity.Library library) { 30 | return false; 31 | } 32 | 33 | @Override 34 | public boolean onLibraryContentLongClicked(final @NonNull View view, final @NonNull com.mikepenz.aboutlibraries.entity.Library library) { 35 | return false; 36 | } 37 | 38 | @Override 39 | public boolean onLibraryBottomLongClicked(final @NonNull View view, final @NonNull com.mikepenz.aboutlibraries.entity.Library library) { 40 | return false; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/DarkModeSetting.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.StringRes; 7 | import androidx.appcompat.app.AppCompatDelegate; 8 | 9 | /** 10 | * Possible values of the Dark Mode Setting. 11 | *

12 | * The Dark Mode Setting can be stored in {@link android.content.SharedPreferences} as String by using {@link DarkModeSetting#getPreferenceValue(Context)} and received via {@link DarkModeSetting#valueOf(String)}. 13 | *

14 | * Additionally, the equivalent {@link AppCompatDelegate}-Mode can be received via {@link #getModeId()}. 15 | * 16 | * @see AppCompatDelegate#MODE_NIGHT_YES 17 | * @see AppCompatDelegate#MODE_NIGHT_NO 18 | * @see AppCompatDelegate#MODE_NIGHT_FOLLOW_SYSTEM 19 | */ 20 | public enum DarkModeSetting { 21 | 22 | /** 23 | * Always use light mode. 24 | */ 25 | LIGHT(AppCompatDelegate.MODE_NIGHT_NO, R.string.pref_value_theme_light), 26 | /** 27 | * Always use dark mode. 28 | */ 29 | DARK(AppCompatDelegate.MODE_NIGHT_YES, R.string.pref_value_theme_dark), 30 | /** 31 | * Follow the global system setting for dark mode. 32 | */ 33 | SYSTEM_DEFAULT(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, R.string.pref_value_theme_system_default); 34 | 35 | private final @AppCompatDelegate.NightMode 36 | int modeId; 37 | private final @StringRes 38 | int preferenceValue; 39 | 40 | DarkModeSetting(final @AppCompatDelegate.NightMode int modeId, final @StringRes int preferenceValue) { 41 | this.modeId = modeId; 42 | this.preferenceValue = preferenceValue; 43 | } 44 | 45 | public int getModeId() { 46 | return modeId; 47 | } 48 | 49 | public String getPreferenceValue(final @NonNull Context context) { 50 | return context.getString(preferenceValue); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/FileBrowserActivity.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.app.DownloadManager; 4 | import android.content.ActivityNotFoundException; 5 | import android.content.Intent; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | import android.provider.DocumentsContract; 9 | import android.widget.Toast; 10 | 11 | import androidx.activity.result.ActivityResultLauncher; 12 | import androidx.activity.result.contract.ActivityResultContracts; 13 | import androidx.annotation.Nullable; 14 | import androidx.appcompat.app.AppCompatActivity; 15 | 16 | public class FileBrowserActivity extends AppCompatActivity { 17 | 18 | private final ActivityResultLauncher viewFileResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> openStorageFolder()); 19 | 20 | private final ActivityResultLauncher fileBrowserResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { 21 | if (result.getData() != null) { 22 | try { 23 | viewFileResultLauncher.launch(new Intent(Intent.ACTION_VIEW).setData(result.getData().getData())); 24 | } catch (ActivityNotFoundException e) { 25 | Toast.makeText(this, R.string.err_no_app, Toast.LENGTH_SHORT).show(); 26 | } 27 | } else { 28 | finish(); 29 | } 30 | }); 31 | 32 | @Override 33 | protected void onCreate(@Nullable Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | openStorageFolder(); 36 | } 37 | 38 | private void openStorageFolder() { 39 | if (MainActivity.isCustomSaveLocation() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 40 | final Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT); 41 | i.addCategory(Intent.CATEGORY_OPENABLE); 42 | i.setType("*/*"); 43 | i.putExtra(DocumentsContract.EXTRA_INITIAL_URI, MainActivity.getSaveLocation().getUri()); 44 | fileBrowserResultLauncher.launch(i); 45 | } else { 46 | startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)); 47 | finish(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/FloatingTextActivity.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Intent; 5 | import android.os.Build; 6 | import android.os.Bundle; 7 | 8 | import androidx.annotation.RequiresApi; 9 | import androidx.appcompat.app.AppCompatActivity; 10 | import androidx.core.content.IntentSanitizer; 11 | 12 | public class FloatingTextActivity extends AppCompatActivity { 13 | 14 | @RequiresApi(api = Build.VERSION_CODES.M) 15 | @Override 16 | protected void onCreate(final Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | final Intent intent = new IntentSanitizer.Builder() 19 | .allowComponent(new ComponentName(getApplicationContext(), MainActivity.class)) 20 | .allowAction(Intent.ACTION_PROCESS_TEXT) 21 | .allowType("text/plain") 22 | .allowExtra(Intent.EXTRA_PROCESS_TEXT, String.class) 23 | .allowExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, Boolean.class) 24 | .build() 25 | .sanitizeByFiltering(getIntent().setClass(this, MainActivity.class)); 26 | if (!isTaskRoot()) { 27 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 28 | } 29 | startActivity(intent); 30 | finish(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/ObservableProperty.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import androidx.core.util.Consumer; 4 | 5 | import java.util.Objects; 6 | 7 | public class ObservableProperty { 8 | private T value; 9 | private Consumer listener; 10 | 11 | public ObservableProperty(final T value) { 12 | this.value = value; 13 | } 14 | 15 | public void set(final T value) { 16 | if (Objects.equals(this.value, value)) { 17 | return; 18 | } 19 | this.value = value; 20 | if (listener != null) { 21 | listener.accept(value); 22 | } 23 | } 24 | 25 | public T get() { 26 | return value; 27 | } 28 | 29 | public void setOnChangedListener(final Consumer listener) { 30 | this.listener = listener; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/OnboardingActivity.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | 7 | import androidx.activity.OnBackPressedCallback; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import androidx.lifecycle.ViewModelProvider; 10 | import androidx.preference.PreferenceManager; 11 | 12 | 13 | public class OnboardingActivity extends AppCompatActivity { 14 | private static final String EXTRA_ONLY_SERVER_SELECTION = "extra_server"; 15 | 16 | OnboardingViewModel viewModel; 17 | 18 | public OnboardingActivity() { 19 | super(R.layout.activity_onboarding); 20 | } 21 | 22 | public static void launchOnboarding(final Activity context) { 23 | context.startActivity(new Intent(context, OnboardingActivity.class)); 24 | context.finish(); 25 | } 26 | 27 | public static Intent getServerSelectionIntent(final Activity context) { 28 | final Intent intent = new Intent(context, OnboardingActivity.class); 29 | intent.putExtra(EXTRA_ONLY_SERVER_SELECTION, true); 30 | return intent; 31 | } 32 | 33 | @Override 34 | protected void onCreate(final Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | 37 | viewModel = new ViewModelProvider(this).get(OnboardingViewModel.class); 38 | viewModel.setOnlyServerSelection(getIntent().getBooleanExtra(EXTRA_ONLY_SERVER_SELECTION, false)); 39 | 40 | viewModel.getFragment().observe(this, fragment -> getSupportFragmentManager() 41 | .beginTransaction() 42 | .setReorderingAllowed(true) 43 | .add(R.id.fragment_container_view, fragment, null) 44 | .commit()); 45 | 46 | viewModel.getUrl().observe(this, url -> PreferenceManager.getDefaultSharedPreferences(this).edit() 47 | .putBoolean(getString(R.string.pref_first_use), false) 48 | .putString(getString(R.string.pref_baseurl), url) 49 | .apply()); 50 | 51 | if (savedInstanceState == null) { 52 | if (viewModel.isOnlyServerSelection()) { 53 | viewModel.launchFragment(OnboardingFragment2.class); 54 | } else { 55 | viewModel.launchFragment(OnboardingFragment1.class); 56 | } 57 | } 58 | 59 | getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(!viewModel.isOnlyServerSelection()) { 60 | @Override 61 | public void handleOnBackPressed() { 62 | // block back press while onboarding 63 | } 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/OnboardingFragment1.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.content.pm.PackageManager; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.view.animation.DecelerateInterpolator; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.core.content.ContextCompat; 11 | import androidx.fragment.app.Fragment; 12 | import androidx.lifecycle.ViewModelProvider; 13 | 14 | import com.fmsys.snapdrop.databinding.FragmentOnboarding1Binding; 15 | import com.fmsys.snapdrop.utils.ViewUtils; 16 | 17 | public class OnboardingFragment1 extends Fragment { 18 | public OnboardingFragment1() { 19 | super(R.layout.fragment_onboarding_1); 20 | } 21 | 22 | @Override 23 | public void onViewCreated(final @NonNull View view, final Bundle savedInstanceState) { 24 | final FragmentOnboarding1Binding binding = FragmentOnboarding1Binding.bind(view); 25 | final OnboardingViewModel viewModel = new ViewModelProvider(requireActivity()).get(OnboardingViewModel.class); 26 | 27 | view.postDelayed(() -> { 28 | binding.appIcon.animate().alpha(1).setDuration(1000).setInterpolator(new DecelerateInterpolator()).start(); 29 | binding.appName.animate().alpha(1).translationY(ViewUtils.dpToPixel(20)).setDuration(1000).setInterpolator(new DecelerateInterpolator()).start(); 30 | binding.slogan.animate().setStartDelay(750).alpha(1).setInterpolator(new DecelerateInterpolator()).start(); 31 | binding.continueButton.animate().setStartDelay(750).alpha(1).setInterpolator(new DecelerateInterpolator()).start(); 32 | }, 500); 33 | 34 | binding.continueButton.setOnClickListener(v -> { 35 | viewModel.url("https://pairdrop.net"); 36 | if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) 37 | && (ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) { 38 | viewModel.launchFragment(OnboardingFragmentPermission.class); 39 | } else { 40 | viewModel.launchFragment(OnboardingFragment3.class); 41 | } 42 | }); 43 | binding.continueButton.requestFocus(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/OnboardingFragment3.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.fragment.app.Fragment; 9 | import androidx.lifecycle.ViewModelProvider; 10 | 11 | import com.fmsys.snapdrop.databinding.FragmentOnboarding3Binding; 12 | 13 | public class OnboardingFragment3 extends Fragment { 14 | 15 | public OnboardingFragment3() { 16 | super(R.layout.fragment_onboarding_3); 17 | } 18 | 19 | @Override 20 | public void onViewCreated(final @NonNull View view, final Bundle savedInstanceState) { 21 | final FragmentOnboarding3Binding binding = FragmentOnboarding3Binding.bind(view); 22 | final OnboardingViewModel viewModel = new ViewModelProvider(requireActivity()).get(OnboardingViewModel.class); 23 | 24 | binding.description.setText(getString(R.string.onboarding_howto_summary, viewModel.getUrl().getValue())); 25 | binding.continueButton.setOnClickListener(v -> { 26 | startActivity(new Intent(requireActivity(), MainActivity.class)); 27 | requireActivity().finish(); 28 | }); 29 | binding.continueButton.requestFocus(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/OnboardingFragmentPermission.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.Manifest; 4 | import android.content.pm.PackageManager; 5 | import android.os.Build; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | 9 | import androidx.activity.result.ActivityResultLauncher; 10 | import androidx.activity.result.contract.ActivityResultContracts; 11 | import androidx.annotation.NonNull; 12 | import androidx.core.content.ContextCompat; 13 | import androidx.fragment.app.Fragment; 14 | import androidx.lifecycle.ViewModelProvider; 15 | 16 | import com.fmsys.snapdrop.databinding.FragmentOnboardingPermissionBinding; 17 | 18 | public class OnboardingFragmentPermission extends Fragment { 19 | 20 | OnboardingViewModel viewModel; 21 | private final ActivityResultLauncher permissionResult = registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> { 22 | if (granted) { 23 | viewModel.launchFragment(OnboardingFragment2.class); 24 | } 25 | }); 26 | 27 | public OnboardingFragmentPermission() { 28 | super(R.layout.fragment_onboarding_permission); 29 | } 30 | 31 | @Override 32 | public void onViewCreated(final @NonNull View view, final Bundle savedInstanceState) { 33 | final FragmentOnboardingPermissionBinding binding = FragmentOnboardingPermissionBinding.bind(view); 34 | viewModel = new ViewModelProvider(requireActivity()).get(OnboardingViewModel.class); 35 | 36 | binding.continueButton.setOnClickListener(v -> { 37 | if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) 38 | && (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) { 39 | permissionResult.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE); 40 | } else { 41 | viewModel.launchFragment(OnboardingFragment2.class); 42 | } 43 | }); 44 | binding.continueButton.requestFocus(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/OnboardingViewModel.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import androidx.fragment.app.Fragment; 4 | import androidx.lifecycle.LiveData; 5 | import androidx.lifecycle.MutableLiveData; 6 | import androidx.lifecycle.ViewModel; 7 | 8 | public class OnboardingViewModel extends ViewModel { 9 | private final MutableLiveData> fragment = new MutableLiveData<>(); 10 | private final MutableLiveData url = new MutableLiveData<>(); 11 | private boolean onlyServerSelection; 12 | 13 | public void launchFragment(final Class item) { 14 | fragment.setValue(item); 15 | } 16 | 17 | public LiveData> getFragment() { 18 | return fragment; 19 | } 20 | 21 | public void url(final String url) { 22 | this.url.setValue(url); 23 | } 24 | 25 | public LiveData getUrl() { 26 | return url; 27 | } 28 | 29 | public boolean isOnlyServerSelection() { 30 | return onlyServerSelection; 31 | } 32 | 33 | public void setOnlyServerSelection(final boolean onlyServerSelection) { 34 | this.onlyServerSelection = onlyServerSelection; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/OpenUrlActivity.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | 7 | import androidx.annotation.Nullable; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | public class OpenUrlActivity extends AppCompatActivity { 11 | @Override 12 | protected void onCreate(@Nullable final Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getIntent().getStringExtra(Intent.EXTRA_TEXT)))); 15 | finish(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/PrefixEditText.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.util.AttributeSet; 6 | 7 | import com.google.android.material.textfield.TextInputEditText; 8 | 9 | 10 | /** 11 | * Custom view for adding a prefix to EditText 12 | * code by: https://medium.com/@ali.muzaffar/adding-a-prefix-to-an-edittext-2a17a62c77e1 13 | **/ 14 | public class PrefixEditText extends TextInputEditText { 15 | private float mOriginalLeftPadding = -1; 16 | 17 | public PrefixEditText(final Context context) { 18 | super(context); 19 | } 20 | 21 | public PrefixEditText(final Context context, final AttributeSet attrs) { 22 | super(context, attrs); 23 | } 24 | 25 | public PrefixEditText(final Context context, final AttributeSet attrs, 26 | final int defStyleAttr) { 27 | super(context, attrs, defStyleAttr); 28 | } 29 | 30 | @Override 31 | protected void onMeasure(final int widthMeasureSpec, 32 | final int heightMeasureSpec) { 33 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 34 | calculatePrefix(); 35 | } 36 | 37 | private void calculatePrefix() { 38 | if (mOriginalLeftPadding == -1) { 39 | final String prefix = (String) getTag(); 40 | float textWidth = 0; 41 | if (prefix != null) { 42 | final float[] widths = new float[prefix.length()]; 43 | getPaint().getTextWidths(prefix, widths); 44 | for (float w : widths) { 45 | textWidth += w; 46 | } 47 | } 48 | mOriginalLeftPadding = getCompoundPaddingLeft(); 49 | setPadding((int) (textWidth + mOriginalLeftPadding), 50 | getPaddingRight(), getPaddingTop(), 51 | getPaddingBottom()); 52 | } 53 | } 54 | 55 | @Override 56 | protected void onDraw(final Canvas canvas) { 57 | super.onDraw(canvas); 58 | final String prefix = (String) getTag(); 59 | if (prefix != null) { 60 | canvas.drawText(prefix, mOriginalLeftPadding, 61 | getLineBounds(0, null), getPaint()); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/QuickTileService.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.PendingIntent; 5 | import android.content.Intent; 6 | import android.os.Build; 7 | import android.service.quicksettings.Tile; 8 | import android.service.quicksettings.TileService; 9 | 10 | import androidx.annotation.RequiresApi; 11 | 12 | @RequiresApi(api = Build.VERSION_CODES.N) 13 | public class QuickTileService extends TileService { 14 | 15 | @Override 16 | public void onStartListening() { 17 | super.onStartListening(); 18 | 19 | final Tile tile = getQsTile(); 20 | if (tile != null) { 21 | tile.setState(Tile.STATE_INACTIVE); 22 | tile.updateTile(); 23 | } 24 | } 25 | 26 | @Override 27 | public void onClick() { 28 | super.onClick(); 29 | 30 | if (isLocked()) { 31 | unlockAndRun(this::startSnapdrop); 32 | } else { 33 | startSnapdrop(); 34 | } 35 | } 36 | 37 | @SuppressLint("StartActivityAndCollapseDeprecated") 38 | private void startSnapdrop() { 39 | final Intent intent = new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 40 | 41 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 42 | startActivityAndCollapse( 43 | PendingIntent.getActivity( 44 | this, 45 | 0, 46 | intent, 47 | PendingIntent.FLAG_IMMUTABLE 48 | ) 49 | ); 50 | } else { 51 | startActivityAndCollapse(intent); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.MenuItem; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | import androidx.appcompat.widget.Toolbar; 10 | 11 | public class SettingsActivity extends AppCompatActivity { 12 | 13 | @Override 14 | protected void onCreate(final Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | SnapdropApplication.setAppTheme(this); 17 | 18 | setContentView(R.layout.activity_settings); 19 | final Toolbar toolbar = findViewById(R.id.toolbar); 20 | setSupportActionBar(toolbar); 21 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 22 | 23 | getSupportFragmentManager() 24 | .beginTransaction() 25 | .replace(R.id.settings_fragment_root, new SettingsFragment()) 26 | .commit(); 27 | 28 | setResult(Activity.RESULT_OK); 29 | } 30 | 31 | @Override 32 | public boolean onOptionsItemSelected(@NonNull final MenuItem item) { 33 | if (item.getItemId() == android.R.id.home) { 34 | finish(); 35 | return true; 36 | } 37 | return super.onOptionsItemSelected(item); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/SnapdropApplication.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | import android.content.res.Configuration; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.appcompat.app.AppCompatDelegate; 10 | import androidx.preference.PreferenceManager; 11 | 12 | import com.fmsys.snapdrop.utils.LogUtils; 13 | 14 | 15 | public class SnapdropApplication extends Application { 16 | 17 | private static SnapdropApplication instance; 18 | 19 | public SnapdropApplication() { 20 | instance = this; 21 | } 22 | 23 | public static Application getInstance() { 24 | return instance; 25 | } 26 | 27 | @Override 28 | public void onCreate() { 29 | LogUtils.installUncaughtExceptionHandler(); 30 | setAppTheme(getApplicationContext()); 31 | super.onCreate(); 32 | } 33 | 34 | public static void setAppTheme(final @NonNull Context context) { 35 | setAppTheme(getAppTheme(context)); 36 | context.setTheme(R.style.AppTheme); 37 | } 38 | 39 | public static void setAppTheme(final DarkModeSetting setting) { 40 | AppCompatDelegate.setDefaultNightMode(setting.getModeId()); 41 | } 42 | 43 | private static DarkModeSetting getAppTheme(final @NonNull Context context) { 44 | final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 45 | return DarkModeSetting.valueOf(prefs.getString(context.getString(R.string.pref_theme_setting), DarkModeSetting.SYSTEM_DEFAULT.getPreferenceValue(context))); 46 | } 47 | 48 | private static boolean isDarkThemeActive(final @NonNull Context context, final DarkModeSetting setting) { 49 | if (setting == DarkModeSetting.SYSTEM_DEFAULT) { 50 | return isDarkThemeActive(context); 51 | } else { 52 | return setting == DarkModeSetting.DARK; 53 | } 54 | } 55 | 56 | private static boolean isDarkThemeActive(final @NonNull Context context) { 57 | final int uiMode = context.getResources().getConfiguration().uiMode; 58 | return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; 59 | } 60 | 61 | public static boolean isDarkTheme(final @NonNull Context context) { 62 | return isDarkThemeActive(context, getAppTheme(context)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/WebsiteLocalizer.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import android.webkit.WebView; 6 | 7 | import androidx.annotation.StringRes; 8 | 9 | public class WebsiteLocalizer { 10 | 11 | private enum TranslationElement { 12 | 13 | NO_PEERS_INFO("x-no-peers>h2", R.string.website_no_peers_info), 14 | INSTRUCTION_DESKTOP("x-instructions", "setAttribute('desktop', %s)", R.string.website_instruction), 15 | INSTRUCTION_MOBILE("x-instructions", "setAttribute('mobile', %s)", R.string.website_instruction), 16 | FILE_RECEIVED("#receiveDialog>x-background>x-paper>h3", R.string.website_file_received), 17 | FILE_ASK_EACH_TIME("label[for='autoDownload']", R.string.website_file_ask_before_download), 18 | FILE_DOWNLOAD("#download", R.string.website_file_download), 19 | FOOTER_DISCOVERY("footer>div.font-body2", R.string.website_footer_discovery), 20 | ABOUT_SUBHEADING("x-about>section>div.font-subheading", R.string.website_about_subheading), 21 | ; 22 | 23 | final String selector; 24 | final String modifier; 25 | final @StringRes int translationRes; 26 | 27 | TranslationElement(final String selector, final String modifier, final @StringRes int translationRes) { 28 | this.selector = selector; 29 | this.modifier = modifier; 30 | this.translationRes = translationRes; 31 | } 32 | 33 | TranslationElement(final String selector, final @StringRes int translationRes) { 34 | this.selector = selector; 35 | this.modifier = "innerHTML=%s"; 36 | this.translationRes = translationRes; 37 | } 38 | } 39 | 40 | private WebsiteLocalizer() { 41 | // no instances 42 | } 43 | 44 | public static void localizeIfNotBuiltIn(final WebView webView) { 45 | webView.evaluateJavascript("(function() { return !!document.getElementById('language-selector'); })();", localizationBuiltIn -> { 46 | if (localizationBuiltIn.equals("false")) { 47 | //Localization is not built into instance. Localize via snapdrop-android. 48 | localize(webView); 49 | } 50 | }); 51 | } 52 | 53 | public static void localize(final WebView webView) { 54 | for (TranslationElement element : TranslationElement.values()) { 55 | webView.evaluateJavascript(getTranslationJS(element, webView.getContext()), null); 56 | } 57 | } 58 | 59 | private static String getTranslationJS(final TranslationElement element, final Context context) { 60 | return "javascript: " + 61 | "var x = document.querySelector(\"" + element.selector + "\")." + String.format(element.modifier, "\"" + TextUtils.htmlEncode(context.getString(element.translationRes)) + "\"") + ";"; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/utils/ClipboardUtils.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop.utils; 2 | 3 | import android.content.ClipData; 4 | import android.content.ClipboardManager; 5 | import android.content.Context; 6 | 7 | public class ClipboardUtils { 8 | private ClipboardUtils() { 9 | // utility class 10 | } 11 | 12 | public static void copy(final Context context, final String text) { 13 | final ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 14 | final ClipData clip = ClipData.newPlainText("SnapdropAndroid", text); 15 | clipboard.setPrimaryClip(clip); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/utils/Link.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop.utils; 2 | 3 | import androidx.annotation.StringRes; 4 | 5 | public class Link { 6 | 7 | public String url; 8 | 9 | public @StringRes 10 | int description; 11 | 12 | private Link(final String url, final @StringRes int description) { 13 | this.url = url; 14 | this.description = description; 15 | } 16 | 17 | public static Link bind(final String url, final @StringRes int description) { 18 | return new Link(url, description); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/utils/LiveCallback.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop.utils; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.lifecycle.LifecycleOwner; 5 | import androidx.lifecycle.MutableLiveData; 6 | 7 | public class LiveCallback { 8 | private final MutableLiveData liveData = new MutableLiveData<>(); 9 | 10 | /** 11 | * Call the registered observers. 12 | */ 13 | public void call() { 14 | liveData.setValue(null); 15 | } 16 | 17 | /** 18 | * Posts a task to a main thread to call the registered observers. 19 | */ 20 | public void post() { 21 | liveData.postValue(null); 22 | } 23 | 24 | /** 25 | * Adds the given observer to the observers list within the lifespan of the given owner. 26 | * The events are dispatched on the main thread. 27 | * If a call was already triggered, the given action gets called directly. 28 | */ 29 | public void observe(final @NonNull LifecycleOwner owner, final @NonNull Runnable action) { 30 | liveData.observe(owner, none -> action.run()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/utils/Nullable.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop.utils; 2 | 3 | import androidx.core.util.Consumer; 4 | 5 | public class Nullable { 6 | 7 | private final T object; 8 | 9 | private Nullable(T object) { 10 | this.object = object; 11 | } 12 | 13 | public static Nullable of(T object) { 14 | return new Nullable<>(object); 15 | } 16 | 17 | public Nullable ifNotNull(Consumer consumer) { 18 | if (object != null) { 19 | consumer.accept(object); 20 | } 21 | return this; 22 | } 23 | 24 | public Nullable ifNull(Runnable runnable) { 25 | if (object == null) { 26 | runnable.run(); 27 | } 28 | return this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/utils/ShareUtils.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop.utils; 2 | 3 | import android.content.ActivityNotFoundException; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | 8 | import androidx.fragment.app.Fragment; 9 | 10 | import com.fmsys.snapdrop.OpenUrlActivity; 11 | import com.fmsys.snapdrop.R; 12 | import com.google.android.material.snackbar.Snackbar; 13 | 14 | public class ShareUtils { 15 | private ShareUtils() { 16 | // utility class 17 | } 18 | 19 | public static void shareUrl(final Context context, final String text) { 20 | final Intent sendIntent = new Intent() 21 | .setAction(Intent.ACTION_SEND) 22 | .setType("text/plain") 23 | .putExtra(Intent.EXTRA_TEXT, text); 24 | 25 | final Intent[] extraIntents = { new Intent(context, OpenUrlActivity.class) 26 | .putExtra(Intent.EXTRA_TEXT, text) }; 27 | 28 | final Intent shareIntent = Intent.createChooser(sendIntent, null); // pass null as title, as it will otherwise trigger the ugly EMUI share sheet 29 | shareIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents); 30 | 31 | context.startActivity(shareIntent); 32 | } 33 | 34 | public static void openUrl(final Fragment fragment, final String url) { 35 | try { 36 | fragment.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); 37 | } catch (ActivityNotFoundException e) { 38 | Snackbar.make(fragment.requireView(), R.string.err_no_browser, Snackbar.LENGTH_LONG).show(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/utils/StateHandler.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop.utils; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | public class StateHandler { 6 | private boolean currentlyOffline = false; 7 | private boolean currentlyLoading = true; 8 | private boolean currentlyStarting = true; 9 | 10 | private final AtomicInteger loadProgress = new AtomicInteger(); 11 | 12 | public boolean isCurrentlyOffline() { 13 | return currentlyOffline; 14 | } 15 | 16 | public void setCurrentlyOffline(final boolean currentlyOffline) { 17 | this.currentlyOffline = currentlyOffline; 18 | } 19 | 20 | public boolean isCurrentlyLoading() { 21 | return currentlyLoading; 22 | } 23 | 24 | public void setCurrentlyLoading(final boolean currentlyLoading) { 25 | this.currentlyLoading = currentlyLoading; 26 | 27 | if (!currentlyLoading) { 28 | currentlyStarting = false; 29 | } 30 | } 31 | 32 | public boolean isCurrentlyStarting() { 33 | return currentlyStarting; 34 | } 35 | 36 | public int getLoadProgress() { 37 | return loadProgress.get(); 38 | } 39 | 40 | public void setLoadProgress(final int newProgress) { 41 | loadProgress.set(newProgress); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/utils/ViewUtils.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop.utils; 2 | 3 | import android.content.res.Resources; 4 | import android.view.ContextThemeWrapper; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.widget.EditText; 8 | import android.widget.TextView; 9 | 10 | import androidx.appcompat.app.AlertDialog; 11 | import androidx.core.util.Consumer; 12 | import androidx.fragment.app.Fragment; 13 | 14 | import com.fmsys.snapdrop.R; 15 | import com.fmsys.snapdrop.SnapdropApplication; 16 | 17 | public class ViewUtils { 18 | 19 | private static final Resources APP_RESOURCES = SnapdropApplication.getInstance() == null || SnapdropApplication.getInstance().getApplicationContext() == null ? null : SnapdropApplication.getInstance().getApplicationContext().getResources(); 20 | 21 | private ViewUtils() { 22 | //no instance 23 | } 24 | 25 | public static int dpToPixel(final float dp) { 26 | return (int) (dp * (APP_RESOURCES == null ? 20f : APP_RESOURCES.getDisplayMetrics().density)); 27 | } 28 | 29 | public static void showEditTextWithResetPossibility(final Fragment fragment, final CharSequence title, final String prefix, final String initialValue, final Link link, final Consumer resultCallback) { 30 | final View dialogView = LayoutInflater.from(new ContextThemeWrapper(fragment.requireContext(), R.style.AlertDialogTheme)).inflate(R.layout.edit_text_dialog, null); 31 | final EditText editText = dialogView.findViewById(R.id.textInput); 32 | editText.setTag(prefix); 33 | editText.setText(initialValue); 34 | editText.requestFocus(); 35 | 36 | if (link != null) { 37 | final TextView helperText = dialogView.findViewById(R.id.helperText); 38 | helperText.setVisibility(View.VISIBLE); 39 | helperText.setText(link.description); 40 | helperText.setOnClickListener(v -> ShareUtils.openUrl(fragment, link.url)); 41 | } 42 | 43 | final AlertDialog.Builder builder = new AlertDialog.Builder(fragment.requireContext()) 44 | .setTitle(title) 45 | .setView(dialogView) 46 | .setPositiveButton(android.R.string.ok, (dialog, id) -> resultCallback.accept(editText.getText().toString().trim())) 47 | .setNegativeButton(R.string.reset, (dialog, id) -> resultCallback.accept(null)); 48 | builder.create().show(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmsys/snapdrop/utils/ZipUtils.java: -------------------------------------------------------------------------------- 1 | package com.fmsys.snapdrop.utils; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | 6 | import androidx.documentfile.provider.DocumentFile; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.util.List; 13 | import java.util.zip.ZipEntry; 14 | import java.util.zip.ZipOutputStream; 15 | 16 | public class ZipUtils { 17 | 18 | private ZipUtils() { 19 | // utility class 20 | } 21 | 22 | public static void createZipFromUris(final Context context, final List uris, final OutputStream outputStream) throws IOException { 23 | final ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream); 24 | final byte[] buffer = new byte[1024]; 25 | 26 | for (Uri uri : uris) { 27 | try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { 28 | if (inputStream == null) { 29 | continue; 30 | } 31 | final String fileName = getFileNameFromUri(context, uri); 32 | zipOutputStream.putNextEntry(new ZipEntry(fileName)); 33 | 34 | int length; 35 | while ((length = inputStream.read(buffer)) > 0) { 36 | zipOutputStream.write(buffer, 0, length); 37 | } 38 | zipOutputStream.closeEntry(); 39 | } 40 | } 41 | 42 | zipOutputStream.finish(); 43 | zipOutputStream.close(); 44 | } 45 | 46 | private static String getFileNameFromUri(final Context context, final Uri uri) { 47 | final DocumentFile file = DocumentFile.fromSingleUri(context, uri); 48 | if (file != null) { 49 | return file.getName(); 50 | } 51 | return uri.getLastPathSegment(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_launcher_actionbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fm-sys/snapdrop-android/537f43b1cd35c45739b337231808b4750f54007b/app/src/main/res/drawable-xxxhdpi/ic_launcher_actionbar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_download.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open_url.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_snapdrop.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/network_off.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/onboarding_folder_open.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/onboarding_sample_devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fm-sys/snapdrop-android/537f43b1cd35c45739b337231808b4750f54007b/app/src/main/res/drawable/onboarding_sample_devices.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/phonelink_off.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_about.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_baseurl.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_community.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_connectivity_card.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_debug.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_floatingtext.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_license.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_localization.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_location_metadata.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_name.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_notifications.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_savelocation.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_screen.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pref_theme.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/snackbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/snackbar_larger_margin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tv_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fm-sys/snapdrop-android/537f43b1cd35c45739b337231808b4750f54007b/app/src/main/res/drawable/tv_banner.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_onboarding.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 19 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/debug_logs_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/edit_text_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 20 | 21 | 22 | 23 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_onboarding_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 23 | 24 | 36 | 37 | 50 | 51 | 52 |