diff --git a/app/build.gradle b/app/build.gradle index 6ac0f0e1..b436e1bb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -167,24 +167,24 @@ gradle.taskGraph.whenReady { graph -> ext { markwon_version = '4.6.2' - roomVersion = '2.8.4' + roomVersion = '2.8.5' } dependencies { implementation platform('com.google.firebase:firebase-bom:34.18.0') implementation 'com.google.firebase:firebase-auth' - implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'androidx.appcompat:appcompat:1.8.0' implementation 'com.google.android.material:material:1.14.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.2' implementation 'androidx.preference:preference:1.2.1' implementation "androidx.lifecycle:lifecycle-viewmodel:2.11.0" implementation 'androidx.core:core-splashscreen:1.2.0' implementation 'androidx.activity:activity:1.13.0' - implementation 'androidx.work:work-runtime:2.10.1' + implementation 'androidx.work:work-runtime:2.12.0' // Google Credential Manager (authentication only; Drive authorization is requested separately) - implementation 'androidx.credentials:credentials:1.5.0' - implementation 'androidx.credentials:credentials-play-services-auth:1.5.0' - implementation 'com.google.android.libraries.identity.googleid:googleid:1.2.0' + implementation 'androidx.credentials:credentials:1.6.0' + implementation 'androidx.credentials:credentials-play-services-auth:1.6.0' + implementation 'com.google.android.libraries.identity.googleid:googleid:1.2.1' implementation 'com.google.android.gms:play-services-auth:21.4.0' //DI (Hilt) @@ -203,7 +203,7 @@ dependencies { implementation 'com.android.billingclient:billing:8.3.0' //Other lib (Gson/PhotoView) - implementation 'com.google.code.gson:gson:2.14.0' + implementation 'com.google.code.gson:gson:2.15.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'androidx.exifinterface:exifinterface:1.4.2' @@ -214,9 +214,9 @@ dependencies { // Unit tests testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:5.23.0' - testImplementation 'com.google.truth:truth:1.4.2' - testImplementation 'org.robolectric:robolectric:4.16.1' + testImplementation 'org.mockito:mockito-core:5.24.0' + testImplementation 'com.google.truth:truth:1.4.5' + testImplementation 'org.robolectric:robolectric:4.17' // Room testing (integration tests) testImplementation "androidx.room:room-testing:$roomVersion" @@ -224,7 +224,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.7.0' androidTestImplementation 'androidx.test:runner:1.7.0' androidTestImplementation "androidx.room:room-testing:$roomVersion" - androidTestImplementation 'org.mockito:mockito-android:5.23.0' + androidTestImplementation 'org.mockito:mockito-android:5.24.0' androidTestImplementation 'com.google.truth:truth:1.4.5' } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/MediaSettingsFragment.java b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/MediaSettingsFragment.java index 94c6ab4e..e626436c 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/MediaSettingsFragment.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/MediaSettingsFragment.java @@ -8,6 +8,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -31,6 +32,8 @@ @AndroidEntryPoint public class MediaSettingsFragment extends Fragment { + private static final String TAG = "MediaSettings"; + @Inject ThemePreferencesCache themePreferencesCache; @Inject AppPreferencesCache appPreferencesCache; @@ -123,8 +126,18 @@ private void initListeners() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser && audioManager != null) { - audioManager.setStreamVolume( - AudioManager.STREAM_NOTIFICATION, progress, 0); + try { + audioManager.setStreamVolume( + AudioManager.STREAM_NOTIFICATION, progress, 0); + } catch (SecurityException e) { + // With Do Not Disturb on, the system refuses volume changes + // that would toggle it unless the app has notification policy + // access. The slider goes back to the volume actually in effect. + Log.w(TAG, "Notification volume change refused", e); + seekBar.setProgress( + audioManager.getStreamVolume( + AudioManager.STREAM_NOTIFICATION)); + } } } diff --git a/app/src/main/java/com/pasich/mynotes/utils/editor/EditableLinkMovementMethod.java b/app/src/main/java/com/pasich/mynotes/utils/editor/EditableLinkMovementMethod.java index febc6ce8..095601f3 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/editor/EditableLinkMovementMethod.java +++ b/app/src/main/java/com/pasich/mynotes/utils/editor/EditableLinkMovementMethod.java @@ -60,7 +60,16 @@ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event } } } - return super.onTouchEvent(widget, buffer, event); + try { + return super.onTouchEvent(widget, buffer, event); + } catch (IndexOutOfBoundsException e) { + // In selecting mode (Shift held on a hardware keyboard) the platform extends the + // selection from the offset it recorded on the down. When the down came before + // selecting started, nothing was recorded and it selects from -1, which throws. The + // selection is left as it was rather than taking the app down. + Log.w(TAG, "Selection gesture without a recorded start", e); + return true; + } } private boolean isTap(TextView widget, MotionEvent up) { diff --git a/app/src/test/java/com/pasich/mynotes/utils/editor/EditableLinkMovementMethodTest.java b/app/src/test/java/com/pasich/mynotes/utils/editor/EditableLinkMovementMethodTest.java new file mode 100644 index 00000000..cf95abe0 --- /dev/null +++ b/app/src/test/java/com/pasich/mynotes/utils/editor/EditableLinkMovementMethodTest.java @@ -0,0 +1,71 @@ +package com.pasich.mynotes.utils.editor; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.Activity; +import android.os.SystemClock; +import android.text.Editable; +import android.text.Selection; +import android.text.method.TextKeyListener; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.widget.EditText; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = {28, 35}) +public class EditableLinkMovementMethodTest { + + private EditText field; + + @Before + public void setUp() { + Activity activity = Robolectric.buildActivity(Activity.class).setup().get(); + field = new EditText(activity); + activity.setContentView(field); + field.setText("Some note text"); + field.measure( + View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(400, View.MeasureSpec.EXACTLY)); + field.layout(0, 0, 600, 400); + field.setMovementMethod(EditableLinkMovementMethod.getInstance()); + field.requestFocus(); + field.setSelection(3); + } + + /** + * Play Console crash in 2.7.56 ({@code setSpan (-1 ... -1) starts before 0}): the down lands + * while the field is not selecting, selecting mode turns on (Shift on a hardware keyboard) + * before the up, and {@code ArrowKeyMovementMethod} then selects from a start it never + * recorded. + */ + @Test + public void upInSelectingModeWithoutARecordedDown_doesNotCrashAndKeepsTheCaret() { + Editable buffer = field.getText(); + long now = SystemClock.uptimeMillis(); + MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 50f, 10f, 0); + field.onTouchEvent(down); + down.recycle(); + TextKeyListener.getInstance() + .onKeyDown( + field, + buffer, + KeyEvent.KEYCODE_SHIFT_LEFT, + new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT)); + int caretBefore = Selection.getSelectionStart(buffer); + + MotionEvent up = MotionEvent.obtain(now, now + 50, MotionEvent.ACTION_UP, 50f, 10f, 0); + boolean handled = EditableLinkMovementMethod.getInstance().onTouchEvent(field, buffer, up); + up.recycle(); + + assertThat(handled).isTrue(); + assertThat(Selection.getSelectionStart(buffer)).isEqualTo(caretBefore); + assertThat(Selection.getSelectionEnd(buffer)).isEqualTo(caretBefore); + } +} diff --git a/build.gradle b/build.gradle index 8ff5ba79..b5142724 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - daggerHilt = '2.57.2' + daggerHilt = '2.60.1' } repositories { @@ -9,15 +9,15 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:9.0.1' + classpath 'com.android.tools.build:gradle:9.5.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerHilt" } } plugins { id 'com.google.dagger.hilt.android' version "$daggerHilt" apply false - id 'com.diffplug.spotless' version '8.10.1' apply false - id 'com.google.gms.google-services' version '4.4.4' apply false + id 'com.diffplug.spotless' version '8.11.0' apply false + id 'com.google.gms.google-services' version '4.6.0' apply false } tasks.register('clean', Delete) { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b95..51cbd2a6 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 23449a2b..04d79595 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.9.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index f3b75f3b..249efbb0 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -205,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a218..3185a43f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,39 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +@rem Catch executions from older scripts and ensure they exit cleanly. +@rem This can be removed once we can be reasonably confident that few people +@rem will be migrating directly to this new wrapper. +goto afterSafetyNet +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto exitWithErrorLevel +:afterSafetyNet set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -45,13 +72,14 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -59,36 +87,26 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +1>&2 echo. +1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +1>&2 echo. +1>&2 echo Please set the JAVA_HOME variable in your environment to match the +1>&2 echo location of your Java installation. -goto fail +"%COMSPEC%" /c exit 1 +goto exitWithErrorLevel :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem This label must not be changed. We rely on old scripts being able to jump to this point. +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/notes_editor/package-lock.json b/notes_editor/package-lock.json index d6d02363..ddf8176d 100644 --- a/notes_editor/package-lock.json +++ b/notes_editor/package-lock.json @@ -821,9 +821,9 @@ "license": "MIT" }, "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.12.0.tgz", + "integrity": "sha512-SpiONmQPZDqe50ivOTgkVikYsnp6l1HTFYqPRLNBCJJdA2Qympw2yYUzX99XlSbbKZswjiG6r+xsUpPPOHqY+Q==", "dev": true, "license": "MIT" }, @@ -2040,13 +2040,12 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.6.tgz", + "integrity": "sha512-BK87/Qgti3DIcgS2PaCVoteFIkjIFvCJBizZDIVSjdlFNg4Jv+SRqbN2JONb/qQUH4ZFqfXYF+izg15W2HVRvA==", "dev": true, "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" }, "engines": { @@ -2241,9 +2240,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.1.tgz", - "integrity": "sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.4.tgz", + "integrity": "sha512-FqlEuoTq/zRmA4Lg6aOqyH3lCHbUtCB0bmHCeFniX2GBdDqMJM3FwdvEhlE5Pim1dlt2FfBdVPmUNNiGhhKqpw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2268,9 +2267,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "peer": true,