From 56e3b3f7f9fd695f1f65505eb3ac1c76dc6b8792 Mon Sep 17 00:00:00 2001 From: Mario Zorz Date: Thu, 11 Jun 2020 16:18:29 -0300 Subject: [PATCH] Squashed 'libs/utils/' changes from a15de7604a..1a43e016c6 1a43e016c6 Merge pull request #29 from wordpress-mobile/issue/28-empty-img-tag-crash-fix a4c24373e5 Updated the release version to 1.24 a33296f4d6 Check if img source is null before checking for image smileys 1dd7406ad5 Merge pull request #27 from wordpress-mobile/combined-updates-from-wpandroid b918b23ac5 Check if announcement is available in App Settings before showing option. Updated AppLog to include announcement related tracker. 94e23e28c1 Rename build.gradle fields with invalid name syntax d0be1240ca Make sure url utils recognizes atomic image proxy as valid image url. 9bdbdff3a9 Merge branch 'feature/support-private-at-sites' of https://github.com/wordpress-mobile/WordPress-Android into feature/support-private-at-sites fcc5c99e37 Updated Photon utils to utilize new url for private AT proxy. 130195092f Gutenberg/integrate release 1.25.0 with dark mode (#11580) a25096c291 Restoring Utils lib version to 1.24 edfef1e58c Merge branch 'develop' into issue/11249-media-not-load-jetpack 60d516266e Merge pull request #11536 from wordpress-mobile/add/log-persistence 1aceca9950 Make the linter happy 18c8447fe3 Change provider visibility dac4e5ce38 Fixes to LogFileHelpersTest 76f9f75f5d Variablize recent created files test a4d0a773e7 Small fixes for LogFileCleanerTest 24ac53fabf Make the queue private 64ea6a8cdf Use correct hungarian notation for static member 1c978ca61a Refactor tests to match project refactoring 565b88903c Persist log files in a background thread 3ae86d48e4 Refactor LogEntry.toString method b900bdda83 Refactor LogFileHelpers to not use context and be a LogFileProvider 225e1db645 Upgrading lib version. 6fd47e8eb6 Using the assertThat pattern. 0948a93056 Adding connected testing. dc5c2e3cd6 Refactor LogFileWriter to use LogFileProvider instead of context 2d5d0b65c0 Refactor LogFileCleaner to not need context and use a LogFileProvider instead 84b7a8a87d Minor changes in LogFileWriter to make it more idiomatic 8cfa0950aa Merge branch 'develop' into issue/11249-media-not-load-jetpack 31a14daeb4 Bump the utils minSdkVersion to 18 f65fd78414 Add log persistence fe3960d6b7 Updating ssl logic for photon addresses. 1b4b31bf88 Merge pull request #11524 from wordpress-mobile/fix/wordpressutils-tests 6c80419806 Fix WordPressUtils tests def5be4e88 Revert "Feature/material theme and Dark Theme support (#11469)" (#11486) 16540e85f1 Feature/material theme and Dark Theme support (#11469) 0ae297bbd3 Adding ssl parameter to query for Photon https. 89725645a6 made method name more meaningful. 76343d3d6d Added util function to check if a Uri is a content one. 75790052ef Merge pull request #11072 from wordpress-mobile/issue-10843/latitude_fix_api_29 981eb82158 Merge branch 'develop' into try/media-upload-completion-processor 8799a19ec4 removes safe check due to nature of RecyclerView 7906f07645 Fixed latitude missing column issue on api 29 882a64faf3 updated gradle wrapper of utils project. b012d38c3f resolved conflicts and merged utils subtree e8aae6d282 Merge branch 'develop' into try/media-upload-completion-processor da19376391 Add MediaFile method to generate attachment page url e35a9e0cd8 Used delegate safe method for focused accessibility event listener de02499434 Merge remote-tracking branch 'origin/develop' into issue/10894-aztec-media-talkback 1812393e22 Added accessibility heading 30aa9a3132 Added a safe way to set accessibility delegates. 870518ff07 Moved method to util class that's more appropriate for it's behavior a998f6e877 added annotations to event listener method 43013a5a79 Moved behavior to Utils so that it can be replicated in other areas. 54ca204429 Removed unused imports. 373db7c13f Removed unneeded whitespace and added necessary whiteline 785da6617b WP Media Picker now announces when an image is selected. 75f0913259 "Done" button now has a content description of "Cancel" 42ec1fb96c Disabled hint announcements and applied accessibility headings 916e2b0d96 Add comment to downloadExternalMedia method 573ba7f5e8 Clean up AddMediaToEditor logic dda3a1ebb8 Merge pull request #10565 from wordpress-mobile/clean-up-after-legacy-editor d84fb1f61f Upgrade Gradle to 5.4.1, gradle plugin to 3.5.1 and fix various errors 0511354994 Remove WPEditText 19d3f4aaf0 Optimize AppLog b4401cd84f Fix simple date format concurrency issue 15ee1b27d0 Remove usages of buggy DateTimeUtils methods e9f01e5e3c Fixed loop. 47eec63356 Merge branch 'develop' of github.com:wordpress-mobile/WordPress-Android into issue/9815-email-verification-reminder 25d1310312 Fix AndroidX import order a21c7120d8 Fix import ordering for androidx 35b99bc73e Migrate to AndroidX d40e8773bb Merge branch 'issue/9815-create-call-to-action' into issue/9815-add-reminder-message 892eacce49 Make getPath private again ae6291c612 Use existing method instead of creating a new one in MediaUtils 10b59f8d06 Implement comments from PR f1b21d0acc Add logging for the failing cases 702881b26f A crash fix for uploaded docs cc1c0334a8 Added reminder message toast after domain has been registered 9a14564929 Merge branch 'develop' of github.com:wordpress-mobile/WordPress-Android into issue/9452-domain-suggestions a4d8873a2e Update style config from style-config-android 1c10935b8c Merge branch 'develop' of github.com:wordpress-mobile/WordPress-Android into issue/9452-domain-suggestions 971cf76d96 Remove unused get snackbar duration method from accessibility utils class 28b0685ae7 Update is accessibility enabled method in accessibility utils for simplicity a418cb7dc8 Update get snackbar duration method in accessibility utils for indefinite constant e0dd4824c9 Merge branch 'develop' of github.com:wordpress-mobile/WordPress-Android into issue/9452-domain-suggestions 506f9883ce refactored ToastUtils to be able to pass gravity as param. Also showing toast after back-dating a scheduled post on top anchor now e164cd7a61 Fixing merge conflicts from domain register branch. 586222ec51 Merge pull request #8164 from usamahamid/feature/domain-register 50de29e20a Introduced Domain Suggestion Network request in ViewModel git-subtree-dir: libs/utils git-subtree-split: 1a43e016c63bd9dc19f3e27d39b8dc1cba7c5263 --- WordPressUtils/build.gradle | 38 ++++++- .../android/util/ImageUtilsTest.java | 14 ++- .../wordpress/android/util/JSONUtilsTest.java | 11 +- .../android/util/PhotonUtilsTest.java | 106 ++++++++++++++++++ .../android/util/ShortcodeUtilsTest.java | 11 +- .../wordpress/android/util/UrlUtilsTest.java | 26 ++++- .../util/AccessibilityEventListener.java | 7 ++ .../android/util/AccessibilityUtils.java | 95 ++++++++++++++-- .../wordpress/android/util/ActivityUtils.java | 3 +- .../org/wordpress/android/util/AppLog.java | 78 ++++++++----- .../android/util/AutoForeground.java | 7 +- .../util/AutoForegroundNotification.java | 9 +- .../wordpress/android/util/DeviceUtils.java | 3 +- .../android/util/EmoticonsUtils.java | 3 + .../wordpress/android/util/LanguageUtils.java | 3 +- .../org/wordpress/android/util/ListUtils.java | 2 +- .../wordpress/android/util/MediaUtils.java | 47 +++++--- .../android/util/PermissionUtils.java | 7 +- .../wordpress/android/util/PhotonUtils.java | 51 +++++++-- .../wordpress/android/util/StringUtils.java | 3 +- .../wordpress/android/util/ToastUtils.java | 18 ++- .../org/wordpress/android/util/UrlUtils.java | 15 +++ .../org/wordpress/android/util/ViewUtils.java | 9 +- .../android/util/helpers/MediaFile.java | 7 ++ .../RecyclerViewScrollPositionManager.java | 5 +- .../util/helpers/SwipeToRefreshHelper.java | 34 +++--- .../util/helpers/logfile/LogFileCleaner.kt | 22 ++++ .../util/helpers/logfile/LogFileHelpers.kt | 42 +++++++ .../logfile/LogFileProviderInterface.kt | 12 ++ .../util/helpers/logfile/LogFileWriter.kt | 45 ++++++++ .../util/widgets/AutoResizeTextView.java | 5 +- .../widgets/CustomSwipeRefreshLayout.java | 3 +- .../android/util/widgets/WPEditText.java | 65 ----------- .../util/widgets/WPTextInputLayout.java | 5 +- .../android/util/LogFileCleanerTest.kt | 75 +++++++++++++ .../android/util/LogFileHelpersTest.kt | 64 +++++++++++ .../android/util/LogFileWriterTest.kt | 53 +++++++++ config/checkstyle.xml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 39 files changed, 824 insertions(+), 183 deletions(-) create mode 100644 WordPressUtils/src/androidTest/java/org/wordpress/android/util/PhotonUtilsTest.java create mode 100644 WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityEventListener.java create mode 100644 WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileCleaner.kt create mode 100644 WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileHelpers.kt create mode 100644 WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileProviderInterface.kt create mode 100644 WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileWriter.kt delete mode 100644 WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java create mode 100644 WordPressUtils/src/test/java/org/wordpress/android/util/LogFileCleanerTest.kt create mode 100644 WordPressUtils/src/test/java/org/wordpress/android/util/LogFileHelpersTest.kt create mode 100644 WordPressUtils/src/test/java/org/wordpress/android/util/LogFileWriterTest.kt diff --git a/WordPressUtils/build.gradle b/WordPressUtils/build.gradle index 68772d45373d..e9c893f35ba8 100644 --- a/WordPressUtils/build.gradle +++ b/WordPressUtils/build.gradle @@ -1,15 +1,20 @@ buildscript { + ext.kotlinVersion = '1.3.61' + repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' - classpath 'com.novoda:bintray-release:0.9' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + classpath 'com.android.tools.build:gradle:3.5.1' + classpath 'com.novoda:bintray-release:0.9.1' } } apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' apply plugin: 'com.novoda.bintray-release' repositories { @@ -19,15 +24,25 @@ repositories { dependencies { implementation 'org.apache.commons:commons-text:1.1' - implementation 'com.mcxiaoke.volley:library:1.0.18' - implementation 'com.android.support:design:28.0.0' - implementation 'com.android.support:recyclerview-v7:28.0.0' + implementation 'com.android.volley:volley:1.1.1' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'org.greenrobot:eventbus:3.0.0' + implementation "androidx.core:core-ktx:+" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + testImplementation 'junit:junit:4.12' testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation "org.robolectric:robolectric:4.3.1" + testImplementation 'androidx.test:core:1.0.0' lintChecks 'org.wordpress:lint:1.0.1' + androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test:rules:1.1.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' + androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2' + } android { @@ -38,8 +53,19 @@ android { defaultConfig { versionName "1.24" - minSdkVersion 15 + minSdkVersion 18 targetSdkVersion 26 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + testOptions { + unitTests { + includeAndroidResources = true + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } } diff --git a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ImageUtilsTest.java b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ImageUtilsTest.java index 872f7e74b7dd..05b0af2f7ade 100644 --- a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ImageUtilsTest.java +++ b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ImageUtilsTest.java @@ -1,9 +1,13 @@ package org.wordpress.android.util; import android.graphics.BitmapFactory; -import android.test.InstrumentationTestCase; -public class ImageUtilsTest extends InstrumentationTestCase { +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ImageUtilsTest { + @Test public void testGetScaleForResizingReturnsOneWhenMaxSizeIsZero() { BitmapFactory.Options options = new BitmapFactory.Options(); int scale = ImageUtils.getScaleForResizing(0, options); @@ -11,6 +15,7 @@ public void testGetScaleForResizingReturnsOneWhenMaxSizeIsZero() { assertEquals(1, scale); } + @Test public void testGetScaleForResizingSameSizeReturnsOne() { BitmapFactory.Options options = new BitmapFactory.Options(); options.outHeight = 100; @@ -22,6 +27,7 @@ public void testGetScaleForResizingSameSizeReturnsOne() { assertEquals(1, scale); } + @Test public void testGetScaleForResizingPortraitMaxHeightSameAsMaxSizeReturnsOne() { BitmapFactory.Options options = new BitmapFactory.Options(); options.outHeight = 100; @@ -33,6 +39,7 @@ public void testGetScaleForResizingPortraitMaxHeightSameAsMaxSizeReturnsOne() { assertEquals(1, scale); } + @Test public void testGetScaleForResizingLandscapeMaxWidthSameAsMaxSizeReturnsOne() { BitmapFactory.Options options = new BitmapFactory.Options(); options.outHeight = 1; @@ -44,6 +51,7 @@ public void testGetScaleForResizingLandscapeMaxWidthSameAsMaxSizeReturnsOne() { assertEquals(1, scale); } + @Test public void testGetScaleForResizingDoubleSizeReturnsTwo() { BitmapFactory.Options options = new BitmapFactory.Options(); options.outHeight = 100; @@ -55,6 +63,7 @@ public void testGetScaleForResizingDoubleSizeReturnsTwo() { assertEquals(2, scale); } + @Test public void testGetScaleForResizingThreeTimesSizeReturnsTwo() { BitmapFactory.Options options = new BitmapFactory.Options(); options.outHeight = 100; @@ -66,6 +75,7 @@ public void testGetScaleForResizingThreeTimesSizeReturnsTwo() { assertEquals(2, scale); } + @Test public void testGetScaleForResizingEightTimesSizeReturnsEight() { BitmapFactory.Options options = new BitmapFactory.Options(); options.outHeight = 100; diff --git a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java index f7c747ff7024..e02656fe4f3f 100644 --- a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java +++ b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java @@ -1,31 +1,36 @@ package org.wordpress.android.util; -import android.test.InstrumentationTestCase; - import org.json.JSONArray; import org.json.JSONObject; +import org.junit.Test; -public class JSONUtilsTest extends InstrumentationTestCase { +public class JSONUtilsTest { + @Test public void testQueryJSONNullSource1() { JSONUtils.queryJSON((JSONObject) null, "", ""); } + @Test public void testQueryJSONNullSource2() { JSONUtils.queryJSON((JSONArray) null, "", ""); } + @Test public void testQueryJSONNullQuery1() { JSONUtils.queryJSON(new JSONObject(), null, ""); } + @Test public void testQueryJSONNullQuery2() { JSONUtils.queryJSON(new JSONArray(), null, ""); } + @Test public void testQueryJSONNullReturnValue1() { JSONUtils.queryJSON(new JSONObject(), "", null); } + @Test public void testQueryJSONNullReturnValue2() { JSONUtils.queryJSON(new JSONArray(), "", null); } diff --git a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/PhotonUtilsTest.java b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/PhotonUtilsTest.java new file mode 100644 index 000000000000..0e2d4b301706 --- /dev/null +++ b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/PhotonUtilsTest.java @@ -0,0 +1,106 @@ +package org.wordpress.android.util; + +import org.junit.Test; +import org.wordpress.android.util.PhotonUtils.Quality; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + + +public class PhotonUtilsTest { + @Test + public void getPhotonImageUrlIsEmptyWhenUrlIsNull() { + String photonUrl = PhotonUtils.getPhotonImageUrl(null, 0, 1); + + assertThat(photonUrl, equalTo("")); + } + + @Test + public void getPhotonImageUrlIsEmptyWhenUrlIsEmpty() { + String photonUrl = PhotonUtils.getPhotonImageUrl("", 0, 1); + + assertThat(photonUrl, equalTo("")); + } + + @Test + public void getPhotonImageUrlReturnsImageUrlOnNoScheme() { + String imageUrl = "wordpress.com"; + String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 0, 1); + + assertThat(photonUrl, equalTo(imageUrl)); + } + + @Test + public void getPhotonImageUrlReturnsMshots() { + String imageUrl = "http://test.wordpress.com/mshots/test.jpg?query=dummy"; + String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 0, 1); + + assertThat(photonUrl, equalTo("http://test.wordpress.com/mshots/test.jpg?w=0&h=1")); + } + + @Test + public void getPhotonImageUrlReturnsCorrectQuality() { + Map qualities = new HashMap<>(); + qualities.put(Quality.HIGH, "100"); + qualities.put(Quality.MEDIUM, "65"); + qualities.put(Quality.LOW, "35"); + + String imageUrl = "http://test.wordpress.com/test.jpg?query=dummy"; + + for (Quality quality : qualities.keySet()) { + String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 0, 1, quality); + assertThat(photonUrl, containsString("&quality=" + qualities.get(quality))); + } + } + + @Test + public void getPhotonImageUrlUsesResize() { + String imageUrl = "http://test.wordpress.com/test.jpg?query=dummy"; + String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); + + assertThat(photonUrl, equalTo("http://test.wordpress.com/test.jpg?strip=info&quality=65&resize=2,1")); + } + + @Test + public void getPhotonImageUrlManageSslOnPhotonUrl() { + String imageUrl = "https://i0.wp.com/test.jpg?query=dummy"; + String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); + + assertThat(photonUrl, equalTo("https://i0.wp.com/test.jpg?strip=info&quality=65&resize=2,1")); + + imageUrl = "https://i0.wp.com/test.jpg?query=dummy&ssl=1"; + photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); + + assertThat(photonUrl, equalTo("https://i0.wp.com/test.jpg?strip=info&quality=65&resize=2,1&ssl=1")); + } + + @Test + public void getPhotonImageUrlDoNotUseSslOnWordPressCom() { + String imageUrl = "https://test.wordpress.com/test.jpg?query=dummy"; + String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); + + assertThat(photonUrl, equalTo("https://test.wordpress.com/test.jpg?strip=info&quality=65&resize=2,1")); + + imageUrl = "https://test.wordpress.com/test.jpg?query=dummy&ssl=1"; + photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); + + assertThat(photonUrl, equalTo("https://test.wordpress.com/test.jpg?strip=info&quality=65&resize=2,1")); + } + + @Test + public void getPhotonImageUrlUsesSslOnHttpsImageUrl() { + String imageUrl = "http://mysite.com/test.jpg?query=dummy"; + String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); + + assertThat(photonUrl, equalTo("https://i0.wp.com/mysite.com/test.jpg?strip=info&quality=65&resize=2,1")); + + imageUrl = "https://mysite.com/test.jpg?query=dummy&ssl=1"; + photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); + + assertThat(photonUrl, equalTo("https://i0.wp.com/mysite.com/test.jpg?strip=info&quality=65&resize=2,1&ssl=1")); + } +} diff --git a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java index c506a452ee5d..32435e40e6e8 100644 --- a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java +++ b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java @@ -1,24 +1,31 @@ package org.wordpress.android.util; -import android.test.InstrumentationTestCase; +import org.junit.Test; -public class ShortcodeUtilsTest extends InstrumentationTestCase { +import static org.junit.Assert.assertEquals; + +public class ShortcodeUtilsTest { + @Test public void testGetVideoPressShortcodeFromId() { assertEquals("[wpvideo abcd1234]", ShortcodeUtils.getVideoPressShortcodeFromId("abcd1234")); } + @Test public void testGetVideoPressShortcodeFromNullId() { assertEquals("", ShortcodeUtils.getVideoPressShortcodeFromId(null)); } + @Test public void testGetVideoPressIdFromCorrectShortcode() { assertEquals("abcd1234", ShortcodeUtils.getVideoPressIdFromShortCode("[wpvideo abcd1234]")); } + @Test public void testGetVideoPressIdFromInvalidShortcode() { assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode("[other abcd1234]")); } + @Test public void testGetVideoPressIdFromNullShortcode() { assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode(null)); } diff --git a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java index f2b4f13705c7..9cf88220712d 100644 --- a/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java +++ b/WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java @@ -1,21 +1,29 @@ package org.wordpress.android.util; -import android.test.InstrumentationTestCase; +import org.junit.Test; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; -public class UrlUtilsTest extends InstrumentationTestCase { +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class UrlUtilsTest { + @Test public void testGetDomainFromUrlWithEmptyStringDoesNotReturnNull() { assertNotNull(UrlUtils.getHost("")); } + @Test public void testGetDomainFromUrlWithNoHostDoesNotReturnNull() { assertNotNull(UrlUtils.getHost("wordpress")); } + @Test public void testGetDomainFromUrlWithHostReturnsHost() { String url = "http://www.wordpress.com"; String host = UrlUtils.getHost(url); @@ -23,41 +31,49 @@ public void testGetDomainFromUrlWithHostReturnsHost() { assertTrue(host.equals("www.wordpress.com")); } + @Test public void testAppendUrlParameter1() { String url = UrlUtils.appendUrlParameter("http://wp.com/test", "preview", "true"); assertEquals("http://wp.com/test?preview=true", url); } + @Test public void testAppendUrlParameter2() { String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony", "preview", "true"); assertEquals("http://wp.com/test?q=pony&preview=true", url); } + @Test public void testAppendUrlParameter3() { String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony#unicorn", "preview", "true"); assertEquals("http://wp.com/test?q=pony&preview=true#unicorn", url); } + @Test public void testAppendUrlParameter4() { String url = UrlUtils.appendUrlParameter("/relative/test", "preview", "true"); assertEquals("/relative/test?preview=true", url); } + @Test public void testAppendUrlParameter5() { String url = UrlUtils.appendUrlParameter("/relative/", "preview", "true"); assertEquals("/relative/?preview=true", url); } + @Test public void testAppendUrlParameter6() { String url = UrlUtils.appendUrlParameter("http://wp.com/test/", "preview", "true"); assertEquals("http://wp.com/test/?preview=true", url); } + @Test public void testAppendUrlParameter7() { String url = UrlUtils.appendUrlParameter("http://wp.com/test/?q=pony", "preview", "true"); assertEquals("http://wp.com/test/?q=pony&preview=true", url); } + @Test public void testAppendUrlParameters1() { Map params = new HashMap<>(); params.put("w", "200"); @@ -68,6 +84,7 @@ public void testAppendUrlParameters1() { } } + @Test public void testAppendUrlParameters2() { Map params = new HashMap<>(); params.put("h", "300"); @@ -78,22 +95,27 @@ public void testAppendUrlParameters2() { } } + @Test public void testHttps1() { assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php"))); } + @Test public void testHttps2() { assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com#.b.com/test"))); } + @Test public void testHttps3() { assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php"))); } + @Test public void testHttps4() { assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com"))); } + @Test public void testHttps5() { assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com/test#test"))); } diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityEventListener.java b/WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityEventListener.java new file mode 100644 index 000000000000..78b9ee6d9fd6 --- /dev/null +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityEventListener.java @@ -0,0 +1,7 @@ +package org.wordpress.android.util; + +import android.view.accessibility.AccessibilityEvent; + +public interface AccessibilityEventListener { + void onResult(AccessibilityEvent event); +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityUtils.java index 47bc3cf05eef..3c6ad25eebd5 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityUtils.java @@ -1,33 +1,108 @@ package org.wordpress.android.util; +import android.app.Activity; import android.content.Context; -import android.support.design.widget.Snackbar; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.google.android.material.snackbar.Snackbar; + +import org.wordpress.android.util.AppLog.T; import static android.content.Context.ACCESSIBILITY_SERVICE; +import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; public class AccessibilityUtils { private static final int SNACKBAR_WITH_ACTION_DURATION_IN_MILLIS = 10000; public static boolean isAccessibilityEnabled(Context ctx) { AccessibilityManager am = (AccessibilityManager) ctx.getSystemService(ACCESSIBILITY_SERVICE); - return am != null ? am.isEnabled() : false; - } - - /** - * If the accessibility is enabled, returns increased snackbar duration, otherwise returns LENGTH_LONG duration. - */ - public static int getSnackbarDuration(Context ctx) { - return getSnackbarDuration(ctx, Snackbar.LENGTH_LONG); + return am != null && am.isEnabled(); } /** + * If the default duration is LENGTH_INDEFINITE, ignore accessibility duration and return LENGTH_INDEFINITE. * If the accessibility is enabled, returns increased snackbar duration, otherwise returns defaultDuration. * * @param defaultDuration Either be one of the predefined lengths: LENGTH_SHORT, LENGTH_LONG, or a custom duration * in milliseconds. */ public static int getSnackbarDuration(Context ctx, int defaultDuration) { - return isAccessibilityEnabled(ctx) ? SNACKBAR_WITH_ACTION_DURATION_IN_MILLIS : defaultDuration; + return defaultDuration == Snackbar.LENGTH_INDEFINITE ? Snackbar.LENGTH_INDEFINITE + : isAccessibilityEnabled(ctx) ? SNACKBAR_WITH_ACTION_DURATION_IN_MILLIS : defaultDuration; + } + + public static void setActionModeDoneButtonContentDescription(@Nullable final Activity activity, + @NonNull final String contentDescription) { + if (activity != null) { + View decorView = activity.getWindow().getDecorView(); + + decorView.post(new Runnable() { + @Override public void run() { + View doneButton = activity.findViewById(androidx.appcompat.R.id.action_mode_close_button); + + if (doneButton != null) { + doneButton.setContentDescription(contentDescription); + } + } + }); + } + } + + public static void addPopulateAccessibilityEventFocusedListener(@NonNull final View target, + @NonNull final AccessibilityEventListener + listener) { + ViewCompat.setAccessibilityDelegate(target, new AccessibilityDelegateCompat() { + @Override public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + if (event.getEventType() == TYPE_VIEW_ACCESSIBILITY_FOCUSED) { + listener.onResult(event); + } + super.onPopulateAccessibilityEvent(host, event); + } + }); + } + + public static void disableHintAnnouncement(@NonNull TextView textView) { + setAccessibilityDelegateSafely(textView, new AccessibilityDelegateCompat() { + @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setHintText(null); + } + }); + } + + /** + * When the minsdk is 28 this can be replaced by adding android:accessibilityHeading="true" as a property to the + * view's xml declaration. + * @param view that will become a heading. + */ + public static void enableAccessibilityHeading(@NonNull View view) { + setAccessibilityDelegateSafely(view, new AccessibilityDelegateCompat() { + @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setHeading(true); + } + }); + } + + public static void setAccessibilityDelegateSafely(View view, + AccessibilityDelegateCompat accessibilityDelegateCompat) { + if (ViewCompat.hasAccessibilityDelegate(view)) { + final String errorMessage = "View already has an AccessibilityDelegate."; + if (PackageUtils.isDebugBuild()) { + throw new RuntimeException(errorMessage); + } + AppLog.e(T.UTILS, errorMessage); + } else { + ViewCompat.setAccessibilityDelegate(view, accessibilityDelegateCompat); + } } } diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java index e9c4cb7b4e08..0751b0a5d977 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java @@ -3,10 +3,11 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.support.annotation.Nullable; import android.view.View; import android.view.inputmethod.InputMethodManager; +import androidx.annotation.Nullable; + public class ActivityUtils { /** * Hides the keyboard in the given {@link Activity}'s current focus using the diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java b/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java index 522ea218f75c..b0bb2a6e467a 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/AppLog.java @@ -4,18 +4,25 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; + +import org.wordpress.android.util.helpers.logfile.LogFileCleaner; +import org.wordpress.android.util.helpers.logfile.LogFileProvider; +import org.wordpress.android.util.helpers.logfile.LogFileWriter; + import java.io.PrintWriter; import java.io.StringWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.NoSuchElementException; +import java.util.TimeZone; import static java.lang.String.format; @@ -51,13 +58,16 @@ public enum T { ACTIVITY_LOG, JETPACK_REMOTE_INSTALL, SUPPORT, - SITE_CREATION + SITE_CREATION, + DOMAIN_REGISTRATION, + FEATURE_ANNOUNCEMENT } public static final String TAG = "WordPress"; public static final int HEADER_LINE_COUNT = 2; private static boolean mEnableRecording = false; private static List mListeners = new ArrayList<>(0); + private static TimeZone mUtcTimeZone = TimeZone.getTimeZone("UTC"); private AppLog() { throw new AssertionError(); @@ -83,6 +93,22 @@ public interface AppLogListener { void onLog(T tag, LogLevel logLevel, String message); } + /** + * Add a LogFileWriter that will persist logs to disk + * @param context The current application context + * @param maxLogCount The maximum number of logs that should be stored + */ + public static void enableLogFilePersistence(Context context, int maxLogCount) { + LogFileProvider logFileProvider = LogFileProvider.fromContext(context); + new LogFileCleaner(logFileProvider, maxLogCount).clean(); + + sLogFileWriter = new LogFileWriter(logFileProvider); + sLogFileWriter.write(getAppInfoHeaderText(context) + "\n"); + sLogFileWriter.write(getDeviceInfoHeaderText(context) + "\n"); + } + + private static LogFileWriter sLogFileWriter; + /** * Sends a VERBOSE log message * @param tag Used to identify the source of a log message. @@ -196,22 +222,6 @@ public static void e(T tag, String volleyErrorMsg, int statusCode) { public enum LogLevel { v, d, i, w, e; - - private String toHtmlColor() { - switch (this) { - case v: - return "grey"; - case i: - return "black"; - case w: - return "purple"; - case e: - return "red"; - case d: - default: - return "teal"; - } - } } private static class LogEntry { @@ -222,7 +232,7 @@ private static class LogEntry { LogEntry(LogLevel logLevel, String logText, T logTag) { mLogLevel = logLevel; - mDate = DateTimeUtils.nowUTC(); + mDate = new Date(); if (logText == null) { mLogText = "null"; } else { @@ -232,23 +242,32 @@ private static class LogEntry { } private String formatLogDate() { - return new SimpleDateFormat("MMM-dd kk:mm", Locale.US).format(mDate); + SimpleDateFormat sdf = new SimpleDateFormat("MMM-dd kk:mm", Locale.US); + sdf.setTimeZone(mUtcTimeZone); + return sdf.format(mDate); } private String toHtml() { StringBuilder sb = new StringBuilder(); - sb.append(""); sb.append("["); sb.append(formatLogDate()).append(" "); sb.append(mLogTag.name()).append(" "); sb.append(mLogLevel.name()); sb.append("] "); sb.append(TextUtils.htmlEncode(mLogText).replace("\n", "
")); - sb.append("
"); return sb.toString(); } + + @Override + public @NonNull String toString() { + return "[" + + formatLogDate() + + " " + + mLogTag.name() + + "] " + + mLogText + + "\n"; + } } private static class LogEntryList extends ArrayList { @@ -283,6 +302,10 @@ private static void addEntry(T tag, LogLevel level, String text) { if (mEnableRecording) { LogEntry entry = new LogEntry(level, text, tag); mLogEntries.addEntry(entry); + + if (sLogFileWriter != null) { + sLogFileWriter.write(entry.toString()); + } } } @@ -350,12 +373,7 @@ public static synchronized String toPlainText(Context context) { while (it.hasNext()) { LogEntry entry = it.next(); sb.append(format(Locale.US, "%02d - ", lineNum)) - .append("[") - .append(entry.formatLogDate()).append(" ") - .append(entry.mLogTag.name()) - .append("] ") - .append(entry.mLogText) - .append("\n"); + .append(entry.toString()); lineNum++; } return sb.toString(); diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForeground.java b/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForeground.java index b141ae85cdbe..4cdc2911058f 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForeground.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForeground.java @@ -8,9 +8,10 @@ import android.content.ServiceConnection; import android.os.Binder; import android.os.IBinder; -import android.support.annotation.CallSuper; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationManagerCompat; + +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationManagerCompat; import org.greenrobot.eventbus.EventBus; import org.wordpress.android.util.AutoForeground.ServiceState; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForegroundNotification.java b/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForegroundNotification.java index d7a130a050aa..db8c8b32b592 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForegroundNotification.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/AutoForegroundNotification.java @@ -4,10 +4,11 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.support.annotation.ColorRes; -import android.support.annotation.DrawableRes; -import android.support.annotation.StringRes; -import android.support.v4.app.NotificationCompat; + +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; +import androidx.core.app.NotificationCompat; import static org.wordpress.android.util.AutoForeground.NOTIFICATION_ID_FAILURE; import static org.wordpress.android.util.AutoForeground.NOTIFICATION_ID_PROGRESS; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java index fd8213a6baad..dcd7d1d4ec0a 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java @@ -7,7 +7,8 @@ import android.os.Build; import android.os.Environment; import android.os.StatFs; -import android.support.annotation.NonNull; + +import androidx.annotation.NonNull; import org.wordpress.android.util.AppLog.T; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java index 8b65f41e4f35..08d0d3417c2d 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java @@ -72,6 +72,9 @@ public static String lookupImageSmiley(String url) { } public static String lookupImageSmiley(String url, String ifNone) { + if (url == null) { + return ifNone; + } String file = url.substring(url.lastIndexOf("/") + 1); if (WP_SMILIES.containsKey(file)) { return WP_SMILIES.get(file); diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java index 586c1e18b553..1deaa603efd7 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java @@ -1,7 +1,8 @@ package org.wordpress.android.util; import android.content.Context; -import android.support.annotation.Nullable; + +import androidx.annotation.Nullable; import java.util.Locale; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/ListUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/ListUtils.java index fcaae4a21e71..30d38e36c514 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/ListUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/ListUtils.java @@ -1,6 +1,6 @@ package org.wordpress.android.util; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import org.apache.commons.lang3.ArrayUtils; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java index 9a28d305bc3c..a23ab7200819 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/MediaUtils.java @@ -1,7 +1,6 @@ package org.wordpress.android.util; import android.annotation.TargetApi; -import android.app.Activity; import android.content.ContentUris; import android.content.Context; import android.content.CursorLoader; @@ -13,10 +12,11 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.provider.OpenableColumns; -import android.support.annotation.Nullable; import android.text.TextUtils; import android.webkit.MimeTypeMap; +import androidx.annotation.Nullable; + import org.wordpress.android.util.AppLog.T; import java.io.DataInputStream; @@ -105,11 +105,11 @@ public static boolean isLocalFile(String state) { || state.equalsIgnoreCase("failed"); } - public static Uri getLastRecordedVideoUri(Activity activity) { + public static Uri getLastRecordedVideoUri(Context appContext) { String[] proj = {MediaStore.Video.Media._ID}; Uri contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; String sortOrder = MediaStore.Video.VideoColumns.DATE_TAKEN + " DESC"; - CursorLoader loader = new CursorLoader(activity, contentUri, proj, null, null, sortOrder); + CursorLoader loader = new CursorLoader(appContext, contentUri, proj, null, null, sortOrder); Cursor cursor = loader.loadInBackground(); cursor.moveToFirst(); long value = cursor.getLong(0); @@ -170,7 +170,8 @@ public static boolean isInMediaStore(Uri mediaUri) { } public static @Nullable String getFilenameFromURI(Context context, Uri uri) { - Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, + null, null, null); try { String result = null; if (cursor != null && cursor.moveToFirst()) { @@ -188,6 +189,11 @@ public static boolean isInMediaStore(Uri mediaUri) { } } + /* + * Some media providers (eg. Google Photos) give us a limited access to media files just so we can copy them and + * then they revoke the access. Copying these files must be performed on the UI thread, otherwise the access might + * be revoked before the action completes. See https://github.com/wordpress-mobile/WordPress-Android/issues/5818 + */ public static Uri downloadExternalMedia(Context context, Uri imageUri) { if (context == null || imageUri == null) { return null; @@ -436,16 +442,31 @@ private static String getDocumentProviderPathKitkatOrHigher(final Context contex } // TODO handle non-primary volumes - } else if (isDownloadsDocument(uri)) { // DownloadsProvider + } else if (isDownloadsDocument(uri)) { final String id = DocumentsContract.getDocumentId(uri); - try { - final Uri contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); - return getDataColumn(context, contentUri, null, null); - } catch (NumberFormatException e) { - AppLog.e(AppLog.T.UTILS, "Can't read the path for file with ID " + id); - return null; + + if (id != null && id.startsWith("raw:")) { + return id.substring(4); + } + + String[] contentUriPrefixesToTry = new String[]{ + "content://downloads/public_downloads", + "content://downloads/my_downloads", + "content://downloads/all_downloads" + }; + + for (String contentUriPrefix : contentUriPrefixesToTry) { + Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id)); + try { + String path = getDataColumn(context, contentUri, null, null); + if (path != null) { + return path; + } + } catch (Exception e) { + AppLog.e(AppLog.T.UTILS, "Error reading _data column for URI: " + contentUri, e); + } } + return downloadExternalMedia(context, uri).getPath(); } else if (isMediaDocument(uri)) { // MediaProvider final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java index daecb3e11015..8d484d847519 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java @@ -4,9 +4,10 @@ import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; import java.util.ArrayList; import java.util.List; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java index 5a2177c87f6d..2f12eb05fae7 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java @@ -2,6 +2,9 @@ import android.text.TextUtils; +import java.net.MalformedURLException; +import java.net.URL; + /** * routines related to the Photon API * http://developer.wordpress.com/docs/photon/ @@ -12,8 +15,8 @@ private PhotonUtils() { } /* - * returns true if the passed url is an obvious "mshots" url - */ + * returns true if the passed url is an obvious "mshots" url + */ public static boolean isMshotsUrl(final String imageUrl) { return (imageUrl != null && imageUrl.contains("/mshots/")); } @@ -28,11 +31,23 @@ public enum Quality { LOW } + public static final String ATOMIC_MEDIA_PROXY_URL_PREFIX = "https://public-api.wordpress.com/wpcom/v2/sites/"; + public static final String ATOMIC_MEDIA_PROXY_URL_SUFFIX = "/atomic-auth-proxy/file"; + public static String getPhotonImageUrl(String imageUrl, int width, int height) { return getPhotonImageUrl(imageUrl, width, height, Quality.MEDIUM); } + public static String getPhotonImageUrl(String imageUrl, int width, int height, boolean isPrivateAtomicSite) { + return getPhotonImageUrl(imageUrl, width, height, Quality.MEDIUM, isPrivateAtomicSite); + } + public static String getPhotonImageUrl(String imageUrl, int width, int height, Quality quality) { + return getPhotonImageUrl(imageUrl, width, height, quality, false); + } + + public static String getPhotonImageUrl(String imageUrl, int width, int height, Quality quality, + boolean isPrivateAtomicSite) { if (TextUtils.isEmpty(imageUrl)) { return ""; } @@ -50,6 +65,8 @@ public static String getPhotonImageUrl(String imageUrl, int width, int height, Q imageUrl = imageUrl.substring(0, fragmentPos); } + String urlCopy = imageUrl; + // remove existing query string since it may contain params that conflict with the passed ones imageUrl = UrlUtils.removeQuery(imageUrl); @@ -83,9 +100,28 @@ public static String getPhotonImageUrl(String imageUrl, int width, int height, Q query += "&h=" + height; } + if (isPrivateAtomicSite) { + try { + URL url = new URL(imageUrl); + String slug = url.getHost(); + String path = url.getPath(); + return ATOMIC_MEDIA_PROXY_URL_PREFIX + slug + ATOMIC_MEDIA_PROXY_URL_SUFFIX + + "?path=" + path + "&" + query; + } catch (MalformedURLException e) { + e.printStackTrace(); + return ""; + } + } + // return passed url+query if it's already a photon url if (imageUrl.contains(".wp.com")) { if (imageUrl.contains("i0.wp.com") || imageUrl.contains("i1.wp.com") || imageUrl.contains("i2.wp.com")) { + boolean useSsl = urlCopy.indexOf("?") > 0 && urlCopy.contains("ssl=1"); + + if (useSsl) { + query += "&ssl=1"; + } + return imageUrl + query; } } @@ -96,11 +132,12 @@ public static String getPhotonImageUrl(String imageUrl, int width, int height, Q return imageUrl + query; } - // must use https for https image urls - if (UrlUtils.isHttps(imageUrl)) { - return "https://i0.wp.com/" + imageUrl.substring(schemePos + 3, imageUrl.length()) + query; - } else { - return "http://i0.wp.com/" + imageUrl.substring(schemePos + 3, imageUrl.length()) + query; + // must use ssl=1 parameter for https image urls + boolean useSSl = UrlUtils.isHttps(imageUrl); + if (useSSl) { + query += "&ssl=1"; } + + return "https://i0.wp.com/" + imageUrl.substring(schemePos + 3, imageUrl.length()) + query; } } diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java index bc3f4eaf508f..ef96282eb1fb 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/StringUtils.java @@ -1,9 +1,10 @@ package org.wordpress.android.util; import android.content.Context; -import android.support.annotation.StringRes; import android.text.TextUtils; +import androidx.annotation.StringRes; + import org.wordpress.android.util.AppLog.T; import java.math.BigInteger; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java index 9bbf95d60ef8..470867364b8c 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java @@ -30,9 +30,23 @@ public static Toast showToast(Context context, String text) { } public static Toast showToast(Context context, String text, Duration duration) { + return showToast(context, text, duration, Gravity.CENTER); + } + + public static Toast showToast(Context context, String text, Duration duration, int gravity) { + return showToast(context, text, duration, gravity, 0, 0); + } + + public static Toast showToast( + Context context, + String text, + Duration duration, + int gravity, + int xOffset, + int yOffset) { Toast toast = Toast.makeText(context, text, - (duration == Duration.SHORT ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG)); - toast.setGravity(Gravity.CENTER, 0, 0); + (duration == Duration.SHORT ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG)); + toast.setGravity(gravity, xOffset, yOffset); toast.show(); return toast; } diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java index 430a0344d5a8..de16bb91cce8 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/UrlUtils.java @@ -18,6 +18,9 @@ import java.util.Locale; import java.util.Map; +import static org.wordpress.android.util.PhotonUtils.ATOMIC_MEDIA_PROXY_URL_PREFIX; +import static org.wordpress.android.util.PhotonUtils.ATOMIC_MEDIA_PROXY_URL_SUFFIX; + public class UrlUtils { public static String urlEncode(final String text) { try { @@ -49,6 +52,10 @@ public static String getHost(final String urlString) { return ""; } + public static boolean isContentUri(String uri) { + return "content".equals(Uri.parse(uri).getScheme()); + } + /** * Convert IDN names to punycode if necessary */ @@ -242,10 +249,18 @@ public static boolean isImageUrl(String url) { String cleanedUrl = removeQuery(url.toLowerCase(Locale.ROOT)); + if (isAtomicImageProxyUrl(cleanedUrl)) { + return true; + } + return cleanedUrl.endsWith("jpg") || cleanedUrl.endsWith("jpeg") || cleanedUrl.endsWith("gif") || cleanedUrl.endsWith("png"); } + private static boolean isAtomicImageProxyUrl(String urlString) { + return urlString.startsWith(ATOMIC_MEDIA_PROXY_URL_PREFIX) && urlString.endsWith(ATOMIC_MEDIA_PROXY_URL_SUFFIX); + } + public static String appendUrlParameter(String url, String paramName, String paramValue) { Map parameters = new HashMap<>(); parameters.put(paramName, paramValue); diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/ViewUtils.java b/WordPressUtils/src/main/java/org/wordpress/android/util/ViewUtils.java index f1d138d97eca..9f7a11eeb6e9 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/ViewUtils.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/ViewUtils.java @@ -6,13 +6,14 @@ import android.content.res.TypedArray; import android.graphics.Outline; import android.os.Build; -import android.support.annotation.AttrRes; -import android.support.annotation.NonNull; -import android.support.annotation.StyleRes; -import android.support.v4.view.ViewCompat; import android.view.View; import android.view.ViewOutlineProvider; +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.StyleRes; +import androidx.core.view.ViewCompat; + import java.util.concurrent.atomic.AtomicInteger; public class ViewUtils { diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java index 36df0a574bda..98d815802ed2 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaFile.java @@ -5,6 +5,7 @@ import org.wordpress.android.util.MapUtils; import org.wordpress.android.util.StringUtils; +import org.wordpress.android.util.UrlUtils; import java.util.Date; import java.util.Locale; @@ -338,4 +339,10 @@ public String getImageHtmlForUrls(String fullSizeUrl, String resizedPictureURL, return content; } + + public String getAttachmentPageURL(String siteUrl) { + siteUrl = UrlUtils.makeHttps(siteUrl); + String attachmentPageUrl = UrlUtils.appendUrlParameter(siteUrl, "p", mMediaId); + return attachmentPageUrl; + } } diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/RecyclerViewScrollPositionManager.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/RecyclerViewScrollPositionManager.java index 48f67e4d7fd5..93021257449d 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/RecyclerViewScrollPositionManager.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/RecyclerViewScrollPositionManager.java @@ -1,10 +1,11 @@ package org.wordpress.android.util.helpers; import android.os.Bundle; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.View; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + public class RecyclerViewScrollPositionManager { private static final String RV_POSITION = "rv_position"; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java index c5fb9d715ea5..14cc5efef9a8 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java @@ -1,8 +1,12 @@ package org.wordpress.android.util.helpers; import android.content.Context; -import android.support.annotation.ColorRes; -import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener; + +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.core.content.ContextCompat; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener; import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout; @@ -17,7 +21,7 @@ public interface RefreshListener { /** * Helps {@link org.wordpress.android.util.widgets.CustomSwipeRefreshLayout} by passing the - * {@link android.support.v4.widget.SwipeRefreshLayout}, {@link RefreshListener}, and color. + * {@link SwipeRefreshLayout}, {@link RefreshListener}, and color. * * @param context {@link Context} in which this layout is used. * @param swipeRefreshLayout {@link CustomSwipeRefreshLayout} for refreshing the contents @@ -25,49 +29,53 @@ public interface RefreshListener { * @param listener {@link RefreshListener} notified when a refresh is triggered * via the swipe gesture. * - * @deprecated Use {@link #SwipeToRefreshHelper(CustomSwipeRefreshLayout, RefreshListener, int...)} instead. + * @deprecated Use {@link #SwipeToRefreshHelper(CustomSwipeRefreshLayout, RefreshListener, int, int...)} instead. */ @Deprecated public SwipeToRefreshHelper(Context context, CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener) { - init(swipeRefreshLayout, listener, android.R.color.holo_blue_dark); + init(swipeRefreshLayout, listener, ContextCompat.getColor(context, android.R.color.white), + android.R.color.holo_blue_dark); } /** * Helps {@link org.wordpress.android.util.widgets.CustomSwipeRefreshLayout} by passing the - * {@link android.support.v4.widget.SwipeRefreshLayout}, {@link RefreshListener}, and color(s). + * {@link SwipeRefreshLayout}, {@link RefreshListener}, and color(s). * * @param swipeRefreshLayout {@link CustomSwipeRefreshLayout} for refreshing the contents * of a view via a vertical swipe gesture. * @param listener {@link RefreshListener} notified when a refresh is triggered * via the swipe gesture. - * @param colorResIds Comma-separated color resource integers used in the progress + * @param progressAnimationColors Comma-separated color resource integers used in the progress * animation. The first color will also be the color of the bar * that grows in response to a user swipe gesture. */ public SwipeToRefreshHelper(CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener, - @ColorRes int... colorResIds) { - init(swipeRefreshLayout, listener, colorResIds); + @ColorInt int backgroundColor, + @ColorRes int... progressAnimationColors) { + init(swipeRefreshLayout, listener, backgroundColor, progressAnimationColors); } /** * Initializes {@link org.wordpress.android.util.widgets.CustomSwipeRefreshLayout} by assigning - * {@link android.support.v4.widget.SwipeRefreshLayout}, {@link RefreshListener}, and color(s). + * {@link SwipeRefreshLayout}, {@link RefreshListener}, and color(s). * * @param swipeRefreshLayout {@link CustomSwipeRefreshLayout} for refreshing the contents * of a view via a vertical swipe gesture. * @param listener {@link RefreshListener} notified when a refresh is triggered * via the swipe gesture. - * @param colorResIds Comma-separated color resource integers used in the progress + * @param progressAnimationColors Comma-separated color resource integers used in the progress * animation. The first color will also be the color of the bar * that grows in response to a user swipe gesture. */ public void init(CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener, - @ColorRes int... colorResIds) { + @ColorInt int backgroundColor, + @ColorRes int... progressAnimationColors) { mRefreshListener = listener; mSwipeRefreshLayout = swipeRefreshLayout; mSwipeRefreshLayout.setOnRefreshListener(this); - mSwipeRefreshLayout.setColorSchemeResources(colorResIds); + mSwipeRefreshLayout.setProgressBackgroundColorSchemeColor(backgroundColor); + mSwipeRefreshLayout.setColorSchemeResources(progressAnimationColors); } public void setRefreshing(boolean refreshing) { diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileCleaner.kt b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileCleaner.kt new file mode 100644 index 000000000000..732e831c27ac --- /dev/null +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileCleaner.kt @@ -0,0 +1,22 @@ +package org.wordpress.android.util.helpers.logfile + +/** + * Prunes the Log File Store by retaining only the last `maxLogFileCount` log files. + * + * The file list is created upon instantiation – any files added + * afterwards won't be modified. + * + * @param logFileProvider: An interface where the log files will be retrieved from + * @param maxLogFileCount: The number of log files to retain + */ +class LogFileCleaner(private val logFileProvider: LogFileProviderInterface, private val maxLogFileCount: Int) { + /** + * Immediately removes all log files known to exist by this instance except for + * the most recent `maxLogFileCount` items. + */ + fun clean() { + logFileProvider.getLogFiles() + .dropLast(maxLogFileCount) + .forEach { it.delete() } + } +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileHelpers.kt b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileHelpers.kt new file mode 100644 index 000000000000..0b59ec372b7b --- /dev/null +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileHelpers.kt @@ -0,0 +1,42 @@ +package org.wordpress.android.util.helpers.logfile + +import android.content.Context +import java.io.File + +private const val LOG_FILE_DIRECTORY = "logs" + +/** + * A collection of helpers for Log Files. + */ +class LogFileProvider(private val logFileDirectoryPath: String) : LogFileProviderInterface { + /** + * Provides a {@link java.io.File} directory in which to store log files. + * + * If the directory doesn't already exist, it will be created. + */ + override fun getLogFileDirectory(): File { + val logFileDirectory = File(logFileDirectoryPath, LOG_FILE_DIRECTORY) + + if (!logFileDirectory.exists()) { + logFileDirectory.mkdir() + } + + return logFileDirectory + } + + /** + * Provides a list of stored log files, ordered oldest to newest. + */ + override fun getLogFiles(): List { + return getLogFileDirectory() + .listFiles() + .sortedBy { it.lastModified() } + } + + companion object { + @JvmStatic + fun fromContext(context: Context): LogFileProvider { + return LogFileProvider(context.applicationInfo.dataDir) + } + } +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileProviderInterface.kt b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileProviderInterface.kt new file mode 100644 index 000000000000..e415ed5b738e --- /dev/null +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileProviderInterface.kt @@ -0,0 +1,12 @@ +package org.wordpress.android.util.helpers.logfile + +import java.io.File + +/** + * An interface to retrieve log files + */ +interface LogFileProviderInterface { + fun getLogFiles(): List + + fun getLogFileDirectory(): File +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileWriter.kt b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileWriter.kt new file mode 100644 index 000000000000..41e900f1ec0a --- /dev/null +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileWriter.kt @@ -0,0 +1,45 @@ +package org.wordpress.android.util.helpers.logfile + +import org.jetbrains.annotations.TestOnly +import java.io.File +import java.io.FileWriter +import java.util.Date +import org.wordpress.android.util.DateTimeUtils +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * A class that manages writing to a log file. + * + * This class creates and writes to a log file, and will typically persist for the entire lifecycle + * of its host application. + */ +class LogFileWriter @JvmOverloads constructor( + logFileProvider: LogFileProviderInterface, + fileId: String = DateTimeUtils.iso8601FromDate(Date()) +) { + private val file = File(logFileProvider.getLogFileDirectory(), "$fileId.log") + private val fileWriter: FileWriter = FileWriter(file) + + /** + * A serial executor used to write to the file in a background thread + */ + private val queue: ExecutorService = Executors.newSingleThreadExecutor() + + /** + * A reference to the underlying {@link Java.IO.File} file. + * Should only be used for testing. + */ + @TestOnly + fun getFile(): File = file + + /** + * Writes the provided string to the log file synchronously + */ + fun write(data: String) { + queue.execute { + fileWriter.write(data) + fileWriter.flush() + } + } +} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java index 4e4327619d21..5f2c62e9a0b6 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/AutoResizeTextView.java @@ -2,8 +2,6 @@ import android.annotation.SuppressLint; import android.content.Context; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.AppCompatTextView; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; @@ -11,6 +9,9 @@ import android.util.TypedValue; import android.widget.TextView; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.core.view.ViewCompat; + /** * Text view that auto adjusts text size to fit within the view. * If the text size equals the minimum text size and still does not diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java index adda92e15f36..91c72674d487 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java @@ -1,10 +1,11 @@ package org.wordpress.android.util.widgets; import android.content.Context; -import android.support.v4.widget.SwipeRefreshLayout; import android.util.AttributeSet; import android.view.MotionEvent; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java deleted file mode 100644 index 909a1ee49b44..000000000000 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPEditText.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.wordpress.android.util.widgets; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.widget.EditText; - -/* - * @deprecated This custom EditText is used solely by the "legacy" editor in WP Android. - * It will be removed when we drop the legacy editor and should not be used in new code. - */ -@SuppressLint("AppCompatCustomView") -@Deprecated -public class WPEditText extends EditText { - private EditTextImeBackListener mOnImeBack; - private OnSelectionChangedListener mOnSelectionChangedListener; - - public WPEditText(Context context) { - super(context); - } - - public WPEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public WPEditText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected void onSelectionChanged(int selStart, int selEnd) { - if (mOnSelectionChangedListener != null) { - mOnSelectionChangedListener.onSelectionChanged(); - } - } - - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK - && event.getAction() == KeyEvent.ACTION_UP) { - if (mOnImeBack != null) { - mOnImeBack.onImeBack(this, this.getText().toString()); - } - } - - return super.onKeyPreIme(keyCode, event); - } - - public void setOnEditTextImeBackListener(EditTextImeBackListener listener) { - mOnImeBack = listener; - } - - public interface EditTextImeBackListener { - void onImeBack(WPEditText ctrl, String text); - } - - public void setOnSelectionChangedListener(OnSelectionChangedListener listener) { - mOnSelectionChangedListener = listener; - } - - public interface OnSelectionChangedListener { - void onSelectionChanged(); - } -} diff --git a/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPTextInputLayout.java b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPTextInputLayout.java index a6e24a6d1501..2c3951c61ec5 100644 --- a/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPTextInputLayout.java +++ b/WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPTextInputLayout.java @@ -1,11 +1,12 @@ package org.wordpress.android.util.widgets; import android.content.Context; -import android.support.design.widget.TextInputLayout; import android.util.AttributeSet; import android.view.View; import android.widget.EditText; +import com.google.android.material.textfield.TextInputLayout; + import org.wordpress.android.util.R; /** @@ -38,7 +39,7 @@ public void setErrorEnabled(boolean enabled) { // remove hardcoded side padding of the error view if (enabled) { - View errorView = findViewById(android.support.design.R.id.textinput_error); + View errorView = findViewById(com.google.android.material.R.id.textinput_error); if (errorView != null && errorView.getParent() != null) { ((View) errorView.getParent()) .setPadding(0, errorView.getPaddingTop(), 0, errorView.getPaddingBottom()); diff --git a/WordPressUtils/src/test/java/org/wordpress/android/util/LogFileCleanerTest.kt b/WordPressUtils/src/test/java/org/wordpress/android/util/LogFileCleanerTest.kt new file mode 100644 index 000000000000..71789bd844dc --- /dev/null +++ b/WordPressUtils/src/test/java/org/wordpress/android/util/LogFileCleanerTest.kt @@ -0,0 +1,75 @@ +package org.wordpress.android.util + +import android.content.Context +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import java.io.File +import java.io.FileReader +import kotlin.random.Random +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.wordpress.android.util.helpers.logfile.LogFileCleaner +import org.wordpress.android.util.helpers.logfile.LogFileProvider + +/** + * The number of test files to create for each test run + */ +private const val MAX_FILES = 10 + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O_MR1]) +class LogFileCleanerTest { + private lateinit var logFileProvider: LogFileProvider + + @Before + fun setup() { + val context: Context = ApplicationProvider.getApplicationContext() + logFileProvider = LogFileProvider.fromContext(context) + + repeat(MAX_FILES) { + val file = File(logFileProvider.getLogFileDirectory(), "$it.log") + file.writeText("$it") + file.setLastModified(it * 10_000L) + } + + assert(logFileProvider.getLogFileDirectory().listFiles().count() == MAX_FILES) + } + + @After + fun tearDown() { + // Delete the test directory after each test + logFileProvider.getLogFileDirectory().deleteRecursively() + } + + @Test + fun testThatCleanerPreservesMostRecentlyCreatedFiles() { + val maxLogFileCount = Random.nextInt(MAX_FILES) + LogFileCleaner(logFileProvider, maxLogFileCount).clean() + + // Strings are easier to assert against than arrays + val remainingFileIds = logFileProvider.getLogFiles().joinToString(",") { + FileReader(it).readText() + } + + val expectedValue = (MAX_FILES - 1 downTo 0).take(maxLogFileCount).reversed().joinToString(",") + assertEquals(expectedValue, remainingFileIds) + } + + @Test + fun testThatCleanerPreservesCorrectNumberOfFiles() { + val numberOfFiles = Random.nextInt(MAX_FILES) + LogFileCleaner(logFileProvider, numberOfFiles).clean() + assertEquals(numberOfFiles, logFileProvider.getLogFileDirectory().listFiles().count()) + } + + @Test + fun testThatCleanerErasesAllFilesIfGivenZero() { + LogFileCleaner(logFileProvider, 0).clean() + assert(logFileProvider.getLogFileDirectory().listFiles().isEmpty()) + } +} diff --git a/WordPressUtils/src/test/java/org/wordpress/android/util/LogFileHelpersTest.kt b/WordPressUtils/src/test/java/org/wordpress/android/util/LogFileHelpersTest.kt new file mode 100644 index 000000000000..e4921c21e750 --- /dev/null +++ b/WordPressUtils/src/test/java/org/wordpress/android/util/LogFileHelpersTest.kt @@ -0,0 +1,64 @@ +package org.wordpress.android.util + +import android.content.Context +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import java.io.File +import java.util.UUID +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.wordpress.android.util.helpers.logfile.LogFileProvider + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O_MR1]) +class LogFileHelpersTest { + private lateinit var testProvider: LogFileProvider + + @Before + fun setup() { + val context: Context = ApplicationProvider.getApplicationContext() + testProvider = LogFileProvider.fromContext(context) + } + + @After + fun tearDown() { + // Delete the test directory after each test + testProvider.getLogFileDirectory().deleteRecursively() + } + + @Test + fun testThatLogFileDirectoryIsCreatedIfNotExists() { + val directory = testProvider.getLogFileDirectory() + assert(directory.exists()) + } + + @Test + fun testThatLogFilesListsAllFiles() { + val directory = testProvider.getLogFileDirectory() + File(directory, UUID.randomUUID().toString()).createNewFile() + Assert.assertEquals(testProvider.getLogFiles().count(), 1) + } + + @Test + fun testThatLogFilesSortsFilesWithMostRecentFirst() { + val directory = testProvider.getLogFileDirectory() + + listOf(1_000L, 1_000_000L).shuffled().forEach { modifiedDate -> + File(directory, UUID.randomUUID().toString()).also { file -> + // Use timestamps in increments of 1000 to avoid issues from the File System's date precision + val date = modifiedDate * 1000 + file.createNewFile() + file.setLastModified(date) + assert(file.lastModified() == date) + } + } + + val files = testProvider.getLogFiles() + assert(files.first().lastModified() < files.last().lastModified()) + } +} diff --git a/WordPressUtils/src/test/java/org/wordpress/android/util/LogFileWriterTest.kt b/WordPressUtils/src/test/java/org/wordpress/android/util/LogFileWriterTest.kt new file mode 100644 index 000000000000..998a29398783 --- /dev/null +++ b/WordPressUtils/src/test/java/org/wordpress/android/util/LogFileWriterTest.kt @@ -0,0 +1,53 @@ +package org.wordpress.android.util + +import android.content.Context +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import java.io.FileReader +import java.util.UUID +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.wordpress.android.util.helpers.logfile.LogFileProvider +import org.wordpress.android.util.helpers.logfile.LogFileWriter + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O_MR1]) +class LogFileWriterTest { + private lateinit var testProvider: LogFileProvider + + @Before + fun setup() { + val context: Context = ApplicationProvider.getApplicationContext() + testProvider = LogFileProvider.fromContext(context) + } + + @After + fun tearDown() { + // Delete the test directory after each test + testProvider.getLogFileDirectory().deleteRecursively() + } + + @Test + fun testThatFileWriterCreatesLogFile() { + val writer = LogFileWriter(testProvider) + assert(writer.getFile().exists()) + } + + @Test + fun testThatContentsAreWrittenToFile() { + val randomString = UUID.randomUUID().toString() + val writer = LogFileWriter(testProvider) + writer.write(randomString) + + // Allow the async process to persist the file changes + Thread.sleep(1000) + + val contents = FileReader(writer.getFile()).readText() + assertEquals(randomString, contents) + } +} diff --git a/config/checkstyle.xml b/config/checkstyle.xml index ae088941c51b..2ebaaac1412c 100644 --- a/config/checkstyle.xml +++ b/config/checkstyle.xml @@ -154,7 +154,7 @@ - + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5d5db35cfd1d..a4d1bcfaf935 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip