16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/play/release-notes/fr-FR/default.txt:
--------------------------------------------------------------------------------
1 | V1.6
2 | - Split sync mode into several independently configurable events
3 | - Added content statistics (can be disabled)
4 | - Improve dialog icons and styles
5 | - Fix notification permission request
6 |
7 | V1.5
8 | - Envoyé du texte à l'application
9 | - Éditer le texte sélectionné (Android 6.0+)
10 |
11 | V1.4
12 | - Nouveau raccourci pour effacer le presse-papiers directement sans ouvrir l'application
13 | - Nouveau paramètre: désactiver la synchronisation automatique
14 | - Nouvelle traduction en portugais (Brésil) grâce à mezysinc !
15 | - Nouvelle traduction en espagnol
16 | - Correction de l'icône non affichée dans les anciennes versions d'Android
17 |
18 | v1.3
19 | - Nouveau paramètre: capitaliser les phrases lors de la saisie
20 | - Ajustements des textes et des visuels des boîtes de dialogue
21 |
22 | V1.2
23 | - Mise à jour des styles
24 |
25 | V1.1
26 | - Ajout de liens vers Github et Blog dans l'écran "À Propos"
27 | - Partage fixe
28 |
29 | V1.0
30 | - Première version sur Play Store
31 |
--------------------------------------------------------------------------------
/app/src/main/play/release-notes/ru-RU/default.txt:
--------------------------------------------------------------------------------
1 | Версия 1.6
2 | - Разделён режим синхронизации на несколько независимо настраиваемых событий
3 | - Добавлена статистика содержимого (можно отключить)
4 | - Улучшены иконки и стили диалогов
5 | - Исправлен запрос разрешения на уведомления
6 |
7 | Версия 1.5
8 | - Отправка текста в приложение
9 | - Редактирование выделенного текста (Android 6.0+)
10 |
11 | Версия 1.4
12 | - Новое сочетание клавиш для очистки буфера обмена без открытия приложения
13 | - Настройка для отключения автоматической синхронизации
14 | - Новый перевод на португальский (Бразилия) — спасибо mezysinc!
15 | - Новый перевод на испанский
16 | - Исправлена иконка лаунчера, не отображавшаяся на старых версиях Android
17 |
18 | Версия 1.3
19 | - Новая настройка: автоматическая капитализация предложений при вводе
20 | - Исправлены тексты и улучшено визуальное оформление диалогов
21 |
22 | Версия 1.2
23 | - Обновление стиля оформления
24 |
25 | Версия 1.1
26 | - Добавлены ссылки на GitHub и блог в разделе «О приложении»
27 | - Исправлена функция совместного использования
28 |
29 | Версия 1.0
30 | - Первоначальный выпуск в Google Play
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/trianguloy/clipboardeditor/Shortcuts.java:
--------------------------------------------------------------------------------
1 | package com.trianguloy.clipboardeditor;
2 |
3 | import android.app.Activity;
4 | import android.content.ClipData;
5 | import android.content.ClipboardManager;
6 | import android.os.Build;
7 | import android.os.Bundle;
8 | import android.widget.Toast;
9 |
10 | /**
11 | * Activity that will clear the clipboard when launched, then exit
12 | */
13 | public class Shortcuts extends Activity {
14 | @Override
15 | protected void onCreate(Bundle savedInstanceState) {
16 | super.onCreate(savedInstanceState);
17 |
18 | // get
19 | var clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
20 |
21 | // clear
22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
23 | // the easy way, just call 'clear'
24 | clipboard.clearPrimaryClip();
25 | } else {
26 | // the not-so-easy way, manually set as empty
27 | clipboard.setPrimaryClip(ClipData.newPlainText("", ""));
28 | }
29 |
30 | Toast.makeText(this, R.string.toast_cleared, Toast.LENGTH_SHORT).show();
31 | finish();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'com.github.triplet.play' version '3.12.1'
4 | }
5 |
6 | android {
7 | namespace 'com.trianguloy.clipboardeditor'
8 | compileSdkVersion 36
9 |
10 | defaultConfig {
11 | applicationId "com.trianguloy.clipboardeditor"
12 | minSdk 14
13 | targetSdkVersion 36
14 | versionCode 9
15 | versionName "1.6"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled true
21 | shrinkResources true
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_17
27 | targetCompatibility JavaVersion.VERSION_17
28 | }
29 |
30 | lint {
31 | // users are free to update translations whenever
32 | // this mean that they are not usually up to date
33 | // and that there are usually lots of missing translations
34 | // so we ignore missing strings
35 | disable 'MissingTranslation'
36 | }
37 | }
38 |
39 | dependencies {
40 |
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | android.nonTransitiveRClass=false
21 | android.defaults.buildfeatures.buildconfig=true
22 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **English** | [Русский](README_RU.md)
2 |
3 |
4 |
5 |
6 |
7 | # Simple Clipboard Editor
8 |
9 | View, edit or delete the phone clipboard's text with a simple and fast editor.
10 |
11 | [](https://play.google.com/store/apps/details?id=com.trianguloy.clipboardeditor)
14 | [](https://f-droid.org/packages/com.trianguloy.clipboardeditor/)
17 |
18 | Android app made by TrianguloY
19 |
20 |
21 |
22 | ---
23 |
24 | You can use parts of this project in your own ones, create pull request, or upload modified versions of it AS LONG AS you credit me.
25 |
26 | How to credit:
27 | - You must add my nick (TrianguloY) in an 'about' or 'acknowledgments' section visible to the user.
28 | - You must add a link to this GitHub main page (`https://github.com/TrianguloY/SimpleClipboardEditor`) or subpage (if you used a part of the code or an asset) in an 'about' or 'acknowledgments' section visible to the user.
29 |
--------------------------------------------------------------------------------
/README_RU.md:
--------------------------------------------------------------------------------
1 | [English](README.md) | **Русский**
2 |
3 |
4 |
5 |
6 |
7 | # Simple Clipboard Editor
8 |
9 | Просматривайте, редактируйте или удаляйте текст из буфера обмена телефона с помощью простого и быстрого редактора.
10 |
11 | [](https://play.google.com/store/apps/details?id=com.trianguloy.clipboardeditor)
14 | [](https://f-droid.org/packages/com.trianguloy.clipboardeditor/)
17 |
18 | Приложение для Android от TrianguloY
19 |
20 |
21 |
22 | ---
23 |
24 | Вы можете использовать части этого проекта в своих собственных проектах, создавать pull request или загружать изменённые версии, ПРИ УСЛОВИИ, что вы укажете меня как автора.
25 |
26 | Как указать авторство:
27 | - Вы должны добавить мой ник (TrianguloY) в разделе «О приложении» или «Благодарности», где это будет видно пользователю.
28 | - Вы должны добавить ссылку на главную страницу этого GitHub-репозитория (`https://github.com/TrianguloY/SimpleClipboardEditor`) или на подраздел репозитория (если использовалась часть кода или ресурса) в разделе «О приложении» или «Благодарности», где это будет видно пользователю.
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/trianguloy/clipboardeditor/Process.java:
--------------------------------------------------------------------------------
1 | package com.trianguloy.clipboardeditor;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.Activity;
5 | import android.content.ClipData;
6 | import android.content.Intent;
7 | import android.os.Build;
8 | import android.os.Bundle;
9 |
10 | /**
11 | * This activity receives the PROCESS_TEXT intent and calls the main activity with it.
12 | * Separated to allow having a different label, and also because this feature is for Android 6.0+ only
13 | */
14 | @TargetApi(Build.VERSION_CODES.M)
15 | public class Process extends Activity {
16 |
17 | @Override
18 | protected void onCreate(Bundle savedInstanceState) {
19 | super.onCreate(savedInstanceState);
20 |
21 | // get the text
22 | var clipData = ClipData.newPlainText(
23 | getString(R.string.clip_selection),
24 | getIntent().getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
25 | );
26 |
27 | // process the text
28 | var intent = new Intent(this, Editor.class);
29 | intent.putExtra(getPackageName(), clipData);
30 | startActivityForResult(intent, 0);
31 | }
32 |
33 | @Override
34 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
35 | super.onActivityResult(requestCode, resultCode, data);
36 |
37 | // get the result
38 | var clipData = data == null ? null : data.getParcelableExtra(getPackageName());
39 | var result = clipData == null ? "" : clipData.getItemAt(0).coerceToText(this);
40 |
41 | // return it (we ignore the readonly attribute, it's unnecessary)
42 | var intent = new Intent();
43 | intent.putExtra(Intent.EXTRA_PROCESS_TEXT, result);
44 | setResult(RESULT_OK, intent);
45 | finish();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | .idea/jarRepositories.xml
48 | # Android Studio 3 in .gitignore file.
49 | .idea/caches
50 | .idea/modules.xml
51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
52 | .idea/navEditor.xml
53 |
54 | # Keystore files
55 | # Uncomment the following lines if you do not want to check your keystore files in.
56 | #*.jks
57 | #*.keystore
58 |
59 | # External native build folder generated in Android Studio 2.2 and later
60 | .externalNativeBuild
61 | .cxx/
62 |
63 | # Google Services (e.g. APIs or Firebase)
64 | # google-services.json
65 |
66 | # Freeline
67 | freeline.py
68 | freeline/
69 | freeline_project_description.json
70 |
71 | # fastlane
72 | fastlane/report.xml
73 | fastlane/Preview.html
74 | fastlane/screenshots
75 | fastlane/test_output
76 | fastlane/readme.md
77 |
78 | # Version control
79 | vcs.xml
80 |
81 | # lint
82 | lint/intermediates/
83 | lint/generated/
84 | lint/outputs/
85 | lint/tmp/
86 | # lint/reports/
87 |
88 | # Android Profiling
89 | *.hprof
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/about.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
20 |
21 |
29 |
30 |
38 |
39 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
35 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/trianguloy/clipboardeditor/Preferences.java:
--------------------------------------------------------------------------------
1 | package com.trianguloy.clipboardeditor;
2 |
3 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_BTN_IC;
4 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_EXTERNAL;
5 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_PAUSE;
6 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_START;
7 |
8 | import android.content.SharedPreferences;
9 |
10 | /** Simple Preferences wrapper */
11 | public class Preferences {
12 | private final SharedPreferences prefs; // the prefs
13 |
14 | /** @param prefs preferences to wrap */
15 | public Preferences(SharedPreferences prefs) {
16 | this.prefs = prefs;
17 |
18 | // migrations
19 | if (prefs.contains("sync")) {
20 | if (!prefs.getBoolean("sync", true)) {
21 | // sync = false -> toggle appropriate
22 | set(SYNC_START, false);
23 | set(SYNC_BTN_IC, true);
24 | set(SYNC_EXTERNAL, false);
25 | set(SYNC_BTN_IC, true);
26 | set(SYNC_PAUSE, false);
27 | }
28 | prefs.edit().remove("sync").apply();
29 | }
30 | }
31 |
32 | public enum Pref {
33 | SHOW_KEYBOARD("showKeyboard", true),
34 | CAPITALIZE("capitalize", false),
35 | STATISTICS("statistics", true),
36 | SYNC_START("syncStart", true),
37 | SYNC_BTN_CI("syncBtnCi", false),
38 | SYNC_EXTERNAL("syncExternal", true),
39 | SYNC_INPUT("syncInput", false),
40 | SYNC_BTN_IC("syncBtnIC", false),
41 | SYNC_PAUSE("syncPause", true),
42 | ;
43 |
44 | private final String key;
45 | private final boolean defaultValue;
46 |
47 | Pref(String key, boolean defaultValue) {
48 | this.key = key;
49 | this.defaultValue = defaultValue;
50 | }
51 | }
52 |
53 | public boolean is(Pref pref) {
54 | return prefs.getBoolean(pref.key, pref.defaultValue);
55 | }
56 |
57 | public void set(Pref pref, boolean value) {
58 | prefs.edit().putBoolean(pref.key, value).apply();
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Simple Clipboard Editor
5 | "Este app mostra o conteúdo atual do clipboard de seu telefone.
6 | Se o conteúdo for um texto, você pode editar, substituir, excluir ou realizar qualquer outra modificação em texto, em tempo real.
7 | Além disso, também pode imediatamente compartilhar o conteúdo e colocar ele como notificação para usar mais tarde.
8 |
9 | Aviso: Devido às restrições do Google, o app não pode interagir com o clipboard enquanto estiver fechado. No entanto, será automaticamente atualizado assim que for reaberto. O app não possui a funcionalidade de histórico de entradas (pelo menos por enquanto).
10 |
11 |
12 | Feito por TrianguloY, traduzido por mezysinc. Extremamente pequeno, eficiente, sem nenhuma permissão, gratuito e sem anúncios."
13 | Blog
14 | Código-fonte
15 | Play Store
16 |
17 |
18 | Categoria:
19 | Conteúdo:
20 | Mimetype:
21 | Colocar como notificação
22 | Compartilhar
23 | Limpar
24 | Configurações
25 | Detalhes
26 | Substitui conteúdo do clipboard por texto digitado
27 | Substitui o texto digitado por conteúdo do clipboard
28 | inexistente
29 | \nnúmero de itens =
30 |
31 |
32 | Texto
33 | Texto do clipboard
34 |
35 |
36 | Mostrar teclado quando o app for aberto
37 | Colocar a primeira letra em maiúscula em frases de entrada (alguns teclados podem ignorar isso)
38 |
39 |
40 | Limpar clipboard
41 | O conteúdo do clipboard foi removido
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fr-rFR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple Clipboard Editor
4 | "Cette application affiche le contenu actuel du presse-papiers de votre téléphone.
5 | Lorsque ce contenu est du texte, vous pouvez modifier, remplacer, supprimer ou effectuer toute autre modification de texte, en direct.
6 | En outre, vous pouvez également partager le contenu directement ou créer une notification avec celui-ci pour une utilisation ultérieure.
7 |
8 | Remarque : En raison des restrictions de Google, l'application ne peut pas interagir avec le presse-papiers lorsqu'il est fermé. Cependant, il sera automatiquement mis à jour dès que vous le rouvrirez. L'application ne contient aucune fonctionnalité d'historique (du moins pas encore).
9 |
10 |
11 | Réalisé par TrianguloY. Extrêmement léger, efficace, sans aucune autorisation, gratuit et sans publicité."
12 | Blog
13 | Code source
14 | Play Store
15 |
16 |
17 | Label:
18 | Contenu:
19 | Mimetype:
20 | Définir en tant que notification
21 | Partager
22 | Effacer
23 | Paramètres
24 | Info
25 | Remplacer les données du presse-papiers par la valeur d\'entrée
26 | Remplacer la valeur d\'entrée par les données du presse-papiers
27 | Vierge
28 | \nNombre d\'éléments =
29 | envoyer
30 |
31 |
32 | Texte
33 | Texte du presse-papiers
34 |
35 |
36 | "Faire apparaître le clavier à l'ouverture de l'application"
37 | Capitaliser la première lettre des phrases saisies (certains claviers peuvent ignorer cela)
38 |
39 |
40 | Effacer le presse-papier
41 | Presse-papiers effacé
42 |
43 |
44 | Éditeur de presse-papiers
45 | Sélection
46 |
47 |
48 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ru-rRU/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Простой редактор буфера обмена
3 | Это приложение отображает текущее содержимое буфера обмена вашего телефона.
4 | Если это текст, вы можете редактировать, заменять, удалять или выполнять любые другие изменения прямо в приложении.
5 | Также вы можете поделиться содержимым или создать уведомление с ним для дальнейшего использования.
6 |
7 | Примечание: из-за ограничений Google приложение не может работать с буфером обмена, когда закрыто. Оно автоматически обновится при повторном открытии. Функция истории пока отсутствует.
8 |
9 | Разработано TrianguloY. Очень маленькое, эффективное, без разрешений, бесплатное и без рекламы.
10 | Блог
11 | Исходный код
12 | Play Store
13 | Метка:
14 | Содержимое:
15 | MIME-тип:
16 | Установить как уведомление
17 | Поделиться
18 | Очистить
19 | Настройки
20 | Информация
21 | Заменить данные буфера обмена значением из поля ввода
22 | Заменить значение поля ввода данными из буфера обмена
23 | пусто
24 | \nКоличество элементов =
25 | отправлено
26 | строк: %d, слов: %d, символов: %s
27 | Текст
28 | Текст из буфера обмена
29 | Разрешение не предоставлено
30 | Показывать клавиатуру при запуске приложения
31 | Делать заглавной первую букву предложений (некоторые клавиатуры могут игнорировать)
32 | Отображать статистику содержимого
33 | Синхронизация Буфер обмена → Ввод:
34 | При запуске приложения (если текст не передан извне)
35 | Вручную (кнопка)
36 | При внешних изменениях буфера обмена
37 | Синхронизация Ввод → Буфер обмена:
38 | При изменениях поля ввода
39 | Вручную (кнопка)
40 | При закрытии диалогового окна или потере фокуса
41 | Очистить буфер обмена
42 | Буфер обмена очищен
43 | Редактор буфера обмена
44 | выделение
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple Clipboard Editor
4 | "This app displays the current content of your phone's clipboard.
5 | When that content is text you can edit, replace, delete or perform any other text modification, live.
6 | Additionally, you can also share the content directly or create a notification with it for later use.
7 |
8 | Note: Due to Google's restrictions the app can't interact with the clipboard while it's closed. It will be automatically updated as soon as you reopen it though. The app doesn't contains any history functionality (not yet at least).
9 |
10 |
11 | Made by TrianguloY. Extremely small, efficient, without any permissions, free and without ads."
12 | Blog
13 | Source code
14 | Play Store
15 |
16 |
17 | Label:
18 | Content:
19 | Mimetype:
20 | Set as notification
21 | Share
22 | Clear
23 | Settings
24 | Info
25 | Replace the clipboard data with the inputs value
26 | Replace the inputs value with the clipboard data
27 | empty
28 | \nItem count =
29 | sent
30 | lines: %d , words: %d , chars: %s
31 |
32 |
33 | Text
34 | Clipboard text
35 | Permission not granted
36 |
37 |
38 | Show keyboard when app opens
39 | Capitalize first letter of input sentences (some keyboards may ignore this)
40 | Show content statistics
41 | Clipboard → Input sync:
42 | When app starts (unless text were sent to the app)
43 | Manually (button)
44 | When clipboard changes externally
45 | Input → Clipboard sync:
46 | When input textbox changes
47 | Manually (button)
48 | When dialog closes/loses focus
49 |
50 |
51 | Clear clipboard
52 | Clipboard cleared
53 |
54 |
55 | Clipboard editor
56 | selection
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple editor del portapapeles
4 | "Esta aplicación muestra el contenido del portapapeles de tu dispositivo.
5 | Cuando el contenido es texto puedes editarlo, reemplazarlo, borrarlo o realizar cualquier modificación sobre él en vivo.
6 | Además, puedes compartir el contenido directamente o crear una notificación con él para usar mas tarde.
7 |
8 | Nota: Debido a las restricciones de Google la aplicación no puede interactuar con el portapapeles mientras está cerrada pero se sincronizará automáticamente al abrirse. La aplicación no contiene ninguna funcionalidad de historial (al menos no de momento).
9 |
10 |
11 | Hecha por TrianguloY. Extremadamente pequeña, eficiente, sin permisos, gratuita y sin anuncios."
12 | Blog
13 | Código fuente
14 | Play Store
15 | Categoría
16 | Contenido
17 | Tipo (mimetype)
18 | Poner como notificación
19 | Compartir
20 | Borrar
21 | Ajustes
22 | Información
23 | vacío
24 | \nnúmero de elementos =
25 | líneas: %d , palabras: %d , caracteres: %s
26 | Texto
27 | Texto del portapapeles
28 | Permiso no concedido
29 | Mostrar teclado al abrir la aplicación
30 | Poner en mayúsculas la primera letra de cada frase (algunos teclados puede que lo ignoren)
31 | Reemplaza el valor del portapapeles con el texto introducido
32 | Reemplaza el texto introducido con el valor del portapapeles
33 | Cuando el diálogo se cierra o pierde foco
34 | Borrar portapapeles
35 | Portapapeles borrado
36 | Editor del portapapelsseleccióncompartido
37 | Mostrar estadísticas del contenido
38 | Sincronizar portapapeles → cuadro de texto:
39 | Cuando la aplicación se inicia (a no ser que se envió texto a ella)
40 | Manual (botón)
41 | Cuando el portapapeles cambia externamente
42 | Sincronizar cuadro de texto → portapapeles:
43 | Cuando el cuadro de texto cambia
44 | Manual (botón)
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/configuration.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
21 |
22 |
28 |
29 |
36 |
37 |
44 |
45 |
51 |
52 |
58 |
59 |
65 |
66 |
73 |
74 |
80 |
81 |
87 |
88 |
94 |
95 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_editor.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
19 |
20 |
25 |
26 |
35 |
36 |
41 |
42 |
53 |
54 |
58 |
59 |
60 |
61 |
62 |
67 |
68 |
73 |
74 |
79 |
80 |
89 |
90 |
99 |
100 |
106 |
107 |
108 |
109 |
110 |
114 |
115 |
119 |
120 |
129 |
130 |
139 |
140 |
149 |
150 |
159 |
160 |
168 |
169 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/app/src/main/java/com/trianguloy/clipboardeditor/Editor.java:
--------------------------------------------------------------------------------
1 | package com.trianguloy.clipboardeditor;
2 |
3 | import static android.content.pm.PackageManager.PERMISSION_GRANTED;
4 | import static android.view.View.GONE;
5 | import static android.view.View.NOT_FOCUSABLE;
6 | import static android.view.View.VISIBLE;
7 | import static com.trianguloy.clipboardeditor.Preferences.Pref.CAPITALIZE;
8 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SHOW_KEYBOARD;
9 | import static com.trianguloy.clipboardeditor.Preferences.Pref.STATISTICS;
10 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_BTN_CI;
11 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_BTN_IC;
12 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_EXTERNAL;
13 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_INPUT;
14 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_PAUSE;
15 | import static com.trianguloy.clipboardeditor.Preferences.Pref.SYNC_START;
16 |
17 | import android.Manifest;
18 | import android.app.Activity;
19 | import android.app.AlertDialog;
20 | import android.app.Notification;
21 | import android.app.NotificationChannel;
22 | import android.app.NotificationManager;
23 | import android.app.PendingIntent;
24 | import android.content.ClipData;
25 | import android.content.ClipboardManager;
26 | import android.content.Context;
27 | import android.content.Intent;
28 | import android.net.Uri;
29 | import android.os.Build;
30 | import android.os.Bundle;
31 | import android.text.Editable;
32 | import android.text.InputType;
33 | import android.util.Log;
34 | import android.view.View;
35 | import android.view.Window;
36 | import android.view.inputmethod.InputMethodManager;
37 | import android.widget.EditText;
38 | import android.widget.Switch;
39 | import android.widget.TextView;
40 | import android.widget.Toast;
41 |
42 | import java.util.List;
43 |
44 | /**
45 | * The main activity, a clipboard editor
46 | */
47 | public class Editor extends Activity {
48 | private static final String CHANNEL_ID = "text"; // id for the channel for notifications
49 | private static final int NOTIFICATIONS_REQUEST_CODE = 1;
50 |
51 | // ------------------- data -------------------
52 |
53 | // classes
54 | private ClipboardManager clipboard; // system clipboard
55 | private NotificationManager notification; // system notifications
56 | private Preferences prefs; // preferences wrapper
57 |
58 | // views
59 | private EditText v_content; // content input
60 | private EditText v_label; // label input
61 | private TextView v_extra; // extra text
62 |
63 | private TextView v_statistics; // statistics text
64 |
65 | // internal data
66 | private boolean noListener = false; // to avoid firing clipboardToInput and inputToClipboard recursively
67 | private boolean syncOnHasFocus = true; // to run when app starts only once
68 |
69 | // ------------------- init -------------------
70 |
71 | @Override
72 | protected void onCreate(Bundle savedInstanceState) {
73 | super.onCreate(savedInstanceState);
74 |
75 | // activity content
76 | this.requestWindowFeature(Window.FEATURE_NO_TITLE);
77 | setContentView(R.layout.activity_editor);
78 |
79 | // views
80 | v_content = findViewById(R.id.content);
81 | v_label = findViewById(R.id.label);
82 | v_extra = findViewById(R.id.description);
83 | v_statistics = findViewById(R.id.statistics);
84 |
85 | // descriptions
86 | for (var viewId : new int[]{R.id.notify, R.id.share, R.id.clear, R.id.configure, R.id.info, R.id.sync_to, R.id.sync_from}) {
87 | findViewById(viewId).setOnLongClickListener(view -> {
88 | Toast.makeText(Editor.this, view.getContentDescription().toString(), Toast.LENGTH_SHORT).show();
89 | return true;
90 | });
91 | }
92 |
93 | // preferences
94 | prefs = new Preferences(getPreferences(MODE_PRIVATE));
95 |
96 | // clipboard
97 | clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
98 | notification = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
99 |
100 | // statistics
101 | v_content.addTextChangedListener(new SimpleTextWatcher() {
102 | @Override
103 | public void afterTextChanged(Editable s) {
104 | computeStatistics(s);
105 | }
106 | });
107 | computeStatistics(v_content.getText());
108 | v_statistics.setVisibility(prefs.is(STATISTICS) ? VISIBLE : GONE);
109 |
110 | // enable clipboard to input
111 | clipboard.addPrimaryClipChangedListener(() -> {
112 | if (prefs.is(SYNC_EXTERNAL)) {
113 | clipboardToInput();
114 | }
115 | });
116 |
117 | // enable input to clipboard
118 | var watcher = new SimpleTextWatcher() {
119 | @Override
120 | public void afterTextChanged(Editable s) {
121 | if (prefs.is(SYNC_INPUT)) {
122 | // sync on input
123 | inputToClipboard();
124 | }
125 | }
126 | };
127 | v_content.addTextChangedListener(watcher);
128 | v_label.addTextChangedListener(watcher);
129 |
130 | // manual buttons
131 | findViewById(R.id.sync_to).setVisibility(prefs.is(SYNC_BTN_IC) ? VISIBLE : GONE);
132 | findViewById(R.id.sync_from).setVisibility(prefs.is(SYNC_BTN_CI) ? VISIBLE : GONE);
133 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
134 | // avoid focusing the statistics if the manual buttons are both disabled
135 | // for some reason the xml property does not work
136 | findViewById(R.id.extra_parent).setFocusable(NOT_FOCUSABLE);
137 | }
138 |
139 | // auto-update result
140 | v_content.addTextChangedListener(new SimpleTextWatcher() {
141 | @Override
142 | public void afterTextChanged(Editable s) {
143 | var intent = new Intent();
144 | intent.putExtra(getPackageName(), inputAsPrimaryClip());
145 | setResult(RESULT_OK, intent);
146 | }
147 | });
148 |
149 | // give focus to the content
150 | // & show keyboard if enabled in settings
151 | if (v_content.requestFocus() && prefs.is(SHOW_KEYBOARD)) {
152 | ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE))
153 | .showSoftInput(v_content, InputMethodManager.SHOW_IMPLICIT);
154 | }
155 |
156 | // capitalize input state if enabled in settings
157 | setCapitalizeState(prefs.is(CAPITALIZE));
158 |
159 | // start intent
160 | parseIntent(getIntent());
161 | setIntent(null);
162 | }
163 |
164 | @Override
165 | public void onWindowFocusChanged(boolean hasFocus) {
166 | super.onWindowFocusChanged(hasFocus);
167 | if (hasFocus) {
168 | if (syncOnHasFocus) {
169 | syncOnHasFocus = false;
170 |
171 | // when windows is focused, update clipboard
172 | // this is the moment the clipboard is available after the app starts
173 | if (prefs.is(SYNC_START)) {
174 | clipboardToInput();
175 | }
176 | }
177 | } else {
178 | if (prefs.is(SYNC_PAUSE)) inputToClipboard();
179 | }
180 | }
181 |
182 | @Override
183 | protected void onNewIntent(Intent intent) {
184 | super.onNewIntent(intent);
185 | // on new intent (probably from a notification or from a sent text) load it
186 | parseIntent(intent);
187 | }
188 |
189 | /**
190 | * @param intent intent to parse for clipboard data
191 | */
192 | private void parseIntent(Intent intent) {
193 | if (intent == null) return;
194 | ClipData data = null;
195 |
196 | // set by ourselves
197 | if (intent.hasExtra(getPackageName()))
198 | data = intent.getParcelableExtra(getPackageName());
199 | // sent
200 | if (data == null && intent.hasExtra(Intent.EXTRA_TEXT))
201 | data = ClipData.newPlainText(getString(R.string.clip_sent), intent.getStringExtra(Intent.EXTRA_TEXT));
202 |
203 | // set
204 | if (data != null) {
205 | clipToInput(data);
206 | syncOnHasFocus = false;
207 | }
208 | }
209 |
210 | /**
211 | * Update
212 | */
213 | private void setCapitalizeState(boolean state) {
214 | if (state) {
215 | // set flag
216 | v_content.setInputType(v_content.getInputType() | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
217 | v_label.setInputType(v_label.getInputType() | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
218 | } else {
219 | // remove flag
220 | v_content.setInputType(v_content.getInputType() & ~InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
221 | v_label.setInputType(v_label.getInputType() & ~InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
222 | }
223 | }
224 |
225 | // ------------------- buttons -------------------
226 |
227 | /** Shows a notification with the clipboard content */
228 | public void onNotification(View ignored) {
229 |
230 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notification.areNotificationsEnabled()) {
231 | // request notifications permission
232 | requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, NOTIFICATIONS_REQUEST_CODE);
233 | return;
234 | }
235 |
236 | Notification.Builder builder;
237 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
238 | // setup a notification channel in Oreo+
239 | var channel = new NotificationChannel(CHANNEL_ID, getString(R.string.channel_name), NotificationManager.IMPORTANCE_DEFAULT);
240 | channel.setDescription(getString(R.string.channel_description));
241 | notification.createNotificationChannel(channel);
242 |
243 | builder = new Notification.Builder(this, CHANNEL_ID);
244 | } else {
245 | // no notification channel before Oreo
246 | builder = new Notification.Builder(this);
247 | }
248 |
249 | // sets the label as notification title (if any), the content and icon
250 | if (v_label.getText().length() == 0) builder.setContentTitle(v_label.getText());
251 | builder.setContentText(v_content.getText());
252 | builder.setSmallIcon(R.drawable.ic_notification);
253 |
254 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
255 | // when allowed, set the content as big text (improved display)
256 | builder.setStyle(new Notification.BigTextStyle()
257 | .bigText(v_content.getText()));
258 | }
259 |
260 | // sets the intent for when you click the notification. It will open the app with the current clipboard content
261 | var intent = new Intent(this, Editor.class);
262 | intent.putExtra(getPackageName(), inputAsPrimaryClip());
263 | builder.setContentIntent(PendingIntent.getActivity(this, getUniqueId(), intent, PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0))); // change the requestCode with an unique id for multiple independent pendingIntents
264 |
265 |
266 | // publish the notification
267 | var notification = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
268 | var id = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? notification.getActiveNotifications().length : getUniqueId(); // ensure unique id
269 | notification.notify(id,
270 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? builder.build() : builder.getNotification()
271 | );
272 | }
273 |
274 | @Override
275 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
276 | if (requestCode == NOTIFICATIONS_REQUEST_CODE) {
277 | if (grantResults.length >= 1 && grantResults[0] == PERMISSION_GRANTED) {
278 | onNotification(null);
279 | }
280 | Toast.makeText(this, R.string.noPermission, Toast.LENGTH_SHORT).show();
281 | return;
282 | }
283 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
284 | }
285 |
286 | /**
287 | * Share the clipboard content
288 | */
289 | public void onShare(View view) {
290 | // create a SEND text intent with the clipboard content
291 | var sendIntent = new Intent();
292 | sendIntent.setAction(Intent.ACTION_SEND);
293 | sendIntent.putExtra(Intent.EXTRA_TEXT, v_content.getText().toString());
294 | sendIntent.setType("text/plain");
295 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
296 | // not sure what it does, but maybe it allows to share images (even if the app can't display them)
297 | sendIntent.setClipData(inputAsPrimaryClip());
298 | }
299 |
300 | // start a chooser, use the label as title
301 | startActivity(Intent.createChooser(sendIntent, v_label.getText()));
302 | }
303 |
304 | /**
305 | * Clears the clipboard content
306 | */
307 | public void onClear(View view) {
308 | v_content.setText("");
309 | v_label.setText("");
310 | }
311 |
312 | /**
313 | * Open the configure screen
314 | */
315 | public void onConfigure(View view) {
316 | // setup
317 | var content = getLayoutInflater().inflate(R.layout.configuration, null);
318 |
319 | for (var preferenceSwitch : List.of(
320 | new PreferenceSwitch(SHOW_KEYBOARD, R.id.autokeyboard, null),
321 | new PreferenceSwitch(CAPITALIZE, R.id.capitalize, this::setCapitalizeState),
322 | new PreferenceSwitch(STATISTICS, R.id.statistics, checked -> {
323 | if (checked) computeStatistics(v_content.getEditableText());
324 | v_statistics.setVisibility(checked ? VISIBLE : GONE);
325 | }),
326 | new PreferenceSwitch(SYNC_START, R.id.sync_start, null),
327 | new PreferenceSwitch(SYNC_BTN_CI, R.id.sync_btn_ci, checked -> findViewById(R.id.sync_from).setVisibility(checked ? VISIBLE : GONE)),
328 | new PreferenceSwitch(SYNC_EXTERNAL, R.id.sync_external, null),
329 | new PreferenceSwitch(SYNC_INPUT, R.id.sync_input, null),
330 | new PreferenceSwitch(SYNC_BTN_IC, R.id.sync_btn_ic, checked -> findViewById(R.id.sync_to).setVisibility(checked ? VISIBLE : GONE)),
331 | new PreferenceSwitch(SYNC_PAUSE, R.id.sync_pause, null)
332 | )) {
333 | var switchView = content.findViewById(preferenceSwitch.id);
334 | switchView.setChecked(prefs.is(preferenceSwitch.preference));
335 | switchView.setOnCheckedChangeListener((checkbox, checked) -> {
336 | prefs.set(preferenceSwitch.preference, checked);
337 | if (preferenceSwitch.onChange != null) preferenceSwitch.onChange.onChange(checked);
338 | });
339 | }
340 |
341 | // show
342 | new AlertDialog.Builder(this)
343 | .setIcon(R.mipmap.ic_launcher)
344 | .setTitle(R.string.descr_configure)
345 | .setView(content)
346 | .show();
347 | }
348 |
349 | record PreferenceSwitch(Preferences.Pref preference, int id, OnPrefChange onChange) {
350 | interface OnPrefChange {
351 | void onChange(boolean state);
352 | }
353 | }
354 |
355 | /**
356 | * Show the about screen
357 | */
358 | public void onInfo(View view) {
359 | // before-setup
360 | var content = getLayoutInflater().inflate(R.layout.about, null);
361 |
362 | // show
363 | var dialog = new AlertDialog.Builder(this)
364 | .setIcon(R.mipmap.ic_launcher)
365 | .setTitle(getString(R.string.descr_info))
366 | .setView(content)
367 | .show();
368 |
369 | // after-setup
370 | for (var id : new int[]{R.id.blog, R.id.github, R.id.playstore}) {
371 | // setup all three buttons (its tag is the url)
372 | var button = content.findViewById(id);
373 | button.setOnClickListener(btn -> {
374 | // click to open in browser
375 | var url = btn.getTag().toString();
376 | try {
377 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
378 | } catch (Exception e) {
379 | // on error, like no browser, invoke long click (set the url in the clipboard)
380 | btn.performLongClick();
381 | }
382 | dialog.dismiss();
383 | });
384 | button.setOnLongClickListener(btn -> {
385 | // long click to set in input
386 | v_label.setText("TrianguloY");
387 | v_content.setText(btn.getTag().toString());
388 | dialog.dismiss();
389 | return true;
390 | });
391 | }
392 | }
393 |
394 | /**
395 | * @see this#clipboardToInput
396 | */
397 | public void inputFromClipboard(View view) {
398 | clipboardToInput();
399 | }
400 |
401 | /**
402 | * @see this#inputToClipboard()
403 | */
404 | public void inputToClipboard(View view) {
405 | inputToClipboard();
406 | }
407 |
408 |
409 | // ------------------- transfer -------------------
410 |
411 | /**
412 | * Sets the input values to the clipboard ones
413 | * Ensures it doesn't fire inputToClipboard
414 | */
415 | private void clipboardToInput() {
416 | if (noListener) return;
417 | noListener = true;
418 |
419 | // get
420 | clipToInput(clipboard.getPrimaryClip());
421 |
422 | noListener = false;
423 | }
424 |
425 | /**
426 | * Sets the inputs to the values of the clipdata
427 | */
428 | private void clipToInput(ClipData clip) {
429 |
430 | // set
431 | if (clip == null) {
432 | // no content
433 | v_extra.setText(String.format("[%s]", getString(R.string.txt_empty)));
434 | v_label.setText("");
435 | v_content.setText("");
436 |
437 | Log.d("CLIPBOARD", "--> null");
438 | } else {
439 | // content
440 | var description = clip.getDescription();
441 |
442 | // mimetype
443 | v_extra.setText(R.string.label_mimetype);
444 | var empty = true;
445 | for (var i = 0; i < description.getMimeTypeCount(); i++) {
446 | if (!empty) v_extra.append(" -");
447 | empty = false;
448 | v_extra.append(" " + description.getMimeType(i));
449 | }
450 | if (empty) v_extra.append(getString(R.string.txt_empty));
451 |
452 | // item count
453 | var itemCount = clip.getItemCount();
454 | if (itemCount > 1) v_extra.append(getString(R.string.txt_itemcount) + itemCount);
455 |
456 | // label
457 | var label = toStringNonNull(description.getLabel());
458 | if (!toStringNonNull(v_label.getText()).equals(label)) {
459 | v_label.setText(label);
460 | if (v_label.hasFocus()) v_label.setSelection(v_label.getText().length());
461 | }
462 |
463 | // text
464 | var content = toStringNonNull(clip.getItemAt(0).coerceToText(this));
465 | if (!toStringNonNull(v_content.getText()).equals(content)) {
466 | v_content.setText(content);
467 | if (v_content.hasFocus()) v_content.setSelection(v_content.getText().length());
468 | }
469 |
470 |
471 | Log.d("CLIPBOARD", "--> [" + label + "] " + content);
472 | }
473 | }
474 |
475 | /**
476 | * Sets the clipboard value to the input ones
477 | * Ensures it doesn't fire clipboardToInput
478 | */
479 | private void inputToClipboard() {
480 | if (noListener) return;
481 | noListener = true;
482 |
483 | // set
484 | var clip = inputAsPrimaryClip();
485 | clipboard.setPrimaryClip(clip);
486 |
487 | Log.d("CLIPBOARD", "Input --> " + clip);
488 |
489 | noListener = false;
490 | }
491 |
492 | /**
493 | * Returns the input as primary clip
494 | */
495 | private ClipData inputAsPrimaryClip() {
496 | return ClipData.newPlainText(v_label.getText().toString(), v_content.getText().toString());
497 | }
498 |
499 | /** Computes and diplays statistics about the textview content */
500 | private void computeStatistics(Editable editable) {
501 | if (prefs.is(STATISTICS)) {
502 | var string = editable.toString();
503 | var trimmed = string.trim();
504 | v_statistics.setText(getString(R.string.statistics,
505 | /*lines*/ string.isEmpty() ? 0 : string.split("\\n", -1).length,
506 | /*words*/ trimmed.isEmpty() ? 0 : trimmed.split("\\s+").length,
507 | /*length*/ editable.length() == string.length() ? Integer.toString(string.length()) : string.length() + "(" + editable.length() + ")"
508 | ));
509 | }
510 | }
511 |
512 |
513 | // ------------------- utils -------------------
514 |
515 | /**
516 | * return object?.toString() ?: "";
517 | *
518 | * @param object any object, including null
519 | * @return non null string
520 | */
521 | static private String toStringNonNull(Object object) {
522 | return object == null ? "" : object.toString();
523 | }
524 |
525 | /**
526 | * @return a 'unique' id (should be unique unless called at the same millisecond or after a very VERY long time)
527 | */
528 | private static int getUniqueId() {
529 | return Long.valueOf(System.currentTimeMillis()).intValue();
530 | }
531 |
532 | }
--------------------------------------------------------------------------------