├── .buildkite ├── pipeline.yml ├── schedules │ └── dependency-analysis.yml └── shared-pipeline-vars ├── .editorconfig ├── .github └── workflows │ └── gradle-wrapper-validation.yml ├── .gitignore ├── .idea ├── checkstyle-idea.xml ├── codeStyleSettings.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── externalDependencies.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml └── vcs.xml ├── .java-version ├── README.md ├── WordPressUtils ├── README.md ├── build.gradle ├── gradle.properties-example └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── wordpress │ │ └── android │ │ └── util │ │ ├── ImageUtilsTest.java │ │ ├── JSONUtilsTest.java │ │ ├── PhotonUtilsTest.java │ │ ├── ShortcodeUtilsTest.java │ │ └── UrlUtilsTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── org │ │ │ └── wordpress │ │ │ └── android │ │ │ └── util │ │ │ ├── AccessibilityEventListener.java │ │ │ ├── AccessibilityUtils.java │ │ │ ├── ActivityUtils.java │ │ │ ├── AlertUtils.java │ │ │ ├── AppLog.java │ │ │ ├── AutoForeground.java │ │ │ ├── AutoForegroundNotification.java │ │ │ ├── DateTimeUtils.java │ │ │ ├── DeviceUtils.java │ │ │ ├── DisplayUtils.java │ │ │ ├── EditTextUtils.java │ │ │ ├── EmoticonsUtils.java │ │ │ ├── FileUtils.java │ │ │ ├── FormatUtils.java │ │ │ ├── GeocoderUtils.java │ │ │ ├── GravatarUtils.java │ │ │ ├── HtmlUtils.java │ │ │ ├── ImageUtils.java │ │ │ ├── JSONUtils.java │ │ │ ├── LanguageUtils.java │ │ │ ├── ListUtils.java │ │ │ ├── MapUtils.java │ │ │ ├── MediaUtils.java │ │ │ ├── NetworkUtils.java │ │ │ ├── PackageUtils.java │ │ │ ├── PermissionUtils.java │ │ │ ├── PhotonUtils.java │ │ │ ├── ProfilingUtils.java │ │ │ ├── ServiceUtils.java │ │ │ ├── ShortcodeUtils.java │ │ │ ├── SqlUtils.java │ │ │ ├── StringUtils.java │ │ │ ├── SystemServiceFactory.java │ │ │ ├── SystemServiceFactoryAbstract.java │ │ │ ├── SystemServiceFactoryDefault.java │ │ │ ├── ToastUtils.java │ │ │ ├── UrlUtils.java │ │ │ ├── VersionUtils.kt │ │ │ ├── VideoUtils.java │ │ │ ├── ViewUtils.java │ │ │ ├── WebViewUtils.java │ │ │ ├── helpers │ │ │ ├── Debouncer.java │ │ │ ├── ListScrollPositionManager.java │ │ │ ├── MediaFile.java │ │ │ ├── MediaGallery.java │ │ │ ├── MediaGalleryImageSpan.java │ │ │ ├── RecyclerViewScrollPositionManager.java │ │ │ ├── SwipeToRefreshHelper.java │ │ │ ├── Version.java │ │ │ ├── WPHtmlTagHandler.java │ │ │ ├── WPImageSpan.java │ │ │ ├── WPQuoteSpan.java │ │ │ ├── WPUnderlineSpan.java │ │ │ ├── WPWebChromeClient.java │ │ │ ├── WebChromeClientWithVideoPoster.kt │ │ │ └── logfile │ │ │ │ ├── LogFileCleaner.kt │ │ │ │ ├── LogFileProvider.kt │ │ │ │ ├── LogFileProviderInterface.kt │ │ │ │ └── LogFileWriter.kt │ │ │ └── widgets │ │ │ ├── AutoResizeTextView.java │ │ │ ├── CustomSwipeRefreshLayout.java │ │ │ └── WPTextInputLayout.java │ └── res │ │ └── values │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── tags.xml │ └── test │ └── java │ └── org │ └── wordpress │ └── android │ └── util │ ├── DateTimeUtilsTest.java │ ├── LogFileCleanerTest.kt │ ├── LogFileHelpersTest.kt │ ├── LogFileWriterTest.kt │ └── VersionUtilsTest.kt ├── build.gradle ├── config └── checkstyle.xml ├── gradle.properties-example ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json 2 | --- 3 | 4 | agents: 5 | queue: "android" 6 | 7 | steps: 8 | - label: "Gradle Wrapper Validation" 9 | command: | 10 | validate_gradle_wrapper 11 | plugins: [$CI_TOOLKIT] 12 | 13 | # Wait for Gradle Wrapper to be validated before running any other jobs 14 | - wait 15 | 16 | - label: "Lint & Checkstyle" 17 | key: "lint_and_checkstyle" 18 | plugins: [$CI_TOOLKIT] 19 | command: | 20 | cp gradle.properties-example gradle.properties 21 | ./gradlew lintRelease checkstyle 22 | artifact_paths: 23 | - "**/build/reports/lint-results.*" 24 | - "**/build/reports/checkstyle/checkstyle.*" 25 | 26 | - label: "Test" 27 | key: "test" 28 | plugins: [$CI_TOOLKIT] 29 | command: | 30 | cp gradle.properties-example gradle.properties 31 | ./gradlew testRelease 32 | 33 | - label: "Build and upload to S3" 34 | depends_on: 35 | - "lint_and_checkstyle" 36 | - "test" 37 | plugins: [$CI_TOOLKIT] 38 | command: | 39 | cp gradle.properties-example gradle.properties 40 | ./gradlew \ 41 | :WordPressUtils:prepareToPublishToS3 $(prepare_to_publish_to_s3_params) \ 42 | :WordPressUtils:publish 43 | -------------------------------------------------------------------------------- /.buildkite/schedules/dependency-analysis.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json 2 | --- 3 | 4 | agents: 5 | queue: "android" 6 | 7 | steps: 8 | - label: "dependency analysis" 9 | command: | 10 | echo "--- 📊 Analyzing" 11 | cp gradle.properties-example gradle.properties 12 | ./gradlew buildHealth 13 | plugins: [$CI_TOOLKIT] 14 | artifact_paths: 15 | - "build/reports/dependency-analysis/build-health-report.*" 16 | notify: 17 | - slack: "#android-core-notifs" 18 | if: build.state == "failed" 19 | -------------------------------------------------------------------------------- /.buildkite/shared-pipeline-vars: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This file is `source`'d before calling `buildkite-agent pipeline upload`, and can be used 4 | # to set up some variables that will be interpolated in the `.yml` pipeline before uploading it. 5 | 6 | export CI_TOOLKIT="automattic/a8c-ci-toolkit#3.4.2" 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | max_line_length=120 3 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | validation: 6 | name: "Validation" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: gradle/wrapper-validation-action@v1 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X generated file 2 | .DS_Store 3 | 4 | # built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # files for the dex VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # generated files 15 | bin/ 16 | gen/ 17 | build/ 18 | build.log 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Eclipse project files 24 | .settings/ 25 | .classpath 26 | .project 27 | 28 | # Intellij project files 29 | *.iml 30 | *.ipr 31 | *.iws 32 | /.idea/* 33 | 34 | # IntelliJ/Android Studio exceptions 35 | !/.idea/vcs.xml 36 | !/.idea/codeStyles/ 37 | !/.idea/fileTemplates/ 38 | !/.idea/inspectionProfiles/ 39 | !/.idea/scopes/ 40 | !/.idea/codeStyleSettings.xml 41 | !/.idea/encodings.xml 42 | !/.idea/copyright/ 43 | !/.idea/compiler.xml 44 | # Enforce plugins 45 | !/.idea/externalDependencies.xml 46 | # Checkstyle configuration 47 | !/.idea/checkstyle-idea.xml 48 | 49 | # Gradle 50 | .gradle/ 51 | gradle.properties 52 | 53 | # Silver Searcher ignore file 54 | .agignore 55 | 56 | # Windows Backup 57 | *.bak 58 | -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/externalDependencies.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 17.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress-Utils-Android 2 | 3 | Collection of utility methods for Android and WordPress. 4 | 5 | ## Use the library in your project 6 | 7 | * In your `build.gradle`: 8 | ```groovy 9 | repositories { 10 | maven { url "https://a8c-libs.s3.amazonaws.com/android" } 11 | } 12 | 13 | dependencies { 14 | implementation 'org.wordpress:utils:2.0.0' 15 | } 16 | ``` 17 | 18 | ## Publishing a new version 19 | 20 | In the following cases, the CI will publish a new version with the following format to our remote Maven repo: 21 | 22 | * For each commit in an open PR: `-` 23 | * Each time a PR is merged to `trunk`: `trunk-` 24 | * Each time a new tag is created: `{tag-name}` 25 | 26 | ## Apps and libraries using WordPress-Utils-Android: 27 | 28 | - [WordPress for Android][2] 29 | - [FluxC][3] 30 | - [Woo for Android][4] 31 | 32 | ## License 33 | Dual licensed under MIT, and GPL. 34 | 35 | [1]: https://github.com/wordpress-mobile/WordPress-Utils-Android/blob/a9fbe8e6597d44055ec2180dbf45aecbfc332a20/WordPressUtils/build.gradle#L37 36 | [2]: https://github.com/wordpress-mobile/WordPress-Android 37 | [3]: https://github.com/wordpress-mobile/WordPress-FluxC-Android 38 | [4]: https://github.com/woocommerce/woocommerce-android 39 | -------------------------------------------------------------------------------- /WordPressUtils/README.md: -------------------------------------------------------------------------------- 1 | # org.wordpress.android.util -------------------------------------------------------------------------------- /WordPressUtils/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.library" 3 | id "org.jetbrains.kotlin.android" 4 | id "com.automattic.android.publish-to-s3" 5 | } 6 | 7 | repositories { 8 | google() 9 | mavenCentral() 10 | maven { url 'https://a8c-libs.s3.amazonaws.com/android' } 11 | } 12 | 13 | dependencies { 14 | implementation "org.apache.commons:commons-text:$commonsTextVersion" 15 | implementation "com.google.android.material:material:$materialVersion" 16 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidxSwipeRefreshLayoutVersion" 17 | implementation "androidx.recyclerview:recyclerview:$androidxRecyclerViewVersion" 18 | implementation "org.greenrobot:eventbus:$eventBusVersion" 19 | implementation "org.greenrobot:eventbus-java:$eventBusVersion" 20 | 21 | implementation "androidx.core:core:$androidxCoreVersion" 22 | 23 | testImplementation "junit:junit:$junitVersion" 24 | testImplementation "org.assertj:assertj-core:$assertjVersion" 25 | testImplementation "org.robolectric:robolectric:$robolectricVersion" 26 | testImplementation "androidx.test:core:$androidxTestCoreVersion" 27 | 28 | lintChecks "org.wordpress:lint:$wordpressLintVersion" 29 | 30 | androidTestImplementation "androidx.test:runner:$androidxTestCoreVersion" 31 | } 32 | 33 | dependencyAnalysis { 34 | issues { 35 | onUnusedDependencies { 36 | // This dependency is actually needed otherwise the app will crash with a runtime exception. 37 | exclude("org.greenrobot:eventbus") 38 | } 39 | } 40 | } 41 | 42 | android { 43 | namespace "org.wordpress.android.util" 44 | 45 | useLibrary 'org.apache.http.legacy' 46 | 47 | compileSdkVersion rootProject.compileSdkVersion 48 | 49 | defaultConfig { 50 | minSdkVersion rootProject.minSdkVersion 51 | targetSdkVersion rootProject.targetSdkVersion 52 | 53 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 54 | } 55 | testOptions { 56 | unitTests { 57 | includeAndroidResources = true 58 | } 59 | } 60 | compileOptions { 61 | sourceCompatibility = JavaVersion.VERSION_1_8 62 | targetCompatibility = JavaVersion.VERSION_1_8 63 | } 64 | buildFeatures { 65 | buildConfig true 66 | } 67 | } 68 | 69 | project.afterEvaluate { 70 | publishing { 71 | publications { 72 | UtilsPublication(MavenPublication) { 73 | from components.release 74 | 75 | groupId "org.wordpress" 76 | artifactId "utils" 77 | // version is set by 'publish-to-s3' plugin 78 | } 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /WordPressUtils/gradle.properties-example: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | ossrhUsername=hello 3 | ossrhPassword=world 4 | 5 | signing.keyId=byebye 6 | signing.password=secret 7 | signing.secretKeyRingFile=/home/user/secret.gpg 8 | -------------------------------------------------------------------------------- /WordPressUtils/src/androidTest/java/org/wordpress/android/util/ImageUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.graphics.BitmapFactory; 4 | 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | public class ImageUtilsTest { 10 | @Test 11 | public void testGetScaleForResizingReturnsOneWhenMaxSizeIsZero() { 12 | BitmapFactory.Options options = new BitmapFactory.Options(); 13 | int scale = ImageUtils.getScaleForResizing(0, options); 14 | 15 | assertEquals(1, scale); 16 | } 17 | 18 | @Test 19 | public void testGetScaleForResizingSameSizeReturnsOne() { 20 | BitmapFactory.Options options = new BitmapFactory.Options(); 21 | options.outHeight = 100; 22 | options.outWidth = 100; 23 | int maxSize = 100; 24 | 25 | int scale = ImageUtils.getScaleForResizing(maxSize, options); 26 | 27 | assertEquals(1, scale); 28 | } 29 | 30 | @Test 31 | public void testGetScaleForResizingPortraitMaxHeightSameAsMaxSizeReturnsOne() { 32 | BitmapFactory.Options options = new BitmapFactory.Options(); 33 | options.outHeight = 100; 34 | options.outWidth = 1; 35 | int maxSize = 100; 36 | 37 | int scale = ImageUtils.getScaleForResizing(maxSize, options); 38 | 39 | assertEquals(1, scale); 40 | } 41 | 42 | @Test 43 | public void testGetScaleForResizingLandscapeMaxWidthSameAsMaxSizeReturnsOne() { 44 | BitmapFactory.Options options = new BitmapFactory.Options(); 45 | options.outHeight = 1; 46 | options.outWidth = 100; 47 | int maxSize = 100; 48 | 49 | int scale = ImageUtils.getScaleForResizing(maxSize, options); 50 | 51 | assertEquals(1, scale); 52 | } 53 | 54 | @Test 55 | public void testGetScaleForResizingDoubleSizeReturnsTwo() { 56 | BitmapFactory.Options options = new BitmapFactory.Options(); 57 | options.outHeight = 100; 58 | options.outWidth = 200; 59 | int maxSize = 100; 60 | 61 | int scale = ImageUtils.getScaleForResizing(maxSize, options); 62 | 63 | assertEquals(2, scale); 64 | } 65 | 66 | @Test 67 | public void testGetScaleForResizingThreeTimesSizeReturnsTwo() { 68 | BitmapFactory.Options options = new BitmapFactory.Options(); 69 | options.outHeight = 100; 70 | options.outWidth = 300; 71 | int maxSize = 100; 72 | 73 | int scale = ImageUtils.getScaleForResizing(maxSize, options); 74 | 75 | assertEquals(2, scale); 76 | } 77 | 78 | @Test 79 | public void testGetScaleForResizingEightTimesSizeReturnsEight() { 80 | BitmapFactory.Options options = new BitmapFactory.Options(); 81 | options.outHeight = 100; 82 | options.outWidth = 800; 83 | int maxSize = 100; 84 | 85 | int scale = ImageUtils.getScaleForResizing(maxSize, options); 86 | 87 | assertEquals(8, scale); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /WordPressUtils/src/androidTest/java/org/wordpress/android/util/JSONUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONObject; 5 | import org.junit.Test; 6 | 7 | public class JSONUtilsTest { 8 | @Test 9 | public void testQueryJSONNullSource1() { 10 | JSONUtils.queryJSON((JSONObject) null, "", ""); 11 | } 12 | 13 | @Test 14 | public void testQueryJSONNullSource2() { 15 | JSONUtils.queryJSON((JSONArray) null, "", ""); 16 | } 17 | 18 | @Test 19 | public void testQueryJSONNullQuery1() { 20 | JSONUtils.queryJSON(new JSONObject(), null, ""); 21 | } 22 | 23 | @Test 24 | public void testQueryJSONNullQuery2() { 25 | JSONUtils.queryJSON(new JSONArray(), null, ""); 26 | } 27 | 28 | @Test 29 | public void testQueryJSONNullReturnValue1() { 30 | JSONUtils.queryJSON(new JSONObject(), "", null); 31 | } 32 | 33 | @Test 34 | public void testQueryJSONNullReturnValue2() { 35 | JSONUtils.queryJSON(new JSONArray(), "", null); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /WordPressUtils/src/androidTest/java/org/wordpress/android/util/PhotonUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import org.junit.Test; 4 | import org.wordpress.android.util.PhotonUtils.Quality; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import static org.hamcrest.CoreMatchers.containsString; 10 | import static org.hamcrest.core.IsEqual.equalTo; 11 | import static org.junit.Assert.assertThat; 12 | 13 | 14 | public class PhotonUtilsTest { 15 | @Test 16 | public void getPhotonImageUrlIsEmptyWhenUrlIsNull() { 17 | String photonUrl = PhotonUtils.getPhotonImageUrl(null, 0, 1); 18 | 19 | assertThat(photonUrl, equalTo("")); 20 | } 21 | 22 | @Test 23 | public void getPhotonImageUrlIsEmptyWhenUrlIsEmpty() { 24 | String photonUrl = PhotonUtils.getPhotonImageUrl("", 0, 1); 25 | 26 | assertThat(photonUrl, equalTo("")); 27 | } 28 | 29 | @Test 30 | public void getPhotonImageUrlReturnsImageUrlOnNoScheme() { 31 | String imageUrl = "wordpress.com"; 32 | String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 0, 1); 33 | 34 | assertThat(photonUrl, equalTo(imageUrl)); 35 | } 36 | 37 | @Test 38 | public void getPhotonImageUrlReturnsMshots() { 39 | String imageUrl = "http://test.wordpress.com/mshots/test.jpg?query=dummy"; 40 | String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 0, 1); 41 | 42 | assertThat(photonUrl, equalTo("http://test.wordpress.com/mshots/test.jpg?w=0&h=1")); 43 | } 44 | 45 | @Test 46 | public void getPhotonImageUrlReturnsCorrectQuality() { 47 | Map qualities = new HashMap<>(); 48 | qualities.put(Quality.HIGH, "100"); 49 | qualities.put(Quality.MEDIUM, "65"); 50 | qualities.put(Quality.LOW, "35"); 51 | 52 | String imageUrl = "http://test.wordpress.com/test.jpg?query=dummy"; 53 | 54 | for (Quality quality : qualities.keySet()) { 55 | String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 0, 1, quality); 56 | assertThat(photonUrl, containsString("&quality=" + qualities.get(quality))); 57 | } 58 | } 59 | 60 | @Test 61 | public void getPhotonImageUrlUsesResize() { 62 | String imageUrl = "http://test.wordpress.com/test.jpg?query=dummy"; 63 | String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); 64 | 65 | assertThat(photonUrl, equalTo("http://test.wordpress.com/test.jpg?strip=info&quality=65&resize=2,1")); 66 | } 67 | 68 | @Test 69 | public void getPhotonImageUrlManageSslOnPhotonUrl() { 70 | String imageUrl = "https://i0.wp.com/test.jpg?query=dummy"; 71 | String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); 72 | 73 | assertThat(photonUrl, equalTo("https://i0.wp.com/test.jpg?strip=info&quality=65&resize=2,1")); 74 | 75 | imageUrl = "https://i0.wp.com/test.jpg?query=dummy&ssl=1"; 76 | photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); 77 | 78 | assertThat(photonUrl, equalTo("https://i0.wp.com/test.jpg?strip=info&quality=65&resize=2,1&ssl=1")); 79 | } 80 | 81 | @Test 82 | public void getPhotonImageUrlDoNotUseSslOnWordPressCom() { 83 | String imageUrl = "https://test.wordpress.com/test.jpg?query=dummy"; 84 | String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); 85 | 86 | assertThat(photonUrl, equalTo("https://test.wordpress.com/test.jpg?strip=info&quality=65&resize=2,1")); 87 | 88 | imageUrl = "https://test.wordpress.com/test.jpg?query=dummy&ssl=1"; 89 | photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); 90 | 91 | assertThat(photonUrl, equalTo("https://test.wordpress.com/test.jpg?strip=info&quality=65&resize=2,1")); 92 | } 93 | 94 | @Test 95 | public void getPhotonImageUrlUsesSslOnHttpsImageUrl() { 96 | String imageUrl = "http://mysite.com/test.jpg?query=dummy"; 97 | String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); 98 | 99 | assertThat(photonUrl, equalTo("https://i0.wp.com/mysite.com/test.jpg?strip=info&quality=65&resize=2,1")); 100 | 101 | imageUrl = "https://mysite.com/test.jpg?query=dummy&ssl=1"; 102 | photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 2, 1); 103 | 104 | assertThat(photonUrl, equalTo("https://i0.wp.com/mysite.com/test.jpg?strip=info&quality=65&resize=2,1&ssl=1")); 105 | } 106 | 107 | @Test 108 | public void getPhotonImageUrlWithErroneousSchemePosition() { 109 | String imageUrl = "mysite.com/test.jpg#http://another.com"; 110 | String photonUrl = PhotonUtils.getPhotonImageUrl(imageUrl, 1, 1); 111 | 112 | assertThat(photonUrl, equalTo(imageUrl)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /WordPressUtils/src/androidTest/java/org/wordpress/android/util/ShortcodeUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class ShortcodeUtilsTest { 8 | @Test 9 | public void testGetVideoPressShortcodeFromId() { 10 | assertEquals("[wpvideo abcd1234]", ShortcodeUtils.getVideoPressShortcodeFromId("abcd1234")); 11 | } 12 | 13 | @Test 14 | public void testGetVideoPressShortcodeFromNullId() { 15 | assertEquals("", ShortcodeUtils.getVideoPressShortcodeFromId(null)); 16 | } 17 | 18 | @Test 19 | public void testGetVideoPressIdFromCorrectShortcode() { 20 | assertEquals("abcd1234", ShortcodeUtils.getVideoPressIdFromShortCode("[wpvideo abcd1234]")); 21 | } 22 | 23 | @Test 24 | public void testGetVideoPressIdFromInvalidShortcode() { 25 | assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode("[other abcd1234]")); 26 | } 27 | 28 | @Test 29 | public void testGetVideoPressIdFromNullShortcode() { 30 | assertEquals("", ShortcodeUtils.getVideoPressIdFromShortCode(null)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /WordPressUtils/src/androidTest/java/org/wordpress/android/util/UrlUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import org.junit.Test; 4 | 5 | import java.net.MalformedURLException; 6 | import java.net.URL; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | import static org.junit.Assert.assertFalse; 12 | import static org.junit.Assert.assertNotNull; 13 | import static org.junit.Assert.assertTrue; 14 | 15 | public class UrlUtilsTest { 16 | @Test 17 | public void testGetDomainFromUrlWithEmptyStringDoesNotReturnNull() { 18 | assertNotNull(UrlUtils.getHost("")); 19 | } 20 | 21 | @Test 22 | public void testGetDomainFromUrlWithNoHostDoesNotReturnNull() { 23 | assertNotNull(UrlUtils.getHost("wordpress")); 24 | } 25 | 26 | @Test 27 | public void testGetDomainFromUrlWithHostReturnsHost() { 28 | String url = "http://www.wordpress.com"; 29 | String host = UrlUtils.getHost(url); 30 | 31 | assertTrue(host.equals("www.wordpress.com")); 32 | } 33 | 34 | @Test 35 | public void testAppendUrlParameter1() { 36 | String url = UrlUtils.appendUrlParameter("http://wp.com/test", "preview", "true"); 37 | assertEquals("http://wp.com/test?preview=true", url); 38 | } 39 | 40 | @Test 41 | public void testAppendUrlParameter2() { 42 | String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony", "preview", "true"); 43 | assertEquals("http://wp.com/test?q=pony&preview=true", url); 44 | } 45 | 46 | @Test 47 | public void testAppendUrlParameter3() { 48 | String url = UrlUtils.appendUrlParameter("http://wp.com/test?q=pony#unicorn", "preview", "true"); 49 | assertEquals("http://wp.com/test?q=pony&preview=true#unicorn", url); 50 | } 51 | 52 | @Test 53 | public void testAppendUrlParameter4() { 54 | String url = UrlUtils.appendUrlParameter("/relative/test", "preview", "true"); 55 | assertEquals("/relative/test?preview=true", url); 56 | } 57 | 58 | @Test 59 | public void testAppendUrlParameter5() { 60 | String url = UrlUtils.appendUrlParameter("/relative/", "preview", "true"); 61 | assertEquals("/relative/?preview=true", url); 62 | } 63 | 64 | @Test 65 | public void testAppendUrlParameter6() { 66 | String url = UrlUtils.appendUrlParameter("http://wp.com/test/", "preview", "true"); 67 | assertEquals("http://wp.com/test/?preview=true", url); 68 | } 69 | 70 | @Test 71 | public void testAppendUrlParameter7() { 72 | String url = UrlUtils.appendUrlParameter("http://wp.com/test/?q=pony", "preview", "true"); 73 | assertEquals("http://wp.com/test/?q=pony&preview=true", url); 74 | } 75 | 76 | @Test 77 | public void testAppendUrlParameters1() { 78 | Map params = new HashMap<>(); 79 | params.put("w", "200"); 80 | params.put("h", "300"); 81 | String url = UrlUtils.appendUrlParameters("http://wp.com/test", params); 82 | if (!url.equals("http://wp.com/test?h=300&w=200") && !url.equals("http://wp.com/test?w=200&h=300")) { 83 | assertTrue("failed test on url: " + url, false); 84 | } 85 | } 86 | 87 | @Test 88 | public void testAppendUrlParameters2() { 89 | Map params = new HashMap<>(); 90 | params.put("h", "300"); 91 | params.put("w", "200"); 92 | String url = UrlUtils.appendUrlParameters("/relative/test", params); 93 | if (!url.equals("/relative/test?h=300&w=200") && !url.equals("/relative/test?w=200&h=300")) { 94 | assertTrue("failed test on url: " + url, false); 95 | } 96 | } 97 | 98 | @Test 99 | public void testHttps1() { 100 | assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php"))); 101 | } 102 | 103 | @Test 104 | public void testHttps2() { 105 | assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com#.b.com/test"))); 106 | } 107 | 108 | @Test 109 | public void testHttps3() { 110 | assertFalse(UrlUtils.isHttps(buildURL("http://wordpress.com/xmlrpc.php"))); 111 | } 112 | 113 | @Test 114 | public void testHttps4() { 115 | assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com"))); 116 | } 117 | 118 | @Test 119 | public void testHttps5() { 120 | assertTrue(UrlUtils.isHttps(buildURL("https://wordpress.com/test#test"))); 121 | } 122 | 123 | private URL buildURL(String address) { 124 | URL url = null; 125 | try { 126 | url = new URL(address); 127 | } catch (MalformedURLException e) { 128 | } 129 | return url; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityEventListener.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.view.accessibility.AccessibilityEvent; 4 | 5 | public interface AccessibilityEventListener { 6 | void onResult(AccessibilityEvent event); 7 | } 8 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/AccessibilityUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.view.View; 6 | import android.view.accessibility.AccessibilityEvent; 7 | import android.view.accessibility.AccessibilityManager; 8 | import android.widget.TextView; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | import androidx.core.view.AccessibilityDelegateCompat; 13 | import androidx.core.view.ViewCompat; 14 | import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 15 | 16 | import com.google.android.material.snackbar.Snackbar; 17 | 18 | import org.wordpress.android.util.AppLog.T; 19 | 20 | import static android.content.Context.ACCESSIBILITY_SERVICE; 21 | import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; 22 | 23 | public class AccessibilityUtils { 24 | private static final int SNACKBAR_WITH_ACTION_DURATION_IN_MILLIS = 10000; 25 | 26 | public static boolean isAccessibilityEnabled(Context ctx) { 27 | AccessibilityManager am = (AccessibilityManager) ctx.getSystemService(ACCESSIBILITY_SERVICE); 28 | return am != null && am.isEnabled(); 29 | } 30 | 31 | /** 32 | * If the default duration is LENGTH_INDEFINITE, ignore accessibility duration and return LENGTH_INDEFINITE. 33 | * If the accessibility is enabled, returns increased snackbar duration, otherwise returns defaultDuration. 34 | * 35 | * @param defaultDuration Either be one of the predefined lengths: LENGTH_SHORT, LENGTH_LONG, or a custom duration 36 | * in milliseconds. 37 | */ 38 | public static int getSnackbarDuration(Context ctx, int defaultDuration) { 39 | return defaultDuration == Snackbar.LENGTH_INDEFINITE ? Snackbar.LENGTH_INDEFINITE 40 | : isAccessibilityEnabled(ctx) ? SNACKBAR_WITH_ACTION_DURATION_IN_MILLIS : defaultDuration; 41 | } 42 | 43 | public static void setActionModeDoneButtonContentDescription(@Nullable final Activity activity, 44 | @NonNull final String contentDescription) { 45 | if (activity != null) { 46 | View decorView = activity.getWindow().getDecorView(); 47 | 48 | decorView.post(new Runnable() { 49 | @Override public void run() { 50 | View doneButton = activity.findViewById(androidx.appcompat.R.id.action_mode_close_button); 51 | 52 | if (doneButton != null) { 53 | doneButton.setContentDescription(contentDescription); 54 | } 55 | } 56 | }); 57 | } 58 | } 59 | 60 | public static void addPopulateAccessibilityEventFocusedListener(@NonNull final View target, 61 | @NonNull final AccessibilityEventListener 62 | listener) { 63 | ViewCompat.setAccessibilityDelegate(target, new AccessibilityDelegateCompat() { 64 | @Override public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 65 | if (event.getEventType() == TYPE_VIEW_ACCESSIBILITY_FOCUSED) { 66 | listener.onResult(event); 67 | } 68 | super.onPopulateAccessibilityEvent(host, event); 69 | } 70 | }); 71 | } 72 | 73 | public static void disableHintAnnouncement(@NonNull TextView textView) { 74 | setAccessibilityDelegateSafely(textView, new AccessibilityDelegateCompat() { 75 | @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 76 | super.onInitializeAccessibilityNodeInfo(host, info); 77 | info.setHintText(null); 78 | } 79 | }); 80 | } 81 | 82 | /** 83 | * When the minsdk is 28 this can be replaced by adding android:accessibilityHeading="true" as a property to the 84 | * view's xml declaration. 85 | * @param view that will become a heading. 86 | */ 87 | public static void enableAccessibilityHeading(@NonNull View view) { 88 | setAccessibilityDelegateSafely(view, new AccessibilityDelegateCompat() { 89 | @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 90 | super.onInitializeAccessibilityNodeInfo(host, info); 91 | info.setHeading(true); 92 | } 93 | }); 94 | } 95 | 96 | public static void setAccessibilityDelegateSafely(View view, 97 | AccessibilityDelegateCompat accessibilityDelegateCompat) { 98 | if (ViewCompat.hasAccessibilityDelegate(view)) { 99 | final String errorMessage = "View already has an AccessibilityDelegate."; 100 | if (PackageUtils.isDebugBuild()) { 101 | throw new RuntimeException(errorMessage); 102 | } 103 | AppLog.e(T.UTILS, errorMessage); 104 | } else { 105 | ViewCompat.setAccessibilityDelegate(view, accessibilityDelegateCompat); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/ActivityUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.view.View; 7 | import android.view.inputmethod.InputMethodManager; 8 | 9 | import androidx.annotation.Nullable; 10 | 11 | public class ActivityUtils { 12 | /** 13 | * Hides the keyboard in the given {@link Activity}'s current focus using the 14 | * {@link InputMethodManager#HIDE_NOT_ALWAYS} flag, which will hide the keyboard unless it was originally shown 15 | * with {@link InputMethodManager#SHOW_FORCED}. 16 | */ 17 | public static void hideKeyboard(Activity activity) { 18 | if (activity != null && activity.getCurrentFocus() != null) { 19 | InputMethodManager inputManager = (InputMethodManager) activity.getSystemService( 20 | Context.INPUT_METHOD_SERVICE); 21 | inputManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(), 22 | InputMethodManager.HIDE_NOT_ALWAYS); 23 | } 24 | } 25 | 26 | /** 27 | * Hides the keyboard for the given {@link View}. No {@link InputMethodManager} flag is used, therefore the 28 | * keyboard is forcibly hidden regardless of the circumstances. 29 | */ 30 | public static void hideKeyboardForced(@Nullable final View view) { 31 | if (view == null) { 32 | return; 33 | } 34 | InputMethodManager inputMethodManager = 35 | (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 36 | inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); 37 | } 38 | 39 | /** 40 | * Shows the keyboard for the given {@link View} using the {@link InputMethodManager#SHOW_IMPLICIT} flag, 41 | * which is an implicit request (i.e. not requested by the user) to show the keyboard. 42 | */ 43 | public static void showKeyboard(@Nullable final View view) { 44 | if (view == null) { 45 | return; 46 | } 47 | InputMethodManager inputMethodManager = 48 | (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 49 | inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); 50 | } 51 | 52 | public static boolean isDeepLinking(Intent intent) { 53 | return Intent.ACTION_VIEW.equals(intent.getAction()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/AlertUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 wordpress.org 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.wordpress.android.util; 18 | 19 | import android.app.AlertDialog; 20 | import android.app.Dialog; 21 | import android.content.Context; 22 | import android.content.DialogInterface; 23 | 24 | public class AlertUtils { 25 | /** 26 | * Show Alert Dialog 27 | * @param context 28 | * @param titleId 29 | * @param messageId 30 | */ 31 | public static void showAlert(Context context, int titleId, int messageId) { 32 | Dialog dlg = new AlertDialog.Builder(context) 33 | .setTitle(titleId) 34 | .setPositiveButton(android.R.string.ok, null) 35 | .setMessage(messageId) 36 | .create(); 37 | 38 | dlg.show(); 39 | } 40 | 41 | /** 42 | * Show Alert Dialog 43 | * @param context 44 | * @param titleId 45 | * @param message 46 | */ 47 | public static void showAlert(Context context, int titleId, String message) { 48 | Dialog dlg = new AlertDialog.Builder(context) 49 | .setTitle(titleId) 50 | .setPositiveButton(android.R.string.ok, null) 51 | .setMessage(message) 52 | .create(); 53 | 54 | dlg.show(); 55 | } 56 | 57 | /** 58 | * Show Alert Dialog 59 | * @param context 60 | * @param titleId 61 | * @param messageId 62 | * @param positiveButtontxt 63 | * @param positiveListener 64 | * @param negativeButtontxt 65 | * @param negativeListener 66 | */ 67 | public static void showAlert(Context context, int titleId, int messageId, 68 | CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener, 69 | CharSequence negativeButtontxt, DialogInterface.OnClickListener negativeListener) { 70 | Dialog dlg = new AlertDialog.Builder(context) 71 | .setTitle(titleId) 72 | .setPositiveButton(positiveButtontxt, positiveListener) 73 | .setNegativeButton(negativeButtontxt, negativeListener) 74 | .setMessage(messageId) 75 | .setCancelable(false) 76 | .create(); 77 | 78 | dlg.show(); 79 | } 80 | 81 | /** 82 | * Show Alert Dialog 83 | * @param context 84 | * @param titleId 85 | * @param message 86 | * @param positiveButtontxt 87 | * @param positiveListener 88 | */ 89 | public static void showAlert(Context context, int titleId, String message, 90 | CharSequence positiveButtontxt, DialogInterface.OnClickListener positiveListener) { 91 | Dialog dlg = new AlertDialog.Builder(context) 92 | .setTitle(titleId) 93 | .setPositiveButton(positiveButtontxt, positiveListener) 94 | .setMessage(message) 95 | .setCancelable(false) 96 | .create(); 97 | 98 | dlg.show(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/AutoForeground.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.app.Notification; 4 | import android.app.Service; 5 | import android.content.ComponentName; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.ServiceConnection; 9 | import android.os.Binder; 10 | import android.os.IBinder; 11 | 12 | import androidx.annotation.CallSuper; 13 | import androidx.annotation.Nullable; 14 | import androidx.core.app.NotificationManagerCompat; 15 | 16 | import org.greenrobot.eventbus.EventBus; 17 | import org.wordpress.android.util.AutoForeground.ServiceState; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | public abstract class AutoForeground 23 | extends Service { 24 | public static final int NOTIFICATION_ID_PROGRESS = 1; 25 | public static final int NOTIFICATION_ID_SUCCESS = 2; 26 | public static final int NOTIFICATION_ID_FAILURE = 3; 27 | 28 | public interface ServiceState { 29 | boolean isIdle(); 30 | 31 | boolean isInProgress(); 32 | 33 | boolean isError(); 34 | 35 | boolean isTerminal(); 36 | 37 | String getStepName(); 38 | } 39 | 40 | public static class ServiceEventConnection { 41 | private final ServiceConnection mServiceConnection; 42 | 43 | public ServiceEventConnection(Context context, Class clazz, Object client) { 44 | EventBus.getDefault().register(client); 45 | 46 | mServiceConnection = new ServiceConnection() { 47 | @Override 48 | public void onServiceConnected(ComponentName componentName, IBinder iBinder) { 49 | // nothing here 50 | } 51 | 52 | @Override 53 | public void onServiceDisconnected(ComponentName componentName) { 54 | // nothing here 55 | } 56 | }; 57 | 58 | context.bindService(new Intent(context, clazz), mServiceConnection, Context.BIND_AUTO_CREATE); 59 | } 60 | 61 | public void disconnect(Context context, Object client) { 62 | context.unbindService(mServiceConnection); 63 | EventBus.getDefault().unregister(client); 64 | } 65 | } 66 | 67 | private class LocalBinder extends Binder { 68 | } 69 | 70 | private final IBinder mBinder = new LocalBinder(); 71 | 72 | private final Class mStateClass; 73 | 74 | private boolean mIsForeground; 75 | 76 | protected abstract void onProgressStart(); 77 | 78 | protected abstract void onProgressEnd(); 79 | 80 | protected abstract Notification getNotification(StateClass state); 81 | 82 | protected abstract void trackStateUpdate(Map props); 83 | 84 | @SuppressWarnings("unchecked") 85 | protected AutoForeground(StateClass initialState) { 86 | mStateClass = (Class) initialState.getClass(); 87 | 88 | // initialize the sticky phase if it hasn't already 89 | if (EventBus.getDefault().getStickyEvent(mStateClass) == null) { 90 | notifyState(initialState); 91 | } 92 | } 93 | 94 | public boolean isForeground() { 95 | return mIsForeground; 96 | } 97 | 98 | @Nullable 99 | private StateClass getState() { 100 | return getState(mStateClass); 101 | } 102 | 103 | @Nullable 104 | protected static StateClass getState(Class stateClass) { 105 | return EventBus.getDefault().getStickyEvent(stateClass); 106 | } 107 | 108 | @Nullable 109 | @CallSuper 110 | @Override 111 | public IBinder onBind(Intent intent) { 112 | clearAllNotifications(); 113 | return mBinder; 114 | } 115 | 116 | @CallSuper 117 | @Override 118 | public void onRebind(Intent intent) { 119 | super.onRebind(intent); 120 | 121 | clearAllNotifications(); 122 | background(); 123 | } 124 | 125 | @CallSuper 126 | @Override 127 | public boolean onUnbind(Intent intent) { 128 | if (!hasConnectedClients()) { 129 | final StateClass state = getState(); 130 | if (state != null && state.isInProgress()) { 131 | promoteForeground(state); 132 | } 133 | } 134 | 135 | return true; // call onRebind() if new clients connect 136 | } 137 | 138 | protected void clearAllNotifications() { 139 | NotificationManagerCompat.from(this).cancel(NOTIFICATION_ID_PROGRESS); 140 | NotificationManagerCompat.from(this).cancel(NOTIFICATION_ID_SUCCESS); 141 | NotificationManagerCompat.from(this).cancel(NOTIFICATION_ID_FAILURE); 142 | } 143 | 144 | private EventBus getEventBus() { 145 | return EventBus.getDefault(); 146 | } 147 | 148 | private boolean hasConnectedClients() { 149 | return getEventBus().hasSubscriberForEvent(mStateClass); 150 | } 151 | 152 | private void promoteForeground(StateClass currentState) { 153 | startForeground(NOTIFICATION_ID_PROGRESS, getNotification(currentState)); 154 | mIsForeground = true; 155 | } 156 | 157 | private void background() { 158 | stopForeground(true); 159 | mIsForeground = false; 160 | } 161 | 162 | @CallSuper 163 | protected void setState(StateClass newState) { 164 | StateClass currentState = getState(); 165 | if ((currentState == null || !currentState.isInProgress()) && newState.isInProgress()) { 166 | onProgressStart(); 167 | } 168 | 169 | track(newState); 170 | notifyState(newState); 171 | 172 | if (newState.isTerminal()) { 173 | onProgressEnd(); 174 | stopSelf(); 175 | } 176 | } 177 | 178 | protected void track(ServiceState state) { 179 | Map props = new HashMap<>(); 180 | props.put("login_phase", state == null ? "null" : state.getStepName()); 181 | props.put("login_service_is_foreground", isForeground()); 182 | trackStateUpdate(props); 183 | } 184 | 185 | protected static void clearServiceState(Class klass) { 186 | EventBus.getDefault().removeStickyEvent(klass); 187 | } 188 | 189 | @CallSuper 190 | protected void notifyState(StateClass state) { 191 | // sticky emit the state. The stickiness serves as a state keeping mechanism for clients to re-read upon connect 192 | getEventBus().postSticky(state); 193 | 194 | if (hasConnectedClients()) { 195 | // there are connected clients so, nothing more to do here 196 | return; 197 | } 198 | 199 | // ok, no connected clients so, update might need to be delivered to a notification as well 200 | 201 | if (state.isIdle()) { 202 | // no need to have a notification when idle 203 | return; 204 | } 205 | 206 | if (state.isInProgress()) { 207 | // operation still is progress so, update the notification 208 | NotificationManagerCompat.from(this).notify(NOTIFICATION_ID_PROGRESS, getNotification(state)); 209 | return; 210 | } 211 | 212 | // operation has ended so, demote the Service to a background one 213 | background(); 214 | 215 | // dismiss the sticky notification 216 | NotificationManagerCompat.from(this).cancel(NOTIFICATION_ID_PROGRESS); 217 | 218 | // put out a simple success/failure notification 219 | NotificationManagerCompat.from(this).notify( 220 | state.isError() ? NOTIFICATION_ID_FAILURE : NOTIFICATION_ID_SUCCESS, 221 | getNotification(state)); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/AutoForegroundNotification.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.app.Notification; 4 | import android.app.PendingIntent; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | 8 | import androidx.annotation.ColorRes; 9 | import androidx.annotation.DrawableRes; 10 | import androidx.annotation.StringRes; 11 | import androidx.core.app.NotificationCompat; 12 | 13 | import static org.wordpress.android.util.AutoForeground.NOTIFICATION_ID_FAILURE; 14 | import static org.wordpress.android.util.AutoForeground.NOTIFICATION_ID_PROGRESS; 15 | import static org.wordpress.android.util.AutoForeground.NOTIFICATION_ID_SUCCESS; 16 | 17 | public class AutoForegroundNotification { 18 | private static Intent getResumeIntent(Context context) { 19 | // Let's get an Intent with the sole purpose of _resuming_ the app from the background 20 | Intent resumeIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); 21 | 22 | // getLaunchIntentForPackage() seems to set the Package Name but if we construct a launcher Intent manually 23 | // the package name is not set so, let's null it out here to match the manual Intent. 24 | resumeIntent.setSelector(null); 25 | resumeIntent.setPackage(null); 26 | 27 | return resumeIntent; 28 | } 29 | 30 | private static NotificationCompat.Builder getNotificationBuilder(Context context, String channelId, int requestCode, 31 | @StringRes int title, @StringRes int content, 32 | @DrawableRes int icon, @ColorRes int accentColor) { 33 | NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); 34 | bigTextStyle.setBigContentTitle(context.getString(title)); 35 | bigTextStyle.bigText(context.getString(content)); 36 | 37 | return new NotificationCompat.Builder(context, channelId) 38 | .setStyle(bigTextStyle) 39 | .setContentTitle(context.getString(title)) 40 | .setContentText(context.getString(content)) 41 | .setSmallIcon(icon) 42 | .setColor(context.getResources().getColor(accentColor)) 43 | .setAutoCancel(true) 44 | .setOnlyAlertOnce(true) 45 | .setContentIntent(PendingIntent.getActivity( 46 | context, 47 | requestCode, 48 | getResumeIntent(context), 49 | PendingIntent.FLAG_IMMUTABLE)); 50 | } 51 | 52 | public static Notification progress(Context context, String channelId, int progress, @StringRes int title, 53 | @StringRes int content, 54 | @DrawableRes int icon, @ColorRes int accentColor) { 55 | return getNotificationBuilder(context, channelId, NOTIFICATION_ID_PROGRESS, title, content, icon, accentColor) 56 | .setProgress(100, progress, false) 57 | .build(); 58 | } 59 | 60 | public static Notification progressIndeterminate(Context context, String channelId, @StringRes int title, 61 | @StringRes int content, @DrawableRes int icon, 62 | @ColorRes int accentColor) { 63 | return getNotificationBuilder(context, channelId, NOTIFICATION_ID_PROGRESS, title, content, icon, accentColor) 64 | .setProgress(0, 0, true) 65 | .build(); 66 | } 67 | 68 | public static Notification success(Context context, String channelId, @StringRes int title, @StringRes int content, 69 | @DrawableRes int icon, @ColorRes int accentColor) { 70 | return getNotificationBuilder(context, channelId, NOTIFICATION_ID_SUCCESS, title, content, icon, accentColor) 71 | .build(); 72 | } 73 | 74 | public static Notification failure(Context context, String channelId, @StringRes int title, @StringRes int content, 75 | @DrawableRes int icon, @ColorRes int accentColor) { 76 | return getNotificationBuilder(context, channelId, NOTIFICATION_ID_FAILURE, title, content, icon, accentColor) 77 | .build(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | import android.text.format.DateUtils; 6 | 7 | import java.text.DateFormat; 8 | import java.text.ParseException; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | import java.util.Locale; 12 | import java.util.TimeZone; 13 | 14 | public class DateTimeUtils { 15 | private DateTimeUtils() { 16 | throw new AssertionError(); 17 | } 18 | 19 | // See http://drdobbs.com/java/184405382 20 | private static final ThreadLocal ISO8601_FORMAT = new ThreadLocal() { 21 | @Override 22 | protected DateFormat initialValue() { 23 | return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); 24 | } 25 | }; 26 | 27 | public static String javaDateToTimeSpan(final Date date, Context context, long currentTime) { 28 | if (date == null) { 29 | return ""; 30 | } 31 | 32 | long passedTime = date.getTime(); 33 | 34 | // return "now" if less than a minute has elapsed 35 | long secondsSince = (currentTime - passedTime) / 1000; 36 | if (secondsSince < 60) { 37 | return context.getString(R.string.timespan_now); 38 | } 39 | 40 | long daysSince = secondsSince / (60 * 60 * 24); 41 | 42 | // less than a year old, let `DateUtils.getRelativeTimeSpanString` do the job 43 | if (daysSince < 365) { 44 | return DateUtils.getRelativeTimeSpanString(passedTime, currentTime, DateUtils.MINUTE_IN_MILLIS, 45 | DateUtils.FORMAT_ABBREV_ALL).toString(); 46 | } 47 | 48 | // date is older, so include year (ex: Jan 30, 2013) 49 | return DateUtils.formatDateTime(context, passedTime, DateUtils.FORMAT_ABBREV_ALL); 50 | } 51 | 52 | /** 53 | * Converts a date to a localized relative time span ("Now", "8 hr. ago", "Yesterday", "3 days ago", "Jul 10, 1940") 54 | * We're using a call to `DateUtils.getRelativeTimeSpanString` in most cases. 55 | */ 56 | public static String javaDateToTimeSpan(final Date date, Context context) { 57 | return javaDateToTimeSpan(date, context, System.currentTimeMillis()); 58 | } 59 | 60 | /** 61 | * Given an ISO 8601-formatted date as a String, returns a {@link Date}. 62 | */ 63 | public static Date dateFromIso8601(final String strDate) { 64 | try { 65 | DateFormat formatter = ISO8601_FORMAT.get(); 66 | return formatter.parse(strDate); 67 | } catch (ParseException e) { 68 | return null; 69 | } 70 | } 71 | 72 | /** 73 | * Given an ISO 8601-formatted date as a String, returns a {@link Date} in UTC. 74 | */ 75 | public static Date dateUTCFromIso8601(String iso8601date) { 76 | try { 77 | iso8601date = iso8601date.replace("Z", "+0000").replace("+00:00", "+0000"); 78 | DateFormat formatter = ISO8601_FORMAT.get(); 79 | formatter.setTimeZone(TimeZone.getTimeZone("UTC")); 80 | return formatter.parse(iso8601date); 81 | } catch (ParseException e) { 82 | return null; 83 | } 84 | } 85 | 86 | /** 87 | * Given a {@link Date}, returns an ISO 8601-formatted String. 88 | */ 89 | public static String iso8601FromDate(Date date) { 90 | if (date == null) { 91 | return ""; 92 | } 93 | DateFormat formatter = ISO8601_FORMAT.get(); 94 | return formatter.format(date); 95 | } 96 | 97 | /** 98 | * Given a {@link Date}, returns an ISO 8601-formatted String in UTC. 99 | */ 100 | public static String iso8601UTCFromDate(Date date) { 101 | if (date == null) { 102 | return ""; 103 | } 104 | TimeZone tz = TimeZone.getTimeZone("UTC"); 105 | DateFormat formatter = ISO8601_FORMAT.get(); 106 | formatter.setTimeZone(tz); 107 | 108 | String iso8601date = formatter.format(date); 109 | 110 | // Use "+00:00" notation rather than "+0000" to be consistent with the WP.COM API 111 | return iso8601date.replace("+0000", "+00:00"); 112 | } 113 | 114 | /** 115 | * Returns the current UTC date. 116 | * 117 | * @deprecated This method doesn't work as expected and shouldn't be used in production code. It doesn't take 118 | * into account that `Date` class uses TimeZone.getDefault(). It substracts the currentOffsetFromUTC, but the 119 | * final date still uses system default timezone. 120 | */ 121 | @Deprecated 122 | public static Date nowUTC() { 123 | Date dateTimeNow = new Date(); 124 | return localDateToUTC(dateTimeNow); 125 | } 126 | 127 | /** 128 | * 129 | * @deprecated This method doesn't work as expected and shouldn't be used in production code. It doesn't take 130 | * into account that `Date` class uses TimeZone.getDefault(). It substracts the currentOffsetFromUTC, but the 131 | * final date still uses system default timezone. 132 | */ 133 | @Deprecated 134 | public static Date localDateToUTC(Date dtLocal) { 135 | if (dtLocal == null) { 136 | return null; 137 | } 138 | TimeZone tz = TimeZone.getDefault(); 139 | int currentOffsetFromUTC = tz.getRawOffset() + (tz.inDaylightTime(dtLocal) ? tz.getDSTSavings() : 0); 140 | return new Date(dtLocal.getTime() - currentOffsetFromUTC); 141 | } 142 | 143 | // Routines to return a diff between two dates - always return a positive number 144 | 145 | public static int daysBetween(Date dt1, Date dt2) { 146 | long hrDiff = hoursBetween(dt1, dt2); 147 | if (hrDiff == 0) { 148 | return 0; 149 | } 150 | return (int) (hrDiff / 24); 151 | } 152 | 153 | public static int hoursBetween(Date dt1, Date dt2) { 154 | long minDiff = minutesBetween(dt1, dt2); 155 | if (minDiff == 0) { 156 | return 0; 157 | } 158 | return (int) (minDiff / 60); 159 | } 160 | 161 | public static int minutesBetween(Date dt1, Date dt2) { 162 | long msDiff = millisecondsBetween(dt1, dt2); 163 | if (msDiff == 0) { 164 | return 0; 165 | } 166 | return (int) (msDiff / 60000); 167 | } 168 | 169 | public static int secondsBetween(Date dt1, Date dt2) { 170 | long msDiff = millisecondsBetween(dt1, dt2); 171 | if (msDiff == 0) { 172 | return 0; 173 | } 174 | return (int) (msDiff / 1000); 175 | } 176 | 177 | public static long millisecondsBetween(Date dt1, Date dt2) { 178 | if (dt1 == null || dt2 == null) { 179 | return 0; 180 | } 181 | return Math.abs(dt1.getTime() - dt2.getTime()); 182 | } 183 | 184 | public static boolean isSameYear(Date dt1, Date dt2) { 185 | if (dt1 == null || dt2 == null) { 186 | return false; 187 | } 188 | return dt1.getYear() == dt2.getYear(); 189 | } 190 | 191 | public static boolean isSameMonthAndYear(Date dt1, Date dt2) { 192 | if (dt1 == null || dt2 == null) { 193 | return false; 194 | } 195 | return dt1.getYear() == dt2.getYear() && dt1.getMonth() == dt2.getMonth(); 196 | } 197 | 198 | // Routines involving Unix timestamps (GMT assumed) 199 | 200 | /** 201 | * Given an ISO 8601-formatted date as a String, returns the corresponding UNIX timestamp. 202 | */ 203 | public static long timestampFromIso8601(final String strDate) { 204 | return timestampFromIso8601Millis(strDate) / 1000; 205 | } 206 | 207 | /** 208 | * Given an ISO 8601-formatted date as a String, returns the corresponding timestamp in milliseconds. 209 | * 210 | * @return 0 if the parameter is null, empty or not a date. 211 | */ 212 | public static long timestampFromIso8601Millis(final String strDate) { 213 | if (TextUtils.isEmpty(strDate)) { 214 | return 0; 215 | } 216 | Date date = dateFromIso8601(strDate); 217 | if (date == null) { 218 | return 0; 219 | } 220 | return date.getTime(); 221 | } 222 | 223 | /** 224 | * Given a UNIX timestamp, returns the corresponding {@link Date}. 225 | */ 226 | public static Date dateFromTimestamp(long timestamp) { 227 | return new java.util.Date(timestamp * 1000); 228 | } 229 | 230 | /** 231 | * Given a UNIX timestamp, returns an ISO 8601-formatted date as a String. 232 | */ 233 | public static String iso8601FromTimestamp(long timestamp) { 234 | return iso8601FromDate(dateFromTimestamp(timestamp)); 235 | } 236 | 237 | /** 238 | * Given a UNIX timestamp, returns an ISO 8601-formatted date in UTC as a String. 239 | */ 240 | public static String iso8601UTCFromTimestamp(long timestamp) { 241 | return iso8601UTCFromDate(dateFromTimestamp(timestamp)); 242 | } 243 | 244 | /** 245 | * Given a UNIX timestamp, returns a relative time span ("8h", "3d", etc.). 246 | */ 247 | public static String timeSpanFromTimestamp(long timestamp, Context context) { 248 | Date dateGMT = dateFromTimestamp(timestamp); 249 | return javaDateToTimeSpan(dateGMT, context); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/DeviceUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.app.KeyguardManager; 4 | import android.content.Context; 5 | import android.content.pm.PackageManager; 6 | import android.content.res.Configuration; 7 | import android.os.Build; 8 | import android.os.Environment; 9 | import android.os.StatFs; 10 | 11 | import androidx.annotation.NonNull; 12 | 13 | import org.wordpress.android.util.AppLog.T; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.util.Properties; 19 | 20 | public class DeviceUtils { 21 | private static final String APP_RUNTIME_ON_CHROME_FLAG = "org.chromium.arc.device_management"; 22 | 23 | private static DeviceUtils instance; 24 | private boolean mIsKindleFire = false; 25 | 26 | public boolean isKindleFire() { 27 | return mIsKindleFire; 28 | } 29 | 30 | public static DeviceUtils getInstance() { 31 | if (instance == null) { 32 | instance = new DeviceUtils(); 33 | } 34 | return instance; 35 | } 36 | 37 | private DeviceUtils() { 38 | mIsKindleFire = android.os.Build.MODEL.equalsIgnoreCase("kindle fire") ? true : false; 39 | } 40 | 41 | /** 42 | * Checks camera availability recursively based on API level. 43 | * 44 | * TODO: change "android.hardware.camera.front" and "android.hardware.camera.any" to 45 | * {@link PackageManager#FEATURE_CAMERA_FRONT} and {@link PackageManager#FEATURE_CAMERA_ANY}, 46 | * respectively, once they become accessible or minSdk version is incremented. 47 | * 48 | * @param context The context. 49 | * @return Whether camera is available. 50 | */ 51 | public boolean hasCamera(Context context) { 52 | final PackageManager pm = context.getPackageManager(); 53 | return pm.hasSystemFeature("android.hardware.camera.any"); 54 | } 55 | 56 | public String getDeviceName(Context context) { 57 | String manufacturer = Build.MANUFACTURER; 58 | String undecodedModel = Build.MODEL; 59 | String model = null; 60 | 61 | try { 62 | Properties prop = new Properties(); 63 | InputStream fileStream; 64 | // Read the device name from a precomplied list: 65 | // see http://making.meetup.com/post/29648976176/human-readble-android-device-names 66 | fileStream = context.getAssets().open("android_models.properties"); 67 | prop.load(fileStream); 68 | fileStream.close(); 69 | String decodedModel = prop.getProperty(undecodedModel.replaceAll(" ", "_")); 70 | if (decodedModel != null && !decodedModel.trim().equals("")) { 71 | model = decodedModel; 72 | } 73 | } catch (IOException e) { 74 | AppLog.e(T.UTILS, "Can't read `android_models.properties` file from assets, or it's in the wrong form.", e); 75 | AppLog.d(T.UTILS, 76 | "If you need more info about the file, please check the reference implementation available here: " 77 | + "https://github.com/wordpress-mobile/WordPress-Android/blob/dd989429bd701a66bcba911de08f2e8d336798ef" 78 | + "/WordPress/src/main/assets/android_models.properties"); 79 | } 80 | 81 | if (model == null) { // Device model not found in the list 82 | if (undecodedModel.startsWith(manufacturer)) { 83 | model = capitalize(undecodedModel); 84 | } else { 85 | model = capitalize(manufacturer) + " " + undecodedModel; 86 | } 87 | } 88 | return model; 89 | } 90 | 91 | public boolean isDeviceLocked(Context context) { 92 | KeyguardManager myKM = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); 93 | return myKM.inKeyguardRestrictedInputMode(); 94 | } 95 | 96 | /** 97 | * Checks if the current device runtime is ARC which effectively means it is a chromebook. 98 | * 99 | * @param context The context. 100 | * @return Whether the device is a chromebook. 101 | */ 102 | public boolean isChromebook(Context context) { 103 | return context.getPackageManager().hasSystemFeature(APP_RUNTIME_ON_CHROME_FLAG); 104 | } 105 | 106 | /** 107 | * Checks if the device has a hardware keyboard - note this will return true for emulators 108 | */ 109 | public boolean hasHardwareKeyboard(@NonNull Context context) { 110 | return context.getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS; 111 | } 112 | 113 | private String capitalize(String s) { 114 | if (s == null || s.length() == 0) { 115 | return ""; 116 | } 117 | char first = s.charAt(0); 118 | if (Character.isUpperCase(first)) { 119 | return s; 120 | } else { 121 | return Character.toUpperCase(first) + s.substring(1); 122 | } 123 | } 124 | 125 | // Taken and modified from https://stackoverflow.com/a/8133437 126 | public static String getTotalAvailableMemorySize() { 127 | File internalMemoryPath = Environment.getDataDirectory(); 128 | long availableInternal = availableSpaceAtFilePath(internalMemoryPath); 129 | long availableExternal = 0L; 130 | if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) { 131 | File externalStoragePath = Environment.getExternalStorageDirectory(); 132 | availableExternal = availableSpaceAtFilePath(externalStoragePath); 133 | } 134 | return formatSize(availableInternal + availableExternal); 135 | } 136 | 137 | private static long availableSpaceAtFilePath(File path) { 138 | StatFs stat = new StatFs(path.getPath()); 139 | return stat.getBlockSizeLong() * stat.getAvailableBlocksLong(); 140 | } 141 | 142 | private static String formatSize(long size) { 143 | String suffix = null; 144 | 145 | if (size >= 1024) { 146 | suffix = "KB"; 147 | size /= 1024; 148 | if (size >= 1024) { 149 | suffix = "MB"; 150 | size /= 1024; 151 | } 152 | } 153 | 154 | StringBuilder resultBuffer = new StringBuilder(Long.toString(size)); 155 | 156 | int commaOffset = resultBuffer.length() - 3; 157 | while (commaOffset > 0) { 158 | resultBuffer.insert(commaOffset, ','); 159 | commaOffset -= 3; 160 | } 161 | 162 | if (suffix != null) { 163 | resultBuffer.append(suffix); 164 | } 165 | return resultBuffer.toString(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/DisplayUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | import android.content.res.Configuration; 5 | import android.content.res.Resources; 6 | import android.graphics.Rect; 7 | import android.os.Build; 8 | import android.util.DisplayMetrics; 9 | import android.util.TypedValue; 10 | import android.view.Display; 11 | import android.view.Window; 12 | import android.view.WindowManager; 13 | 14 | import androidx.annotation.NonNull; 15 | 16 | public class DisplayUtils { 17 | private DisplayUtils() { 18 | throw new AssertionError(); 19 | } 20 | 21 | public static boolean isLandscape(Context context) { 22 | if (context == null) { 23 | return false; 24 | } 25 | return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; 26 | } 27 | 28 | /** 29 | * Calculates the width of the application's window 30 | * @param context 31 | * @return the width of the window 32 | */ 33 | public static int getWindowPixelWidth(@NonNull Context context) { 34 | return getWindowSize(context).width(); 35 | } 36 | 37 | /** 38 | * Calculates the height of the application's window 39 | * @param context 40 | * @return the height of the window 41 | */ 42 | public static int getWindowPixelHeight(@NonNull Context context) { 43 | return getWindowSize(context).height(); 44 | } 45 | 46 | public static Rect getWindowSize(Context context) { 47 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 48 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 49 | return wm.getCurrentWindowMetrics().getBounds(); 50 | } else { 51 | Display display = wm.getDefaultDisplay(); 52 | Rect rect = new Rect(); 53 | display.getRectSize(rect); 54 | return rect; 55 | } 56 | } 57 | 58 | /** 59 | * @return the width of the device's screen 60 | */ 61 | public static int getDisplayPixelWidth() { 62 | return Resources.getSystem().getDisplayMetrics().widthPixels; 63 | } 64 | 65 | public static float spToPx(Context context, float sp) { 66 | DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); 67 | final float scale = displayMetrics.scaledDensity; 68 | return sp * scale; 69 | } 70 | 71 | public static int dpToPx(Context context, int dp) { 72 | float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, 73 | context.getResources().getDisplayMetrics()); 74 | return (int) px; 75 | } 76 | 77 | public static int pxToDp(Context context, int px) { 78 | DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); 79 | return (int) ((px / displayMetrics.density) + 0.5); 80 | } 81 | 82 | public static boolean isXLargeTablet(Context context) { 83 | if ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) 84 | == Configuration.SCREENLAYOUT_SIZE_XLARGE) { 85 | return true; 86 | } 87 | return false; 88 | } 89 | 90 | public static boolean isTablet(Context context) { 91 | return (context.getResources().getConfiguration().screenLayout 92 | & Configuration.SCREENLAYOUT_SIZE_MASK) 93 | == Configuration.SCREENLAYOUT_SIZE_LARGE; 94 | } 95 | 96 | /** 97 | * returns the height of the ActionBar if one is enabled - supports both the native ActionBar 98 | * and ActionBarSherlock - http://stackoverflow.com/a/15476793/1673548 99 | */ 100 | public static int getActionBarHeight(Context context) { 101 | if (context == null) { 102 | return 0; 103 | } 104 | TypedValue tv = new TypedValue(); 105 | if (context.getTheme() != null 106 | && context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { 107 | return TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics()); 108 | } 109 | 110 | // if we get this far, it's because the device doesn't support an ActionBar, 111 | // so return the standard ActionBar height (48dp) 112 | return dpToPx(context, 48); 113 | } 114 | 115 | /** 116 | * detect when FEATURE_ACTION_BAR_OVERLAY has been set 117 | */ 118 | public static boolean hasActionBarOverlay(Window window) { 119 | return window.hasFeature(Window.FEATURE_ACTION_BAR_OVERLAY); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/EditTextUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.text.TextUtils; 6 | import android.view.View; 7 | import android.view.inputmethod.InputMethodManager; 8 | import android.widget.EditText; 9 | import android.widget.TextView; 10 | 11 | /** 12 | * EditText utils 13 | */ 14 | public class EditTextUtils { 15 | private EditTextUtils() { 16 | throw new AssertionError(); 17 | } 18 | 19 | /** 20 | * returns non-null text string from passed TextView 21 | */ 22 | public static String getText(TextView textView) { 23 | return (textView != null) ? textView.getText().toString() : ""; 24 | } 25 | 26 | /** 27 | * moves caret to end of text 28 | */ 29 | public static void moveToEnd(EditText edit) { 30 | if (edit.getText() == null) { 31 | return; 32 | } 33 | edit.setSelection(edit.getText().toString().length()); 34 | } 35 | 36 | /** 37 | * returns true if nothing has been entered into passed editor 38 | */ 39 | public static boolean isEmpty(EditText edit) { 40 | return TextUtils.isEmpty(getText(edit)); 41 | } 42 | 43 | /** 44 | * hide the soft keyboard for the passed EditText 45 | * 46 | * @deprecated Use {@link ActivityUtils#hideKeyboard(Activity)} or {@link ActivityUtils#hideKeyboardForced(View)} 47 | * instead. 48 | */ 49 | // TODO: Replace instances with ActivityUtils#showKeyboard(Activity) or ActivityUtils#showKeyboardForced(View) to 50 | // consolidate similar methods and favor library version. 51 | @Deprecated 52 | public static void hideSoftInput(EditText edit) { 53 | if (edit == null) { 54 | return; 55 | } 56 | 57 | InputMethodManager imm = getInputMethodManager(edit); 58 | if (imm != null) { 59 | imm.hideSoftInputFromWindow(edit.getWindowToken(), 0); 60 | } 61 | } 62 | 63 | /** 64 | * show the soft keyboard for the passed EditText 65 | * 66 | * @deprecated Use {@link ActivityUtils#showKeyboard(View)} instead. 67 | */ 68 | // TODO: Replace instances with ActivityUtils#showKeyboard(View) to consolidate similar methods and favor library 69 | // version. 70 | @Deprecated 71 | public static void showSoftInput(EditText edit) { 72 | if (edit == null) { 73 | return; 74 | } 75 | 76 | edit.requestFocus(); 77 | 78 | InputMethodManager imm = getInputMethodManager(edit); 79 | if (imm != null) { 80 | imm.showSoftInput(edit, InputMethodManager.SHOW_IMPLICIT); 81 | } 82 | } 83 | 84 | private static InputMethodManager getInputMethodManager(EditText edit) { 85 | Context context = edit.getContext(); 86 | return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/EmoticonsUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.text.Html; 4 | import android.text.SpannableStringBuilder; 5 | import android.text.Spanned; 6 | import android.text.style.ForegroundColorSpan; 7 | import android.text.style.ImageSpan; 8 | import android.util.SparseArray; 9 | 10 | import java.util.Collections; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class EmoticonsUtils { 15 | public static final int EMOTICON_COLOR = 0xFF21759B; 16 | private static final Map WP_SMILIES; 17 | public static final SparseArray WP_SMILIES_CODE_POINT_TO_TEXT; 18 | 19 | static { 20 | Map smilies = new HashMap<>(); 21 | smilies.put("icon_mrgreen.gif", "\uD83D\uDE00"); 22 | smilies.put("icon_neutral.gif", "\uD83D\uDE14"); 23 | smilies.put("icon_twisted.gif", "\uD83D\uDE16"); 24 | smilies.put("icon_arrow.gif", "\u27A1"); 25 | smilies.put("icon_eek.gif", "\uD83D\uDE32"); 26 | smilies.put("icon_smile.gif", "\uD83D\uDE0A"); 27 | smilies.put("icon_confused.gif", "\uD83D\uDE15"); 28 | smilies.put("icon_cool.gif", "\uD83D\uDE0A"); 29 | smilies.put("icon_evil.gif", "\uD83D\uDE21"); 30 | smilies.put("icon_biggrin.gif", "\uD83D\uDE03"); 31 | smilies.put("icon_idea.gif", "\uD83D\uDCA1"); 32 | smilies.put("icon_redface.gif", "\uD83D\uDE33"); 33 | smilies.put("icon_razz.gif", "\uD83D\uDE1D"); 34 | smilies.put("icon_rolleyes.gif", "\uD83D\uDE0F"); 35 | smilies.put("icon_wink.gif", "\uD83D\uDE09"); 36 | smilies.put("icon_cry.gif", "\uD83D\uDE22"); 37 | smilies.put("icon_surprised.gif", "\uD83D\uDE32"); 38 | smilies.put("icon_lol.gif", "\uD83D\uDE03"); 39 | smilies.put("icon_mad.gif", "\uD83D\uDE21"); 40 | smilies.put("icon_sad.gif", "\uD83D\uDE1E"); 41 | smilies.put("icon_exclaim.gif", "\u2757"); 42 | smilies.put("icon_question.gif", "\u2753"); 43 | 44 | WP_SMILIES = Collections.unmodifiableMap(smilies); 45 | 46 | WP_SMILIES_CODE_POINT_TO_TEXT = new SparseArray<>(20); 47 | WP_SMILIES_CODE_POINT_TO_TEXT.put(10145, ":arrow:"); 48 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128161, ":idea:"); 49 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128512, ":mrgreen:"); 50 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128515, ":D"); 51 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128522, ":)"); 52 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128521, ";)"); 53 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128532, ":|"); 54 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128533, ":?"); 55 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128534, ":twisted:"); 56 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128542, ":("); 57 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128545, ":evil:"); 58 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128546, ":'("); 59 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128562, ":o"); 60 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128563, ":oops:"); 61 | WP_SMILIES_CODE_POINT_TO_TEXT.put(128527, ":roll:"); 62 | WP_SMILIES_CODE_POINT_TO_TEXT.put(10071, ":!:"); 63 | WP_SMILIES_CODE_POINT_TO_TEXT.put(10067, ":?:"); 64 | } 65 | 66 | public static String lookupImageSmiley(String url) { 67 | return lookupImageSmiley(url, ""); 68 | } 69 | 70 | public static String lookupImageSmiley(String url, String ifNone) { 71 | if (url == null) { 72 | return ifNone; 73 | } 74 | String file = url.substring(url.lastIndexOf("/") + 1); 75 | if (WP_SMILIES.containsKey(file)) { 76 | return WP_SMILIES.get(file); 77 | } 78 | return ifNone; 79 | } 80 | 81 | public static Spanned replaceEmoticonsWithEmoji(SpannableStringBuilder html) { 82 | ImageSpan[] imgs = html.getSpans(0, html.length(), ImageSpan.class); 83 | for (ImageSpan img : imgs) { 84 | String emoticon = EmoticonsUtils.lookupImageSmiley(img.getSource()); 85 | if (!emoticon.equals("")) { 86 | int start = html.getSpanStart(img); 87 | html.replace(start, html.getSpanEnd(img), emoticon); 88 | html.setSpan(new ForegroundColorSpan(EMOTICON_COLOR), start, 89 | start + emoticon.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 90 | html.removeSpan(img); 91 | } 92 | } 93 | return html; 94 | } 95 | 96 | public static String replaceEmoticonsWithEmoji(final String text) { 97 | if (text != null && text.contains("icon_")) { 98 | final SpannableStringBuilder html = 99 | (SpannableStringBuilder) replaceEmoticonsWithEmoji((SpannableStringBuilder) Html.fromHtml(text)); 100 | // Html.toHtml() is used here rather than toString() since the latter strips html 101 | return Html.toHtml(html); 102 | } else { 103 | return text; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/FileUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.io.File; 6 | 7 | public class FileUtils { 8 | /** 9 | * Returns the length of the file denoted by this abstract pathname. 10 | * The return value is unspecified if this pathname denotes a directory. 11 | * 12 | * @return The length, in bytes, of the file denoted by this abstract 13 | * pathname, or -1L if the file does not exist, or an 14 | * exception is thrown accessing the file. 15 | * Some operating systems may return 0L for pathnames 16 | * denoting system-dependent entities such as devices or pipes. 17 | */ 18 | public static long length(String path) { 19 | // File not found 20 | File file = new File(path); 21 | try { 22 | if (!file.exists()) { 23 | AppLog.w(AppLog.T.MEDIA, "Can't access the file. It doesn't exists anymore?"); 24 | return -1L; 25 | } 26 | 27 | return file.length(); 28 | } catch (SecurityException e) { 29 | AppLog.e(AppLog.T.MEDIA, "Can't access the file.", e); 30 | return -1L; 31 | } 32 | } 33 | 34 | /** 35 | * Given the full file path, or the filename with extension (i.e. my-picture.jpg), returns the filename part only 36 | * (my-picture). 37 | * 38 | * @param filePath The path to the file or the full filename 39 | * @return filename part only or null 40 | */ 41 | public static String getFileNameFromPath(String filePath) { 42 | if (TextUtils.isEmpty(filePath)) { 43 | return null; 44 | } 45 | if (filePath.contains("/")) { 46 | if (filePath.lastIndexOf("/") + 1 >= filePath.length()) { 47 | filePath = filePath.substring(0, filePath.length() - 1); 48 | } 49 | filePath = filePath.substring(filePath.lastIndexOf("/") + 1); 50 | } 51 | 52 | String filename; 53 | int dotPos = filePath.indexOf('.'); 54 | if (dotPos > 0) { 55 | filename = filePath.substring(0, dotPos); 56 | } else { 57 | filename = filePath; 58 | } 59 | return filename; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/FormatUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import java.text.DecimalFormat; 4 | import java.text.NumberFormat; 5 | 6 | public class FormatUtils { 7 | /* 8 | * NumberFormat isn't synchronized, so a separate instance must be created for each thread 9 | * http://developer.android.com/reference/java/text/NumberFormat.html 10 | */ 11 | private static final ThreadLocal INTEGER_INSTANCE = new ThreadLocal() { 12 | @Override 13 | protected NumberFormat initialValue() { 14 | return NumberFormat.getIntegerInstance(); 15 | } 16 | }; 17 | 18 | private static final ThreadLocal DECIMAL_INSTANCE = new ThreadLocal() { 19 | @Override 20 | protected DecimalFormat initialValue() { 21 | return (DecimalFormat) DecimalFormat.getInstance(); 22 | } 23 | }; 24 | 25 | /* 26 | * returns the passed integer formatted with thousands-separators based on the current locale 27 | */ 28 | public static final String formatInt(int value) { 29 | return INTEGER_INSTANCE.get().format(value).toString(); 30 | } 31 | 32 | public static final String formatDecimal(int value) { 33 | return DECIMAL_INSTANCE.get().format(value).toString(); 34 | } 35 | 36 | /* 37 | * returns the passed long formatted as an human readable filesize. Ex: 10 GB 38 | * unitStrings is expected to be an array of all possible sizes from byte to TeraByte, in the current locale 39 | */ 40 | public static final String formatFileSize(long size, final String[] unitStrings) { 41 | final double log1024 = Math.log10(1024); 42 | if (size <= 0) { 43 | return "0"; 44 | } 45 | int digitGroups = (int) (Math.log10(size) / log1024); 46 | 47 | NumberFormat f = NumberFormat.getInstance(); 48 | if (f instanceof DecimalFormat) { 49 | ((DecimalFormat) f).applyPattern("#,##0.#"); 50 | } 51 | return String.format(unitStrings[digitGroups], f.format(size / Math.pow(1024, digitGroups))); 52 | } 53 | 54 | /* 55 | * returns the passed double percentage (0 to 1) formatted as an human readable percentage. Ex: 0.25 returns 25% 56 | */ 57 | public static final String formatPercentage(double value) { 58 | return formatPercentageLimit100(value, false); 59 | } 60 | 61 | /* 62 | * returns the passed double percentage (0 to 1) formatted as an human readable percentage. Ex: 0.251 returns 25.1% 63 | * if limit100 is true, it limits the percentage to 100% 64 | */ 65 | public static final String formatPercentageLimit100(double value, boolean limit100) { 66 | double limit = 1.0001; 67 | 68 | NumberFormat percentFormat = NumberFormat.getPercentInstance(); 69 | percentFormat.setMaximumFractionDigits(1); 70 | 71 | if (limit100 && value > limit) { 72 | value = limit; 73 | } 74 | 75 | String percentage = percentFormat.format(value); 76 | 77 | return percentage; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/GeocoderUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | import android.location.Address; 5 | import android.location.Geocoder; 6 | 7 | import java.io.IOException; 8 | import java.util.List; 9 | 10 | public final class GeocoderUtils { 11 | private GeocoderUtils() { 12 | throw new AssertionError(); 13 | } 14 | 15 | public static Geocoder getGeocoder(Context context) { 16 | // first make sure a Geocoder service exists on this device (requires API 9) 17 | if (!Geocoder.isPresent()) { 18 | return null; 19 | } 20 | 21 | Geocoder gcd; 22 | 23 | try { 24 | gcd = new Geocoder(context, LanguageUtils.getCurrentDeviceLanguage(context)); 25 | } catch (NullPointerException cannotIstantiateEx) { 26 | AppLog.e(AppLog.T.UTILS, "Cannot instantiate Geocoder", cannotIstantiateEx); 27 | return null; 28 | } 29 | 30 | return gcd; 31 | } 32 | 33 | public static Address getAddressFromCoords(Context context, double latitude, double longitude) { 34 | Address address = null; 35 | List
addresses = null; 36 | 37 | Geocoder gcd = getGeocoder(context); 38 | 39 | if (gcd == null) { 40 | return null; 41 | } 42 | 43 | try { 44 | addresses = gcd.getFromLocation(latitude, longitude, 1); 45 | } catch (IOException e) { 46 | // may get "Unable to parse response from server" IOException here if Geocoder 47 | // service is hit too frequently 48 | AppLog.e(AppLog.T.UTILS, 49 | "Unable to parse response from server. Is Geocoder service hitting the server too frequently?", 50 | e 51 | ); 52 | } 53 | 54 | // addresses may be null or empty if network isn't connected 55 | if (addresses != null && addresses.size() > 0) { 56 | address = addresses.get(0); 57 | } 58 | 59 | return address; 60 | } 61 | 62 | public static Address getAddressFromLocationName(Context context, String locationName) { 63 | int maxResults = 1; 64 | Address address = null; 65 | List
addresses = null; 66 | 67 | Geocoder gcd = getGeocoder(context); 68 | 69 | if (gcd == null) { 70 | return null; 71 | } 72 | 73 | try { 74 | addresses = gcd.getFromLocationName(locationName, maxResults); 75 | } catch (IOException e) { 76 | AppLog.e(AppLog.T.UTILS, "Failed to get coordinates from location", e); 77 | } 78 | 79 | // addresses may be null or empty if network isn't connected 80 | if (addresses != null && addresses.size() > 0) { 81 | address = addresses.get(0); 82 | } 83 | 84 | return address; 85 | } 86 | 87 | public static String getLocationNameFromAddress(Address address) { 88 | String locality = "", adminArea = "", country = ""; 89 | if (address.getLocality() != null) { 90 | locality = address.getLocality(); 91 | } 92 | 93 | if (address.getAdminArea() != null) { 94 | adminArea = address.getAdminArea(); 95 | } 96 | 97 | if (address.getCountryName() != null) { 98 | country = address.getCountryName(); 99 | } 100 | 101 | return ((locality.equals("")) ? locality : locality + ", ") 102 | + ((adminArea.equals("")) ? adminArea : adminArea + " ") + country; 103 | } 104 | 105 | public static double[] getCoordsFromAddress(Address address) { 106 | double[] coordinates = new double[2]; 107 | 108 | if (address.hasLatitude() && address.hasLongitude()) { 109 | coordinates[0] = address.getLatitude(); 110 | coordinates[1] = address.getLongitude(); 111 | } 112 | 113 | return coordinates; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/GravatarUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.text.TextUtils; 4 | 5 | /** 6 | * see https://en.gravatar.com/site/implement/images/ 7 | */ 8 | public class GravatarUtils { 9 | // by default tell gravatar to respond to non-existent images with a 404 - this means 10 | // it's up to the caller to catch the 404 and provide a suitable default image 11 | private static final DefaultImage DEFAULT_GRAVATAR = DefaultImage.MYSTERY_MAN; 12 | 13 | public enum DefaultImage { 14 | MYSTERY_MAN, 15 | STATUS_404, 16 | IDENTICON, 17 | MONSTER, 18 | WAVATAR, 19 | RETRO, 20 | BLANK; 21 | 22 | @Override 23 | public String toString() { 24 | switch (this) { 25 | case MYSTERY_MAN: 26 | return "mm"; 27 | case STATUS_404: 28 | return "404"; 29 | case IDENTICON: 30 | return "identicon"; 31 | case MONSTER: 32 | return "monsterid"; 33 | case WAVATAR: 34 | return "wavatar"; 35 | case RETRO: 36 | return "retro"; 37 | default: 38 | return "blank"; 39 | } 40 | } 41 | } 42 | 43 | /* 44 | * gravatars often contain the ?s= parameter which determines their size - detect this and 45 | * replace it with a new ?s= parameter which requests the avatar at the exact size needed 46 | */ 47 | public static String fixGravatarUrl(final String imageUrl, int avatarSz) { 48 | return fixGravatarUrl(imageUrl, avatarSz, DEFAULT_GRAVATAR); 49 | } 50 | 51 | public static String fixGravatarUrl(final String imageUrl, int avatarSz, DefaultImage defaultImage) { 52 | if (TextUtils.isEmpty(imageUrl)) { 53 | return ""; 54 | } 55 | 56 | // if this isn't a gravatar image, return as resized photon image url 57 | if (!imageUrl.contains("gravatar.com")) { 58 | return PhotonUtils.getPhotonImageUrl(imageUrl, avatarSz, avatarSz); 59 | } 60 | 61 | // remove all other params, then add query string for size and default image 62 | return UrlUtils.removeQuery(imageUrl) + "?s=" + avatarSz + "&d=" + defaultImage.toString(); 63 | } 64 | 65 | public static String gravatarFromEmail(final String email, int size) { 66 | return gravatarFromEmail(email, size, DEFAULT_GRAVATAR); 67 | } 68 | 69 | public static String gravatarFromEmail(final String email, int size, DefaultImage defaultImage) { 70 | return "http://gravatar.com/avatar/" 71 | + StringUtils.getSha256Hash(StringUtils.notNullStr(email)) 72 | + "?d=" + defaultImage.toString() 73 | + "&size=" + size; 74 | } 75 | 76 | public static String blavatarFromUrl(final String url, int size) { 77 | return blavatarFromUrl(url, size, DEFAULT_GRAVATAR); 78 | } 79 | 80 | public static String blavatarFromUrl(final String url, int size, DefaultImage defaultImage) { 81 | return "http://gravatar.com/blavatar/" 82 | + StringUtils.getSha256Hash(UrlUtils.getHost(url)) 83 | + "?d=" + defaultImage.toString() 84 | + "&size=" + size; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/HtmlUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.text.Html; 6 | import android.text.Html.ImageGetter; 7 | import android.text.SpannableStringBuilder; 8 | import android.text.Spanned; 9 | import android.text.TextUtils; 10 | import android.text.style.ForegroundColorSpan; 11 | import android.text.style.QuoteSpan; 12 | 13 | import org.apache.commons.text.StringEscapeUtils; 14 | import org.wordpress.android.util.helpers.WPHtmlTagHandler; 15 | import org.wordpress.android.util.helpers.WPQuoteSpan; 16 | 17 | import static org.wordpress.android.util.AppLog.T.UTILS; 18 | 19 | public class HtmlUtils { 20 | /** 21 | * Removes html from the passed string - relies on Html.fromHtml which handles invalid HTML, 22 | * but it's very slow, so avoid using this where performance is important 23 | * @param text String containing html 24 | * @return String without HTML 25 | */ 26 | public static String stripHtml(final String text) { 27 | if (TextUtils.isEmpty(text)) { 28 | return ""; 29 | } 30 | return Html.fromHtml(text).toString().trim(); 31 | } 32 | 33 | /** 34 | * This is much faster than stripHtml() but should only be used when we know the html is valid 35 | * since the regex will be unpredictable with invalid html 36 | * @param str String containing only valid html 37 | * @return String without HTML 38 | */ 39 | public static String fastStripHtml(String str) { 40 | if (TextUtils.isEmpty(str)) { 41 | return str; 42 | } 43 | 44 | // insert a line break before P tags unless the only one is at the start 45 | if (str.lastIndexOf(" 0) { 46 | str = str.replaceAll("", "\n

"); 47 | } 48 | 49 | // convert BR tags to line breaks 50 | if (str.contains("", "\n"); 52 | } 53 | 54 | // use regex to strip tags, then convert entities in the result 55 | return trimStart(StringEscapeUtils.unescapeHtml4(str.replaceAll("<(.|\n)*?>", ""))); 56 | } 57 | 58 | /* 59 | * Same as apache.commons.lang.StringUtils.stripStart() but also removes non-breaking 60 | * space (160) chars 61 | */ 62 | private static String trimStart(final String str) { 63 | int strLen; 64 | if (str == null || (strLen = str.length()) == 0) { 65 | return ""; 66 | } 67 | int start = 0; 68 | while (start != strLen && (Character.isWhitespace(str.charAt(start)) || str.charAt(start) == 160)) { 69 | start++; 70 | } 71 | return str.substring(start); 72 | } 73 | 74 | /** 75 | * Converts an R.color.xxx resource to an HTML hex color 76 | * @param context Android Context 77 | * @param resId Android R.color.xxx 78 | * @return A String HTML hex color code 79 | */ 80 | public static String colorResToHtmlColor(Context context, int resId) { 81 | try { 82 | return String.format("#%06X", 0xFFFFFF & context.getResources().getColor(resId)); 83 | } catch (Resources.NotFoundException e) { 84 | return "#000000"; 85 | } 86 | } 87 | 88 | /** 89 | * Remove {@code } blocks from the passed string - added to project after noticing 90 | * comments on posts that use the "Sociable" plugin ( http://wordpress.org/plugins/sociable/ ) 91 | * may have a script block which contains {@code } followed by a CDATA section followed by {@code ,} 92 | * all of which will show up if we don't strip it here. 93 | * @see Wordpress Sociable Plugin 94 | * @return String without {@code }, {@code } blocks followed by a CDATA section 95 | * followed by {@code ,} 96 | * @param text String containing script tags 97 | */ 98 | public static String stripScript(final String text) { 99 | if (text == null) { 100 | return null; 101 | } 102 | 103 | StringBuilder sb = new StringBuilder(text); 104 | int start = sb.indexOf(" -1) { 107 | int end = sb.indexOf("", start); 108 | if (end == -1) { 109 | return sb.toString(); 110 | } 111 | sb.delete(start, end + 9); 112 | start = sb.indexOf("}, {@code

    }, {@code
    } 120 | * tags and replacing EmoticonsUtils with Emojis 121 | * @param source 122 | * @param imageGetter 123 | */ 124 | public static SpannableStringBuilder fromHtml(String source, ImageGetter imageGetter) { 125 | source = replaceListTagsWithCustomTags(source); 126 | SpannableStringBuilder html; 127 | try { 128 | html = (SpannableStringBuilder) Html.fromHtml(source, imageGetter, new WPHtmlTagHandler()); 129 | } catch (RuntimeException runtimeException) { 130 | // In case our tag handler fails 131 | try { 132 | html = (SpannableStringBuilder) Html.fromHtml(source, imageGetter, null); 133 | } catch (IllegalArgumentException illegalArgumentException) { 134 | // In case the html is missing a required parameter (for example: "src" missing from img) 135 | html = new SpannableStringBuilder(""); 136 | AppLog.w(UTILS, "Could not parse html"); 137 | } 138 | } 139 | 140 | EmoticonsUtils.replaceEmoticonsWithEmoji(html); 141 | QuoteSpan[] spans = html.getSpans(0, html.length(), QuoteSpan.class); 142 | for (QuoteSpan span : spans) { 143 | html.setSpan(new WPQuoteSpan(), html.getSpanStart(span), html.getSpanEnd(span), html.getSpanFlags(span)); 144 | html.setSpan(new ForegroundColorSpan(0xFF666666), html.getSpanStart(span), html.getSpanEnd(span), 145 | html.getSpanFlags(span)); 146 | html.removeSpan(span); 147 | } 148 | return html; 149 | } 150 | 151 | private static String replaceListTagsWithCustomTags(String source) { 152 | return source.replace("", "") 154 | .replace("", "") 156 | .replace("", ""); 158 | } 159 | 160 | public static Spanned fromHtml(String source) { 161 | return fromHtml(source, null); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/LanguageUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.Nullable; 6 | 7 | import java.util.Locale; 8 | 9 | /** 10 | * Methods for dealing with i18n messages 11 | */ 12 | public class LanguageUtils { 13 | /** 14 | * @deprecated Use {@link #getCurrentDeviceLanguage()}. As of API 25, setting the locale by updating the 15 | * configuration on the resources object was deprecated, so this method stopped working for newer versions 16 | * of Android. The current active locale should always be set in {@link Locale#getDefault()}. When manually 17 | * setting the active locale, the developer should set it in {@link Locale#setDefault(Locale)}. 18 | */ 19 | @SuppressWarnings("DeprecatedIsStillUsed") 20 | @Deprecated 21 | public static Locale getCurrentDeviceLanguage(@Nullable Context context) { 22 | return getCurrentDeviceLanguage(); 23 | } 24 | 25 | @SuppressWarnings("WeakerAccess") 26 | public static Locale getCurrentDeviceLanguage() { 27 | return Locale.getDefault(); 28 | } 29 | 30 | /** 31 | * @deprecated Use {@link #getCurrentDeviceLanguageCode()}. 32 | */ 33 | @SuppressWarnings("WeakerAccess,DeprecatedIsStillUsed") 34 | @Deprecated 35 | public static String getCurrentDeviceLanguageCode(@Nullable Context context) { 36 | return getCurrentDeviceLanguageCode(); 37 | } 38 | 39 | @SuppressWarnings("WeakerAccess") 40 | public static String getCurrentDeviceLanguageCode() { 41 | return getCurrentDeviceLanguage().toString(); 42 | } 43 | 44 | public static String getPatchedCurrentDeviceLanguage(Context context) { 45 | return patchDeviceLanguageCode(getCurrentDeviceLanguageCode(context)); 46 | } 47 | 48 | /** 49 | * Patches a deviceLanguageCode if any of deprecated values iw, id, or yi 50 | */ 51 | @SuppressWarnings("WeakerAccess") 52 | public static String patchDeviceLanguageCode(String deviceLanguageCode) { 53 | String patchedCode = deviceLanguageCode; 54 | /* 55 |

    Note that Java uses several deprecated two-letter codes. The Hebrew ("he") language 56 | * code is rewritten as "iw", Indonesian ("id") as "in", and Yiddish ("yi") as "ji". This 57 | * rewriting happens even if you construct your own {@code Locale} object, not just for 58 | * instances returned by the various lookup methods. 59 | */ 60 | if (deviceLanguageCode != null) { 61 | if (deviceLanguageCode.startsWith("iw")) { 62 | patchedCode = deviceLanguageCode.replace("iw", "he"); 63 | } else if (deviceLanguageCode.startsWith("in")) { 64 | patchedCode = deviceLanguageCode.replace("in", "id"); 65 | } else if (deviceLanguageCode.startsWith("ji")) { 66 | patchedCode = deviceLanguageCode.replace("ji", "yi"); 67 | } 68 | } 69 | 70 | return patchedCode; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/ListUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import org.apache.commons.lang3.ArrayUtils; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | public class ListUtils { 12 | @Nullable 13 | public static ArrayList fromLongArray(long[] array) { 14 | if (array == null) { 15 | return null; 16 | } 17 | Long[] longObjects = ArrayUtils.toObject(array); 18 | return new ArrayList<>(Arrays.asList(longObjects)); 19 | } 20 | 21 | @Nullable 22 | public static long[] toLongArray(List list) { 23 | if (list == null) { 24 | return null; 25 | } 26 | Long[] array = list.toArray(new Long[list.size()]); 27 | return ArrayUtils.toPrimitive(array); 28 | } 29 | 30 | @Nullable 31 | public static ArrayList fromIntArray(int[] array) { 32 | if (array == null) { 33 | return null; 34 | } 35 | Integer[] intObjects = ArrayUtils.toObject(array); 36 | return new ArrayList<>(Arrays.asList(intObjects)); 37 | } 38 | 39 | @Nullable 40 | public static int[] toIntArray(List list) { 41 | if (list == null) { 42 | return null; 43 | } 44 | Integer[] array = list.toArray(new Integer[list.size()]); 45 | return ArrayUtils.toPrimitive(array); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/MapUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import java.util.Date; 4 | import java.util.Map; 5 | 6 | /** 7 | * wrappers for extracting values from a Map object 8 | */ 9 | public class MapUtils { 10 | /* 11 | * returns a String value for the passed key in the passed map 12 | * always returns "" instead of null 13 | */ 14 | public static String getMapStr(final Map map, final String key) { 15 | if (map == null || key == null || !map.containsKey(key) || map.get(key) == null) { 16 | return ""; 17 | } 18 | return map.get(key).toString(); 19 | } 20 | 21 | /* 22 | * returns an int value for the passed key in the passed map 23 | * defaultValue is returned if key doesn't exist or isn't a number 24 | */ 25 | public static int getMapInt(final Map map, final String key) { 26 | return getMapInt(map, key, 0); 27 | } 28 | 29 | public static int getMapInt(final Map map, final String key, int defaultValue) { 30 | try { 31 | return Integer.parseInt(getMapStr(map, key)); 32 | } catch (NumberFormatException e) { 33 | return defaultValue; 34 | } 35 | } 36 | 37 | /* 38 | * long version of above 39 | */ 40 | public static long getMapLong(final Map map, final String key) { 41 | return getMapLong(map, key, 0); 42 | } 43 | 44 | public static long getMapLong(final Map map, final String key, long defaultValue) { 45 | try { 46 | return Long.parseLong(getMapStr(map, key)); 47 | } catch (NumberFormatException e) { 48 | return defaultValue; 49 | } 50 | } 51 | 52 | /* 53 | * float version of above 54 | */ 55 | public static float getMapFloat(final Map map, final String key) { 56 | return getMapFloat(map, key, 0); 57 | } 58 | 59 | public static float getMapFloat(final Map map, final String key, float defaultValue) { 60 | try { 61 | return Float.parseFloat(getMapStr(map, key)); 62 | } catch (NumberFormatException e) { 63 | return defaultValue; 64 | } 65 | } 66 | 67 | /* 68 | * double version of above 69 | */ 70 | public static double getMapDouble(final Map map, final String key) { 71 | return getMapDouble(map, key, 0); 72 | } 73 | 74 | public static double getMapDouble(final Map map, final String key, double defaultValue) { 75 | try { 76 | return Double.parseDouble(getMapStr(map, key)); 77 | } catch (NumberFormatException e) { 78 | return defaultValue; 79 | } 80 | } 81 | 82 | /* 83 | * returns a date object from the passed key in the passed map 84 | * returns null if key doesn't exist or isn't a date 85 | */ 86 | public static Date getMapDate(final Map map, final String key) { 87 | if (map == null || key == null || !map.containsKey(key)) { 88 | return null; 89 | } 90 | try { 91 | return (Date) map.get(key); 92 | } catch (ClassCastException e) { 93 | return null; 94 | } 95 | } 96 | 97 | /* 98 | * returns a boolean value from the passed key in the passed map 99 | * returns true unless key doesn't exist, or the value is "0" or "false" 100 | */ 101 | public static boolean getMapBool(final Map map, final String key) { 102 | String value = getMapStr(map, key); 103 | if (value.isEmpty()) { 104 | return false; 105 | } 106 | if (value.startsWith("0")) { // handles "0" and "0.0" 107 | return false; 108 | } 109 | if (value.equalsIgnoreCase("false")) { 110 | return false; 111 | } 112 | // all other values are assume to be true 113 | return true; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/NetworkUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.net.ConnectivityManager; 6 | import android.net.NetworkInfo; 7 | import android.provider.Settings; 8 | 9 | /** 10 | * requires android.permission.ACCESS_NETWORK_STATE 11 | */ 12 | @SuppressLint("MissingPermission") 13 | public class NetworkUtils { 14 | public static final int TYPE_UNKNOWN = -1; 15 | 16 | /** 17 | * returns information on the active network connection 18 | */ 19 | @SuppressLint("MissingPermission") 20 | public static NetworkInfo getActiveNetworkInfo(Context context) { 21 | if (context == null) { 22 | return null; 23 | } 24 | ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 25 | if (cm == null) { 26 | return null; 27 | } 28 | // note that this may return null if no network is currently active 29 | return cm.getActiveNetworkInfo(); 30 | } 31 | 32 | /** 33 | * returns the ConnectivityManager.TYPE_xxx if there's an active connection, otherwise 34 | * returns TYPE_UNKNOWN 35 | */ 36 | private static int getActiveNetworkType(Context context) { 37 | NetworkInfo info = getActiveNetworkInfo(context); 38 | if (info == null || !info.isConnected()) { 39 | return TYPE_UNKNOWN; 40 | } 41 | return info.getType(); 42 | } 43 | 44 | /** 45 | * returns true if a network connection is available 46 | */ 47 | public static boolean isNetworkAvailable(Context context) { 48 | NetworkInfo info = getActiveNetworkInfo(context); 49 | return (info != null && info.isConnected()); 50 | } 51 | 52 | /** 53 | * returns true if the user is connected to WiFi 54 | */ 55 | public static boolean isWiFiConnected(Context context) { 56 | return (getActiveNetworkType(context) == ConnectivityManager.TYPE_WIFI); 57 | } 58 | 59 | /** 60 | * returns true if the user is connected with the mobile data connection 61 | */ 62 | public static boolean isMobileConnected(Context context) { 63 | int networkType = getActiveNetworkType(context); 64 | return (networkType == ConnectivityManager.TYPE_MOBILE 65 | || networkType == ConnectivityManager.TYPE_MOBILE_DUN); 66 | } 67 | 68 | /** 69 | * returns true if airplane mode has been enabled 70 | */ 71 | @SuppressWarnings("deprecation") 72 | public static boolean isAirplaneModeOn(Context context) { 73 | // prior to JellyBean 4.2 this was Settings.System.AIRPLANE_MODE_ON, JellyBean 4.2 74 | // moved it to Settings.Global 75 | return Settings.Global.getInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0; 76 | } 77 | 78 | /** 79 | * returns true if there's an active network connection, otherwise displays a toast error 80 | * and returns false 81 | */ 82 | public static boolean checkConnection(Context context) { 83 | if (context == null) { 84 | return false; 85 | } 86 | if (isNetworkAvailable(context)) { 87 | return true; 88 | } 89 | ToastUtils.showToast(context, R.string.no_network_message); 90 | return false; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/PackageUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | 7 | public class PackageUtils { 8 | /** 9 | * Return true if Debug build. false otherwise. 10 | */ 11 | public static boolean isDebugBuild() { 12 | return BuildConfig.DEBUG; 13 | } 14 | 15 | public static PackageInfo getPackageInfo(Context context) { 16 | try { 17 | PackageManager manager = context.getPackageManager(); 18 | return manager.getPackageInfo(context.getPackageName(), 0); 19 | } catch (PackageManager.NameNotFoundException e) { 20 | return null; 21 | } 22 | } 23 | 24 | /** 25 | * Return version code, or 0 if it can't be read 26 | */ 27 | public static int getVersionCode(Context context) { 28 | PackageInfo packageInfo = getPackageInfo(context); 29 | if (packageInfo != null) { 30 | return packageInfo.versionCode; 31 | } 32 | return 0; 33 | } 34 | 35 | /** 36 | * Return version name, or the string "0" if it can't be read 37 | */ 38 | public static String getVersionName(Context context) { 39 | PackageInfo packageInfo = getPackageInfo(context); 40 | if (packageInfo != null) { 41 | return packageInfo.versionName; 42 | } 43 | return "0"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/PermissionUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.Manifest.permission; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.pm.PackageManager; 7 | import android.os.Build; 8 | 9 | import androidx.core.app.ActivityCompat; 10 | import androidx.core.content.ContextCompat; 11 | import androidx.fragment.app.Fragment; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | public class PermissionUtils { 17 | /** 18 | * Check for permissions, request them if they're not granted. 19 | * 20 | * @return true if permissions are already granted, else request them and return false. 21 | */ 22 | public static boolean checkAndRequestPermissions(Activity activity, int requestCode, String[] permissionList) { 23 | List toRequest = new ArrayList<>(); 24 | for (String permission : permissionList) { 25 | if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { 26 | toRequest.add(permission); 27 | } 28 | } 29 | if (toRequest.size() > 0) { 30 | String[] requestedPermissions = toRequest.toArray(new String[toRequest.size()]); 31 | ActivityCompat.requestPermissions(activity, requestedPermissions, requestCode); 32 | return false; 33 | } 34 | return true; 35 | } 36 | 37 | /** 38 | * Check for permissions, request them if they're not granted. 39 | * 40 | * @return true if permissions are already granted, else request them and return false. 41 | */ 42 | private static boolean checkAndRequestPermissions(Fragment fragment, int requestCode, String[] permissionList) { 43 | List toRequest = new ArrayList<>(); 44 | for (String permission : permissionList) { 45 | Context context = fragment.getActivity(); 46 | if (context != null && ContextCompat.checkSelfPermission(context, permission) != PackageManager 47 | .PERMISSION_GRANTED) { 48 | toRequest.add(permission); 49 | } 50 | } 51 | if (toRequest.size() > 0) { 52 | String[] requestedPermissions = toRequest.toArray(new String[toRequest.size()]); 53 | fragment.requestPermissions(requestedPermissions, requestCode); 54 | return false; 55 | } 56 | return true; 57 | } 58 | 59 | /** 60 | * Check for permissions without requesting them 61 | * 62 | * @return true if all permissions are granted 63 | */ 64 | public static boolean checkPermissions(Context context, String[] permissionList) { 65 | for (String permission : permissionList) { 66 | if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { 67 | return false; 68 | } 69 | } 70 | return true; 71 | } 72 | 73 | /** 74 | * Check for permissions without requesting them 75 | * 76 | * @return true if all permissions are granted 77 | */ 78 | public static boolean checkPermissions(Activity activity, String[] permissionList) { 79 | return checkPermissions((Context) activity, permissionList); 80 | } 81 | 82 | public static boolean checkCameraAndStoragePermissions(Context context) { 83 | return checkPermissions(context, getCameraAndStoragePermissions()); 84 | } 85 | 86 | public static boolean checkCameraAndStoragePermissions(Activity activity) { 87 | return checkPermissions(activity, getCameraAndStoragePermissions()); 88 | } 89 | 90 | public static boolean checkNotificationsPermission(Activity activity) { 91 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 92 | return checkPermissions(activity, new String[]{permission.POST_NOTIFICATIONS}); 93 | } else { 94 | return true; 95 | } 96 | } 97 | 98 | public static boolean checkAndRequestCameraAndStoragePermissions(Fragment fragment, int requestCode) { 99 | return checkAndRequestPermissions(fragment, requestCode, getCameraAndStoragePermissions()); 100 | } 101 | 102 | public static boolean checkAndRequestCameraAndStoragePermissions(Activity activity, int requestCode) { 103 | return checkAndRequestPermissions(activity, requestCode, getCameraAndStoragePermissions()); 104 | } 105 | 106 | public static boolean checkAndRequestFileDownloadPermission(Fragment fragment, int requestCode) { 107 | return checkAndRequestPermissions(fragment, requestCode, getFileDownloadPermission()); 108 | } 109 | 110 | public static String[] getCameraAndStoragePermissions() { 111 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 112 | return new String[]{permission.CAMERA}; 113 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 114 | return new String[]{permission.CAMERA, permission.READ_EXTERNAL_STORAGE}; 115 | } else { 116 | return new String[]{permission.CAMERA, permission.WRITE_EXTERNAL_STORAGE, permission.READ_EXTERNAL_STORAGE}; 117 | } 118 | } 119 | 120 | /** 121 | * Starting from Android Q (SDK 29), the WRITE_EXTERNAL_STORAGE permission is not needed anymore for downloading 122 | * files when using DownloadManager.Request#setDestinationInExternalPublicDir. 123 | */ 124 | private static String[] getFileDownloadPermission() { 125 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 126 | return new String[]{}; 127 | } else { 128 | return new String[]{permission.READ_EXTERNAL_STORAGE, permission.WRITE_EXTERNAL_STORAGE}; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.net.MalformedURLException; 6 | import java.net.URL; 7 | 8 | /** 9 | * routines related to the Photon API 10 | * http://developer.wordpress.com/docs/photon/ 11 | */ 12 | public class PhotonUtils { 13 | private PhotonUtils() { 14 | throw new AssertionError(); 15 | } 16 | 17 | /* 18 | * returns true if the passed url is an obvious "mshots" url 19 | */ 20 | public static boolean isMshotsUrl(final String imageUrl) { 21 | return (imageUrl != null && imageUrl.contains("/mshots/")); 22 | } 23 | 24 | /* 25 | * returns a photon url for the passed image with the resize query set to the passed 26 | * dimensions - note that the passed quality parameter will only affect JPEGs 27 | */ 28 | public enum Quality { 29 | HIGH, 30 | MEDIUM, 31 | LOW 32 | } 33 | 34 | public static final String ATOMIC_MEDIA_PROXY_URL_PREFIX = "https://public-api.wordpress.com/wpcom/v2/sites/"; 35 | public static final String ATOMIC_MEDIA_PROXY_URL_SUFFIX = "/atomic-auth-proxy/file"; 36 | 37 | public static String getPhotonImageUrl(String imageUrl, int width, int height) { 38 | return getPhotonImageUrl(imageUrl, width, height, Quality.MEDIUM); 39 | } 40 | 41 | public static String getPhotonImageUrl(String imageUrl, int width, int height, boolean isPrivateAtomicSite) { 42 | return getPhotonImageUrl(imageUrl, width, height, Quality.MEDIUM, isPrivateAtomicSite); 43 | } 44 | 45 | public static String getPhotonImageUrl(String imageUrl, int width, int height, Quality quality) { 46 | return getPhotonImageUrl(imageUrl, width, height, quality, false); 47 | } 48 | 49 | public static String getPhotonImageUrl(String imageUrl, int width, int height, Quality quality, 50 | boolean isPrivateAtomicSite) { 51 | if (TextUtils.isEmpty(imageUrl)) { 52 | return ""; 53 | } 54 | 55 | String originalUrl = imageUrl; 56 | 57 | // make sure it's valid 58 | int schemePos = imageUrl.indexOf("://"); 59 | if (schemePos == -1) { 60 | return imageUrl; 61 | } 62 | 63 | // we have encountered some image urls that incorrectly have a # fragment part, which 64 | // must be removed before removing the query string 65 | int fragmentPos = imageUrl.indexOf("#"); 66 | if (fragmentPos > 0) { 67 | imageUrl = imageUrl.substring(0, fragmentPos); 68 | } 69 | 70 | String urlCopy = imageUrl; 71 | 72 | // remove existing query string since it may contain params that conflict with the passed ones 73 | imageUrl = UrlUtils.removeQuery(imageUrl); 74 | 75 | // if this is an "mshots" url, skip photon and return it with a query that sets the width/height 76 | if (isMshotsUrl(imageUrl)) { 77 | return imageUrl + "?w=" + width + "&h=" + height; 78 | } 79 | 80 | // strip=info removes Exif, IPTC and comment data from the output image. 81 | String query = "?strip=info"; 82 | 83 | switch (quality) { 84 | case HIGH: 85 | query += "&quality=100"; 86 | break; 87 | case LOW: 88 | query += "&quality=35"; 89 | break; 90 | default: // medium 91 | query += "&quality=65"; 92 | break; 93 | } 94 | 95 | // if both width & height are passed use the "resize" param, use only "w" or "h" if just 96 | // one of them is set 97 | if (width > 0 && height > 0) { 98 | query += "&resize=" + width + "," + height; 99 | } else if (width > 0) { 100 | query += "&w=" + width; 101 | } else if (height > 0) { 102 | query += "&h=" + height; 103 | } 104 | 105 | if (isPrivateAtomicSite) { 106 | try { 107 | URL url = new URL(imageUrl); 108 | String slug = url.getHost(); 109 | String path = url.getPath(); 110 | return ATOMIC_MEDIA_PROXY_URL_PREFIX + slug + ATOMIC_MEDIA_PROXY_URL_SUFFIX 111 | + "?path=" + path + "&" + query; 112 | } catch (MalformedURLException e) { 113 | e.printStackTrace(); 114 | return ""; 115 | } 116 | } 117 | 118 | // return passed url+query if it's already a photon url 119 | if (imageUrl.contains(".wp.com")) { 120 | if (imageUrl.contains("i0.wp.com") || imageUrl.contains("i1.wp.com") || imageUrl.contains("i2.wp.com")) { 121 | boolean useSsl = urlCopy.indexOf("?") > 0 && urlCopy.contains("ssl=1"); 122 | 123 | if (useSsl) { 124 | query += "&ssl=1"; 125 | } 126 | 127 | return imageUrl + query; 128 | } 129 | } 130 | 131 | // use wordpress.com as the host if image is on wordpress.com since it supports the same 132 | // query params and, more importantly, can handle images in private blogs 133 | if (imageUrl.contains("wordpress.com") || imageUrl.endsWith(".avif")) { 134 | return imageUrl + query; 135 | } 136 | 137 | // must use ssl=1 parameter for https image urls 138 | boolean useSSl = UrlUtils.isHttps(imageUrl); 139 | if (useSSl) { 140 | query += "&ssl=1"; 141 | } 142 | 143 | int beginIndex = schemePos + 3; 144 | if (beginIndex < 0 || beginIndex > imageUrl.length()) { 145 | // Fallback to original URL if the beginIndex is invalid to avoid `StringIndexOutOfBoundsException` 146 | // Ref: https://github.com/wordpress-mobile/WordPress-Android/issues/18626 147 | return originalUrl; 148 | } 149 | return "https://i0.wp.com/" + imageUrl.substring(beginIndex) + query; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/ProfilingUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.os.SystemClock; 4 | 5 | import org.wordpress.android.util.AppLog.T; 6 | 7 | import java.util.ArrayList; 8 | 9 | /** 10 | * forked from android.util.TimingLogger to use AppLog instead of Log + new static interface. 11 | */ 12 | public class ProfilingUtils { 13 | private static ProfilingUtils sInstance; 14 | 15 | private String mLabel; 16 | private ArrayList mSplits; 17 | private ArrayList mSplitLabels; 18 | 19 | public static void start(String label) { 20 | getInstance().reset(label); 21 | } 22 | 23 | public static void split(String splitLabel) { 24 | getInstance().addSplit(splitLabel); 25 | } 26 | 27 | public static void dump() { 28 | getInstance().dumpToLog(); 29 | } 30 | 31 | public static void stop() { 32 | getInstance().reset(null); 33 | } 34 | 35 | private static ProfilingUtils getInstance() { 36 | if (sInstance == null) { 37 | sInstance = new ProfilingUtils(); 38 | } 39 | return sInstance; 40 | } 41 | 42 | public ProfilingUtils() { 43 | reset("init"); 44 | } 45 | 46 | public void reset(String label) { 47 | mLabel = label; 48 | reset(); 49 | } 50 | 51 | public void reset() { 52 | if (mSplits == null) { 53 | mSplits = new ArrayList(); 54 | mSplitLabels = new ArrayList(); 55 | } else { 56 | mSplits.clear(); 57 | mSplitLabels.clear(); 58 | } 59 | addSplit(null); 60 | } 61 | 62 | public void addSplit(String splitLabel) { 63 | if (mLabel == null) { 64 | return; 65 | } 66 | long now = SystemClock.elapsedRealtime(); 67 | mSplits.add(now); 68 | mSplitLabels.add(splitLabel); 69 | } 70 | 71 | public void dumpToLog() { 72 | if (mLabel == null) { 73 | return; 74 | } 75 | AppLog.d(T.PROFILING, mLabel + ": begin"); 76 | final long first = mSplits.get(0); 77 | long now = first; 78 | for (int i = 1; i < mSplits.size(); i++) { 79 | now = mSplits.get(i); 80 | final String splitLabel = mSplitLabels.get(i); 81 | final long prev = mSplits.get(i - 1); 82 | AppLog.d(T.PROFILING, mLabel + ": " + (now - prev) + " ms, " + splitLabel); 83 | } 84 | AppLog.d(T.PROFILING, mLabel + ": end, " + (now - first) + " ms"); 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/ServiceUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | 6 | public class ServiceUtils { 7 | public static boolean isServiceRunning(Context context, Class serviceClass) { 8 | ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 9 | for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { 10 | if (serviceClass.getName().equals(service.service.getClassName())) { 11 | return true; 12 | } 13 | } 14 | return false; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/ShortcodeUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | public class ShortcodeUtils { 7 | public static String getVideoPressShortcodeFromId(String videoPressId) { 8 | if (videoPressId == null || videoPressId.isEmpty()) { 9 | return ""; 10 | } 11 | 12 | return "[wpvideo " + videoPressId + "]"; 13 | } 14 | 15 | public static String getVideoPressIdFromShortCode(String shortcode) { 16 | String videoPressId = ""; 17 | 18 | if (shortcode != null) { 19 | String videoPressShortcodeRegex = "^\\[wpvideo (.*)]$"; 20 | 21 | Pattern pattern = Pattern.compile(videoPressShortcodeRegex); 22 | Matcher matcher = pattern.matcher(shortcode); 23 | 24 | if (matcher.find()) { 25 | videoPressId = matcher.group(1); 26 | } 27 | } 28 | 29 | return videoPressId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/SqlUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.database.Cursor; 4 | import android.database.DatabaseUtils; 5 | import android.database.sqlite.SQLiteDatabase; 6 | import android.database.sqlite.SQLiteDoneException; 7 | import android.database.sqlite.SQLiteException; 8 | import android.database.sqlite.SQLiteStatement; 9 | 10 | import org.wordpress.android.util.AppLog.T; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class SqlUtils { 16 | private SqlUtils() { 17 | throw new AssertionError(); 18 | } 19 | 20 | /* 21 | * SQLite doesn't have a boolean datatype, so booleans are stored as 0=false, 1=true 22 | */ 23 | public static long boolToSql(boolean value) { 24 | return (value ? 1 : 0); 25 | } 26 | 27 | public static boolean sqlToBool(int value) { 28 | return (value != 0); 29 | } 30 | 31 | public static void closeStatement(SQLiteStatement stmt) { 32 | if (stmt != null) { 33 | stmt.close(); 34 | } 35 | } 36 | 37 | public static void closeCursor(Cursor c) { 38 | if (c != null && !c.isClosed()) { 39 | c.close(); 40 | } 41 | } 42 | 43 | /* 44 | * wrapper for DatabaseUtils.longForQuery() which returns 0 if query returns no rows 45 | */ 46 | public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { 47 | try { 48 | return DatabaseUtils.longForQuery(db, query, selectionArgs); 49 | } catch (SQLiteDoneException e) { 50 | return 0; 51 | } 52 | } 53 | 54 | public static int intForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { 55 | long value = longForQuery(db, query, selectionArgs); 56 | return (int) value; 57 | } 58 | 59 | public static boolean boolForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { 60 | long value = longForQuery(db, query, selectionArgs); 61 | return sqlToBool((int) value); 62 | } 63 | 64 | /* 65 | * wrapper for DatabaseUtils.stringForQuery(), returns "" if query returns no rows 66 | */ 67 | public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { 68 | try { 69 | return DatabaseUtils.stringForQuery(db, query, selectionArgs); 70 | } catch (SQLiteDoneException e) { 71 | return ""; 72 | } 73 | } 74 | 75 | /* 76 | * returns the number of rows in the passed table 77 | */ 78 | public static long getRowCount(SQLiteDatabase db, String tableName) { 79 | return DatabaseUtils.queryNumEntries(db, tableName); 80 | } 81 | 82 | /* 83 | * removes all rows from the passed table 84 | */ 85 | public static void deleteAllRowsInTable(SQLiteDatabase db, String tableName) { 86 | db.delete(tableName, null, null); 87 | } 88 | 89 | /* 90 | * drop all tables from the passed SQLiteDatabase - make sure to pass a 91 | * writable database 92 | */ 93 | public static boolean dropAllTables(SQLiteDatabase db) throws SQLiteException { 94 | if (db == null) { 95 | return false; 96 | } 97 | 98 | if (db.isReadOnly()) { 99 | throw new SQLiteException("can't drop tables from a read-only database"); 100 | } 101 | 102 | List tableNames = new ArrayList(); 103 | Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null); 104 | if (cursor.moveToFirst()) { 105 | do { 106 | String tableName = cursor.getString(0); 107 | if (!tableName.equals("android_metadata") && !tableName.equals("sqlite_sequence")) { 108 | tableNames.add(tableName); 109 | } 110 | } while (cursor.moveToNext()); 111 | } 112 | 113 | db.beginTransaction(); 114 | try { 115 | for (String tableName : tableNames) { 116 | db.execSQL("DROP TABLE IF EXISTS " + tableName); 117 | } 118 | db.setTransactionSuccessful(); 119 | return true; 120 | } finally { 121 | db.endTransaction(); 122 | closeCursor(cursor); 123 | } 124 | } 125 | 126 | /* 127 | * Android's CursorWindow has a max size of 2MB per row which can be exceeded 128 | * with a very large text column, causing an IllegalStateException when the 129 | * row is read - prevent this by limiting the amount of text that's stored in 130 | * the text column. 131 | * https://github.com/android/platform_frameworks_base/blob/b77bc869241644a662f7e615b0b00ecb5aee373d/core/res/res 132 | * /values/config.xml#L1268 133 | * https://github.com/android/platform_frameworks_base/blob/3bdbf644d61f46b531838558fabbd5b990fc4913/core/java 134 | * /android/database/CursorWindow.java#L103 135 | */ 136 | // Max 512K characters (a UTF-8 char is 4 bytes max, so a 512K characters string is always < 2Mb) 137 | private static final int MAX_TEXT_LEN = 1024 * 1024 / 2; 138 | 139 | public static String maxSQLiteText(final String text) { 140 | if (text.length() <= MAX_TEXT_LEN) { 141 | return text; 142 | } 143 | AppLog.w(T.UTILS, "sqlite > max text exceeded, storing truncated text"); 144 | return text.substring(0, MAX_TEXT_LEN); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactory.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | 5 | public class SystemServiceFactory { 6 | private static SystemServiceFactoryAbstract sFactory; 7 | 8 | public static Object get(Context context, String name) { 9 | if (sFactory == null) { 10 | sFactory = new SystemServiceFactoryDefault(); 11 | } 12 | return sFactory.get(context, name); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryAbstract.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | 5 | public interface SystemServiceFactoryAbstract { 6 | Object get(Context context, String name); 7 | } 8 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/SystemServiceFactoryDefault.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | 5 | public class SystemServiceFactoryDefault implements SystemServiceFactoryAbstract { 6 | public Object get(Context context, String name) { 7 | return context.getSystemService(name); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/ToastUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | import android.view.Gravity; 5 | import android.widget.Toast; 6 | 7 | /** 8 | * Provides a simplified way to show toast messages without having to create the toast, set the 9 | * desired gravity, etc. 10 | */ 11 | public class ToastUtils { 12 | public enum Duration { 13 | SHORT, LONG 14 | } 15 | 16 | private ToastUtils() { 17 | throw new AssertionError(); 18 | } 19 | 20 | public static Toast showToast(Context context, int stringResId) { 21 | return showToast(context, stringResId, Duration.SHORT); 22 | } 23 | 24 | public static Toast showToast(Context context, int stringResId, Duration duration) { 25 | return showToast(context, context.getString(stringResId), duration); 26 | } 27 | 28 | public static Toast showToast(Context context, String text) { 29 | return showToast(context, text, Duration.SHORT); 30 | } 31 | 32 | public static Toast showToast(Context context, String text, Duration duration) { 33 | return showToast(context, text, duration, Gravity.CENTER); 34 | } 35 | 36 | public static Toast showToast(Context context, String text, Duration duration, int gravity) { 37 | return showToast(context, text, duration, gravity, 0, 0); 38 | } 39 | 40 | public static Toast showToast( 41 | Context context, 42 | String text, 43 | Duration duration, 44 | int gravity, 45 | int xOffset, 46 | int yOffset) { 47 | Toast toast = Toast.makeText(context, text, 48 | (duration == Duration.SHORT ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG)); 49 | toast.setGravity(gravity, xOffset, yOffset); 50 | toast.show(); 51 | return toast; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/VersionUtils.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util 2 | 3 | import org.wordpress.android.util.AppLog.T.UTILS 4 | import org.wordpress.android.util.helpers.Version 5 | 6 | object VersionUtils { 7 | /** 8 | * Checks if a given version [String] is equal to or higher than another given minimal version [String]. 9 | * 10 | * Note: This method ignores "-beta", "-alpha" or "-RC" versions, meaning that this will return `true` for 11 | * a version "5.5-beta1" and `minVersion` "5.5", for example. 12 | * 13 | * @param version The version [String] to check. 14 | * @param minVersion A minimal acceptable version [String]. 15 | * @return `true` if the version is equal to or higher than the `minVersion`; `false` otherwise. 16 | */ 17 | @JvmStatic fun checkMinimalVersion(version: String?, minVersion: String?) = 18 | if (!version.isNullOrEmpty() && !minVersion.isNullOrEmpty()) { 19 | try { 20 | Version(stripVersionSuffixes(version)) >= Version(stripVersionSuffixes(minVersion)) 21 | } catch (e: IllegalArgumentException) { 22 | AppLog.e(UTILS, "Invalid version $version, expected $minVersion", e) 23 | false 24 | } 25 | } else false 26 | 27 | // Strip any trailing "-beta", "-alpha" or "-RC" suffixes from the version 28 | private fun stripVersionSuffixes(version: String) = version.substringBefore("-") 29 | } 30 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/VideoUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | 4 | import android.content.Context; 5 | import android.media.MediaMetadataRetriever; 6 | import android.net.Uri; 7 | 8 | import java.io.File; 9 | 10 | public class VideoUtils { 11 | public static long getVideoDurationMS(Context context, File file) { 12 | if (context == null || file == null) { 13 | AppLog.e(AppLog.T.MEDIA, "context and file can't be null."); 14 | return 0L; 15 | } 16 | return getVideoDurationMS(context, Uri.fromFile(file)); 17 | } 18 | 19 | public static long getVideoDurationMS(Context context, Uri videoUri) { 20 | if (context == null || videoUri == null) { 21 | AppLog.e(AppLog.T.MEDIA, "context and videoUri can't be null."); 22 | return 0L; 23 | } 24 | MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 25 | try { 26 | retriever.setDataSource(context, videoUri); 27 | } catch (IllegalArgumentException | SecurityException e) { 28 | AppLog.e(AppLog.T.MEDIA, "Can't read duration of the video.", e); 29 | return 0L; 30 | } catch (RuntimeException e) { 31 | // Ref: https://github.com/wordpress-mobile/WordPress-Android/issues/5431 32 | AppLog.e(AppLog.T.MEDIA, 33 | "Can't read duration of the video due to a Runtime Exception happened setting the datasource", e); 34 | return 0L; 35 | } 36 | 37 | String time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); 38 | if (time == null) { 39 | return 0L; 40 | } 41 | return Long.parseLong(time); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/ViewUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.content.Context; 4 | import android.content.res.ColorStateList; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Outline; 7 | import android.view.View; 8 | import android.view.ViewOutlineProvider; 9 | 10 | import androidx.annotation.AttrRes; 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.StyleRes; 13 | import androidx.core.view.ViewCompat; 14 | 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | public class ViewUtils { 18 | /** 19 | * Generate a value suitable for use in {@link View#setId(int)}. 20 | * This value will not collide with ID values generated at build time by aapt for R.id. 21 | * 22 | * @return a generated ID value 23 | */ 24 | public static int generateViewId() { 25 | return View.generateViewId(); 26 | } 27 | 28 | private static final AtomicInteger NEXT_GENERATED_ID = new AtomicInteger(1); 29 | 30 | /** 31 | * Copied from {@link View#generateViewId()} 32 | * Generate a value suitable for use in {@link View#setId(int)}. 33 | * This value will not collide with ID values generated at build time by aapt for R.id. 34 | * 35 | * @return a generated ID value 36 | */ 37 | private static int copiedGenerateViewId() { 38 | for (;;) { 39 | final int result = NEXT_GENERATED_ID.get(); 40 | // aapt-generated IDs have the high byte nonzero; clamp to the range under that. 41 | int newValue = result + 1; 42 | if (newValue > 0x00FFFFFF) { 43 | newValue = 1; // Roll over to 1, not 0. 44 | } 45 | if (NEXT_GENERATED_ID.compareAndSet(result, newValue)) { 46 | return result; 47 | } 48 | } 49 | } 50 | 51 | public static void setButtonBackgroundColor(Context context, View button, @StyleRes int styleId, 52 | @AttrRes int colorAttribute) { 53 | TypedArray a = context.obtainStyledAttributes(styleId, new int[]{colorAttribute}); 54 | ColorStateList color = a.getColorStateList(0); 55 | a.recycle(); 56 | ViewCompat.setBackgroundTintList(button, color); 57 | } 58 | 59 | /** 60 | * adds an inset circular shadow outline the passed view - note that 61 | * the view should have its elevation set prior to calling this 62 | */ 63 | public static void addCircularShadowOutline(@NonNull View view) { 64 | view.setOutlineProvider(new ViewOutlineProvider() { 65 | @Override 66 | public void getOutline(View view, Outline outline) { 67 | outline.setOval(0, 0, view.getWidth(), view.getHeight()); 68 | } 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/WebViewUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import android.webkit.CookieManager; 4 | import android.webkit.ValueCallback; 5 | 6 | public class WebViewUtils { 7 | public static void clearCookiesAsync() { 8 | clearCookiesAsync(null); 9 | } 10 | 11 | public static void clearCookiesAsync(ValueCallback callback) { 12 | CookieManager cookieManager = CookieManager.getInstance(); 13 | cookieManager.removeAllCookies(callback); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Debouncer.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | import java.util.concurrent.Executors; 5 | import java.util.concurrent.Future; 6 | import java.util.concurrent.ScheduledExecutorService; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | public class Debouncer { 10 | private final ScheduledExecutorService mScheduler = Executors.newSingleThreadScheduledExecutor(); 11 | private final ConcurrentHashMap> mDelayedMap = new ConcurrentHashMap<>(); 12 | 13 | /** 14 | * Debounces {@code callable} by {@code delay}, i.e., schedules it to be executed after {@code delay}, 15 | * or cancels its execution if the method is called with the same key within the {@code delay} again. 16 | */ 17 | public void debounce(final Object key, final Runnable runnable, long delay, TimeUnit unit) { 18 | if (mScheduler.isShutdown()) { 19 | return; 20 | } 21 | final Future prev = mDelayedMap.put(key, mScheduler.schedule(new Runnable() { 22 | @Override 23 | public void run() { 24 | try { 25 | runnable.run(); 26 | } finally { 27 | mDelayedMap.remove(key); 28 | } 29 | } 30 | }, delay, unit)); 31 | if (prev != null) { 32 | prev.cancel(true); 33 | } 34 | } 35 | 36 | public void shutdown() { 37 | mScheduler.shutdownNow(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/ListScrollPositionManager.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.content.SharedPreferences.Editor; 6 | import android.preference.PreferenceManager; 7 | import android.view.View; 8 | import android.widget.ListView; 9 | 10 | public class ListScrollPositionManager { 11 | private int mSelectedPosition; 12 | private int mListViewScrollStateIndex; 13 | private int mListViewScrollStateOffset; 14 | private ListView mListView; 15 | private boolean mSetSelection; 16 | 17 | public ListScrollPositionManager(ListView listView, boolean setSelection) { 18 | mListView = listView; 19 | mSetSelection = setSelection; 20 | } 21 | 22 | public void saveScrollOffset() { 23 | mListViewScrollStateIndex = mListView.getFirstVisiblePosition(); 24 | View view = mListView.getChildAt(0); 25 | mListViewScrollStateOffset = 0; 26 | if (view != null) { 27 | mListViewScrollStateOffset = view.getTop(); 28 | } 29 | if (mSetSelection) { 30 | mSelectedPosition = mListView.getCheckedItemPosition(); 31 | } 32 | } 33 | 34 | public void restoreScrollOffset() { 35 | mListView.setSelectionFromTop(mListViewScrollStateIndex, mListViewScrollStateOffset); 36 | if (mSetSelection) { 37 | mListView.setItemChecked(mSelectedPosition, true); 38 | } 39 | } 40 | 41 | public void saveToPreferences(Context context, String uniqueId) { 42 | saveScrollOffset(); 43 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 44 | Editor editor = settings.edit(); 45 | editor.putInt("scroll-position-manager-index-" + uniqueId, mListViewScrollStateIndex); 46 | editor.putInt("scroll-position-manager-offset-" + uniqueId, mListViewScrollStateOffset); 47 | editor.putInt("scroll-position-manager-selected-position-" + uniqueId, mSelectedPosition); 48 | editor.apply(); 49 | } 50 | 51 | public void restoreFromPreferences(Context context, String uniqueId) { 52 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 53 | mListViewScrollStateIndex = settings.getInt("scroll-position-manager-index-" + uniqueId, 0); 54 | mListViewScrollStateOffset = settings.getInt("scroll-position-manager-offset-" + uniqueId, 0); 55 | mSelectedPosition = settings.getInt("scroll-position-manager-selected-position-" + uniqueId, 0); 56 | restoreScrollOffset(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGallery.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | 6 | /** 7 | * A model representing a Media Gallery. 8 | * A unique id is not used on the website, but only in this app. 9 | * It is used to uniquely determining the instance of the object, as it is 10 | * passed between post and media gallery editor. 11 | */ 12 | public class MediaGallery implements Serializable { 13 | private static final long serialVersionUID = 2359176987182027508L; 14 | 15 | private long mUniqueId; 16 | private boolean mIsRandom; 17 | private String mType; 18 | private int mNumColumns; 19 | private ArrayList mIds; 20 | 21 | public MediaGallery(boolean isRandom, String type, int numColumns, ArrayList ids) { 22 | mIsRandom = isRandom; 23 | mType = type; 24 | mNumColumns = numColumns; 25 | mIds = ids; 26 | mUniqueId = System.currentTimeMillis(); 27 | } 28 | 29 | public MediaGallery() { 30 | mIsRandom = false; 31 | mType = ""; 32 | mNumColumns = 3; 33 | mIds = new ArrayList<>(); 34 | mUniqueId = System.currentTimeMillis(); 35 | } 36 | 37 | public boolean isRandom() { 38 | return mIsRandom; 39 | } 40 | 41 | public void setRandom(boolean isRandom) { 42 | this.mIsRandom = isRandom; 43 | } 44 | 45 | public String getType() { 46 | return mType; 47 | } 48 | 49 | public void setType(String type) { 50 | this.mType = type; 51 | } 52 | 53 | public int getNumColumns() { 54 | return mNumColumns; 55 | } 56 | 57 | public void setNumColumns(int numColumns) { 58 | this.mNumColumns = numColumns; 59 | } 60 | 61 | public ArrayList getIds() { 62 | return mIds; 63 | } 64 | 65 | public String getIdsStr() { 66 | String idsStr = ""; 67 | if (mIds.size() > 0) { 68 | for (Long id : mIds) { 69 | idsStr += id + ","; 70 | } 71 | idsStr = idsStr.substring(0, idsStr.length() - 1); 72 | } 73 | return idsStr; 74 | } 75 | 76 | public void setIds(ArrayList ids) { 77 | this.mIds = ids; 78 | } 79 | 80 | /** 81 | * An id to uniquely identify a media gallery object, so that the same object can be edited in the post editor 82 | */ 83 | public long getUniqueId() { 84 | return mUniqueId; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/MediaGalleryImageSpan.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | import android.content.Context; 4 | import android.text.style.ImageSpan; 5 | 6 | public class MediaGalleryImageSpan extends ImageSpan { 7 | private MediaGallery mMediaGallery; 8 | 9 | public MediaGalleryImageSpan(Context context, MediaGallery mediaGallery, int placeHolder) { 10 | super(context, placeHolder); 11 | setMediaGallery(mediaGallery); 12 | } 13 | 14 | public MediaGallery getMediaGallery() { 15 | return mMediaGallery; 16 | } 17 | 18 | public void setMediaGallery(MediaGallery mediaGallery) { 19 | this.mMediaGallery = mediaGallery; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/RecyclerViewScrollPositionManager.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | 6 | import androidx.recyclerview.widget.LinearLayoutManager; 7 | import androidx.recyclerview.widget.RecyclerView; 8 | 9 | 10 | public class RecyclerViewScrollPositionManager { 11 | private static final String RV_POSITION = "rv_position"; 12 | private static final String RV_OFFSET = "rv_offset"; 13 | private int mRVPosition = 0; 14 | private int mRVOffset = 0; 15 | 16 | public void onSaveInstanceState(Bundle outState, RecyclerView recyclerView) { 17 | // make sure the layout manager is assigned to the RecyclerView 18 | // also take into account this needs to be a LinearLayoutManager, otherwise ClassCastException occurs 19 | outState.putInt(RV_POSITION, 20 | ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition()); 21 | View firstItemView = recyclerView.getChildAt(0); 22 | int offset = (firstItemView == null) ? 0 : (firstItemView.getTop() - recyclerView.getPaddingTop()); 23 | outState.putInt(RV_OFFSET, offset); 24 | } 25 | 26 | public void onRestoreInstanceState(Bundle savedInstanceState) { 27 | mRVPosition = savedInstanceState.getInt(RV_POSITION); 28 | mRVOffset = savedInstanceState.getInt(RV_OFFSET); 29 | } 30 | 31 | public void restoreScrollOffset(RecyclerView recyclerView) { 32 | if (mRVPosition > 0 || mRVOffset > 0) { 33 | ((LinearLayoutManager) recyclerView.getLayoutManager()) 34 | .scrollToPositionWithOffset(mRVPosition, mRVOffset); 35 | } 36 | mRVPosition = 0; 37 | mRVOffset = 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/SwipeToRefreshHelper.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.ColorInt; 6 | import androidx.annotation.ColorRes; 7 | import androidx.core.content.ContextCompat; 8 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 9 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener; 10 | 11 | import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout; 12 | 13 | public class SwipeToRefreshHelper implements OnRefreshListener { 14 | private CustomSwipeRefreshLayout mSwipeRefreshLayout; 15 | private RefreshListener mRefreshListener; 16 | private boolean mRefreshing; 17 | 18 | public interface RefreshListener { 19 | void onRefreshStarted(); 20 | } 21 | 22 | /** 23 | * Helps {@link org.wordpress.android.util.widgets.CustomSwipeRefreshLayout} by passing the 24 | * {@link SwipeRefreshLayout}, {@link RefreshListener}, and color. 25 | * 26 | * @param context {@link Context} in which this layout is used. 27 | * @param swipeRefreshLayout {@link CustomSwipeRefreshLayout} for refreshing the contents 28 | * of a view via a vertical swipe gesture. 29 | * @param listener {@link RefreshListener} notified when a refresh is triggered 30 | * via the swipe gesture. 31 | * 32 | * @deprecated Use {@link #SwipeToRefreshHelper(CustomSwipeRefreshLayout, RefreshListener, int, int...)} instead. 33 | */ 34 | @Deprecated 35 | public SwipeToRefreshHelper(Context context, CustomSwipeRefreshLayout swipeRefreshLayout, 36 | RefreshListener listener) { 37 | init(swipeRefreshLayout, listener, ContextCompat.getColor(context, android.R.color.white), 38 | android.R.color.holo_blue_dark); 39 | } 40 | 41 | /** 42 | * Helps {@link org.wordpress.android.util.widgets.CustomSwipeRefreshLayout} by passing the 43 | * {@link SwipeRefreshLayout}, {@link RefreshListener}, and color(s). 44 | * 45 | * @param swipeRefreshLayout {@link CustomSwipeRefreshLayout} for refreshing the contents 46 | * of a view via a vertical swipe gesture. 47 | * @param listener {@link RefreshListener} notified when a refresh is triggered 48 | * via the swipe gesture. 49 | * @param progressAnimationColors Comma-separated color resource integers used in the progress 50 | * animation. The first color will also be the color of the bar 51 | * that grows in response to a user swipe gesture. 52 | */ 53 | public SwipeToRefreshHelper(CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener, 54 | @ColorInt int backgroundColor, 55 | @ColorRes int... progressAnimationColors) { 56 | init(swipeRefreshLayout, listener, backgroundColor, progressAnimationColors); 57 | } 58 | 59 | /** 60 | * Initializes {@link org.wordpress.android.util.widgets.CustomSwipeRefreshLayout} by assigning 61 | * {@link SwipeRefreshLayout}, {@link RefreshListener}, and color(s). 62 | * 63 | * @param swipeRefreshLayout {@link CustomSwipeRefreshLayout} for refreshing the contents 64 | * of a view via a vertical swipe gesture. 65 | * @param listener {@link RefreshListener} notified when a refresh is triggered 66 | * via the swipe gesture. 67 | * @param progressAnimationColors Comma-separated color resource integers used in the progress 68 | * animation. The first color will also be the color of the bar 69 | * that grows in response to a user swipe gesture. 70 | */ 71 | public void init(CustomSwipeRefreshLayout swipeRefreshLayout, RefreshListener listener, 72 | @ColorInt int backgroundColor, 73 | @ColorRes int... progressAnimationColors) { 74 | mRefreshListener = listener; 75 | mSwipeRefreshLayout = swipeRefreshLayout; 76 | mSwipeRefreshLayout.setOnRefreshListener(this); 77 | mSwipeRefreshLayout.setProgressBackgroundColorSchemeColor(backgroundColor); 78 | mSwipeRefreshLayout.setColorSchemeResources(progressAnimationColors); 79 | } 80 | 81 | public void setRefreshing(boolean refreshing) { 82 | mRefreshing = refreshing; 83 | // Delayed refresh, it fixes https://code.google.com/p/android/issues/detail?id=77712 84 | // 50ms seems a good compromise (always worked during tests) and fast enough so user can't notice the delay 85 | if (refreshing) { 86 | mSwipeRefreshLayout.postDelayed(new Runnable() { 87 | @Override 88 | public void run() { 89 | // use mRefreshing so if the refresh takes less than 50ms, loading indicator won't show up. 90 | mSwipeRefreshLayout.setRefreshing(mRefreshing); 91 | } 92 | }, 50); 93 | } else { 94 | mSwipeRefreshLayout.setRefreshing(false); 95 | } 96 | } 97 | 98 | public boolean isRefreshing() { 99 | return mSwipeRefreshLayout.isRefreshing(); 100 | } 101 | 102 | @Override 103 | public void onRefresh() { 104 | mRefreshListener.onRefreshStarted(); 105 | } 106 | 107 | public void setEnabled(boolean enabled) { 108 | mSwipeRefreshLayout.setEnabled(enabled); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/Version.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | //See: http://stackoverflow.com/a/11024200 4 | public class Version implements Comparable { 5 | private String mVersion; 6 | 7 | public final String get() { 8 | return this.mVersion; 9 | } 10 | 11 | public Version(String version) { 12 | if (version == null) { 13 | throw new IllegalArgumentException("Version can not be null"); 14 | } 15 | if (!version.matches("[0-9]+(\\.[0-9]+)*")) { 16 | throw new IllegalArgumentException("Invalid version format"); 17 | } 18 | this.mVersion = version; 19 | } 20 | 21 | @Override public int compareTo(Version that) { 22 | if (that == null) { 23 | return 1; 24 | } 25 | String[] thisParts = this.get().split("\\."); 26 | String[] thatParts = that.get().split("\\."); 27 | int length = Math.max(thisParts.length, thatParts.length); 28 | for (int i = 0; i < length; i++) { 29 | int thisPart = i < thisParts.length 30 | ? Integer.parseInt(thisParts[i]) : 0; 31 | int thatPart = i < thatParts.length 32 | ? Integer.parseInt(thatParts[i]) : 0; 33 | if (thisPart < thatPart) { 34 | return -1; 35 | } 36 | if (thisPart > thatPart) { 37 | return 1; 38 | } 39 | } 40 | return 0; 41 | } 42 | 43 | @Override public boolean equals(Object that) { 44 | if (this == that) { 45 | return true; 46 | } 47 | if (that == null) { 48 | return false; 49 | } 50 | if (this.getClass() != that.getClass()) { 51 | return false; 52 | } 53 | return this.compareTo((Version) that) == 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPHtmlTagHandler.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | import android.text.Editable; 4 | import android.text.Html; 5 | import android.text.style.BulletSpan; 6 | import android.text.style.LeadingMarginSpan; 7 | 8 | import org.xml.sax.XMLReader; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * Handle tags that the Html class doesn't understand 15 | * Tweaked from source at http://stackoverflow.com/questions/4044509/android-how-to-use-the-html-taghandler 16 | */ 17 | public class WPHtmlTagHandler implements Html.TagHandler { 18 | private static final int SPAN_INDENT_WIDTH = 15; 19 | 20 | private int mListItemCount = 0; 21 | private List mListParents = new ArrayList<>(); 22 | 23 | @Override 24 | public void handleTag(final boolean opening, final String tag, Editable output, 25 | final XMLReader xmlReader) { 26 | if (tag != null) { 27 | switch (tag) { 28 | case "WPUL": 29 | if (opening) { 30 | mListParents.add("ul"); 31 | } else { 32 | mListParents.remove("ul"); 33 | } 34 | break; 35 | case "WPOL": 36 | if (opening) { 37 | mListParents.add("ol"); 38 | } else { 39 | mListParents.remove("ol"); 40 | } 41 | break; 42 | case "WPLI": 43 | if (!opening) { 44 | handleListTag(output); 45 | } 46 | break; 47 | case "dd": 48 | if (opening) { 49 | mListParents.add("dd"); 50 | } else { 51 | mListParents.remove("dd"); 52 | } 53 | break; 54 | } 55 | } 56 | } 57 | 58 | private void handleListTag(Editable output) { 59 | int size = mListParents.size(); 60 | if (size > 0 && output != null) { 61 | if ("ul".equals(mListParents.get(size - 1))) { 62 | output.append("\n"); 63 | String[] split = output.toString().split("\n"); 64 | int start = 0; 65 | if (split.length != 1) { 66 | int lastIndex = split.length - 1; 67 | start = output.length() - split[lastIndex].length() - 1; 68 | } 69 | output.setSpan(new BulletSpan(SPAN_INDENT_WIDTH * mListParents.size()), start, output.length(), 0); 70 | } else if ("ol".equals(mListParents.get(size - 1))) { 71 | mListItemCount++; 72 | output.append("\n"); 73 | String[] split = output.toString().split("\n"); 74 | int start = 0; 75 | if (split.length != 1) { 76 | int lastIndex = split.length - 1; 77 | start = output.length() - split[lastIndex].length() - 1; 78 | } 79 | output.insert(start, mListItemCount + ". "); 80 | output.setSpan(new LeadingMarginSpan.Standard(SPAN_INDENT_WIDTH * mListParents.size()), start, 81 | output.length(), 0); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPImageSpan.java: -------------------------------------------------------------------------------- 1 | //Add WordPress image fields to ImageSpan object 2 | 3 | package org.wordpress.android.util.helpers; 4 | 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.net.Uri; 8 | import android.os.Parcel; 9 | import android.os.Parcelable; 10 | import android.text.style.ImageSpan; 11 | 12 | public class WPImageSpan extends ImageSpan implements Parcelable { 13 | protected Uri mImageSource = null; 14 | protected boolean mNetworkImageLoaded = false; 15 | protected MediaFile mMediaFile; 16 | protected int mStartPosition, mEndPosition; 17 | 18 | protected WPImageSpan() { 19 | super((Bitmap) null); 20 | } 21 | 22 | public WPImageSpan(Context context, Bitmap b, Uri src) { 23 | super(context, b); 24 | this.mImageSource = src; 25 | mMediaFile = new MediaFile(); 26 | } 27 | 28 | public WPImageSpan(Context context, int resId, Uri src) { 29 | super(context, resId); 30 | this.mImageSource = src; 31 | mMediaFile = new MediaFile(); 32 | } 33 | 34 | public void setPosition(int start, int end) { 35 | mStartPosition = start; 36 | mEndPosition = end; 37 | } 38 | 39 | public int getStartPosition() { 40 | return mStartPosition >= 0 ? mStartPosition : 0; 41 | } 42 | 43 | public int getEndPosition() { 44 | return mEndPosition < getStartPosition() ? getStartPosition() : mEndPosition; 45 | } 46 | 47 | public MediaFile getMediaFile() { 48 | return mMediaFile; 49 | } 50 | 51 | public void setMediaFile(MediaFile mediaFile) { 52 | this.mMediaFile = mediaFile; 53 | } 54 | 55 | public void setImageSource(Uri imageSource) { 56 | this.mImageSource = imageSource; 57 | } 58 | 59 | public Uri getImageSource() { 60 | return mImageSource; 61 | } 62 | 63 | public boolean isNetworkImageLoaded() { 64 | return mNetworkImageLoaded; 65 | } 66 | 67 | public void setNetworkImageLoaded(boolean networkImageLoaded) { 68 | this.mNetworkImageLoaded = networkImageLoaded; 69 | } 70 | 71 | protected void setupFromParcel(Parcel in) { 72 | MediaFile mediaFile = new MediaFile(); 73 | 74 | boolean[] booleans = new boolean[2]; 75 | in.readBooleanArray(booleans); 76 | setNetworkImageLoaded(booleans[0]); 77 | mediaFile.setVideo(booleans[1]); 78 | 79 | setImageSource(Uri.parse(in.readString())); 80 | mediaFile.setMediaId(in.readString()); 81 | mediaFile.setBlogId(in.readString()); 82 | mediaFile.setPostID(in.readLong()); 83 | mediaFile.setCaption(in.readString()); 84 | mediaFile.setDescription(in.readString()); 85 | mediaFile.setAlt(in.readString()); 86 | mediaFile.setTitle(in.readString()); 87 | mediaFile.setMimeType(in.readString()); 88 | mediaFile.setFileName(in.readString()); 89 | mediaFile.setThumbnailURL(in.readString()); 90 | mediaFile.setVideoPressShortCode(in.readString()); 91 | mediaFile.setFileURL(in.readString()); 92 | mediaFile.setFilePath(in.readString()); 93 | mediaFile.setDateCreatedGMT(in.readLong()); 94 | mediaFile.setWidth(in.readInt()); 95 | mediaFile.setHeight(in.readInt()); 96 | setPosition(in.readInt(), in.readInt()); 97 | 98 | setMediaFile(mediaFile); 99 | } 100 | 101 | public static final Parcelable.Creator CREATOR 102 | = new Parcelable.Creator() { 103 | public WPImageSpan createFromParcel(Parcel in) { 104 | WPImageSpan imageSpan = new WPImageSpan(); 105 | imageSpan.setupFromParcel(in); 106 | return imageSpan; 107 | } 108 | 109 | public WPImageSpan[] newArray(int size) { 110 | return new WPImageSpan[size]; 111 | } 112 | }; 113 | 114 | @Override 115 | public int describeContents() { 116 | return 0; 117 | } 118 | 119 | @Override 120 | public void writeToParcel(Parcel parcel, int i) { 121 | parcel.writeBooleanArray(new boolean[]{mNetworkImageLoaded, mMediaFile.isVideo()}); 122 | parcel.writeString(mImageSource.toString()); 123 | parcel.writeString(mMediaFile.getMediaId()); 124 | parcel.writeString(mMediaFile.getBlogId()); 125 | parcel.writeLong(mMediaFile.getPostID()); 126 | parcel.writeString(mMediaFile.getCaption()); 127 | parcel.writeString(mMediaFile.getDescription()); 128 | parcel.writeString(mMediaFile.getAlt()); 129 | parcel.writeString(mMediaFile.getTitle()); 130 | parcel.writeString(mMediaFile.getMimeType()); 131 | parcel.writeString(mMediaFile.getFileName()); 132 | parcel.writeString(mMediaFile.getThumbnailURL()); 133 | parcel.writeString(mMediaFile.getVideoPressShortCode()); 134 | parcel.writeString(mMediaFile.getFileURL()); 135 | parcel.writeString(mMediaFile.getFilePath()); 136 | parcel.writeLong(mMediaFile.getDateCreatedGMT()); 137 | parcel.writeInt(mMediaFile.getWidth()); 138 | parcel.writeInt(mMediaFile.getHeight()); 139 | parcel.writeInt(getStartPosition()); 140 | parcel.writeInt(getEndPosition()); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPQuoteSpan.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | import android.text.Layout; 6 | import android.text.style.QuoteSpan; 7 | 8 | /** 9 | * Customzed QuoteSpan for use in SpannableString's 10 | */ 11 | public class WPQuoteSpan extends QuoteSpan { 12 | public static final int STRIPE_COLOR = 0xFF21759B; 13 | private static final int STRIPE_WIDTH = 5; 14 | private static final int GAP_WIDTH = 20; 15 | 16 | public WPQuoteSpan() { 17 | super(STRIPE_COLOR); 18 | } 19 | 20 | @Override 21 | public int getLeadingMargin(boolean first) { 22 | int margin = GAP_WIDTH * 2 + STRIPE_WIDTH; 23 | return margin; 24 | } 25 | 26 | /** 27 | * Draw a nice thick gray bar if Ice Cream Sandwhich or newer. There's a 28 | * bug on older devices that does not respect the increased margin. 29 | */ 30 | @Override 31 | public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, 32 | CharSequence text, int start, int end, boolean first, Layout layout) { 33 | Paint.Style style = p.getStyle(); 34 | int color = p.getColor(); 35 | 36 | p.setStyle(Paint.Style.FILL); 37 | p.setColor(STRIPE_COLOR); 38 | 39 | c.drawRect(GAP_WIDTH + x, top, x + dir * STRIPE_WIDTH, bottom, p); 40 | 41 | p.setStyle(style); 42 | p.setColor(color); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPUnderlineSpan.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2006 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.wordpress.android.util.helpers; 18 | 19 | import android.os.Parcel; 20 | import android.text.style.UnderlineSpan; 21 | 22 | /** 23 | * WPUnderlineSpan is used as an alternative class to UnderlineSpan. UnderlineSpan is used by EditText auto 24 | * correct, so it can get mixed up with our formatting. 25 | */ 26 | public class WPUnderlineSpan extends UnderlineSpan { 27 | public WPUnderlineSpan() { 28 | super(); 29 | } 30 | 31 | public WPUnderlineSpan(Parcel src) { 32 | super(src); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WPWebChromeClient.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers; 2 | 3 | import android.app.Activity; 4 | import android.text.TextUtils; 5 | import android.view.View; 6 | import android.webkit.WebView; 7 | import android.widget.ProgressBar; 8 | 9 | import androidx.annotation.DrawableRes; 10 | 11 | 12 | public class WPWebChromeClient extends WebChromeClientWithVideoPoster { 13 | private final ProgressBar mProgressBar; 14 | private final Activity mActivity; 15 | private final boolean mAutoUpdateActivityTitle; 16 | 17 | public WPWebChromeClient(Activity activity, View view, @DrawableRes int defaultPoster, ProgressBar progressBar) { 18 | this(activity, view, defaultPoster, progressBar, true); 19 | } 20 | 21 | public WPWebChromeClient(Activity activity, 22 | View view, 23 | @DrawableRes int defaultPoster, 24 | ProgressBar progressBar, 25 | boolean autoUpdateActivityTitle) { 26 | super(view, defaultPoster); 27 | mActivity = activity; 28 | mProgressBar = progressBar; 29 | mAutoUpdateActivityTitle = autoUpdateActivityTitle; 30 | } 31 | 32 | public void onProgressChanged(WebView webView, int progress) { 33 | if (mActivity != null 34 | && !mActivity.isFinishing() 35 | && mAutoUpdateActivityTitle 36 | && !TextUtils.isEmpty(webView.getTitle())) { 37 | mActivity.setTitle(webView.getTitle()); 38 | } 39 | if (mProgressBar != null) { 40 | if (progress == 100) { 41 | mProgressBar.setVisibility(View.GONE); 42 | } else { 43 | mProgressBar.setVisibility(View.VISIBLE); 44 | mProgressBar.setProgress(progress); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/WebChromeClientWithVideoPoster.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.view.View 6 | import android.webkit.WebChromeClient 7 | import androidx.annotation.DrawableRes 8 | 9 | abstract class WebChromeClientWithVideoPoster( 10 | view: View?, 11 | @DrawableRes defaultVideoPosterRes: Int 12 | ) : WebChromeClient() { 13 | private val defaultPoster: Bitmap? = view?.context?.let { 14 | BitmapFactory.decodeResource(it.resources, defaultVideoPosterRes) 15 | } 16 | 17 | final override fun getDefaultVideoPoster(): Bitmap? { 18 | return super.getDefaultVideoPoster() ?: defaultPoster 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileCleaner.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers.logfile 2 | 3 | /** 4 | * Prunes the Log File Store by retaining only the last `maxLogFileCount` log files. 5 | * 6 | * The file list is created upon instantiation – any files added 7 | * afterwards won't be modified. 8 | * 9 | * @param logFileProvider: An interface where the log files will be retrieved from 10 | * @param maxLogFileCount: The number of log files to retain 11 | */ 12 | class LogFileCleaner(private val logFileProvider: LogFileProviderInterface, private val maxLogFileCount: Int) { 13 | /** 14 | * Immediately removes all log files known to exist by this instance except for 15 | * the most recent `maxLogFileCount` items. 16 | */ 17 | fun clean() { 18 | logFileProvider.getLogFiles() 19 | .dropLast(maxLogFileCount) 20 | .forEach { it.delete() } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileProvider.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers.logfile 2 | 3 | import android.content.Context 4 | import java.io.File 5 | 6 | private const val LOG_FILE_DIRECTORY = "logs" 7 | 8 | /** 9 | * A collection of helpers for Log Files. 10 | */ 11 | class LogFileProvider(private val logFileDirectoryPath: String) : LogFileProviderInterface { 12 | /** 13 | * Provides a {@link java.io.File} directory in which to store log files. 14 | * 15 | * If the directory doesn't already exist, it will be created. 16 | */ 17 | override fun getLogFileDirectory(): File { 18 | val logFileDirectory = File(logFileDirectoryPath, LOG_FILE_DIRECTORY) 19 | 20 | if (!logFileDirectory.exists()) { 21 | logFileDirectory.mkdir() 22 | } 23 | 24 | return logFileDirectory 25 | } 26 | 27 | /** 28 | * Provides a list of stored log files, ordered oldest to newest. 29 | */ 30 | override fun getLogFiles(): List { 31 | return getLogFileDirectory() 32 | .listFiles() 33 | ?.sortedBy { it.lastModified() } ?: listOf() 34 | } 35 | 36 | companion object { 37 | @JvmStatic 38 | fun fromContext(context: Context): LogFileProvider { 39 | return LogFileProvider(context.applicationInfo.dataDir) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileProviderInterface.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers.logfile 2 | 3 | import java.io.File 4 | 5 | /** 6 | * An interface to retrieve log files 7 | */ 8 | interface LogFileProviderInterface { 9 | fun getLogFiles(): List 10 | 11 | fun getLogFileDirectory(): File 12 | } 13 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/helpers/logfile/LogFileWriter.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.helpers.logfile 2 | 3 | import android.util.Log 4 | import org.jetbrains.annotations.TestOnly 5 | import java.io.File 6 | import java.io.FileWriter 7 | import java.util.Date 8 | import org.wordpress.android.util.DateTimeUtils 9 | import java.io.IOException 10 | import java.util.concurrent.ExecutorService 11 | import java.util.concurrent.Executors 12 | 13 | /** 14 | * A class that manages writing to a log file. 15 | * 16 | * This class creates and writes to a log file, and will typically persist for the entire lifecycle 17 | * of its host application. 18 | */ 19 | class LogFileWriter @JvmOverloads constructor( 20 | logFileProvider: LogFileProviderInterface, 21 | fileId: String = DateTimeUtils.iso8601FromDate(Date()) 22 | ) { 23 | private val file = File(logFileProvider.getLogFileDirectory(), "$fileId.log") 24 | private val fileWriter: FileWriter = FileWriter(file) 25 | private var writingFailed = false 26 | 27 | /** 28 | * A serial executor used to write to the file in a background thread 29 | */ 30 | private val queue: ExecutorService = Executors.newSingleThreadExecutor() 31 | 32 | /** 33 | * A reference to the underlying {@link Java.IO.File} file. 34 | * Should only be used for testing. 35 | */ 36 | @TestOnly 37 | fun getFile(): File = file 38 | 39 | /** 40 | * Writes the provided string to the log file synchronously 41 | */ 42 | fun write(data: String) { 43 | if (!writingFailed) { 44 | queue.execute { 45 | try { 46 | if (!writingFailed) { 47 | fileWriter.write(data) 48 | fileWriter.flush() 49 | } 50 | } catch (ioe: IOException) { 51 | writingFailed = true 52 | Log.e("LogFileWriter", "Writing log failed: ${ioe.stackTrace}") 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/widgets/CustomSwipeRefreshLayout.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.widgets; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | 7 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 8 | 9 | import org.wordpress.android.util.AppLog; 10 | import org.wordpress.android.util.AppLog.T; 11 | 12 | public class CustomSwipeRefreshLayout extends SwipeRefreshLayout { 13 | public CustomSwipeRefreshLayout(Context context) { 14 | super(context); 15 | } 16 | 17 | public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | @Override 22 | public boolean onTouchEvent(MotionEvent event) { 23 | try { 24 | return super.onTouchEvent(event); 25 | } catch (IllegalArgumentException e) { 26 | // Fix for https://github.com/wordpress-mobile/WordPress-Android/issues/2373 27 | // Catch IllegalArgumentException which can be fired by the underlying SwipeRefreshLayout.onTouchEvent() 28 | // method. 29 | // When android support-v4 fixes it, we'll have to remove that custom layout completely. 30 | AppLog.e(T.UTILS, e); 31 | return true; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/java/org/wordpress/android/util/widgets/WPTextInputLayout.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util.widgets; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.widget.EditText; 7 | 8 | import com.google.android.material.textfield.TextInputLayout; 9 | 10 | import org.wordpress.android.util.R; 11 | 12 | /** 13 | * Custom TextInputLayout to provide a usable getBaseline() and error view padding 14 | */ 15 | public class WPTextInputLayout extends TextInputLayout { 16 | public WPTextInputLayout(Context context) { 17 | super(context); 18 | } 19 | 20 | public WPTextInputLayout(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | public WPTextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) { 25 | super(context, attrs, defStyleAttr); 26 | } 27 | 28 | @Override 29 | public int getBaseline() { 30 | EditText editText = getEditText(); 31 | return editText != null ? editText.getBaseline() - editText.getPaddingBottom() 32 | + getResources().getDimensionPixelSize(R.dimen.textinputlayout_baseline_correction) 33 | : 0; 34 | } 35 | 36 | @Override 37 | public void setErrorEnabled(boolean enabled) { 38 | super.setErrorEnabled(enabled); 39 | 40 | // remove hardcoded side padding of the error view 41 | if (enabled) { 42 | View errorView = findViewById(com.google.android.material.R.id.textinput_error); 43 | if (errorView != null && errorView.getParent() != null) { 44 | ((View) errorView.getParent()) 45 | .setPadding(0, errorView.getPaddingTop(), 0, errorView.getPaddingBottom()); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2dp 4 | 4.3dp 5 | -8.6dp 6 | 7 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | There is no network available 4 | Now 5 | 6 | -------------------------------------------------------------------------------- /WordPressUtils/src/main/res/values/tags.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /WordPressUtils/src/test/java/org/wordpress/android/util/DateTimeUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util; 2 | 3 | import org.junit.Ignore; 4 | import org.junit.Test; 5 | 6 | import java.util.Date; 7 | import java.util.TimeZone; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | public class DateTimeUtilsTest { 12 | private final long mDefaultDate = 1564484058163L; // it's Tue Jul 30 2019 10:54:18 in UTC 13 | 14 | @Test 15 | public void testIso8601UTCFromDate() { 16 | // Arrange 17 | TimeZone.setDefault(TimeZone.getTimeZone("GMT+2:00")); 18 | Date date = new Date(mDefaultDate); 19 | String expected = "2019-07-30T10:54:18+00:00"; 20 | 21 | // Act 22 | String actual = DateTimeUtils.iso8601UTCFromDate(date); 23 | 24 | // Assert 25 | assertThat(actual).isEqualTo(expected); 26 | } 27 | 28 | @Test 29 | @Ignore(value = "This test is failing because `DateTimeUtils.localDateToUTC` doesn't work as expected. I've " 30 | + "marked it as deprecated and this tests serves just as a documentation.") 31 | public void testLocalDateToUTC() { 32 | // Arrange 33 | TimeZone.setDefault(TimeZone.getTimeZone("GMT+2:00")); 34 | Date date = new Date(mDefaultDate); 35 | // this succeeds 36 | assertThat(DateTimeUtils.iso8601FromDate(date)).isEqualTo("2019-07-30T12:54:18+0200"); 37 | 38 | // Act 39 | String actual = DateTimeUtils.iso8601FromDate(DateTimeUtils.localDateToUTC(date)); 40 | 41 | // Assert 42 | 43 | // fails because `localDateToUTC` doesn't work as expected. See DateTimeUtils.localDateToUTC for more info. 44 | assertThat(actual).isEqualTo("2019-07-30T10:54:18+00:00"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /WordPressUtils/src/test/java/org/wordpress/android/util/LogFileCleanerTest.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import androidx.test.core.app.ApplicationProvider 6 | import org.junit.After 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Assert.assertTrue 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.robolectric.RobolectricTestRunner 13 | import org.robolectric.annotation.Config 14 | import org.wordpress.android.util.helpers.logfile.LogFileCleaner 15 | import org.wordpress.android.util.helpers.logfile.LogFileProvider 16 | import java.io.File 17 | import java.io.FileReader 18 | import kotlin.random.Random 19 | 20 | /** 21 | * The number of test files to create for each test run 22 | */ 23 | private const val MAX_FILES = 10 24 | 25 | @RunWith(RobolectricTestRunner::class) 26 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 27 | class LogFileCleanerTest { 28 | private val context: Context = ApplicationProvider.getApplicationContext() 29 | private val testProvider = LogFileProvider.fromContext(context) 30 | 31 | @Before 32 | fun setup() { 33 | repeat(MAX_FILES) { 34 | val file = File(testProvider.getLogFileDirectory(), "$it.log") 35 | file.writeText("$it") 36 | file.setLastModified(it * 10_000L) 37 | } 38 | 39 | assert(testProvider.getLogFileDirectory().listFiles()?.count() == MAX_FILES) 40 | } 41 | 42 | @After 43 | fun tearDown() { 44 | // Delete the test directory after each test 45 | testProvider.getLogFileDirectory().deleteRecursively() 46 | } 47 | 48 | @Test 49 | fun testThatCleanerPreservesMostRecentlyCreatedFiles() { 50 | val maxLogFileCount = Random.nextInt(MAX_FILES) 51 | LogFileCleaner(testProvider, maxLogFileCount).clean() 52 | 53 | // Strings are easier to assert against than arrays 54 | val remainingFileIds = testProvider.getLogFiles().joinToString(",") { 55 | FileReader(it).readText() 56 | } 57 | 58 | val expectedValue = (MAX_FILES - 1 downTo 0).take(maxLogFileCount).reversed().joinToString(",") 59 | assertEquals(expectedValue, remainingFileIds) 60 | } 61 | 62 | @Test 63 | fun testThatCleanerPreservesCorrectNumberOfFiles() { 64 | val numberOfFiles = Random.nextInt(MAX_FILES) 65 | LogFileCleaner(testProvider, numberOfFiles).clean() 66 | assertEquals(numberOfFiles, testProvider.getLogFileDirectory().listFiles()?.count()) 67 | } 68 | 69 | @Test 70 | fun testThatCleanerErasesAllFilesIfGivenZero() { 71 | LogFileCleaner(testProvider, 0).clean() 72 | assertTrue(testProvider.getLogFileDirectory().listFiles()?.isEmpty() ?: false) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /WordPressUtils/src/test/java/org/wordpress/android/util/LogFileHelpersTest.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import androidx.test.core.app.ApplicationProvider 6 | import org.junit.After 7 | import org.junit.Assert 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.RobolectricTestRunner 11 | import org.robolectric.annotation.Config 12 | import org.wordpress.android.util.helpers.logfile.LogFileProvider 13 | import java.io.File 14 | import java.util.UUID 15 | 16 | @RunWith(RobolectricTestRunner::class) 17 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 18 | class LogFileHelpersTest { 19 | private val context: Context = ApplicationProvider.getApplicationContext() 20 | private val testProvider = LogFileProvider.fromContext(context) 21 | 22 | @After 23 | fun tearDown() { 24 | // Delete the test directory after each test 25 | testProvider.getLogFileDirectory().deleteRecursively() 26 | } 27 | 28 | @Test 29 | fun testThatLogFileDirectoryIsCreatedIfNotExists() { 30 | val directory = testProvider.getLogFileDirectory() 31 | assert(directory.exists()) 32 | } 33 | 34 | @Test 35 | fun testThatLogFilesListsAllFiles() { 36 | val directory = testProvider.getLogFileDirectory() 37 | File(directory, UUID.randomUUID().toString()).createNewFile() 38 | Assert.assertEquals(testProvider.getLogFiles().count(), 1) 39 | } 40 | 41 | @Test 42 | fun testThatLogFilesSortsFilesWithMostRecentFirst() { 43 | val directory = testProvider.getLogFileDirectory() 44 | 45 | listOf(1_000L, 1_000_000L).shuffled().forEach { modifiedDate -> 46 | File(directory, UUID.randomUUID().toString()).also { file -> 47 | // Use timestamps in increments of 1000 to avoid issues from the File System's date precision 48 | val date = modifiedDate * 1000 49 | file.createNewFile() 50 | file.setLastModified(date) 51 | assert(file.lastModified() == date) 52 | } 53 | } 54 | 55 | val files = testProvider.getLogFiles() 56 | assert(files.first().lastModified() < files.last().lastModified()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /WordPressUtils/src/test/java/org/wordpress/android/util/LogFileWriterTest.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import androidx.test.core.app.ApplicationProvider 6 | import org.junit.After 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.RobolectricTestRunner 11 | import org.robolectric.annotation.Config 12 | import org.wordpress.android.util.helpers.logfile.LogFileProvider 13 | import org.wordpress.android.util.helpers.logfile.LogFileWriter 14 | import java.io.FileReader 15 | import java.util.UUID 16 | 17 | @RunWith(RobolectricTestRunner::class) 18 | @Config(sdk = [Build.VERSION_CODES.O_MR1]) 19 | class LogFileWriterTest { 20 | private val context: Context = ApplicationProvider.getApplicationContext() 21 | private val testProvider = LogFileProvider.fromContext(context) 22 | 23 | @After 24 | fun tearDown() { 25 | // Delete the test directory after each test 26 | testProvider.getLogFileDirectory().deleteRecursively() 27 | } 28 | 29 | @Test 30 | fun testThatFileWriterCreatesLogFile() { 31 | val writer = LogFileWriter(testProvider) 32 | assert(writer.getFile().exists()) 33 | } 34 | 35 | @Test 36 | fun testThatContentsAreWrittenToFile() { 37 | val randomString = UUID.randomUUID().toString() 38 | val writer = LogFileWriter(testProvider) 39 | writer.write(randomString) 40 | 41 | // Allow the async process to persist the file changes 42 | Thread.sleep(1000) 43 | 44 | val contents = FileReader(writer.getFile()).readText() 45 | assertEquals(randomString, contents) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /WordPressUtils/src/test/java/org/wordpress/android/util/VersionUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.android.util 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.Test 5 | 6 | class VersionUtilsTest { 7 | @Test 8 | fun `checkMinimalVersion returns true when the major part of the version is higher than the minimal version`() { 9 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 10 | version = "6.0", 11 | minVersion = "5.5" 12 | ) 13 | assertThat(hasMinimalVersion).isTrue 14 | } 15 | 16 | @Test 17 | fun `checkMinimalVersion returns true when the minor part of the version is higher than the minimal version`() { 18 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 19 | version = "5.6", 20 | minVersion = "5.5" 21 | ) 22 | assertThat(hasMinimalVersion).isTrue 23 | } 24 | 25 | @Test 26 | fun `checkMinimalVersion returns true when the patch part of the version is higher than the minimal version`() { 27 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 28 | version = "5.5.1", 29 | minVersion = "5.5" 30 | ) 31 | assertThat(hasMinimalVersion).isTrue 32 | } 33 | 34 | @Test 35 | fun `checkMinimalVersion returns true when the version is equal to the minimal version`() { 36 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 37 | version = "5.5", 38 | minVersion = "5.5" 39 | ) 40 | assertThat(hasMinimalVersion).isTrue 41 | } 42 | 43 | @Test 44 | fun `checkMinimalVersion returns false when the major part of the version is lower than the minimal version`() { 45 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 46 | version = "4.0", 47 | minVersion = "5.5" 48 | ) 49 | assertThat(hasMinimalVersion).isFalse 50 | } 51 | 52 | @Test 53 | fun `checkMinimalVersion returns false when the minor part of the version is lower than the minimal version`() { 54 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 55 | version = "5.4", 56 | minVersion = "5.5" 57 | ) 58 | assertThat(hasMinimalVersion).isFalse 59 | } 60 | 61 | @Test 62 | fun `checkMinimalVersion returns false when the patch part of the version is lower than the minimal version`() { 63 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 64 | version = "5.5", 65 | minVersion = "5.5.1" 66 | ) 67 | assertThat(hasMinimalVersion).isFalse 68 | } 69 | 70 | @Test 71 | fun `checkMinimalVersion ignores suffixes on the version string`() { 72 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 73 | version = "5.5-beta1", 74 | minVersion = "5.5" 75 | ) 76 | assertThat(hasMinimalVersion).isTrue 77 | } 78 | 79 | @Test 80 | fun `checkMinimalVersion ignores suffixes on the minimal version string`() { 81 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 82 | version = "5.5-beta1", 83 | minVersion = "5.5-beta2" 84 | ) 85 | assertThat(hasMinimalVersion).isTrue 86 | } 87 | 88 | @Test 89 | fun `checkMinimalVersion ignores double suffixes on the version string`() { 90 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 91 | version = "5.5-alpha-51379", 92 | minVersion = "5.5" 93 | ) 94 | assertThat(hasMinimalVersion).isTrue 95 | } 96 | 97 | @Test 98 | fun `checkMinimalVersion ignores double suffixes on the minimal version string`() { 99 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 100 | version = "5.5-alpha-51370", 101 | minVersion = "5.5-alpha-51380" 102 | ) 103 | assertThat(hasMinimalVersion).isTrue 104 | } 105 | 106 | @Test 107 | fun `checkMinimalVersion ignores zero on the patch part of the version string`() { 108 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 109 | version = "5.5.0", 110 | minVersion = "5.5" 111 | ) 112 | assertThat(hasMinimalVersion).isTrue 113 | } 114 | 115 | @Test 116 | fun `checkMinimalVersion ignores zero on the patch part of the minimal version string`() { 117 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 118 | version = "5.5", 119 | minVersion = "5.5.0" 120 | ) 121 | assertThat(hasMinimalVersion).isTrue 122 | } 123 | 124 | @Test 125 | fun `checkMinimalVersion returns false when the version string is null`() { 126 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 127 | version = null, 128 | minVersion = "5.5" 129 | ) 130 | assertThat(hasMinimalVersion).isFalse 131 | } 132 | 133 | @Test 134 | fun `checkMinimalVersion returns false when the version string is empty`() { 135 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 136 | version = "", 137 | minVersion = "5.5" 138 | ) 139 | assertThat(hasMinimalVersion).isFalse 140 | } 141 | 142 | @Test 143 | fun `checkMinimalVersion returns false when the minimal version string is null`() { 144 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 145 | version = "5.5", 146 | minVersion = null 147 | ) 148 | assertThat(hasMinimalVersion).isFalse 149 | } 150 | 151 | @Test 152 | fun `checkMinimalVersion returns false when the minimal version string is empty`() { 153 | val hasMinimalVersion = VersionUtils.checkMinimalVersion( 154 | version = "5.5", 155 | minVersion = "" 156 | ) 157 | assertThat(hasMinimalVersion).isFalse 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id "com.android.library" apply false 5 | id "org.jetbrains.kotlin.android" apply false 6 | id "com.autonomousapps.dependency-analysis" 7 | } 8 | 9 | allprojects { 10 | apply plugin: 'checkstyle' 11 | 12 | if (tasks.findByPath('checkstyle') == null) { 13 | tasks.create(name: 'checkstyle', type: Checkstyle) { 14 | source 'src' 15 | 16 | classpath = files() 17 | } 18 | 19 | checkstyle { 20 | toolVersion = '8.3' 21 | configFile file("${project.rootDir}/config/checkstyle.xml") 22 | } 23 | } 24 | tasks.withType(KotlinCompile).all { 25 | kotlinOptions { 26 | jvmTarget = JavaVersion.VERSION_1_8 27 | allWarningsAsErrors = true 28 | } 29 | } 30 | } 31 | 32 | ext { 33 | minSdkVersion = 24 34 | compileSdkVersion = 34 35 | targetSdkVersion = 34 36 | } 37 | 38 | ext { 39 | // libs 40 | wordpressLintVersion = '2.1.0' 41 | 42 | // main 43 | androidxCoreVersion = '1.5.0' 44 | androidxRecyclerViewVersion = '1.0.0' 45 | androidxSwipeRefreshLayoutVersion = '1.1.0' 46 | commonsTextVersion = '1.10.0' 47 | eventBusVersion = '3.3.1' 48 | materialVersion = '1.2.1' 49 | 50 | // test 51 | androidxTestCoreVersion = '1.4.0' 52 | assertjVersion = '3.11.1' 53 | junitVersion = '4.12' 54 | robolectricVersion = '4.9' 55 | } 56 | -------------------------------------------------------------------------------- /gradle.properties-example: -------------------------------------------------------------------------------- 1 | # WordPress-Utils-Android properties. 2 | 3 | android.useAndroidX=true 4 | android.enableJetifier=false 5 | 6 | android.nonTransitiveRClass=true 7 | 8 | # Dependency Analysis Plugin 9 | dependency.analysis.android.ignored.variants=release 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/WordPress-Utils-Android/ea88675f3a3e259886085c1560503e0726bca88b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | if ! command -v java >/dev/null 2>&1 134 | then 135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 136 | 137 | Please set the JAVA_HOME variable in your environment to match the 138 | location of your Java installation." 139 | fi 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | 201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 203 | 204 | # Collect all arguments for the java command; 205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 206 | # shell script including quotes and variable substitutions, so put them in 207 | # double quotes to make sure that they get re-expanded; and 208 | # * put everything else in single quotes, so that it's not re-expanded. 209 | 210 | set -- \ 211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 212 | -classpath "$CLASSPATH" \ 213 | org.gradle.wrapper.GradleWrapperMain \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | gradle.ext.kotlinVersion = '1.9.24' 3 | gradle.ext.agpVersion = '8.1.0' 4 | gradle.ext.automatticPublishToS3Version = '0.9.0' 5 | gradle.ext.dependencyAnalysisVersion = '1.33.0' 6 | 7 | plugins { 8 | id "org.jetbrains.kotlin.android" version gradle.ext.kotlinVersion 9 | id "com.android.library" version gradle.ext.agpVersion 10 | id "com.automattic.android.publish-to-s3" version gradle.ext.automatticPublishToS3Version 11 | id "com.autonomousapps.dependency-analysis" version gradle.ext.dependencyAnalysisVersion 12 | } 13 | repositories { 14 | maven { 15 | url 'https://a8c-libs.s3.amazonaws.com/android' 16 | content { 17 | includeGroup "com.automattic.android" 18 | includeGroup "com.automattic.android.publish-to-s3" 19 | } 20 | } 21 | gradlePluginPortal() 22 | google() 23 | } 24 | } 25 | 26 | include ':WordPressUtils' 27 | --------------------------------------------------------------------------------