Repository: square/picasso Branch: master Commit: e94d6116e3ff Files: 161 Total size: 598.4 KB Directory structure: gitextract_m46ss2gs/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── build.yaml │ └── release.yaml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── build.gradle ├── deploy_website.sh ├── gradle/ │ ├── libs.versions.toml │ ├── license-header.txt │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── lint.xml ├── picasso/ │ ├── api/ │ │ └── picasso.api │ ├── build.gradle │ ├── consumer-proguard-rules.txt │ ├── gradle.properties │ └── src/ │ ├── androidTest/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── picasso3/ │ │ ├── AssetRequestHandlerTest.kt │ │ ├── BitmapUtilsTest.kt │ │ ├── PicassoDrawableTest.kt │ │ ├── PlatformLruCacheTest.kt │ │ └── TestUtils.kt │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── picasso3/ │ │ ├── Action.kt │ │ ├── AssetRequestHandler.kt │ │ ├── BaseDispatcher.kt │ │ ├── BitmapHunter.kt │ │ ├── BitmapTarget.kt │ │ ├── BitmapTargetAction.kt │ │ ├── BitmapUtils.kt │ │ ├── Callback.kt │ │ ├── ContactsPhotoRequestHandler.kt │ │ ├── ContentStreamRequestHandler.kt │ │ ├── DeferredRequestCreator.kt │ │ ├── Dispatcher.kt │ │ ├── DrawableLoader.kt │ │ ├── DrawableTarget.kt │ │ ├── DrawableTargetAction.kt │ │ ├── EventListener.kt │ │ ├── FetchAction.kt │ │ ├── FileRequestHandler.kt │ │ ├── GetAction.kt │ │ ├── HandlerDispatcher.kt │ │ ├── ImageViewAction.kt │ │ ├── Initializer.kt │ │ ├── InternalCoroutineDispatcher.kt │ │ ├── MatrixTransformation.kt │ │ ├── MediaStoreRequestHandler.kt │ │ ├── MemoryPolicy.kt │ │ ├── NetworkPolicy.kt │ │ ├── NetworkRequestHandler.kt │ │ ├── Picasso.kt │ │ ├── PicassoDrawable.kt │ │ ├── PicassoExecutorService.kt │ │ ├── PlatformLruCache.kt │ │ ├── RemoteViewsAction.kt │ │ ├── Request.kt │ │ ├── RequestCreator.kt │ │ ├── RequestHandler.kt │ │ ├── ResourceDrawableRequestHandler.kt │ │ ├── ResourceRequestHandler.kt │ │ ├── Transformation.kt │ │ └── Utils.kt │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── picasso3/ │ │ ├── BaseDispatcherTest.kt │ │ ├── BitmapHunterTest.kt │ │ ├── BitmapTargetActionTest.kt │ │ ├── DeferredRequestCreatorTest.kt │ │ ├── DrawableTargetActionTest.kt │ │ ├── HandlerDispatcherTest.kt │ │ ├── ImageViewActionTest.kt │ │ ├── InternalCoroutineDispatcherTest.kt │ │ ├── MediaStoreRequestHandlerTest.kt │ │ ├── MemoryPolicyTest.kt │ │ ├── NetworkRequestHandlerTest.kt │ │ ├── PicassoTest.kt │ │ ├── RemoteViewsActionTest.kt │ │ ├── RequestCreatorTest.kt │ │ ├── Shadows.kt │ │ ├── TestContentProvider.kt │ │ ├── TestTransformation.kt │ │ ├── TestUtils.kt │ │ ├── UtilsTest.kt │ │ └── _JavaConsumerIdeCheck.java │ └── resources/ │ ├── mockito-extensions/ │ │ └── org.mockito.plugins.MockMaker │ └── robolectric.properties ├── picasso-compose/ │ ├── README.md │ ├── api/ │ │ └── picasso-compose.api │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── picasso3/ │ │ └── compose/ │ │ └── PicassoPainterTest.kt │ └── main/ │ └── java/ │ └── com/ │ └── squareup/ │ └── picasso3/ │ └── compose/ │ └── PicassoPainter.kt ├── picasso-paparazzi-sample/ │ ├── build.gradle │ └── src/ │ └── test/ │ └── java/ │ └── com/ │ └── example/ │ └── picasso/ │ └── paparazzi/ │ └── PicassoPaparazziTest.kt ├── picasso-pollexor/ │ ├── README.md │ ├── api/ │ │ └── picasso-pollexor.api │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── picasso3/ │ │ └── pollexor/ │ │ └── PollexorRequestTransformer.kt │ └── test/ │ └── java/ │ └── com/ │ └── squareup/ │ └── picasso3/ │ └── pollexor/ │ └── PollexorRequestTransformerTest.kt ├── picasso-sample/ │ ├── build.gradle │ ├── lint.xml │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── picasso/ │ │ ├── Data.kt │ │ ├── GrayscaleTransformation.kt │ │ ├── PicassoInitializer.kt │ │ ├── PicassoSampleActivity.kt │ │ ├── PicassoSampleAdapter.kt │ │ ├── SampleComposeActivity.kt │ │ ├── SampleContactsActivity.kt │ │ ├── SampleContactsAdapter.kt │ │ ├── SampleGalleryActivity.kt │ │ ├── SampleGridViewActivity.kt │ │ ├── SampleGridViewAdapter.kt │ │ ├── SampleListDetailActivity.kt │ │ ├── SampleListDetailAdapter.kt │ │ ├── SampleScrollListener.kt │ │ ├── SampleWidgetProvider.kt │ │ └── SquaredImageView.kt │ └── res/ │ ├── drawable/ │ │ ├── button_selector.xml │ │ ├── list_selector.xml │ │ └── overlay_selector.xml │ ├── layout/ │ │ ├── notification_view.xml │ │ ├── picasso_sample_activity.xml │ │ ├── picasso_sample_activity_item.xml │ │ ├── sample_contacts_activity.xml │ │ ├── sample_contacts_activity_item.xml │ │ ├── sample_gallery_activity.xml │ │ ├── sample_gridview_activity.xml │ │ ├── sample_list_detail_detail.xml │ │ ├── sample_list_detail_item.xml │ │ ├── sample_list_detail_list.xml │ │ └── sample_widget.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── integers.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ ├── values-land/ │ │ └── dimens.xml │ ├── values-w500dp/ │ │ └── integers.xml │ ├── values-w600dp/ │ │ ├── dimens.xml │ │ └── integers.xml │ ├── values-w700dp/ │ │ └── integers.xml │ └── xml/ │ └── sample_widget_info.xml ├── picasso-stats/ │ ├── api/ │ │ └── picasso-stats.api │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── squareup/ │ └── picasso3/ │ └── stats/ │ └── StatsEventListener.kt ├── renovate.json ├── settings.gradle └── website/ ├── index.html └── static/ ├── app-theme.css ├── app.css └── prettify.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_size = 2 indent_style = space charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true end_of_line = lf [*.{kt, kts}] ktlint_code_style = intellij_idea ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,kotlinx.**,^ ij_kotlin_allow_trailing_comma = false ij_kotlin_allow_trailing_comma_on_call_site = false ktlint_standard_argument-list-wrapping = disabled ktlint_function_naming_ignore_when_annotated_with = Composable ================================================ FILE: .gitattributes ================================================ **/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text ================================================ FILE: .github/workflows/build.yaml ================================================ name: build on: pull_request: {} push: branches: - '**' tags-ignore: - '**' env: GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: lfs: true - name: Configure JDK uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 with: validate-wrappers: true - name: Run Tests run: ./gradlew check picasso-paparazzi-sample:verifyPaparazziDebug - name: Upload Test Failures if: failure() uses: actions/upload-artifact@v4 with: name: test-failures path: | **/build/reports/tests/test/ instrumentation-tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: api-level: [21, 27, 28] steps: - name: Checkout uses: actions/checkout@v4 - name: Configure JDK uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 with: validate-wrappers: true - run: ./gradlew assembleAndroidTest - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} script: ./gradlew connectedCheck publish: runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' needs: - build - instrumentation-tests steps: - name: Checkout uses: actions/checkout@v4 - name: Configure JDK uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Publish Artifacts run: ./gradlew publishMavenPublicationToMavenCentralRepository env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} ================================================ FILE: .github/workflows/release.yaml ================================================ name: release on: push: tags: - '**' env: GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Configure JDK uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 - name: Publish Artifacts run: ./gradlew publishMavenPublicationToMavenCentralRepository env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} ================================================ FILE: .gitignore ================================================ # Eclipse .classpath .project .settings eclipsebin # Ant bin gen build out lib # Maven target pom.xml.* release.properties coverage.ec # IntelliJ .idea *.iml *.iws *.ipr classes gen-external-apklibs # Robolectric tmp .DS_Store # Gradle .gradle jniLibs build local.properties reports ================================================ FILE: CHANGELOG.md ================================================ Change Log ========== Version 2.71828 *(2018-03-07)* ------------------------------ This version is not fully backwards compatible with previous 2.x releases! It is intended to be a stable, pre-3.0 release that users of 3.0.0-SNAPSHOT can use in the mean time. Its changes are many, as evidenced by the nearly 3 years since 2.5.2. If you are interested in them you can browse the commits here: https://github.com/square/picasso/compare/picasso-parent-2.5.2...2.71828 Otherwise, stay tuned for 3.0 whose change log will be in relation to 2.5.2 and thus encompass any changes present in this release. Version 2.5.2 *(2015-03-20)* ---------------------------- * Fix: Correct problems with adapter-based recycling of drawables and interop with external libraries like RoundImageView. Version 2.5.1 *(2015-03-19)* ---------------------------- * Specifying transformations in a request now accepts a list. * Fix: Correctly handle `null` values from content providers. * Fix: Ensure contact photo thumbnail Uris are loaded with the correct request handler. * Fix: Eliminate potential (albeit temporary) memory leak on pre-5.0 Android due to message pooling. * Fix: Prevent placeholder image aspect ratio from changing while crossfading in image. Version 2.5.0 *(2015-02-06)* -------------------------- * Update to OkHttp 2.x's native API. If you are using OkHttp you must use version 2.0 or newer (the latest is 2.2 at time of writing) and you no longer need to use the `okhttp-urlconnection` shim. * Memory and Network policy API controls reading and storing bitmaps in memory and/or disk cache. * Allow returning `InputStream` from `RequestHandler`. * Allow removing items from memory cache using `clearKeyUri`. * `fetch()` can now accept a `Callback`. * Provide option with `onlyScaleDown` to perform scaling only if the source bitmap is larger than the target. * Fix: Potential workaround handling improperly cached responses with unknown `Content-Length`. (#632) * Fix: Ensure resized images completely fill ImageView (#769) * Fix: Properly report correct exception when disk cache fails to load (504 gateway error). * Fix: Resize now properly maintains aspect ratio if width or height is 0. * Fix: Update debug indicators for the visually impaired (blue color instead of yellow for disk cache hits). Version 2.4.0 *(2014-11-04)* -------------------------- * New `RequestHandler` beta API adds support for custom bitmap loading. * `priority` API for setting request priority. By default `fetch()` requests are set to `Priority.LOW`. * Requests can now be grouped with a `tag` and can be batch paused, resumed, or canceled. * Resizing with either height or width of 0 will now maintain aspect ratio. * `Picasso.setSingletonInstance` allows setting the global Picasso instance returned from `Picasso.with`. * Request `stableKey` provides an override value for the URI or resource ID when caching. * Fix: Properly calculate sample size for requests with `centerInside()`. * Fix: `ConcurrentModificationException` could occur in the `Dispatcher` when submitting a request. * Fix: Correctly log when a request was canceled due to garbage collection. * Fix: Provide correct target for `RemoteViews` requests. * Fix: Propagate exceptions thrown from custom transformations. * Fix: Invoking `shutdown()` now will close the disk cache. Version 2.3.4 *(2014-08-25)* ---------------------------- * Fix: Revert fail fast when missing internet permission. * Fix: Account for null paths when naming a Request. * Add API to allow canceling of remote views requests. Version 2.3.3 *(2014-07-21)* ---------------------------- * Fix: Crash when attempting to swap dimension for EXIF transformation. * Fix: Properly honor alpha value in PicassoDrawable. * Fix: Use `getWidth()` and `getHeight()` instead of `getMeasuredWidth()` and `getMeasuredHeight()` during `fit()`. Version 2.3.2 *(2014-06-05)* ---------------------------- * Fix: Correctly invalidate PicassoDrawable for GB. * Fix: Attempt to decode responses with missing `Content-Length` header. * Fix: Prevent race condition to initial `with()` call. Version 2.3.1 *(2014-05-29)* ---------------------------- * Fix: Deprecated Response constructor used 0 for content-length. Version 2.3.0 *(2014-05-29)* ---------------------------- * Requests will now be automatically replayed if they failed due to network errors. * Add API for logging. This is mostly useful for debugging Picasso itself. * Add API for loading images into remote views (notifications and widgets). * Stats now provide download statistics. * Updated to use Pollexor 2.0. * When using OkHttp version 1.6 or newer (including 2.0+) is now required. * `MediaStoreBitmapHunter` now properly returns video thumbnails if requested URI is for a video. * All API calls now properly validate the current thread they must run on. * Performance: Various optimizations for reducing object allocations. * Fix: Stats were incorrectly invoked even if the bitmap failed to decode. * Fix: Handle `null` intent case in network broadcast receiver extras. * Fix: `Target` now correctly invokes bitmap failed if an error drawable or resource is supplied. Version 2.2.0 *(2014-01-31)* ---------------------------- * Add support decoding various contact photo URIs. * Add support for loading `android.resource` URIs (e.g. load assets from other packages). * Add support for MICRO/MINI thumbnails for media images. * Add API to supply custom `Bitmap.Config` for decoding. * Performance: Reduce GC by reusing same `StringBuilder` instance on main thread for key creation. * Performance: Reduce default buffer allocation to 4k for `MarkableInputStream`. * Fix: Detect and decode WebP streams from byte array. * Fix: Non-200 HTTP responses will now display error drawable if supplied. * Fix: All exceptions during decode will now dispatch a failure. * Fix: Catch `OutOfMemory` errors, dispatch a failure, and output stats in logcat. * Fix: `fit()` now handles cases where either width or height was not zero. * Fix: Prevent crash from `null` intent on `NetworkBroadcastReceiver`. * Fix: Honor exif orientation when no custom transformations supplied. * Fix: Exceptions during transformations propagate to the main thread. * Fix: Correct skia decoding problem during underflow. * Fix: Placeholder uses full bounds. Version 2.1.1 *(2013-10-04)* ---------------------------- * `Target` now has callback for applying placeholder. This makes it symmetric with image views when using `into()`. * Fix: Another work around for Android's header decoding algorthm readin more than 4K of image data when decoding bounds. * Fix: Ensure default network-based executor is unregistered when instance is shut down. * Fix: Ensure connection is always closed for non-2xx response codes. Version 2.1.0 *(2013-10-01)* ---------------------------- *Duplicate of v2.0.2. Do not use.* Version 2.0.2 *(2013-09-11)* ---------------------------- * Fix: Additional work around for Android's header decoding algorithm reading more than 4K of image data when decoding bounds. Version 2.0.1 *(2013-09-04)* ---------------------------- * Enable filtered bitmaps for higher transform quality. * Fix: Using callbacks with `into()` on `fit()` requests are now always invoked. * Fix: Ensure final frame of cross-fade between place holder and image renders correctly. * Fix: Work around Android's behavior of reading more than 1K of image header data when decoding bounds for some images. Version 2.0.0 *(2013-08-30)* ---------------------------- * New architecture distances Picasso further from the main thread using a dedicated dispatcher thread to manage requests. * Request merging. Two requests on the same key will be combined and the result will be delivered to both at the same time. * `fetch()` requests are now properly wired up to be used as "warm up the cache" type of requests without a target. * `fit()` will now automatically wait for the view to be measured before executing the request. * `shutdown()` API added. Clears the memory cache and stops all threads. Submitting new requests will cause a crash after `shutdown()` has been called. * Batch completed requests to the main thread to reduce main thread re-layout/draw calls. * Variable thread count depending on network connectivity. The faster the network the more threads and vice versa. * Ability to specify a callback with `ImageView` requests. * Picasso will now decode the bounds of the target bitmap over the network. This helps avoid decoding 2000x2000 images meant for 100x100 views. * Support loading asset URIs in the form `file:///android_asset/...`. * BETA: Ability to rewrite requests on the fly. This is useful if you want to add custom logic for wiring up requests differently. Version 1.1.1 *(2013-06-14)* ---------------------------- * Fix: Ensure old requests for targets are cancelled when using a `null` image. Version 1.1.0 *(2013-06-13)* ---------------------------- * `load` method can now take a `Uri`. * Support loading contact photos given a contact `Uri`. * Add `centerInside()` image transformation. * Fix: Prevent network stream decodes from blocking each other. Version 1.0.2 *(2013-05-23)* ---------------------------- * Auto-scale disk cache based on file system size. * `placeholder` now accepts `null` for clearing an existing image when used in an adapter and without an explicit placeholder image. * New global failure listener for reporting load errors to a remote analytics or crash service. * Fix: Ensure disk cache folder is created before initialization. * Fix: Only use the built-in disk cache on API 14+ (but you're all using [OkHttp][1] anyways, right?). Version 1.0.1 *(2013-05-14)* ---------------------------- * Fix: Properly set priority for download threads. * Fix: Ensure stats thread is always initialized. Version 1.0.0 *(2013-05-14)* ---------------------------- Initial release. [1]: http://square.github.io/okhttp/ ================================================ FILE: CONTRIBUTING.md ================================================ Contributing ============ If you would like to contribute code to this project you can do so through GitHub by forking the repository and sending a pull request. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Before your code can be accepted into the project you must also sign the [Individual Contributor License Agreement (CLA)][1]. [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ Picasso ======= **Attention**: This library is deprecated. Please use alternatives like [Coil](https://coil-kt.github.io/coil/) for future projects, and start planning to migrate existing projects, especially if they rely on Compose UI. Existing versions will continue to function, but no new work is planned. While some changes may land in the repo as we support internal legacy usage and migration, there will be no more public releases to Maven Central. Thank you to all who used and/or contributed to Picasso over its decade of image loading. --- A powerful image downloading and caching library for Android ![](website/static/sample.png) For more information please see [the website][1] Download -------- Download the latest AAR from [Maven Central][2] or grab via Gradle: ```groovy implementation 'com.squareup.picasso:picasso:2.8' ``` or Maven: ```xml com.squareup.picasso picasso 2.8 ``` Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. Picasso requires at minimum Java 8 and API 21. ProGuard -------- If you are using ProGuard you might need to add OkHttp's rules: https://github.com/square/okhttp/#r8--proguard License -------- Copyright 2013 Square, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. [1]: https://square.github.io/picasso/ [2]: https://search.maven.org/search?q=g:com.squareup.picasso%20AND%20a:picasso [snap]: https://s01.oss.sonatype.org/content/repositories/snapshots/ ================================================ FILE: RELEASING.md ================================================ Releasing ======== 1. Update `VERSION_NAME` in `gradle.properties` to the release (non-SNAPSHOT) version. 2. Update `CHANGELOG.md` for the impending release. 3. Update the `README.md` with the new version. 4. `git commit -am "Prepare for release X.Y.Z"` (where X.Y.Z is the new version) 5. `git tag -a X.Y.Z -m "X.Y.Z"` (where X.Y.Z is the new version) 6. Update `VERSION_NAME` in `gradle.properties` to the next SNAPSHOT version. 7. `git commit -am "Prepare next development version"` 8. `git push && git push --tags` This will trigger a GitHub Action workflow which will upload the release artifacts to Maven Central. ================================================ FILE: build.gradle ================================================ buildscript { ext.isCi = "true" == System.getenv('CI') repositories { mavenCentral() google() gradlePluginPortal() } dependencies { classpath libs.plugin.android classpath libs.plugin.kotlin classpath libs.plugin.kotlin.compose classpath libs.plugin.publish classpath libs.plugin.spotless classpath libs.plugin.binaryCompatibilityValidator classpath libs.plugin.paparazzi } } apply plugin: 'binary-compatibility-validator' apiValidation { ignoredProjects += ['picasso-sample', 'picasso-paparazzi-sample'] } subprojects { repositories { mavenCentral() google() } tasks.withType(Test).configureEach { testLogging { events "failed" exceptionFormat "full" showExceptions true showStackTraces true showCauses true } } plugins.withId('com.vanniktech.maven.publish') { publishing { repositories { /** * Want to push to an internal repository for testing? * Set the following properties in ~/.gradle/gradle.properties. * * internalUrl=YOUR_INTERNAL_URL * internalUsername=YOUR_USERNAME * internalPassword=YOUR_PASSWORD */ maven { name = "internal" url = providers.gradleProperty("internalUrl") credentials(PasswordCredentials) } } } } apply plugin: 'com.diffplug.spotless' spotless { kotlin { target('**/*.kt') licenseHeaderFile(rootProject.file('gradle/license-header.txt')) ktlint(libs.versions.ktlint.get()) .setEditorConfigPath(rootProject.file(".editorconfig")) } } group = GROUP version = VERSION_NAME } tasks.named('wrapper').configure { distributionType = Wrapper.DistributionType.ALL } configurations { osstrich } dependencies { osstrich 'com.squareup.osstrich:osstrich:1.4.0' } tasks.register('deployJavadoc', JavaExec) { classpath = configurations.osstrich main = 'com.squareup.osstrich.JavadocPublisher' args "$buildDir/osstrich", 'git@github.com:square/picasso.git', 'com.squareup.picasso' } ================================================ FILE: deploy_website.sh ================================================ #!/bin/bash set -ex REPO="git@github.com:square/picasso.git" DIR=temp-clone # Delete any existing temporary website clone rm -rf $DIR # Clone the current repo into temp folder git clone $REPO $DIR # Move working directory into temp folder cd $DIR # Checkout and track the gh-pages branch git checkout -t origin/gh-pages # Delete everything that isn't versioned (1.x, 2.x) ls | grep -E -v '^\d+\.x$' | xargs rm -rf # Copy website files from real repo cp -R ../website/* . # Stage all files in git and create a commit git add . git add -u git commit -m "Website at $(date)" # Push the new files up to GitHub git push origin gh-pages # Delete our temp folder cd .. rm -rf $DIR ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] agp = '8.7.2' coroutines = '1.8.1' composeUi = '1.6.8' javaTarget = '1.8' kotlin = '2.0.0' ktlint = '1.2.1' okhttp = '4.12.0' okio = '3.2.0' paparazzi = '1.3.4' minSdk = '21' compileSdk = '34' [libraries] androidx-annotations = { module = 'androidx.annotation:annotation', version = '1.9.1' } androidx-core = { module = 'androidx.core:core', version = '1.13.1' } androidx-cursorAdapter = { module = 'androidx.cursoradapter:cursoradapter', version = '1.0.0' } androidx-exifInterface = { module = 'androidx.exifinterface:exifinterface', version = '1.3.7' } androidx-fragment = { module = 'androidx.fragment:fragment', version = '1.8.1' } androidx-junit = { module = 'androidx.test.ext:junit', version = '1.2.1' } androidx-lifecycle = { module = 'androidx.lifecycle:lifecycle-common', version = '2.8.7' } androidx-startup = { module = 'androidx.startup:startup-runtime', version = '1.1.1' } androidx-testRunner = { module = 'androidx.test:runner', version = '1.6.2' } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = 'coroutines' } composeUi = { module = 'androidx.compose.ui:ui', version.ref = 'composeUi' } composeRuntime = { module = 'androidx.compose.runtime:runtime', version.ref = 'composeUi' } composeUi-foundation = { module = 'androidx.compose.foundation:foundation', version.ref = 'composeUi' } composeUi-material = { module = 'androidx.compose.material:material', version.ref = 'composeUi' } composeUi-uiTooling = { module = 'androidx.compose.ui:ui-tooling', version.ref = 'composeUi' } composeUi-test = { module = 'androidx.compose.ui:ui-test-junit4', version.ref = 'composeUi' } composeUi-testManifest = { module = 'androidx.compose.ui:ui-test-manifest', version.ref = 'composeUi' } drawablePainter = { module = 'com.google.accompanist:accompanist-drawablepainter', version = '0.34.0' } okio = { module = "com.squareup.okio:okio", version = '3.9.0' } okhttp = { module = 'com.squareup.okhttp3:okhttp', version.ref = 'okhttp' } okhttp-mockWebServer = { module = 'com.squareup.okhttp3:mockwebserver', version.ref = 'okhttp' } pollexor = { module = 'com.squareup:pollexor', version = '3.0.0' } # Test libraries junit = { module = 'junit:junit', version = '4.13.2' } truth = { module = 'com.google.truth:truth', version = '1.4.4' } robolectric = { module = 'org.robolectric:robolectric', version = '4.7' } mockito = { module = 'org.mockito:mockito-core', version = '5.12.0' } # Plugins plugin-android = { module = 'com.android.tools.build:gradle', version.ref = 'agp' } plugin-binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version = '0.16.3' } plugin-kotlin = { module = 'org.jetbrains.kotlin:kotlin-gradle-plugin', version.ref = 'kotlin' } plugin-kotlin-compose = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } plugin-paparazzi = { module = 'app.cash.paparazzi:paparazzi-gradle-plugin', version.ref = 'paparazzi' } plugin-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = '0.29.0' } plugin-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = '6.25.0' } plugin-test-aggregation = { module = "io.github.gmazzo.test.aggregation:plugin", version = '2.2.1' } ================================================ FILE: gradle/license-header.txt ================================================ /* * Copyright (C) $YEAR Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ GROUP=com.squareup.picasso3 VERSION_NAME=3.0.0-SNAPSHOT POM_URL=https://github.com/square/picasso/ POM_SCM_URL=https://github.com/square/picasso/ POM_SCM_CONNECTION=scm:git:git://github.com/square/picasso.git POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/picasso.git POM_LICENCE_NAME=The Apache Software License, Version 2.0 POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt POM_LICENCE_DIST=repo POM_DEVELOPER_ID=square POM_DEVELOPER_NAME=Square, Inc. org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.defaults.buildfeatures.buildconfig=false android.defaults.buildfeatures.aidl=false android.defaults.buildfeatures.renderscript=false android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false SONATYPE_HOST=S01 RELEASE_SIGNING_ENABLED=true SONATYPE_AUTOMATIC_RELEASE=true ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s ' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: lint.xml ================================================ ================================================ FILE: picasso/api/picasso.api ================================================ public abstract interface class com/squareup/picasso3/BitmapTarget { public abstract fun onBitmapFailed (Ljava/lang/Exception;Landroid/graphics/drawable/Drawable;)V public abstract fun onBitmapLoaded (Landroid/graphics/Bitmap;Lcom/squareup/picasso3/Picasso$LoadedFrom;)V public abstract fun onPrepareLoad (Landroid/graphics/drawable/Drawable;)V } public abstract interface class com/squareup/picasso3/Callback { public abstract fun onError (Ljava/lang/Throwable;)V public abstract fun onSuccess ()V } public class com/squareup/picasso3/Callback$EmptyCallback : com/squareup/picasso3/Callback { public fun ()V public fun onError (Ljava/lang/Throwable;)V public fun onSuccess ()V } public abstract interface class com/squareup/picasso3/DrawableTarget { public abstract fun onDrawableFailed (Ljava/lang/Exception;Landroid/graphics/drawable/Drawable;)V public abstract fun onDrawableLoaded (Landroid/graphics/drawable/Drawable;Lcom/squareup/picasso3/Picasso$LoadedFrom;)V public abstract fun onPrepareLoad (Landroid/graphics/drawable/Drawable;)V } public abstract interface class com/squareup/picasso3/EventListener : java/io/Closeable { public abstract fun bitmapDecoded (Landroid/graphics/Bitmap;)V public abstract fun bitmapTransformed (Landroid/graphics/Bitmap;)V public abstract fun cacheHit ()V public abstract fun cacheMaxSize (I)V public abstract fun cacheMiss ()V public abstract fun cacheSize (I)V public abstract fun close ()V public abstract fun downloadFinished (J)V } public final class com/squareup/picasso3/EventListener$DefaultImpls { public static fun close (Lcom/squareup/picasso3/EventListener;)V } public abstract interface annotation class com/squareup/picasso3/Initializer : java/lang/annotation/Annotation { } public final class com/squareup/picasso3/MemoryPolicy : java/lang/Enum { public static final field Companion Lcom/squareup/picasso3/MemoryPolicy$Companion; public static final field NO_CACHE Lcom/squareup/picasso3/MemoryPolicy; public static final field NO_STORE Lcom/squareup/picasso3/MemoryPolicy; public static fun getEntries ()Lkotlin/enums/EnumEntries; public final fun getIndex ()I public static final fun shouldReadFromMemoryCache (I)Z public static final fun shouldWriteToMemoryCache (I)Z public static fun valueOf (Ljava/lang/String;)Lcom/squareup/picasso3/MemoryPolicy; public static fun values ()[Lcom/squareup/picasso3/MemoryPolicy; } public final class com/squareup/picasso3/MemoryPolicy$Companion { public final fun shouldReadFromMemoryCache (I)Z public final fun shouldWriteToMemoryCache (I)Z } public final class com/squareup/picasso3/NetworkPolicy : java/lang/Enum { public static final field Companion Lcom/squareup/picasso3/NetworkPolicy$Companion; public static final field NO_CACHE Lcom/squareup/picasso3/NetworkPolicy; public static final field NO_STORE Lcom/squareup/picasso3/NetworkPolicy; public static final field OFFLINE Lcom/squareup/picasso3/NetworkPolicy; public static fun getEntries ()Lkotlin/enums/EnumEntries; public final fun getIndex ()I public static final fun isOfflineOnly (I)Z public static final fun shouldReadFromDiskCache (I)Z public static final fun shouldWriteToDiskCache (I)Z public static fun valueOf (Ljava/lang/String;)Lcom/squareup/picasso3/NetworkPolicy; public static fun values ()[Lcom/squareup/picasso3/NetworkPolicy; } public final class com/squareup/picasso3/NetworkPolicy$Companion { public final fun isOfflineOnly (I)Z public final fun shouldReadFromDiskCache (I)Z public final fun shouldWriteToDiskCache (I)Z } public final class com/squareup/picasso3/Picasso : androidx/lifecycle/DefaultLifecycleObserver { public final fun cancelRequest (Landroid/widget/ImageView;)V public final fun cancelRequest (Landroid/widget/RemoteViews;I)V public final fun cancelRequest (Lcom/squareup/picasso3/BitmapTarget;)V public final fun cancelRequest (Lcom/squareup/picasso3/DrawableTarget;)V public final fun cancelTag (Ljava/lang/Object;)V public final fun evictAll ()V public final fun getIndicatorsEnabled ()Z public final fun invalidate (Landroid/net/Uri;)V public final fun invalidate (Ljava/io/File;)V public final fun invalidate (Ljava/lang/String;)V public final fun isLoggingEnabled ()Z public final fun load (I)Lcom/squareup/picasso3/RequestCreator; public final fun load (Landroid/net/Uri;)Lcom/squareup/picasso3/RequestCreator; public final fun load (Ljava/io/File;)Lcom/squareup/picasso3/RequestCreator; public final fun load (Ljava/lang/String;)Lcom/squareup/picasso3/RequestCreator; public final fun newBuilder ()Lcom/squareup/picasso3/Picasso$Builder; public fun onDestroy (Landroidx/lifecycle/LifecycleOwner;)V public fun onStart (Landroidx/lifecycle/LifecycleOwner;)V public fun onStop (Landroidx/lifecycle/LifecycleOwner;)V public final fun pauseTag (Ljava/lang/Object;)V public final fun resumeTag (Ljava/lang/Object;)V public final fun setIndicatorsEnabled (Z)V public final fun setLoggingEnabled (Z)V public final fun shutdown ()V } public final class com/squareup/picasso3/Picasso$Builder { public fun (Landroid/content/Context;)V public final fun addEventListener (Lcom/squareup/picasso3/EventListener;)Lcom/squareup/picasso3/Picasso$Builder; public final fun addRequestHandler (Lcom/squareup/picasso3/RequestHandler;)Lcom/squareup/picasso3/Picasso$Builder; public final fun addRequestTransformer (Lcom/squareup/picasso3/Picasso$RequestTransformer;)Lcom/squareup/picasso3/Picasso$Builder; public final fun build ()Lcom/squareup/picasso3/Picasso; public final fun callFactory (Lokhttp3/Call$Factory;)Lcom/squareup/picasso3/Picasso$Builder; public final fun client (Lokhttp3/OkHttpClient;)Lcom/squareup/picasso3/Picasso$Builder; public final fun defaultBitmapConfig (Landroid/graphics/Bitmap$Config;)Lcom/squareup/picasso3/Picasso$Builder; public final fun dispatchers (Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;)Lcom/squareup/picasso3/Picasso$Builder; public static synthetic fun dispatchers$default (Lcom/squareup/picasso3/Picasso$Builder;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lcom/squareup/picasso3/Picasso$Builder; public final fun executor (Ljava/util/concurrent/ExecutorService;)Lcom/squareup/picasso3/Picasso$Builder; public final fun indicatorsEnabled (Z)Lcom/squareup/picasso3/Picasso$Builder; public final fun listener (Lcom/squareup/picasso3/Picasso$Listener;)Lcom/squareup/picasso3/Picasso$Builder; public final fun loggingEnabled (Z)Lcom/squareup/picasso3/Picasso$Builder; public final fun withCacheSize (I)Lcom/squareup/picasso3/Picasso$Builder; } public abstract interface class com/squareup/picasso3/Picasso$Listener { public abstract fun onImageLoadFailed (Lcom/squareup/picasso3/Picasso;Landroid/net/Uri;Ljava/lang/Exception;)V } public final class com/squareup/picasso3/Picasso$LoadedFrom : java/lang/Enum { public static final field DISK Lcom/squareup/picasso3/Picasso$LoadedFrom; public static final field MEMORY Lcom/squareup/picasso3/Picasso$LoadedFrom; public static final field NETWORK Lcom/squareup/picasso3/Picasso$LoadedFrom; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/squareup/picasso3/Picasso$LoadedFrom; public static fun values ()[Lcom/squareup/picasso3/Picasso$LoadedFrom; } public final class com/squareup/picasso3/Picasso$Priority : java/lang/Enum { public static final field HIGH Lcom/squareup/picasso3/Picasso$Priority; public static final field LOW Lcom/squareup/picasso3/Picasso$Priority; public static final field NORMAL Lcom/squareup/picasso3/Picasso$Priority; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lcom/squareup/picasso3/Picasso$Priority; public static fun values ()[Lcom/squareup/picasso3/Picasso$Priority; } public abstract interface class com/squareup/picasso3/Picasso$RequestTransformer { public abstract fun transformRequest (Lcom/squareup/picasso3/Request;)Lcom/squareup/picasso3/Request; } public final class com/squareup/picasso3/PicassoExecutorService : java/util/concurrent/ThreadPoolExecutor { public fun ()V public fun (ILjava/util/concurrent/ThreadFactory;)V public synthetic fun (ILjava/util/concurrent/ThreadFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun submit (Ljava/lang/Runnable;)Ljava/util/concurrent/Future; } public final class com/squareup/picasso3/PicassoKt { public static final field TAG Ljava/lang/String; } public final class com/squareup/picasso3/Request { public static final field KEY_SEPARATOR C public final field centerCrop Z public final field centerCropGravity I public final field centerInside Z public final field config Landroid/graphics/Bitmap$Config; public final field hasRotationPivot Z public final field headers Lokhttp3/Headers; public field id I public field key Ljava/lang/String; public final field memoryPolicy I public final field networkPolicy I public final field onlyScaleDown Z public final field priority Lcom/squareup/picasso3/Picasso$Priority; public final field resourceId I public final field rotationDegrees F public final field rotationPivotX F public final field rotationPivotY F public field started J public final field targetHeight I public final field targetWidth I public field transformations Ljava/util/List; public final field uri Landroid/net/Uri; public final fun getName ()Ljava/lang/String; public final fun getStableKey ()Ljava/lang/String; public final fun getTag ()Ljava/lang/Object; public final fun hasSize ()Z public final fun logId ()Ljava/lang/String; public final fun needsMatrixTransform ()Z public final fun newBuilder ()Lcom/squareup/picasso3/Request$Builder; public final fun plainId ()Ljava/lang/String; public fun toString ()Ljava/lang/String; } public final class com/squareup/picasso3/Request$Builder { public fun (I)V public fun (Landroid/net/Uri;)V public final fun addHeader (Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/picasso3/Request$Builder; public final fun build ()Lcom/squareup/picasso3/Request; public final fun centerCrop ()Lcom/squareup/picasso3/Request$Builder; public final fun centerCrop (I)Lcom/squareup/picasso3/Request$Builder; public static synthetic fun centerCrop$default (Lcom/squareup/picasso3/Request$Builder;IILjava/lang/Object;)Lcom/squareup/picasso3/Request$Builder; public final fun centerInside ()Lcom/squareup/picasso3/Request$Builder; public final fun clearCenterCrop ()Lcom/squareup/picasso3/Request$Builder; public final fun clearCenterInside ()Lcom/squareup/picasso3/Request$Builder; public final fun clearOnlyScaleDown ()Lcom/squareup/picasso3/Request$Builder; public final fun clearResize ()Lcom/squareup/picasso3/Request$Builder; public final fun clearRotation ()Lcom/squareup/picasso3/Request$Builder; public final fun clearTag ()Lcom/squareup/picasso3/Request$Builder; public final fun config (Landroid/graphics/Bitmap$Config;)Lcom/squareup/picasso3/Request$Builder; public final fun getCenterCrop ()Z public final fun getCenterCropGravity ()I public final fun getCenterInside ()Z public final fun getConfig ()Landroid/graphics/Bitmap$Config; public final fun getHasRotationPivot ()Z public final fun getHeaders ()Lokhttp3/Headers; public final fun getMemoryPolicy ()I public final fun getNetworkPolicy ()I public final fun getOnlyScaleDown ()Z public final fun getPriority ()Lcom/squareup/picasso3/Picasso$Priority; public final fun getResourceId ()I public final fun getRotationDegrees ()F public final fun getRotationPivotX ()F public final fun getRotationPivotY ()F public final fun getStableKey ()Ljava/lang/String; public final fun getTag ()Ljava/lang/Object; public final fun getTargetHeight ()I public final fun getTargetWidth ()I public final fun getTransformations ()Ljava/util/List; public final fun getUri ()Landroid/net/Uri; public final fun hasImage ()Z public final fun hasPriority ()Z public final fun hasSize ()Z public final fun memoryPolicy (Lcom/squareup/picasso3/MemoryPolicy;[Lcom/squareup/picasso3/MemoryPolicy;)Lcom/squareup/picasso3/Request$Builder; public final fun networkPolicy (Lcom/squareup/picasso3/NetworkPolicy;[Lcom/squareup/picasso3/NetworkPolicy;)Lcom/squareup/picasso3/Request$Builder; public final fun onlyScaleDown ()Lcom/squareup/picasso3/Request$Builder; public final fun priority (Lcom/squareup/picasso3/Picasso$Priority;)Lcom/squareup/picasso3/Request$Builder; public final fun resize (II)Lcom/squareup/picasso3/Request$Builder; public final fun rotate (F)Lcom/squareup/picasso3/Request$Builder; public final fun rotate (FFF)Lcom/squareup/picasso3/Request$Builder; public final fun setCenterCrop (Z)V public final fun setCenterCropGravity (I)V public final fun setCenterInside (Z)V public final fun setConfig (Landroid/graphics/Bitmap$Config;)V public final fun setHasRotationPivot (Z)V public final fun setHeaders (Lokhttp3/Headers;)V public final fun setMemoryPolicy (I)V public final fun setNetworkPolicy (I)V public final fun setOnlyScaleDown (Z)V public final fun setPriority (Lcom/squareup/picasso3/Picasso$Priority;)V public final fun setResourceId (I)Lcom/squareup/picasso3/Request$Builder; public final fun setResourceId (I)V public final fun setRotationDegrees (F)V public final fun setRotationPivotX (F)V public final fun setRotationPivotY (F)V public final fun setStableKey (Ljava/lang/String;)V public final fun setTag (Ljava/lang/Object;)V public final fun setTargetHeight (I)V public final fun setTargetWidth (I)V public final fun setTransformations (Ljava/util/List;)V public final fun setUri (Landroid/net/Uri;)Lcom/squareup/picasso3/Request$Builder; public final fun setUri (Landroid/net/Uri;)V public final fun stableKey (Ljava/lang/String;)Lcom/squareup/picasso3/Request$Builder; public final fun tag (Ljava/lang/Object;)Lcom/squareup/picasso3/Request$Builder; public final fun transform (Lcom/squareup/picasso3/Transformation;)Lcom/squareup/picasso3/Request$Builder; public final fun transform (Ljava/util/List;)Lcom/squareup/picasso3/Request$Builder; } public final class com/squareup/picasso3/RequestCreator { public final fun addHeader (Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/picasso3/RequestCreator; public final fun centerCrop ()Lcom/squareup/picasso3/RequestCreator; public final fun centerCrop (I)Lcom/squareup/picasso3/RequestCreator; public final fun centerInside ()Lcom/squareup/picasso3/RequestCreator; public final fun config (Landroid/graphics/Bitmap$Config;)Lcom/squareup/picasso3/RequestCreator; public final fun error (I)Lcom/squareup/picasso3/RequestCreator; public final fun error (Landroid/graphics/drawable/Drawable;)Lcom/squareup/picasso3/RequestCreator; public final fun fetch ()V public final fun fetch (Lcom/squareup/picasso3/Callback;)V public static synthetic fun fetch$default (Lcom/squareup/picasso3/RequestCreator;Lcom/squareup/picasso3/Callback;ILjava/lang/Object;)V public final fun fit ()Lcom/squareup/picasso3/RequestCreator; public final fun get ()Landroid/graphics/Bitmap; public final fun into (Landroid/widget/ImageView;)V public final fun into (Landroid/widget/ImageView;Lcom/squareup/picasso3/Callback;)V public final fun into (Landroid/widget/RemoteViews;IILandroid/app/Notification;)V public final fun into (Landroid/widget/RemoteViews;IILandroid/app/Notification;Ljava/lang/String;)V public final fun into (Landroid/widget/RemoteViews;IILandroid/app/Notification;Ljava/lang/String;Lcom/squareup/picasso3/Callback;)V public final fun into (Landroid/widget/RemoteViews;IILcom/squareup/picasso3/Callback;)V public final fun into (Landroid/widget/RemoteViews;I[I)V public final fun into (Landroid/widget/RemoteViews;I[ILcom/squareup/picasso3/Callback;)V public final fun into (Lcom/squareup/picasso3/BitmapTarget;)V public final fun into (Lcom/squareup/picasso3/DrawableTarget;)V public static synthetic fun into$default (Lcom/squareup/picasso3/RequestCreator;Landroid/widget/ImageView;Lcom/squareup/picasso3/Callback;ILjava/lang/Object;)V public static synthetic fun into$default (Lcom/squareup/picasso3/RequestCreator;Landroid/widget/RemoteViews;IILandroid/app/Notification;Ljava/lang/String;Lcom/squareup/picasso3/Callback;ILjava/lang/Object;)V public static synthetic fun into$default (Lcom/squareup/picasso3/RequestCreator;Landroid/widget/RemoteViews;IILcom/squareup/picasso3/Callback;ILjava/lang/Object;)V public static synthetic fun into$default (Lcom/squareup/picasso3/RequestCreator;Landroid/widget/RemoteViews;I[ILcom/squareup/picasso3/Callback;ILjava/lang/Object;)V public final fun memoryPolicy (Lcom/squareup/picasso3/MemoryPolicy;[Lcom/squareup/picasso3/MemoryPolicy;)Lcom/squareup/picasso3/RequestCreator; public final fun networkPolicy (Lcom/squareup/picasso3/NetworkPolicy;[Lcom/squareup/picasso3/NetworkPolicy;)Lcom/squareup/picasso3/RequestCreator; public final fun noFade ()Lcom/squareup/picasso3/RequestCreator; public final fun noPlaceholder ()Lcom/squareup/picasso3/RequestCreator; public final fun onlyScaleDown ()Lcom/squareup/picasso3/RequestCreator; public final fun placeholder (I)Lcom/squareup/picasso3/RequestCreator; public final fun placeholder (Landroid/graphics/drawable/Drawable;)Lcom/squareup/picasso3/RequestCreator; public final fun priority (Lcom/squareup/picasso3/Picasso$Priority;)Lcom/squareup/picasso3/RequestCreator; public final fun resize (II)Lcom/squareup/picasso3/RequestCreator; public final fun resizeDimen (II)Lcom/squareup/picasso3/RequestCreator; public final fun rotate (F)Lcom/squareup/picasso3/RequestCreator; public final fun rotate (FFF)Lcom/squareup/picasso3/RequestCreator; public final fun stableKey (Ljava/lang/String;)Lcom/squareup/picasso3/RequestCreator; public final fun tag (Ljava/lang/Object;)Lcom/squareup/picasso3/RequestCreator; public final fun transform (Lcom/squareup/picasso3/Transformation;)Lcom/squareup/picasso3/RequestCreator; public final fun transform (Ljava/util/List;)Lcom/squareup/picasso3/RequestCreator; } public abstract class com/squareup/picasso3/RequestHandler { public fun ()V public abstract fun canHandleRequest (Lcom/squareup/picasso3/Request;)Z public fun getRetryCount ()I public abstract fun load (Lcom/squareup/picasso3/Picasso;Lcom/squareup/picasso3/Request;Lcom/squareup/picasso3/RequestHandler$Callback;)V public fun shouldRetry (ZLandroid/net/NetworkInfo;)Z public fun supportsReplay ()Z } public abstract interface class com/squareup/picasso3/RequestHandler$Callback { public abstract fun onError (Ljava/lang/Throwable;)V public abstract fun onSuccess (Lcom/squareup/picasso3/RequestHandler$Result;)V } public abstract class com/squareup/picasso3/RequestHandler$Result { public synthetic fun (Lcom/squareup/picasso3/Picasso$LoadedFrom;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Lcom/squareup/picasso3/Picasso$LoadedFrom;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getExifRotation ()I public final fun getLoadedFrom ()Lcom/squareup/picasso3/Picasso$LoadedFrom; } public final class com/squareup/picasso3/RequestHandler$Result$Bitmap : com/squareup/picasso3/RequestHandler$Result { public fun (Landroid/graphics/Bitmap;Lcom/squareup/picasso3/Picasso$LoadedFrom;I)V public synthetic fun (Landroid/graphics/Bitmap;Lcom/squareup/picasso3/Picasso$LoadedFrom;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getBitmap ()Landroid/graphics/Bitmap; } public final class com/squareup/picasso3/RequestHandler$Result$Drawable : com/squareup/picasso3/RequestHandler$Result { public fun (Landroid/graphics/drawable/Drawable;Lcom/squareup/picasso3/Picasso$LoadedFrom;I)V public synthetic fun (Landroid/graphics/drawable/Drawable;Lcom/squareup/picasso3/Picasso$LoadedFrom;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getDrawable ()Landroid/graphics/drawable/Drawable; } public abstract interface class com/squareup/picasso3/Transformation { public abstract fun key ()Ljava/lang/String; public abstract fun transform (Lcom/squareup/picasso3/RequestHandler$Result$Bitmap;)Lcom/squareup/picasso3/RequestHandler$Result$Bitmap; } ================================================ FILE: picasso/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'com.vanniktech.maven.publish' android { namespace 'com.squareup.picasso3' compileSdkVersion libs.versions.compileSdk.get() as int defaultConfig { minSdkVersion libs.versions.minSdk.get() as int consumerProguardFiles 'consumer-proguard-rules.txt' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } compileOptions { sourceCompatibility libs.versions.javaTarget.get() targetCompatibility libs.versions.javaTarget.get() } kotlinOptions { jvmTarget = libs.versions.javaTarget.get() } lintOptions { textOutput 'stdout' textReport true lintConfig rootProject.file('lint.xml') } testOptions { unitTests { includeAndroidResources = true } } } dependencies { api libs.okhttp api libs.okio api libs.androidx.lifecycle implementation libs.androidx.annotations implementation libs.androidx.core implementation libs.androidx.exifInterface testImplementation libs.coroutines.test testImplementation libs.junit testImplementation libs.truth testImplementation libs.robolectric testImplementation libs.mockito testImplementation libs.okhttp.mockWebServer androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.testRunner androidTestImplementation libs.truth } spotless { kotlin { targetExclude( // Non-Square licensed files "src/test/java/com/squareup/picasso3/PlatformLruCacheTest.kt", ) } } ================================================ FILE: picasso/consumer-proguard-rules.txt ================================================ ### OKHTTP # Platform calls Class.forName on types which do not exist on Android to determine platform. -dontnote okhttp3.internal.Platform ### OKIO # java.nio.file.* usage which cannot be used at runtime. Animal sniffer annotation. -dontwarn okio.Okio # JDK 7-only method which is @hide on Android. Animal sniffer annotation. -dontwarn okio.DeflaterSink ================================================ FILE: picasso/gradle.properties ================================================ POM_ARTIFACT_ID=picasso POM_NAME=Picasso POM_DESCRIPTION=A powerful image downloading and caching library for Android POM_PACKAGING=aar ================================================ FILE: picasso/src/androidTest/AndroidManifest.xml ================================================ ================================================ FILE: picasso/src/androidTest/java/com/squareup/picasso3/AssetRequestHandlerTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.net.Uri import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AssetRequestHandlerTest { @Test fun truncatesFilePrefix() { val uri = Uri.parse("file:///android_asset/foo/bar.png") val request = Request.Builder(uri).build() val actual = AssetRequestHandler.getFilePath(request) assertThat(actual).isEqualTo("foo/bar.png") } } ================================================ FILE: picasso/src/androidTest/java/com/squareup/picasso3/BitmapUtilsTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap import android.graphics.Bitmap.Config.RGB_565 import android.graphics.BitmapFactory import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.BitmapUtils.calculateInSampleSize import com.squareup.picasso3.BitmapUtils.createBitmapOptions import com.squareup.picasso3.BitmapUtils.requiresInSampleSize import com.squareup.picasso3.TestUtils.URI_1 import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BitmapUtilsTest { @Test fun bitmapConfig() { for (config in Bitmap.Config.values()) { val data = Request.Builder(URI_1).config(config).build() val copy = data.newBuilder().build() assertThat(createBitmapOptions(data)!!.inPreferredConfig).isSameInstanceAs(config) assertThat(createBitmapOptions(copy)!!.inPreferredConfig).isSameInstanceAs(config) } } @Test fun requiresComputeInSampleSize() { assertThat(requiresInSampleSize(null)).isFalse() val defaultOptions = BitmapFactory.Options() assertThat(requiresInSampleSize(defaultOptions)).isFalse() val justBounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } assertThat(requiresInSampleSize(justBounds)).isTrue() } @Test fun calculateInSampleSizeNoResize() { val options = BitmapFactory.Options() val data = Request.Builder(URI_1).build() calculateInSampleSize(100, 100, 150, 150, options, data) assertThat(options.inSampleSize).isEqualTo(1) } @Test fun calculateInSampleSizeResize() { val options = BitmapFactory.Options() val data = Request.Builder(URI_1).build() calculateInSampleSize(100, 100, 200, 200, options, data) assertThat(options.inSampleSize).isEqualTo(2) } @Test fun calculateInSampleSizeResizeCenterInside() { val options = BitmapFactory.Options() val data = Request.Builder(URI_1).centerInside().resize(100, 100).build() calculateInSampleSize(data.targetWidth, data.targetHeight, 400, 200, options, data) assertThat(options.inSampleSize).isEqualTo(4) } @Test fun calculateInSampleSizeKeepAspectRatioWithWidth() { val options = BitmapFactory.Options() val data = Request.Builder(URI_1).resize(400, 0).build() calculateInSampleSize(data.targetWidth, data.targetHeight, 800, 200, options, data) assertThat(options.inSampleSize).isEqualTo(2) } @Test fun calculateInSampleSizeKeepAspectRatioWithHeight() { val options = BitmapFactory.Options() val data = Request.Builder(URI_1).resize(0, 100).build() calculateInSampleSize(data.targetWidth, data.targetHeight, 800, 200, options, data) assertThat(options.inSampleSize).isEqualTo(2) } @Test fun nullBitmapOptionsIfNoResizing() { // No resize must return no bitmap options val noResize = Request.Builder(URI_1).build() val noResizeOptions = createBitmapOptions(noResize) assertThat(noResizeOptions).isNull() } @Test fun inJustDecodeBoundsIfResizing() { // Resize must return bitmap options with inJustDecodeBounds = true val requiresResize = Request.Builder(URI_1).resize(20, 15).build() val resizeOptions = createBitmapOptions(requiresResize) assertThat(resizeOptions).isNotNull() assertThat(resizeOptions!!.inJustDecodeBounds).isTrue() } @Test fun createWithConfigAndNotInJustDecodeBounds() { // Given a config, must return bitmap options and false inJustDecodeBounds val config = Request.Builder(URI_1).config(RGB_565).build() val configOptions = createBitmapOptions(config) assertThat(configOptions).isNotNull() assertThat(configOptions!!.inJustDecodeBounds).isFalse() } } ================================================ FILE: picasso/src/androidTest/java/com/squareup/picasso3/PicassoDrawableTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap import android.graphics.Color.RED import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.Picasso.LoadedFrom.DISK import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PicassoDrawableTest { private val placeholder: Drawable = ColorDrawable(RED) private val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8) @Test fun createWithNoPlaceholderAnimation() { val pd = PicassoDrawable( placeholder = null, context = ApplicationProvider.getApplicationContext(), bitmap = bitmap, loadedFrom = DISK, noFade = false, debugging = false ) assertThat(pd.bitmap).isSameInstanceAs(bitmap) assertThat(pd.placeholder).isNull() assertThat(pd.animating).isTrue() } @Test fun createWithPlaceholderAnimation() { val pd = PicassoDrawable( context = ApplicationProvider.getApplicationContext(), bitmap = bitmap, placeholder, loadedFrom = DISK, noFade = false, debugging = false ) assertThat(pd.bitmap).isSameInstanceAs(bitmap) assertThat(pd.placeholder).isSameInstanceAs(placeholder) assertThat(pd.animating).isTrue() } @Test fun createWithBitmapCacheHit() { val pd = PicassoDrawable( context = ApplicationProvider.getApplicationContext(), bitmap = bitmap, placeholder, loadedFrom = Picasso.LoadedFrom.MEMORY, noFade = false, debugging = false ) assertThat(pd.bitmap).isSameInstanceAs(bitmap) assertThat(pd.placeholder).isNull() assertThat(pd.animating).isFalse() } } ================================================ FILE: picasso/src/androidTest/java/com/squareup/picasso3/PlatformLruCacheTest.kt ================================================ /* * Copyright (C) 2011 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap import android.graphics.Bitmap.Config.ALPHA_8 import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PlatformLruCacheTest { // The use of ALPHA_8 simplifies the size math in tests since only one byte is used per-pixel. private val bitmapA = Bitmap.createBitmap(1, 1, ALPHA_8) private val bitmapB = Bitmap.createBitmap(1, 1, ALPHA_8) private val bitmapC = Bitmap.createBitmap(1, 1, ALPHA_8) private val bitmapD = Bitmap.createBitmap(1, 1, ALPHA_8) private val bitmapE = Bitmap.createBitmap(1, 1, ALPHA_8) private var expectedPutCount = 0 private var expectedHitCount = 0 private var expectedMissCount = 0 private var expectedEvictionCount = 0 @Test fun testStatistics() { val cache = PlatformLruCache(3) assertStatistics(cache) cache["a"] = bitmapA expectedPutCount++ assertStatistics(cache) assertHit(cache, "a", bitmapA) cache["b"] = bitmapB expectedPutCount++ assertStatistics(cache) assertHit(cache, "a", bitmapA) assertHit(cache, "b", bitmapB) assertSnapshot(cache, "a", bitmapA, "b", bitmapB) cache["c"] = bitmapC expectedPutCount++ assertStatistics(cache) assertHit(cache, "a", bitmapA) assertHit(cache, "b", bitmapB) assertHit(cache, "c", bitmapC) assertSnapshot(cache, "a", bitmapA, "b", bitmapB, "c", bitmapC) cache["d"] = bitmapD expectedPutCount++ expectedEvictionCount++ // a should have been evicted assertStatistics(cache) assertMiss(cache, "a") assertHit(cache, "b", bitmapB) assertHit(cache, "c", bitmapC) assertHit(cache, "d", bitmapD) assertHit(cache, "b", bitmapB) assertHit(cache, "c", bitmapC) assertSnapshot(cache, "d", bitmapD, "b", bitmapB, "c", bitmapC) cache["e"] = bitmapE expectedPutCount++ expectedEvictionCount++ // d should have been evicted assertStatistics(cache) assertMiss(cache, "d") assertMiss(cache, "a") assertHit(cache, "e", bitmapE) assertHit(cache, "b", bitmapB) assertHit(cache, "c", bitmapC) assertSnapshot(cache, "e", bitmapE, "b", bitmapB, "c", bitmapC) } @Test fun evictionWithSingletonCache() { val cache = PlatformLruCache(1) cache["a"] = bitmapA cache["b"] = bitmapB assertSnapshot(cache, "b", bitmapB) } /** * Replacing the value for a key doesn't cause an eviction but it does bring the replaced entry to * the front of the queue. */ @Test fun putCauseEviction() { val cache = PlatformLruCache(3) cache["a"] = bitmapA cache["b"] = bitmapB cache["c"] = bitmapC cache["b"] = bitmapD assertSnapshot(cache, "a", bitmapA, "c", bitmapC, "b", bitmapD) } @Test fun evictAll() { val cache = PlatformLruCache(4) cache["a"] = bitmapA cache["b"] = bitmapB cache["c"] = bitmapC cache.clear() assertThat(cache.cache.snapshot()).isEmpty() } @Test fun clearPrefixedKey() { val cache = PlatformLruCache(3) cache["Hello\nAlice!"] = bitmapA cache["Hello\nBob!"] = bitmapB cache["Hello\nEve!"] = bitmapC cache["Hellos\nWorld!"] = bitmapD cache.clearKeyUri("Hello") assertThat(cache.cache.snapshot()).hasSize(1) assertThat(cache.cache.snapshot()).containsKey("Hellos\nWorld!") } @Test fun invalidate() { val cache = PlatformLruCache(3) cache["Hello\nAlice!"] = bitmapA assertThat(cache.size()).isEqualTo(1) cache.clearKeyUri("Hello") assertThat(cache.size()).isEqualTo(0) } @Test fun overMaxSizeDoesNotClear() { val cache = PlatformLruCache(16) val size4 = Bitmap.createBitmap(2, 2, ALPHA_8) val size16 = Bitmap.createBitmap(4, 4, ALPHA_8) val size25 = Bitmap.createBitmap(5, 5, ALPHA_8) cache["4"] = size4 expectedPutCount++ assertHit(cache, "4", size4) cache["16"] = size16 expectedPutCount++ expectedEvictionCount++ // size4 was evicted. assertMiss(cache, "4") assertHit(cache, "16", size16) cache["25"] = size25 assertHit(cache, "16", size16) assertMiss(cache, "25") assertThat(cache.size()).isEqualTo(16) } @Test fun overMaxSizeRemovesExisting() { val cache = PlatformLruCache(20) val size4 = Bitmap.createBitmap(2, 2, ALPHA_8) val size16 = Bitmap.createBitmap(4, 4, ALPHA_8) val size25 = Bitmap.createBitmap(5, 5, ALPHA_8) cache["small"] = size4 expectedPutCount++ assertHit(cache, "small", size4) cache["big"] = size16 expectedPutCount++ assertHit(cache, "small", size4) assertHit(cache, "big", size16) cache["big"] = size25 assertHit(cache, "small", size4) assertMiss(cache, "big") assertThat(cache.size()).isEqualTo(4) } private fun assertHit(cache: PlatformLruCache, key: String, value: Bitmap) { assertThat(cache[key]).isEqualTo(value) expectedHitCount++ assertStatistics(cache) } private fun assertMiss(cache: PlatformLruCache, key: String) { assertThat(cache[key]).isNull() expectedMissCount++ assertStatistics(cache) } private fun assertStatistics(cache: PlatformLruCache) { assertThat(cache.putCount()).isEqualTo(expectedPutCount) assertThat(cache.hitCount()).isEqualTo(expectedHitCount) assertThat(cache.missCount()).isEqualTo(expectedMissCount) assertThat(cache.evictionCount()).isEqualTo(expectedEvictionCount) } @OptIn(ExperimentalStdlibApi::class) private fun assertSnapshot(cache: PlatformLruCache, vararg keysAndValues: Any) { val actualKeysAndValues = buildList { cache.cache.snapshot().forEach { (key, value) -> add(key) add(value.bitmap) } } // assert using lists because order is important for LRUs assertThat(actualKeysAndValues).isEqualTo(listOf(*keysAndValues)) } } ================================================ FILE: picasso/src/androidTest/java/com/squareup/picasso3/TestUtils.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.net.Uri object TestUtils { val URI_1: Uri = Uri.parse("http://example.com/1.png") } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/Action.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import com.squareup.picasso3.RequestHandler.Result internal abstract class Action( val picasso: Picasso, val request: Request ) { var willReplay = false var cancelled = false abstract fun complete(result: Result) abstract fun error(e: Exception) abstract fun getTarget(): Any? open fun cancel() { cancelled = true } val tag: Any get() = request.tag ?: this } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/AssetRequestHandler.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.ContentResolver import android.content.Context import android.content.res.AssetManager import com.squareup.picasso3.BitmapUtils.decodeStream import com.squareup.picasso3.Picasso.LoadedFrom.DISK import okio.source internal class AssetRequestHandler(private val context: Context) : RequestHandler() { private val lock = Any() @Volatile private var assetManager: AssetManager? = null override fun canHandleRequest(data: Request): Boolean { val uri = data.uri return uri != null && ContentResolver.SCHEME_FILE == uri.scheme && uri.pathSegments.isNotEmpty() && ANDROID_ASSET == uri.pathSegments[0] } override fun load( picasso: Picasso, request: Request, callback: Callback ) { initializeIfFirstTime() var signaledCallback = false try { assetManager!!.open(getFilePath(request)) .source() .use { source -> val bitmap = decodeStream(source, request) signaledCallback = true callback.onSuccess(Result.Bitmap(bitmap, DISK)) } } catch (e: Exception) { if (!signaledCallback) { callback.onError(e) } } } @Initializer private fun initializeIfFirstTime() { if (assetManager == null) { synchronized(lock) { if (assetManager == null) { assetManager = context.assets } } } } companion object { private const val ANDROID_ASSET = "android_asset" private const val ASSET_PREFIX_LENGTH = "${ContentResolver.SCHEME_FILE}:///$ANDROID_ASSET/".length fun getFilePath(request: Request): String { val uri = checkNotNull(request.uri) return uri.toString() .substring(ASSET_PREFIX_LENGTH) } } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/BaseDispatcher.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.Manifest.permission.ACCESS_NETWORK_STATE import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.Intent.ACTION_AIRPLANE_MODE_CHANGED import android.content.IntentFilter import android.net.ConnectivityManager import android.net.ConnectivityManager.CONNECTIVITY_ACTION import android.net.NetworkInfo import android.os.Handler import android.util.Log import androidx.annotation.CallSuper import androidx.annotation.MainThread import androidx.core.content.ContextCompat import com.squareup.picasso3.BitmapHunter.Companion.forRequest import com.squareup.picasso3.MemoryPolicy.Companion.shouldWriteToMemoryCache import com.squareup.picasso3.NetworkPolicy.NO_CACHE import com.squareup.picasso3.NetworkRequestHandler.ContentLengthException import com.squareup.picasso3.RequestHandler.Result.Bitmap import com.squareup.picasso3.Utils.OWNER_DISPATCHER import com.squareup.picasso3.Utils.VERB_CANCELED import com.squareup.picasso3.Utils.VERB_DELIVERED import com.squareup.picasso3.Utils.VERB_ENQUEUED import com.squareup.picasso3.Utils.VERB_IGNORED import com.squareup.picasso3.Utils.VERB_PAUSED import com.squareup.picasso3.Utils.VERB_REPLAYING import com.squareup.picasso3.Utils.VERB_RETRYING import com.squareup.picasso3.Utils.getLogIdsForHunter import com.squareup.picasso3.Utils.hasPermission import com.squareup.picasso3.Utils.isAirplaneModeOn import com.squareup.picasso3.Utils.log import java.util.WeakHashMap internal abstract class BaseDispatcher internal constructor( private val context: Context, private val mainThreadHandler: Handler, private val cache: PlatformLruCache ) : Dispatcher { @get:JvmName("-hunterMap") internal val hunterMap = mutableMapOf() @get:JvmName("-failedActions") internal val failedActions = WeakHashMap() @get:JvmName("-pausedActions") internal val pausedActions = WeakHashMap() @get:JvmName("-pausedTags") internal val pausedTags = mutableSetOf() @get:JvmName("-receiver") internal val receiver: NetworkBroadcastReceiver @get:JvmName("-airplaneMode") @set:JvmName("-airplaneMode") internal var airplaneMode = isAirplaneModeOn(context) private val scansNetworkChanges: Boolean init { scansNetworkChanges = hasPermission(context, ACCESS_NETWORK_STATE) receiver = NetworkBroadcastReceiver(this) receiver.register() } @CallSuper override fun shutdown() { // Unregister network broadcast receiver on the main thread. mainThreadHandler.post { receiver.unregister() } } fun performSubmit(action: Action, dismissFailed: Boolean = true) { if (action.tag in pausedTags) { pausedActions[action.getTarget()] = action if (action.picasso.isLoggingEnabled) { log( owner = OWNER_DISPATCHER, verb = VERB_PAUSED, logId = action.request.logId(), extras = "because tag '${action.tag}' is paused" ) } return } var hunter = hunterMap[action.request.key] if (hunter != null) { hunter.attach(action) return } if (isShutdown()) { if (action.picasso.isLoggingEnabled) { log( owner = OWNER_DISPATCHER, verb = VERB_IGNORED, logId = action.request.logId(), extras = "because shut down" ) } return } hunter = forRequest(action.picasso, this, cache, action) dispatchSubmit(hunter) hunterMap[action.request.key] = hunter if (dismissFailed) { failedActions.remove(action.getTarget()) } if (action.picasso.isLoggingEnabled) { log(owner = OWNER_DISPATCHER, verb = VERB_ENQUEUED, logId = action.request.logId()) } } fun performCancel(action: Action) { val key = action.request.key val hunter = hunterMap[key] if (hunter != null) { hunter.detach(action) if (hunter.cancel()) { hunterMap.remove(key) if (action.picasso.isLoggingEnabled) { log(OWNER_DISPATCHER, VERB_CANCELED, action.request.logId()) } } } if (action.tag in pausedTags) { pausedActions.remove(action.getTarget()) if (action.picasso.isLoggingEnabled) { log( owner = OWNER_DISPATCHER, verb = VERB_CANCELED, logId = action.request.logId(), extras = "because paused request got canceled" ) } } val remove = failedActions.remove(action.getTarget()) if (remove != null && remove.picasso.isLoggingEnabled) { log(OWNER_DISPATCHER, VERB_CANCELED, remove.request.logId(), "from replaying") } } fun performPauseTag(tag: Any) { // Trying to pause a tag that is already paused. if (!pausedTags.add(tag)) { return } // Go through all active hunters and detach/pause the requests // that have the paused tag. val iterator = hunterMap.values.iterator() while (iterator.hasNext()) { val hunter = iterator.next() val loggingEnabled = hunter.picasso.isLoggingEnabled val single = hunter.action val joined = hunter.actions val hasMultiple = !joined.isNullOrEmpty() // Hunter has no requests, bail early. if (single == null && !hasMultiple) { continue } if (single != null && single.tag == tag) { hunter.detach(single) pausedActions[single.getTarget()] = single if (loggingEnabled) { log( owner = OWNER_DISPATCHER, verb = VERB_PAUSED, logId = single.request.logId(), extras = "because tag '$tag' was paused" ) } } if (joined != null) { for (i in joined.indices.reversed()) { val action = joined[i] if (action.tag != tag) { continue } hunter.detach(action) pausedActions[action.getTarget()] = action if (loggingEnabled) { log( owner = OWNER_DISPATCHER, verb = VERB_PAUSED, logId = action.request.logId(), extras = "because tag '$tag' was paused" ) } } } // Check if the hunter can be cancelled in case all its requests // had the tag being paused here. if (hunter.cancel()) { iterator.remove() if (loggingEnabled) { log( owner = OWNER_DISPATCHER, verb = VERB_CANCELED, logId = getLogIdsForHunter(hunter), extras = "all actions paused" ) } } } } fun performResumeTag(tag: Any) { // Trying to resume a tag that is not paused. if (!pausedTags.remove(tag)) { return } val batch = mutableListOf() val iterator = pausedActions.values.iterator() while (iterator.hasNext()) { val action = iterator.next() if (action.tag == tag) { batch += action iterator.remove() } } if (batch.isNotEmpty()) { dispatchBatchResumeMain(batch) } } @SuppressLint("MissingPermission") fun performRetry(hunter: BitmapHunter) { if (hunter.isCancelled) return if (isShutdown()) { performError(hunter) return } var networkInfo: NetworkInfo? = null if (scansNetworkChanges) { val connectivityManager = ContextCompat.getSystemService(context, ConnectivityManager::class.java) if (connectivityManager != null) { networkInfo = connectivityManager.activeNetworkInfo } } if (hunter.shouldRetry(airplaneMode, networkInfo)) { if (hunter.picasso.isLoggingEnabled) { log( owner = OWNER_DISPATCHER, verb = VERB_RETRYING, logId = getLogIdsForHunter(hunter) ) } if (hunter.exception is ContentLengthException) { hunter.data = hunter.data.newBuilder().networkPolicy(NO_CACHE).build() } dispatchSubmit(hunter) } else { performError(hunter) // Mark for replay only if we observe network info changes and support replay. if (scansNetworkChanges && hunter.supportsReplay()) { markForReplay(hunter) } } } fun performComplete(hunter: BitmapHunter) { if (shouldWriteToMemoryCache(hunter.data.memoryPolicy)) { val result = hunter.result if (result != null) { if (result is Bitmap) { val bitmap = result.bitmap cache[hunter.key] = bitmap } } } hunterMap.remove(hunter.key) deliver(hunter) } fun performError(hunter: BitmapHunter) { hunterMap.remove(hunter.key) deliver(hunter) } fun performAirplaneModeChange(airplaneMode: Boolean) { this.airplaneMode = airplaneMode } fun performNetworkStateChange(info: NetworkInfo?) { // Intentionally check only if isConnected() here before we flush out failed actions. if (info != null && info.isConnected) { flushFailedActions() } } @MainThread fun performCompleteMain(hunter: BitmapHunter) { hunter.picasso.complete(hunter) } @MainThread fun performBatchResumeMain(batch: List) { for (i in batch.indices) { val action = batch[i] action.picasso.resumeAction(action) } } private fun flushFailedActions() { if (failedActions.isNotEmpty()) { val iterator = failedActions.values.iterator() while (iterator.hasNext()) { val action = iterator.next() iterator.remove() if (action.picasso.isLoggingEnabled) { log( owner = OWNER_DISPATCHER, verb = VERB_REPLAYING, logId = action.request.logId() ) } performSubmit(action, false) } } } private fun markForReplay(hunter: BitmapHunter) { hunter.action?.let { markForReplay(it) } hunter.actions?.forEach { markForReplay(it) } } private fun markForReplay(action: Action) { val target = action.getTarget() action.willReplay = true failedActions[target] = action } private fun deliver(hunter: BitmapHunter) { if (hunter.isCancelled) { return } val result = hunter.result if (result != null) { if (result is Bitmap) { val bitmap = result.bitmap bitmap.prepareToDraw() } } dispatchCompleteMain(hunter) logDelivery(hunter) } private fun logDelivery(bitmapHunter: BitmapHunter) { val picasso = bitmapHunter.picasso if (picasso.isLoggingEnabled) { log( owner = OWNER_DISPATCHER, verb = VERB_DELIVERED, logId = getLogIdsForHunter(bitmapHunter) ) } } internal class NetworkBroadcastReceiver( private val dispatcher: BaseDispatcher ) : BroadcastReceiver() { fun register() { val filter = IntentFilter() filter.addAction(ACTION_AIRPLANE_MODE_CHANGED) if (dispatcher.scansNetworkChanges) { filter.addAction(CONNECTIVITY_ACTION) } dispatcher.context.registerReceiver(this, filter) } fun unregister() { dispatcher.context.unregisterReceiver(this) } @SuppressLint("MissingPermission") override fun onReceive(context: Context, intent: Intent?) { // On some versions of Android this may be called with a null Intent, // also without extras (getExtras() == null), in such case we use defaults. if (intent == null) { return } when (intent.action) { ACTION_AIRPLANE_MODE_CHANGED -> { if (!intent.hasExtra(EXTRA_AIRPLANE_STATE)) { return // No airplane state, ignore it. Should we query Utils.isAirplaneModeOn? } dispatcher.dispatchAirplaneModeChange(intent.getBooleanExtra(EXTRA_AIRPLANE_STATE, false)) } CONNECTIVITY_ACTION -> { val connectivityManager = ContextCompat.getSystemService(context, ConnectivityManager::class.java) val networkInfo = try { connectivityManager!!.activeNetworkInfo } catch (re: RuntimeException) { Log.w(TAG, "System UI crashed, ignoring attempt to change network state.") return } if (networkInfo == null) { Log.w( TAG, "No default network is currently active, ignoring attempt to change network state." ) return } dispatcher.dispatchNetworkStateChange(networkInfo) } } } internal companion object { const val EXTRA_AIRPLANE_STATE = "state" } } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/BitmapHunter.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.net.NetworkInfo import com.squareup.picasso3.MemoryPolicy.Companion.shouldReadFromMemoryCache import com.squareup.picasso3.Picasso.LoadedFrom import com.squareup.picasso3.RequestHandler.Result.Bitmap import com.squareup.picasso3.Utils.OWNER_HUNTER import com.squareup.picasso3.Utils.THREAD_PREFIX import com.squareup.picasso3.Utils.VERB_DECODED import com.squareup.picasso3.Utils.VERB_EXECUTING import com.squareup.picasso3.Utils.VERB_JOINED import com.squareup.picasso3.Utils.VERB_REMOVED import com.squareup.picasso3.Utils.VERB_TRANSFORMED import com.squareup.picasso3.Utils.getLogIdsForHunter import com.squareup.picasso3.Utils.log import java.io.IOException import java.io.InterruptedIOException import java.util.concurrent.CountDownLatch import java.util.concurrent.Future import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import kotlinx.coroutines.Job internal open class BitmapHunter( val picasso: Picasso, private val dispatcher: Dispatcher, private val cache: PlatformLruCache, action: Action, val requestHandler: RequestHandler ) : Runnable { val sequence: Int = SEQUENCE_GENERATOR.incrementAndGet() var priority: Picasso.Priority = action.request.priority var data: Request = action.request val key: String = action.request.key var retryCount: Int = requestHandler.retryCount var action: Action? = action private set var actions: MutableList? = null private set var future: Future<*>? = null var job: Job? = null var result: RequestHandler.Result? = null private set var exception: Exception? = null private set val isCancelled: Boolean get() = future?.isCancelled ?: job?.isCancelled ?: false override fun run() { val originalName = Thread.currentThread().name try { Thread.currentThread().name = getName() if (picasso.isLoggingEnabled) { log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this)) } result = hunt() dispatcher.dispatchComplete(this) } catch (e: IOException) { exception = e if (retryCount > 0) { dispatcher.dispatchRetry(this) } else { dispatcher.dispatchFailed(this) } } catch (e: Exception) { exception = e dispatcher.dispatchFailed(this) } finally { Thread.currentThread().name = originalName } } fun getName() = NAME_BUILDER.get()!!.also { val name = data.name it.ensureCapacity(THREAD_PREFIX.length + name.length) it.replace(THREAD_PREFIX.length, it.length, name) }.toString() fun hunt(): Bitmap? { if (shouldReadFromMemoryCache(data.memoryPolicy)) { cache[key]?.let { bitmap -> picasso.cacheHit() if (picasso.isLoggingEnabled) { log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache") } return Bitmap(bitmap, LoadedFrom.MEMORY) } } if (retryCount == 0) { data = data.newBuilder().networkPolicy(NetworkPolicy.OFFLINE).build() } val resultReference = AtomicReference() val exceptionReference = AtomicReference() val latch = CountDownLatch(1) try { requestHandler.load( picasso = picasso, request = data, callback = object : RequestHandler.Callback { override fun onSuccess(result: RequestHandler.Result?) { resultReference.set(result) latch.countDown() } override fun onError(t: Throwable) { exceptionReference.set(t) latch.countDown() } } ) latch.await() } catch (ie: InterruptedException) { val interruptedIoException = InterruptedIOException() interruptedIoException.initCause(ie) throw interruptedIoException } exceptionReference.get()?.let { throwable -> when (throwable) { is IOException, is Error, is RuntimeException -> throw throwable else -> throw RuntimeException(throwable) } } val result = resultReference.get() as? Bitmap ?: return null val bitmap = result.bitmap if (picasso.isLoggingEnabled) { log(OWNER_HUNTER, VERB_DECODED, data.logId()) } picasso.bitmapDecoded(bitmap) val transformations = ArrayList(data.transformations.size + 1) if (data.needsMatrixTransform() || result.exifRotation != 0) { transformations += MatrixTransformation(data) } transformations += data.transformations val transformedResult = applyTransformations(picasso, data, transformations, result) ?: return null val transformedBitmap = transformedResult.bitmap picasso.bitmapTransformed(transformedBitmap) return transformedResult } fun attach(action: Action) { val loggingEnabled = picasso.isLoggingEnabled val request = action.request if (this.action == null) { this.action = action if (loggingEnabled) { if (actions.isNullOrEmpty()) { log(OWNER_HUNTER, VERB_JOINED, request.logId(), "to empty hunter") } else { log(OWNER_HUNTER, VERB_JOINED, request.logId(), getLogIdsForHunter(this, "to ")) } } return } if (actions == null) { actions = ArrayList(3) } actions!!.add(action) if (loggingEnabled) { log(OWNER_HUNTER, VERB_JOINED, request.logId(), getLogIdsForHunter(this, "to ")) } val actionPriority = action.request.priority if (actionPriority.ordinal > priority.ordinal) { priority = actionPriority } } fun detach(action: Action) { val detached = when { this.action === action -> { this.action = null true } else -> actions?.remove(action) ?: false } // The action being detached had the highest priority. Update this // hunter's priority with the remaining actions. if (detached && action.request.priority == priority) { priority = computeNewPriority() } if (picasso.isLoggingEnabled) { log(OWNER_HUNTER, VERB_REMOVED, action.request.logId(), getLogIdsForHunter(this, "from ")) } } fun cancel(): Boolean = action == null && actions.isNullOrEmpty() && future?.cancel(false) ?: job?.let { it.cancel() true } ?: false fun shouldRetry(airplaneMode: Boolean, info: NetworkInfo?): Boolean { val hasRetries = retryCount > 0 if (!hasRetries) { return false } retryCount-- return requestHandler.shouldRetry(airplaneMode, info) } fun supportsReplay(): Boolean = requestHandler.supportsReplay() private fun computeNewPriority(): Picasso.Priority { val hasMultiple = actions?.isNotEmpty() ?: false val hasAny = action != null || hasMultiple // Hunter has no requests, low priority. if (!hasAny) { return Picasso.Priority.LOW } var newPriority = action?.request?.priority ?: Picasso.Priority.LOW actions?.let { actions -> // Index-based loop to avoid allocating an iterator. for (i in actions.indices) { val priority = actions[i].request.priority if (priority.ordinal > newPriority.ordinal) { newPriority = priority } } } return newPriority } companion object { internal val NAME_BUILDER: ThreadLocal = object : ThreadLocal() { override fun initialValue(): StringBuilder = StringBuilder(THREAD_PREFIX) } val SEQUENCE_GENERATOR = AtomicInteger() internal val ERRORING_HANDLER: RequestHandler = object : RequestHandler() { override fun canHandleRequest(data: Request): Boolean = true override fun load(picasso: Picasso, request: Request, callback: Callback) { callback.onError(IllegalStateException("Unrecognized type of request: $request")) } } fun forRequest( picasso: Picasso, dispatcher: Dispatcher, cache: PlatformLruCache, action: Action ): BitmapHunter { val request = action.request val requestHandlers = picasso.requestHandlers // Index-based loop to avoid allocating an iterator. for (i in requestHandlers.indices) { val requestHandler = requestHandlers[i] if (requestHandler.canHandleRequest(request)) { return BitmapHunter(picasso, dispatcher, cache, action, requestHandler) } } return BitmapHunter(picasso, dispatcher, cache, action, ERRORING_HANDLER) } fun applyTransformations( picasso: Picasso, data: Request, transformations: List, result: Bitmap ): Bitmap? { var res = result for (i in transformations.indices) { val transformation = transformations[i] val newResult = try { val transformedResult = transformation.transform(res) if (picasso.isLoggingEnabled) { log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from transformations") } transformedResult } catch (e: RuntimeException) { Picasso.HANDLER.post { throw RuntimeException( "Transformation ${transformation.key()} crashed with exception.", e ) } return null } val bitmap = newResult.bitmap if (bitmap.isRecycled) { Picasso.HANDLER.post { throw IllegalStateException( "Transformation ${transformation.key()} returned a recycled Bitmap." ) } return null } res = newResult } return res } } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/BitmapTarget.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap import android.graphics.drawable.Drawable import com.squareup.picasso3.Picasso.LoadedFrom /** * Represents an arbitrary listener for image loading. * * Objects implementing this class **must** have a working implementation of * [Object.equals] and [Object.hashCode] for proper storage internally. * Instances of this interface will also be compared to determine if view recycling is occurring. * It is recommended that you add this interface directly on to a custom view type when using in an * adapter to ensure correct recycling behavior. */ interface BitmapTarget { /** * Callback when an image has been successfully loaded. * * **Note:** You must not recycle the bitmap. */ fun onBitmapLoaded( bitmap: Bitmap, from: LoadedFrom ) /** * Callback indicating the image could not be successfully loaded. * * **Note:** The passed [Drawable] may be `null` if none has been * specified via [RequestCreator.error] or [RequestCreator.error]. */ fun onBitmapFailed( e: Exception, errorDrawable: Drawable? ) /** * Callback invoked right before your request is submitted. * * * **Note:** The passed [Drawable] may be `null` if none has been * specified via [RequestCreator.placeholder] or [RequestCreator.placeholder]. */ fun onPrepareLoad(placeHolderDrawable: Drawable?) } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/BitmapTargetAction.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import com.squareup.picasso3.RequestHandler.Result import com.squareup.picasso3.RequestHandler.Result.Bitmap import java.lang.ref.WeakReference internal class BitmapTargetAction( picasso: Picasso, target: BitmapTarget, data: Request, private val errorDrawable: Drawable?, @DrawableRes val errorResId: Int ) : Action(picasso, data) { private val targetReference = WeakReference(target) override fun complete(result: Result) { val target = targetReference.get() ?: return if (result is Bitmap) { val bitmap = result.bitmap target.onBitmapLoaded(bitmap, result.loadedFrom) check(!bitmap.isRecycled) { "Target callback must not recycle bitmap!" } } } override fun error(e: Exception) { val target = targetReference.get() ?: return val drawable = if (errorResId != 0) { ContextCompat.getDrawable(picasso.context, errorResId) } else { errorDrawable } target.onBitmapFailed(e, drawable) } override fun getTarget(): BitmapTarget? { return targetReference.get() } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/BitmapUtils.kt ================================================ /* * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.annotation.SuppressLint import android.content.Context import android.content.res.Resources import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.ImageDecoder import android.os.Build.VERSION import android.util.TypedValue import androidx.annotation.DrawableRes import androidx.annotation.RequiresApi import okio.Buffer import okio.BufferedSource import okio.ForwardingSource import okio.Source import okio.buffer import java.io.IOException import java.nio.ByteBuffer import kotlin.math.max import kotlin.math.min internal object BitmapUtils { /** * Lazily create [BitmapFactory.Options] based in given * [Request], only instantiating them if needed. */ fun createBitmapOptions(data: Request): BitmapFactory.Options? { val justBounds = data.hasSize() return if (justBounds || data.config != null) { BitmapFactory.Options().apply { inJustDecodeBounds = justBounds if (data.config != null) { inPreferredConfig = data.config } } } else { null } } fun requiresInSampleSize(options: BitmapFactory.Options?): Boolean { return options != null && options.inJustDecodeBounds } fun calculateInSampleSize( reqWidth: Int, reqHeight: Int, options: BitmapFactory.Options, request: Request ) { calculateInSampleSize( reqWidth, reqHeight, options.outWidth, options.outHeight, options, request ) } fun shouldResize( onlyScaleDown: Boolean, inWidth: Int, inHeight: Int, targetWidth: Int, targetHeight: Int ): Boolean { return ( !onlyScaleDown || targetWidth != 0 && inWidth > targetWidth || targetHeight != 0 && inHeight > targetHeight ) } fun calculateInSampleSize( requestWidth: Int, requestHeight: Int, width: Int, height: Int, options: BitmapFactory.Options, request: Request ) { options.inSampleSize = ratio(requestWidth, requestHeight, width, height, request) options.inJustDecodeBounds = false } /** * Decode a byte stream into a Bitmap. This method will take into account additional information * about the supplied request in order to do the decoding efficiently (such as through leveraging * `inSampleSize`). */ fun decodeStream(source: Source, request: Request): Bitmap { val exceptionCatchingSource = ExceptionCatchingSource(source) val bufferedSource = exceptionCatchingSource.buffer() val bitmap = if (VERSION.SDK_INT >= 28) { decodeStreamP(request, bufferedSource) } else { decodeStreamPreP(request, bufferedSource) } exceptionCatchingSource.throwIfCaught() return bitmap } @RequiresApi(28) @SuppressLint("Override") private fun decodeStreamP(request: Request, bufferedSource: BufferedSource): Bitmap { val imageSource = ImageDecoder.createSource(ByteBuffer.wrap(bufferedSource.readByteArray())) return decodeImageSource(imageSource, request) } private fun decodeStreamPreP(request: Request, bufferedSource: BufferedSource): Bitmap { val isWebPFile = Utils.isWebPFile(bufferedSource) val options = createBitmapOptions(request) val calculateSize = requiresInSampleSize(options) // We decode from a byte array because, when decoding a WebP network stream, BitmapFactory // throws a JNI Exception, so we workaround by decoding a byte array. val bitmap = if (isWebPFile) { val bytes = bufferedSource.readByteArray() if (calculateSize) { BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) calculateInSampleSize(request.targetWidth, request.targetHeight, options!!, request) } BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) } else { if (calculateSize) { BitmapFactory.decodeStream(bufferedSource.peek().inputStream(), null, options) calculateInSampleSize(request.targetWidth, request.targetHeight, options!!, request) } BitmapFactory.decodeStream(bufferedSource.inputStream(), null, options) } if (bitmap == null) { // Treat null as an IO exception, we will eventually retry. throw IOException("Failed to decode bitmap.") } return bitmap } fun decodeResource(context: Context, request: Request): Bitmap { val resources = Utils.getResources(context, request) val id = Utils.getResourceId(resources, request) return if (VERSION.SDK_INT >= 28) { decodeResourceP(resources, id, request) } else { decodeResourcePreP(resources, id, request) } } @RequiresApi(28) private fun decodeResourceP(resources: Resources, @DrawableRes id: Int, request: Request): Bitmap { val imageSource = ImageDecoder.createSource(resources, id) return decodeImageSource(imageSource, request) } private fun decodeResourcePreP(resources: Resources, @DrawableRes id: Int, request: Request): Bitmap { val options = createBitmapOptions(request) if (requiresInSampleSize(options)) { BitmapFactory.decodeResource(resources, id, options) calculateInSampleSize(request.targetWidth, request.targetHeight, options!!, request) } return BitmapFactory.decodeResource(resources, id, options) } @RequiresApi(28) private fun decodeImageSource(imageSource: ImageDecoder.Source, request: Request): Bitmap { return ImageDecoder.decodeBitmap(imageSource) { imageDecoder, imageInfo, source -> imageDecoder.isMutableRequired = true if (request.hasSize()) { val size = imageInfo.size val width = size.width val height = size.height val targetWidth = request.targetWidth val targetHeight = request.targetHeight if (shouldResize(request.onlyScaleDown, width, height, targetWidth, targetHeight)) { val ratio = ratio(targetWidth, targetHeight, width, height, request) imageDecoder.setTargetSize(width / ratio, height / ratio) } } } } private fun ratio( requestWidth: Int, requestHeight: Int, width: Int, height: Int, request: Request ): Int = if (height > requestHeight || width > requestWidth) { val ratio = if (requestHeight == 0) { width / requestWidth } else if (requestWidth == 0) { height / requestHeight } else { val heightRatio = height / requestHeight val widthRatio = width / requestWidth if (request.centerInside) { max(heightRatio, widthRatio) } else { min(heightRatio, widthRatio) } } if (ratio != 0) ratio else 1 } else { 1 } fun isXmlResource(resources: Resources, @DrawableRes drawableId: Int): Boolean { val typedValue = TypedValue() resources.getValue(drawableId, typedValue, true) val file = typedValue.string return file != null && file.toString().endsWith(".xml") } internal class ExceptionCatchingSource(delegate: Source) : ForwardingSource(delegate) { var thrownException: IOException? = null override fun read(sink: Buffer, byteCount: Long): Long { return try { super.read(sink, byteCount) } catch (e: IOException) { thrownException = e throw e } } fun throwIfCaught() { if (thrownException is IOException) { // TODO: Log when Android returns a non-null Bitmap after swallowing an IOException. // TODO: https://github.com/square/picasso/issues/2003/ throw thrownException as IOException } } } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/Callback.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 interface Callback { fun onSuccess() fun onError(t: Throwable) open class EmptyCallback : Callback { override fun onSuccess() = Unit override fun onError(t: Throwable) = Unit } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/ContactsPhotoRequestHandler.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.ContentResolver import android.content.Context import android.content.UriMatcher import android.net.Uri import android.provider.ContactsContract import android.provider.ContactsContract.Contacts import com.squareup.picasso3.BitmapUtils.decodeStream import com.squareup.picasso3.Picasso.LoadedFrom.DISK import okio.Source import okio.source import java.io.FileNotFoundException import java.io.IOException internal class ContactsPhotoRequestHandler(private val context: Context) : RequestHandler() { companion object { /** A lookup uri (e.g. content://com.android.contacts/contacts/lookup/3570i61d948d30808e537) */ private const val ID_LOOKUP = 1 /** A contact thumbnail uri (e.g. content://com.android.contacts/contacts/38/photo) */ private const val ID_THUMBNAIL = 2 /** A contact uri (e.g. content://com.android.contacts/contacts/38) */ private const val ID_CONTACT = 3 /** * A contact display photo (high resolution) uri * (e.g. content://com.android.contacts/display_photo/5) */ private const val ID_DISPLAY_PHOTO = 4 private val matcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", ID_LOOKUP) addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", ID_LOOKUP) addURI(ContactsContract.AUTHORITY, "contacts/#/photo", ID_THUMBNAIL) addURI(ContactsContract.AUTHORITY, "contacts/#", ID_CONTACT) addURI(ContactsContract.AUTHORITY, "display_photo/#", ID_DISPLAY_PHOTO) } } override fun canHandleRequest(data: Request): Boolean { val uri = data.uri return uri != null && ContentResolver.SCHEME_CONTENT == uri.scheme && Contacts.CONTENT_URI.host == uri.host && matcher.match(data.uri) != UriMatcher.NO_MATCH } override fun load( picasso: Picasso, request: Request, callback: Callback ) { var signaledCallback = false try { val requestUri = checkNotNull(request.uri) val source = getSource(requestUri) val bitmap = decodeStream(source, request) signaledCallback = true callback.onSuccess(Result.Bitmap(bitmap, DISK)) } catch (e: Exception) { if (!signaledCallback) { callback.onError(e) } } } private fun getSource(uri: Uri): Source { val contentResolver = context.contentResolver val input = when (matcher.match(uri)) { ID_LOOKUP -> { val contactUri = Contacts.lookupContact(contentResolver, uri) ?: throw IOException("no contact found") Contacts.openContactPhotoInputStream(contentResolver, contactUri, true) } ID_CONTACT -> Contacts.openContactPhotoInputStream(contentResolver, uri, true) ID_THUMBNAIL, ID_DISPLAY_PHOTO -> contentResolver.openInputStream(uri) else -> throw IllegalStateException("Invalid uri: $uri") } ?: throw FileNotFoundException("can't open input stream, uri: $uri") return input.source() } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/ContentStreamRequestHandler.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.ContentResolver import android.content.Context import android.net.Uri import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface.ORIENTATION_NORMAL import androidx.exifinterface.media.ExifInterface.TAG_ORIENTATION import com.squareup.picasso3.Picasso.LoadedFrom.DISK import okio.Source import okio.source import java.io.FileNotFoundException internal open class ContentStreamRequestHandler(val context: Context) : RequestHandler() { override fun canHandleRequest(data: Request): Boolean = ContentResolver.SCHEME_CONTENT == data.uri?.scheme ?: false override fun load( picasso: Picasso, request: Request, callback: Callback ) { var signaledCallback = false try { val requestUri = checkNotNull(request.uri) val source = getSource(requestUri) val bitmap = BitmapUtils.decodeStream(source, request) val exifRotation = getExifOrientation(requestUri) signaledCallback = true callback.onSuccess(Result.Bitmap(bitmap, DISK, exifRotation)) } catch (e: Exception) { if (!signaledCallback) { callback.onError(e) } } } fun getSource(uri: Uri): Source { val contentResolver = context.contentResolver val inputStream = contentResolver.openInputStream(uri) ?: throw FileNotFoundException("can't open input stream, uri: $uri") return inputStream.source() } protected open fun getExifOrientation(uri: Uri): Int { val contentResolver = context.contentResolver contentResolver.openInputStream(uri)?.use { input -> return ExifInterface(input).getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL) } ?: throw FileNotFoundException("can't open input stream, uri: $uri") } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/DeferredRequestCreator.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.view.View import android.view.View.OnAttachStateChangeListener import android.view.ViewTreeObserver import android.widget.ImageView import java.lang.ref.WeakReference internal class DeferredRequestCreator( private val creator: RequestCreator, target: ImageView, internal var callback: Callback? ) : ViewTreeObserver.OnPreDrawListener, OnAttachStateChangeListener { private val targetReference = WeakReference(target) init { target.addOnAttachStateChangeListener(this) // Only add the pre-draw listener if the view is already attached. // See: https://github.com/square/picasso/issues/1321 if (target.windowToken != null) { onViewAttachedToWindow(target) } } override fun onViewAttachedToWindow(view: View) { view.viewTreeObserver.addOnPreDrawListener(this) } override fun onViewDetachedFromWindow(view: View) { view.viewTreeObserver.removeOnPreDrawListener(this) } override fun onPreDraw(): Boolean { val target = targetReference.get() ?: return true val vto = target.viewTreeObserver if (!vto.isAlive) { return true } val width = target.width val height = target.height if (width <= 0 || height <= 0) { return true } target.removeOnAttachStateChangeListener(this) vto.removeOnPreDrawListener(this) targetReference.clear() creator.unfit().resize(width, height).into(target, callback) return true } fun cancel() { creator.clearTag() callback = null val target = targetReference.get() ?: return targetReference.clear() target.removeOnAttachStateChangeListener(this) val vto = target.viewTreeObserver if (vto.isAlive) { vto.removeOnPreDrawListener(this) } } val tag: Any? get() = creator.tag } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/Dispatcher.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.net.NetworkInfo internal interface Dispatcher { fun shutdown() fun dispatchSubmit(action: Action) fun dispatchCancel(action: Action) fun dispatchPauseTag(tag: Any) fun dispatchResumeTag(tag: Any) fun dispatchComplete(hunter: BitmapHunter) fun dispatchRetry(hunter: BitmapHunter) fun dispatchFailed(hunter: BitmapHunter) fun dispatchNetworkStateChange(info: NetworkInfo) fun dispatchAirplaneModeChange(airplaneMode: Boolean) fun dispatchSubmit(hunter: BitmapHunter) fun dispatchCompleteMain(hunter: BitmapHunter) fun dispatchBatchResumeMain(batch: MutableList) fun isShutdown(): Boolean companion object { const val RETRY_DELAY = 500L } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/DrawableLoader.kt ================================================ /* * Copyright (C) 2018 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes internal fun interface DrawableLoader { fun load(@DrawableRes resId: Int): Drawable? } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/DrawableTarget.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.drawable.Drawable import com.squareup.picasso3.Picasso.LoadedFrom /** * Represents an arbitrary listener for image loading. * * Objects implementing this class **must** have a working implementation of * [Object.equals] and [Object.hashCode] for proper storage internally. * Instances of this interface will also be compared to determine if view recycling is occurring. * It is recommended that you add this interface directly on to a custom view type when using in an * adapter to ensure correct recycling behavior. */ interface DrawableTarget { /** * Callback when an image has been successfully loaded. * */ fun onDrawableLoaded( drawable: Drawable, from: LoadedFrom ) /** * Callback indicating the image could not be successfully loaded. * * **Note:** The passed [Drawable] may be `null` if none has been * specified via [RequestCreator.error]. */ fun onDrawableFailed( e: Exception, errorDrawable: Drawable? ) /** * Callback invoked right before your request is submitted. * * * **Note:** The passed [Drawable] may be `null` if none has been * specified via [RequestCreator.placeholder]. */ fun onPrepareLoad(placeHolderDrawable: Drawable?) } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/DrawableTargetAction.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import com.squareup.picasso3.RequestHandler.Result import com.squareup.picasso3.RequestHandler.Result.Bitmap internal class DrawableTargetAction( picasso: Picasso, private val target: DrawableTarget, data: Request, private val noFade: Boolean, private val placeholderDrawable: Drawable?, private val errorDrawable: Drawable?, @DrawableRes val errorResId: Int ) : Action(picasso, data) { override fun complete(result: Result) { if (result is Bitmap) { val bitmap = result.bitmap target.onDrawableLoaded( PicassoDrawable( context = picasso.context, bitmap = bitmap, placeholder = placeholderDrawable, loadedFrom = result.loadedFrom, noFade = noFade, debugging = picasso.indicatorsEnabled ), result.loadedFrom ) check(!bitmap.isRecycled) { "Target callback must not recycle bitmap!" } } } override fun error(e: Exception) { val drawable = if (errorResId != 0) { ContextCompat.getDrawable(picasso.context, errorResId) } else { errorDrawable } target.onDrawableFailed(e, drawable) } override fun getTarget(): Any { return target } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/EventListener.kt ================================================ /* * Copyright (C) 2019 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap import java.io.Closeable interface EventListener : Closeable { fun cacheMaxSize(maxSize: Int) fun cacheSize(size: Int) fun cacheHit() fun cacheMiss() fun downloadFinished(size: Long) fun bitmapDecoded(bitmap: Bitmap) fun bitmapTransformed(bitmap: Bitmap) override fun close() = Unit } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/FetchAction.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import com.squareup.picasso3.RequestHandler.Result internal class FetchAction( picasso: Picasso, data: Request, private var callback: Callback? ) : Action(picasso, data) { override fun complete(result: Result) { callback?.onSuccess() } override fun error(e: Exception) { callback?.onError(e) } override fun getTarget() = this override fun cancel() { super.cancel() callback = null } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/FileRequestHandler.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.ContentResolver import android.content.Context import android.net.Uri import androidx.exifinterface.media.ExifInterface import com.squareup.picasso3.BitmapUtils.decodeStream import com.squareup.picasso3.Picasso.LoadedFrom.DISK import java.io.FileNotFoundException internal class FileRequestHandler(context: Context) : ContentStreamRequestHandler(context) { override fun canHandleRequest(data: Request): Boolean { val uri = data.uri return uri != null && ContentResolver.SCHEME_FILE == uri.scheme } override fun load( picasso: Picasso, request: Request, callback: Callback ) { var signaledCallback = false try { val requestUri = checkNotNull(request.uri) val source = getSource(requestUri) val bitmap = decodeStream(source, request) val exifRotation = getExifOrientation(requestUri) signaledCallback = true callback.onSuccess(Result.Bitmap(bitmap, DISK, exifRotation)) } catch (e: Exception) { if (!signaledCallback) { callback.onError(e) } } } override fun getExifOrientation(uri: Uri): Int { val path = uri.path ?: throw FileNotFoundException("path == null, uri: $uri") return ExifInterface(path).getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL ) } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/GetAction.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import com.squareup.picasso3.RequestHandler.Result internal class GetAction( picasso: Picasso, data: Request ) : Action(picasso, data) { override fun complete(result: Result) = Unit override fun error(e: Exception) = Unit override fun getTarget() = throw AssertionError() } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/HandlerDispatcher.kt ================================================ /* * Copyright (C) 2023 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.net.NetworkInfo import android.os.Handler import android.os.HandlerThread import android.os.Looper import android.os.Message import android.os.Process.THREAD_PRIORITY_BACKGROUND import com.squareup.picasso3.Picasso.Priority.HIGH import com.squareup.picasso3.Utils.flushStackLocalLeaks import java.util.concurrent.ExecutorService internal class HandlerDispatcher internal constructor( context: Context, @get:JvmName("-service") val service: ExecutorService, mainThreadHandler: Handler, cache: PlatformLruCache ) : BaseDispatcher(context, mainThreadHandler, cache) { private val dispatcherThread: DispatcherThread private val handler: Handler private val mainHandler: Handler init { dispatcherThread = DispatcherThread() dispatcherThread.start() val dispatcherThreadLooper = dispatcherThread.looper flushStackLocalLeaks(dispatcherThreadLooper) handler = DispatcherHandler(dispatcherThreadLooper, this) mainHandler = MainDispatcherHandler(mainThreadHandler.looper, this) } override fun shutdown() { super.shutdown() // Shutdown the thread pool only if it is the one created by Picasso. (service as? PicassoExecutorService)?.shutdown() dispatcherThread.quit() } override fun dispatchSubmit(action: Action) { handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action)) } override fun dispatchCancel(action: Action) { handler.sendMessage(handler.obtainMessage(REQUEST_CANCEL, action)) } override fun dispatchPauseTag(tag: Any) { handler.sendMessage(handler.obtainMessage(TAG_PAUSE, tag)) } override fun dispatchResumeTag(tag: Any) { handler.sendMessage(handler.obtainMessage(TAG_RESUME, tag)) } override fun dispatchComplete(hunter: BitmapHunter) { handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter)) } override fun dispatchRetry(hunter: BitmapHunter) { handler.sendMessageDelayed(handler.obtainMessage(HUNTER_RETRY, hunter), RETRY_DELAY) } override fun dispatchFailed(hunter: BitmapHunter) { handler.sendMessage(handler.obtainMessage(HUNTER_DECODE_FAILED, hunter)) } override fun dispatchNetworkStateChange(info: NetworkInfo) { handler.sendMessage(handler.obtainMessage(NETWORK_STATE_CHANGE, info)) } override fun dispatchAirplaneModeChange(airplaneMode: Boolean) { handler.sendMessage( handler.obtainMessage( AIRPLANE_MODE_CHANGE, if (airplaneMode) AIRPLANE_MODE_ON else AIRPLANE_MODE_OFF, 0 ) ) } override fun dispatchSubmit(hunter: BitmapHunter) { hunter.future = service.submit(hunter) } override fun dispatchCompleteMain(hunter: BitmapHunter) { val message = mainHandler.obtainMessage(HUNTER_COMPLETE, hunter) if (hunter.priority == HIGH) { mainHandler.sendMessageAtFrontOfQueue(message) } else { mainHandler.sendMessage(message) } } override fun dispatchBatchResumeMain(batch: MutableList) { mainHandler.sendMessage(mainHandler.obtainMessage(REQUEST_BATCH_RESUME, batch)) } override fun isShutdown() = service.isShutdown private class DispatcherHandler( looper: Looper, private val dispatcher: HandlerDispatcher ) : Handler(looper) { override fun handleMessage(msg: Message) { when (msg.what) { REQUEST_SUBMIT -> { val action = msg.obj as Action dispatcher.performSubmit(action) } REQUEST_CANCEL -> { val action = msg.obj as Action dispatcher.performCancel(action) } TAG_PAUSE -> { val tag = msg.obj dispatcher.performPauseTag(tag) } TAG_RESUME -> { val tag = msg.obj dispatcher.performResumeTag(tag) } HUNTER_COMPLETE -> { val hunter = msg.obj as BitmapHunter dispatcher.performComplete(hunter) } HUNTER_RETRY -> { val hunter = msg.obj as BitmapHunter dispatcher.performRetry(hunter) } HUNTER_DECODE_FAILED -> { val hunter = msg.obj as BitmapHunter dispatcher.performError(hunter) } NETWORK_STATE_CHANGE -> { val info = msg.obj as NetworkInfo dispatcher.performNetworkStateChange(info) } AIRPLANE_MODE_CHANGE -> { dispatcher.performAirplaneModeChange(msg.arg1 == AIRPLANE_MODE_ON) } else -> { dispatcher.mainHandler.post { throw AssertionError("Unknown handler message received: ${msg.what}") } } } } } private class MainDispatcherHandler( looper: Looper, val dispatcher: HandlerDispatcher ) : Handler(looper) { override fun handleMessage(msg: Message) { when (msg.what) { HUNTER_COMPLETE -> { val hunter = msg.obj as BitmapHunter dispatcher.performCompleteMain(hunter) } REQUEST_BATCH_RESUME -> { val batch = msg.obj as List dispatcher.performBatchResumeMain(batch) } else -> throw AssertionError("Unknown handler message received: " + msg.what) } } } internal class DispatcherThread : HandlerThread( Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND ) internal companion object { private const val RETRY_DELAY = 500L private const val AIRPLANE_MODE_ON = 1 private const val AIRPLANE_MODE_OFF = 0 private const val REQUEST_SUBMIT = 1 private const val REQUEST_CANCEL = 2 private const val HUNTER_COMPLETE = 4 private const val HUNTER_RETRY = 5 private const val HUNTER_DECODE_FAILED = 6 private const val NETWORK_STATE_CHANGE = 9 private const val AIRPLANE_MODE_CHANGE = 10 private const val TAG_PAUSE = 11 private const val TAG_RESUME = 12 private const val REQUEST_BATCH_RESUME = 13 private const val DISPATCHER_THREAD_NAME = "Dispatcher" } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/ImageViewAction.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import android.widget.ImageView import androidx.annotation.DrawableRes import com.squareup.picasso3.RequestHandler.Result import java.lang.ref.WeakReference internal class ImageViewAction( picasso: Picasso, target: ImageView, data: Request, val errorDrawable: Drawable?, @DrawableRes val errorResId: Int, val noFade: Boolean, var callback: Callback? ) : Action(picasso, data) { private val targetReference = WeakReference(target) override fun complete(result: Result) { val target = targetReference.get() ?: return PicassoDrawable.setResult(target, picasso.context, result, noFade, picasso.indicatorsEnabled) callback?.onSuccess() } override fun error(e: Exception) { val target = targetReference.get() ?: return val placeholder = target.drawable if (placeholder is Animatable) { (placeholder as Animatable).stop() } if (errorResId != 0) { target.setImageResource(errorResId) } else if (errorDrawable != null) { target.setImageDrawable(errorDrawable) } callback?.onError(e) } override fun getTarget(): ImageView? { return targetReference.get() } override fun cancel() { super.cancel() callback = null } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/Initializer.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import androidx.annotation.RestrictTo import androidx.annotation.RestrictTo.Scope.LIBRARY import kotlin.annotation.AnnotationRetention.SOURCE @Retention(SOURCE) @RestrictTo(LIBRARY) annotation class Initializer ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/InternalCoroutineDispatcher.kt ================================================ /* * Copyright (C) 2023 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.net.NetworkInfo import android.os.Handler import com.squareup.picasso3.Dispatcher.Companion.RETRY_DELAY import com.squareup.picasso3.Picasso.Priority.HIGH import com.squareup.picasso3.Utils.OWNER_DISPATCHER import com.squareup.picasso3.Utils.VERB_CANCELED import com.squareup.picasso3.Utils.log import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch internal class InternalCoroutineDispatcher internal constructor( context: Context, mainThreadHandler: Handler, cache: PlatformLruCache, val mainContext: CoroutineContext, val backgroundContext: CoroutineContext ) : BaseDispatcher(context, mainThreadHandler, cache) { private val scope = CoroutineScope(SupervisorJob() + backgroundContext) private val channel = Channel<() -> Unit>(capacity = Channel.UNLIMITED) init { // Using a channel to enforce sequential access for this class' internal state scope.launch { channel.receiveAsFlow().collect { it.invoke() } } } override fun shutdown() { super.shutdown() channel.close() scope.cancel() } override fun dispatchSubmit(action: Action) { channel.trySend { performSubmit(action) } } override fun dispatchCancel(action: Action) { channel.trySend { performCancel(action) } } override fun dispatchPauseTag(tag: Any) { channel.trySend { performPauseTag(tag) } } override fun dispatchResumeTag(tag: Any) { channel.trySend { performResumeTag(tag) } } override fun dispatchComplete(hunter: BitmapHunter) { channel.trySend { performComplete(hunter) } } override fun dispatchRetry(hunter: BitmapHunter) { scope.launch { delay(RETRY_DELAY) channel.send { performRetry(hunter) } } } override fun dispatchFailed(hunter: BitmapHunter) { channel.trySend { performError(hunter) } } override fun dispatchNetworkStateChange(info: NetworkInfo) { channel.trySend { performNetworkStateChange(info) } } override fun dispatchAirplaneModeChange(airplaneMode: Boolean) { channel.trySend { performAirplaneModeChange(airplaneMode) } } override fun dispatchCompleteMain(hunter: BitmapHunter) { scope.launch(mainContext) { performCompleteMain(hunter) } } override fun dispatchBatchResumeMain(batch: MutableList) { scope.launch(mainContext) { performBatchResumeMain(batch) } } override fun dispatchSubmit(hunter: BitmapHunter) { val highPriority = hunter.action?.request?.priority == HIGH val context = if (highPriority) EmptyCoroutineContext else mainContext scope.launch(context) { channel.trySend { if (hunter.action != null) { hunter.job = scope.launch(CoroutineName(hunter.getName())) { hunter.run() } } else { hunterMap.remove(hunter.key) if (hunter.picasso.isLoggingEnabled) { log(OWNER_DISPATCHER, VERB_CANCELED, hunter.key) } } } } } override fun isShutdown() = !scope.isActive } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/MatrixTransformation.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap.createBitmap import android.graphics.Matrix import android.os.Build.VERSION import android.view.Gravity import androidx.annotation.VisibleForTesting import androidx.exifinterface.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL import androidx.exifinterface.media.ExifInterface.ORIENTATION_FLIP_VERTICAL import androidx.exifinterface.media.ExifInterface.ORIENTATION_ROTATE_180 import androidx.exifinterface.media.ExifInterface.ORIENTATION_ROTATE_270 import androidx.exifinterface.media.ExifInterface.ORIENTATION_ROTATE_90 import androidx.exifinterface.media.ExifInterface.ORIENTATION_TRANSPOSE import androidx.exifinterface.media.ExifInterface.ORIENTATION_TRANSVERSE import com.squareup.picasso3.BitmapUtils.shouldResize import com.squareup.picasso3.RequestHandler.Result.Bitmap import kotlin.math.ceil import kotlin.math.cos import kotlin.math.floor import kotlin.math.max import kotlin.math.min import kotlin.math.sin internal class MatrixTransformation(private val data: Request) : Transformation { override fun transform(source: Bitmap): Bitmap { val sourceBitmap = source.bitmap val transformedBitmap = transformResult(data, sourceBitmap, source.exifRotation) return Bitmap(transformedBitmap, source.loadedFrom, source.exifRotation) } override fun key() = "matrixTransformation()" internal companion object { @VisibleForTesting @JvmName("-transformResult") internal fun transformResult( data: Request, result: android.graphics.Bitmap, exifOrientation: Int ): android.graphics.Bitmap { val inWidth = result.width val inHeight = result.height val onlyScaleDown = data.onlyScaleDown var drawX = 0 var drawY = 0 var drawWidth = inWidth var drawHeight = inHeight val matrix = Matrix() if (data.needsMatrixTransform() || exifOrientation != 0) { var targetWidth = data.targetWidth var targetHeight = data.targetHeight val targetRotation = data.rotationDegrees if (targetRotation != 0f) { val cosR = cos(Math.toRadians(targetRotation.toDouble())) val sinR = sin(Math.toRadians(targetRotation.toDouble())) if (data.hasRotationPivot) { matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY) // Recalculate dimensions after rotation around pivot point val x1T = data.rotationPivotX * (1.0 - cosR) + data.rotationPivotY * sinR val y1T = data.rotationPivotY * (1.0 - cosR) - data.rotationPivotX * sinR val x2T = x1T + data.targetWidth * cosR val y2T = y1T + data.targetWidth * sinR val x3T = x1T + data.targetWidth * cosR - data.targetHeight * sinR val y3T = y1T + data.targetWidth * sinR + data.targetHeight * cosR val x4T = x1T - data.targetHeight * sinR val y4T = y1T + data.targetHeight * cosR val maxX = max(x4T, max(x3T, max(x1T, x2T))) val minX = min(x4T, min(x3T, min(x1T, x2T))) val maxY = max(y4T, max(y3T, max(y1T, y2T))) val minY = min(y4T, min(y3T, min(y1T, y2T))) targetWidth = floor(maxX - minX).toInt() targetHeight = floor(maxY - minY).toInt() } else { matrix.setRotate(targetRotation) // Recalculate dimensions after rotation (around origin) val x1T = 0.0 val y1T = 0.0 val x2T = data.targetWidth * cosR val y2T = data.targetWidth * sinR val x3T = data.targetWidth * cosR - data.targetHeight * sinR val y3T = data.targetWidth * sinR + data.targetHeight * cosR val x4T = -(data.targetHeight * sinR) val y4T = data.targetHeight * cosR val maxX = max(x4T, max(x3T, max(x1T, x2T))) val minX = min(x4T, min(x3T, min(x1T, x2T))) val maxY = max(y4T, max(y3T, max(y1T, y2T))) val minY = min(y4T, min(y3T, min(y1T, y2T))) targetWidth = floor(maxX - minX).toInt() targetHeight = floor(maxY - minY).toInt() } } // EXIf interpretation should be done before cropping in case the dimensions need to // be recalculated; SDK 28+ uses ImageDecoder which handles EXIF orientation if (exifOrientation != 0 && VERSION.SDK_INT < 28) { val exifRotation = getExifRotation(exifOrientation) val exifTranslation = getExifTranslation(exifOrientation) if (exifRotation != 0) { matrix.preRotate(exifRotation.toFloat()) if (exifRotation == 90 || exifRotation == 270) { // Recalculate dimensions after exif rotation val tmpHeight = targetHeight targetHeight = targetWidth targetWidth = tmpHeight } } if (exifTranslation != 1) { matrix.postScale(exifTranslation.toFloat(), 1f) } } if (data.centerCrop) { // Keep aspect ratio if one dimension is set to 0 val widthRatio = if (targetWidth != 0) { targetWidth / inWidth.toFloat() } else { targetHeight / inHeight.toFloat() } val heightRatio = if (targetHeight != 0) { targetHeight / inHeight.toFloat() } else { targetWidth / inWidth.toFloat() } val scaleX: Float val scaleY: Float if (widthRatio > heightRatio) { val newSize = ceil((inHeight * (heightRatio / widthRatio)).toDouble()).toInt() drawY = if (data.centerCropGravity and Gravity.TOP == Gravity.TOP) { 0 } else if (data.centerCropGravity and Gravity.BOTTOM == Gravity.BOTTOM) { inHeight - newSize } else { (inHeight - newSize) / 2 } drawHeight = newSize scaleX = widthRatio scaleY = targetHeight / drawHeight.toFloat() } else if (widthRatio < heightRatio) { val newSize = ceil((inWidth * (widthRatio / heightRatio)).toDouble()).toInt() drawX = if (data.centerCropGravity and Gravity.LEFT == Gravity.LEFT) { 0 } else if (data.centerCropGravity and Gravity.RIGHT == Gravity.RIGHT) { inWidth - newSize } else { (inWidth - newSize) / 2 } drawWidth = newSize scaleX = targetWidth / drawWidth.toFloat() scaleY = heightRatio } else { drawX = 0 drawWidth = inWidth scaleY = heightRatio scaleX = scaleY } if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) { matrix.preScale(scaleX, scaleY) } } else if (data.centerInside) { // Keep aspect ratio if one dimension is set to 0 val widthRatio = if (targetWidth != 0) targetWidth / inWidth.toFloat() else targetHeight / inHeight.toFloat() val heightRatio = if (targetHeight != 0) targetHeight / inHeight.toFloat() else targetWidth / inWidth.toFloat() val scale = if (widthRatio < heightRatio) widthRatio else heightRatio if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) { matrix.preScale(scale, scale) } } else if ((targetWidth != 0 || targetHeight != 0) && // (targetWidth != inWidth || targetHeight != inHeight) ) { // If an explicit target size has been specified and they do not match the results bounds, // pre-scale the existing matrix appropriately. // Keep aspect ratio if one dimension is set to 0. val sx = if (targetWidth != 0) targetWidth / inWidth.toFloat() else targetHeight / inHeight.toFloat() val sy = if (targetHeight != 0) targetHeight / inHeight.toFloat() else targetWidth / inWidth.toFloat() if (shouldResize(onlyScaleDown, inWidth, inHeight, targetWidth, targetHeight)) { matrix.preScale(sx, sy) } } } val transformedResult = createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true) if (transformedResult != result) { result.recycle() } return transformedResult } @Suppress("MemberVisibilityCanBePrivate") @JvmName("-getExifRotation") internal fun getExifRotation(orientation: Int) = when (orientation) { ORIENTATION_ROTATE_90, ORIENTATION_TRANSPOSE -> 90 ORIENTATION_ROTATE_180, ORIENTATION_FLIP_VERTICAL -> 180 ORIENTATION_ROTATE_270, ORIENTATION_TRANSVERSE -> 270 else -> 0 } @Suppress("MemberVisibilityCanBePrivate") @JvmName("-getExifTranslation") internal fun getExifTranslation(orientation: Int) = when (orientation) { ORIENTATION_FLIP_HORIZONTAL, ORIENTATION_FLIP_VERTICAL, ORIENTATION_TRANSPOSE, ORIENTATION_TRANSVERSE -> -1 else -> 1 } } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/MediaStoreRequestHandler.kt ================================================ /* * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.ContentResolver import android.content.ContentUris import android.content.Context import android.provider.MediaStore import android.provider.MediaStore.Video import com.squareup.picasso3.BitmapUtils.calculateInSampleSize import com.squareup.picasso3.BitmapUtils.createBitmapOptions import com.squareup.picasso3.BitmapUtils.decodeStream import com.squareup.picasso3.Picasso.LoadedFrom internal class MediaStoreRequestHandler(context: Context) : ContentStreamRequestHandler(context) { override fun canHandleRequest(data: Request): Boolean { val uri = data.uri return uri != null && ContentResolver.SCHEME_CONTENT == uri.scheme && MediaStore.AUTHORITY == uri.authority } override fun load(picasso: Picasso, request: Request, callback: Callback) { var signaledCallback = false try { val contentResolver = context.contentResolver val requestUri = checkNotNull(request.uri, { "request.uri == null" }) val exifOrientation = getExifOrientation(requestUri) val mimeType = contentResolver.getType(requestUri) val isVideo = mimeType != null && mimeType.startsWith("video/") if (request.hasSize()) { val picassoKind = getPicassoKind(request.targetWidth, request.targetHeight) if (!isVideo && picassoKind == PicassoKind.FULL) { val source = getSource(requestUri) val bitmap = decodeStream(source, request) signaledCallback = true callback.onSuccess(Result.Bitmap(bitmap, LoadedFrom.DISK, exifOrientation)) return } val id = ContentUris.parseId(requestUri) val options = checkNotNull(createBitmapOptions(request), { "options == null" }) options.inJustDecodeBounds = true calculateInSampleSize( request.targetWidth, request.targetHeight, picassoKind.width, picassoKind.height, options, request ) val bitmap = if (isVideo) { // Since MediaStore doesn't provide the full screen kind thumbnail, we use the mini kind // instead which is the largest thumbnail size can be fetched from MediaStore. val kind = if (picassoKind == PicassoKind.FULL) Video.Thumbnails.MINI_KIND else picassoKind.androidKind Video.Thumbnails.getThumbnail(contentResolver, id, kind, options) } else { MediaStore.Images.Thumbnails.getThumbnail( contentResolver, id, picassoKind.androidKind, options ) } if (bitmap != null) { signaledCallback = true callback.onSuccess(Result.Bitmap(bitmap, LoadedFrom.DISK, exifOrientation)) return } } val source = getSource(requestUri) val bitmap = decodeStream(source, request) signaledCallback = true callback.onSuccess(Result.Bitmap(bitmap, LoadedFrom.DISK, exifOrientation)) } catch (e: Exception) { if (!signaledCallback) { callback.onError(e) } } } internal enum class PicassoKind(val androidKind: Int, val width: Int, val height: Int) { MICRO(MediaStore.Images.Thumbnails.MICRO_KIND, 96, 96), MINI(MediaStore.Images.Thumbnails.MINI_KIND, 512, 384), FULL(MediaStore.Images.Thumbnails.FULL_SCREEN_KIND, -1, -1) } companion object { fun getPicassoKind(targetWidth: Int, targetHeight: Int): PicassoKind { return if (targetWidth <= PicassoKind.MICRO.width && targetHeight <= PicassoKind.MICRO.height) { PicassoKind.MICRO } else if (targetWidth <= PicassoKind.MINI.width && targetHeight <= PicassoKind.MINI.height) { PicassoKind.MINI } else { PicassoKind.FULL } } } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/MemoryPolicy.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 /** Designates the policy to use when dealing with memory cache. */ enum class MemoryPolicy(val index: Int) { /** Skips memory cache lookup when processing a request. */ NO_CACHE(1 shl 0), /** * Skips storing the final result into memory cache. Useful for one-off requests * to avoid evicting other bitmaps from the cache. */ NO_STORE(1 shl 1); companion object { @JvmStatic fun shouldReadFromMemoryCache(memoryPolicy: Int) = memoryPolicy and NO_CACHE.index == 0 @JvmStatic fun shouldWriteToMemoryCache(memoryPolicy: Int) = memoryPolicy and NO_STORE.index == 0 } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/NetworkPolicy.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 /** Designates the policy to use for network requests. */ enum class NetworkPolicy(val index: Int) { /** * Skips checking the disk cache and forces loading through the network. */ NO_CACHE(1 shl 0), /** * Skips storing the result into the disk cache. */ NO_STORE(1 shl 1), /** * Forces the request through the disk cache only, skipping network. */ OFFLINE(1 shl 2); companion object { @JvmStatic fun shouldReadFromDiskCache(networkPolicy: Int) = networkPolicy and NO_CACHE.index == 0 @JvmStatic fun shouldWriteToDiskCache(networkPolicy: Int) = networkPolicy and NO_STORE.index == 0 @JvmStatic fun isOfflineOnly(networkPolicy: Int) = networkPolicy and OFFLINE.index != 0 } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/NetworkRequestHandler.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.net.NetworkInfo import com.squareup.picasso3.BitmapUtils.decodeStream import com.squareup.picasso3.NetworkPolicy.Companion.isOfflineOnly import com.squareup.picasso3.NetworkPolicy.Companion.shouldReadFromDiskCache import com.squareup.picasso3.NetworkPolicy.Companion.shouldWriteToDiskCache import com.squareup.picasso3.Picasso.LoadedFrom.DISK import com.squareup.picasso3.Picasso.LoadedFrom.NETWORK import okhttp3.CacheControl import okhttp3.Call import okhttp3.Response import java.io.IOException internal class NetworkRequestHandler( private val callFactory: Call.Factory ) : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { val uri = data.uri ?: return false val scheme = uri.scheme return SCHEME_HTTP.equals(scheme, ignoreCase = true) || SCHEME_HTTPS.equals(scheme, ignoreCase = true) } override fun load(picasso: Picasso, request: Request, callback: Callback) { val callRequest = createRequest(request) callFactory .newCall(callRequest) .enqueue(object : okhttp3.Callback { override fun onResponse(call: Call, response: Response) { if (!response.isSuccessful) { callback.onError(ResponseException(response.code, request.networkPolicy)) return } // Cache response is only null when the response comes fully from the network. Both // completely cached and conditionally cached responses will have a non-null cache // response. val loadedFrom = if (response.cacheResponse == null) NETWORK else DISK // Sometimes response content length is zero when requests are being replayed. // Haven't found root cause to this but retrying the request seems safe to do so. val body = response.body if (loadedFrom == DISK && body!!.contentLength() == 0L) { body.close() callback.onError( ContentLengthException("Received response with 0 content-length header.") ) return } if (loadedFrom == NETWORK && body!!.contentLength() > 0) { picasso.downloadFinished(body.contentLength()) } try { val bitmap = decodeStream(body!!.source(), request) callback.onSuccess(Result.Bitmap(bitmap, loadedFrom)) } catch (e: IOException) { body!!.close() callback.onError(e) } } override fun onFailure(call: Call, e: IOException) { callback.onError(e) } }) } override val retryCount: Int get() = 2 override fun shouldRetry(airplaneMode: Boolean, info: NetworkInfo?): Boolean = info == null || info.isConnected override fun supportsReplay(): Boolean = true private fun createRequest(request: Request): okhttp3.Request { var cacheControl: CacheControl? = null val networkPolicy = request.networkPolicy if (networkPolicy != 0) { cacheControl = if (isOfflineOnly(networkPolicy)) { CacheControl.FORCE_CACHE } else { val builder = CacheControl.Builder() if (!shouldReadFromDiskCache(networkPolicy)) { builder.noCache() } if (!shouldWriteToDiskCache(networkPolicy)) { builder.noStore() } builder.build() } } val uri = checkNotNull(request.uri) { "request.uri == null" } val builder = okhttp3.Request.Builder().url(uri.toString()) if (cacheControl != null) { builder.cacheControl(cacheControl) } val requestHeaders = request.headers if (requestHeaders != null) { builder.headers(requestHeaders) } return builder.build() } internal class ContentLengthException(message: String) : RuntimeException(message) internal class ResponseException( val code: Int, val networkPolicy: Int ) : RuntimeException("HTTP $code") private companion object { private const val SCHEME_HTTP = "http" private const val SCHEME_HTTPS = "https" } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/Picasso.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.graphics.Bitmap import android.graphics.Bitmap.Config import android.graphics.Color import android.net.Uri import android.os.Handler import android.os.Looper import android.widget.ImageView import android.widget.RemoteViews import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.squareup.picasso3.MemoryPolicy.Companion.shouldReadFromMemoryCache import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.RemoteViewsAction.RemoteViewsTarget import com.squareup.picasso3.RequestHandler.Result import com.squareup.picasso3.Utils.OWNER_MAIN import com.squareup.picasso3.Utils.VERB_COMPLETED import com.squareup.picasso3.Utils.VERB_ERRORED import com.squareup.picasso3.Utils.VERB_RESUMED import com.squareup.picasso3.Utils.calculateDiskCacheSize import com.squareup.picasso3.Utils.calculateMemoryCacheSize import com.squareup.picasso3.Utils.checkMain import com.squareup.picasso3.Utils.createDefaultCacheDir import com.squareup.picasso3.Utils.log import okhttp3.Cache import okhttp3.Call import okhttp3.OkHttpClient import java.io.File import java.io.IOException import java.util.WeakHashMap import java.util.concurrent.ExecutorService import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers /** * Image downloading, transformation, and caching manager. * * Use [PicassoProvider.get] for a global singleton instance * or construct your own instance with [Picasso.Builder]. */ @OptIn(ExperimentalStdlibApi::class) class Picasso internal constructor( @get:JvmName("-context") internal val context: Context, @get:JvmName("-dispatcher") internal val dispatcher: Dispatcher, @get:JvmName("-callFactory") internal val callFactory: Call.Factory, private val closeableCache: Cache?, @get:JvmName("-cache") internal val cache: PlatformLruCache, @get:JvmName("-listener") internal val listener: Listener?, requestTransformers: List, extraRequestHandlers: List, eventListeners: List, @get:JvmName("-defaultBitmapConfig") internal val defaultBitmapConfig: Config?, /** Toggle whether to display debug indicators on images. */ var indicatorsEnabled: Boolean, /** * Toggle whether debug logging is enabled. * * **WARNING:** Enabling this will result in excessive object allocation. This should be only * be used for debugging purposes. Do NOT pass `BuildConfig.DEBUG`. */ @Volatile var isLoggingEnabled: Boolean ) : DefaultLifecycleObserver { @get:JvmName("-requestTransformers") internal val requestTransformers: List = requestTransformers.toList() @get:JvmName("-requestHandlers") internal val requestHandlers: List @get:JvmName("-eventListeners") internal val eventListeners: List = eventListeners.toList() @get:JvmName("-targetToAction") internal val targetToAction = WeakHashMap() @get:JvmName("-targetToDeferredRequestCreator") internal val targetToDeferredRequestCreator = WeakHashMap() @get:JvmName("-shutdown") @set:JvmName("-shutdown") internal var shutdown = false init { // Adjust this and Builder(Picasso) as internal handlers are added or removed. val builtInHandlers = 8 requestHandlers = buildList(builtInHandlers + extraRequestHandlers.size) { // ResourceRequestHandler needs to be the first in the list to avoid // forcing other RequestHandlers to perform null checks on request.uri // to cover the (request.resourceId != 0) case. add(ResourceDrawableRequestHandler.create(context)) add(ResourceRequestHandler(context)) addAll(extraRequestHandlers) add(ContactsPhotoRequestHandler(context)) add(MediaStoreRequestHandler(context)) add(ContentStreamRequestHandler(context)) add(AssetRequestHandler(context)) add(FileRequestHandler(context)) add(NetworkRequestHandler(callFactory)) } } override fun onDestroy(owner: LifecycleOwner) { super.onDestroy(owner) cancelAll() } @JvmName("-cancelAll") internal fun cancelAll() { checkMain() val actions = targetToAction.values.toList() for (i in actions.indices) { val target = actions[i].getTarget() ?: continue cancelExistingRequest(target) } val deferredRequestCreators = targetToDeferredRequestCreator.values.toList() for (i in deferredRequestCreators.indices) { deferredRequestCreators[i].cancel() } } /** Cancel any existing requests for the specified target [ImageView]. */ fun cancelRequest(view: ImageView) { // checkMain() is called from cancelExistingRequest() cancelExistingRequest(view) } /** Cancel any existing requests for the specified [BitmapTarget] instance. */ fun cancelRequest(target: BitmapTarget) { // checkMain() is called from cancelExistingRequest() cancelExistingRequest(target) } /** Cancel any existing requests for the specified [DrawableTarget] instance. */ fun cancelRequest(target: DrawableTarget) { // checkMain() is called from cancelExistingRequest() cancelExistingRequest(target) } /** * Cancel any existing requests for the specified [RemoteViews] target with the given [viewId]. */ fun cancelRequest(remoteViews: RemoteViews, @IdRes viewId: Int) { // checkMain() is called from cancelExistingRequest() cancelExistingRequest(RemoteViewsTarget(remoteViews, viewId)) } /** * Cancel any existing requests with given tag. You can set a tag * on new requests with [RequestCreator.tag]. * * @see RequestCreator.tag */ fun cancelTag(tag: Any) { checkMain() val actions = targetToAction.values.toList() for (i in actions.indices) { val action = actions[i] if (tag == action.tag) { val target = action.getTarget() ?: continue cancelExistingRequest(target) } } val deferredRequestCreators = targetToDeferredRequestCreator.values.toList() for (i in deferredRequestCreators.indices) { val deferredRequestCreator = deferredRequestCreators[i] if (tag == deferredRequestCreator.tag) { deferredRequestCreator.cancel() } } } override fun onStop(owner: LifecycleOwner) { super.onStop(owner) pauseAll() } @JvmName("-pauseAll") internal fun pauseAll() { checkMain() val actions = targetToAction.values.toList() for (i in actions.indices) { dispatcher.dispatchPauseTag(actions[i].tag) } val deferredRequestCreators = targetToDeferredRequestCreator.values.toList() for (i in deferredRequestCreators.indices) { val tag = deferredRequestCreators[i].tag if (tag != null) { dispatcher.dispatchPauseTag(tag) } } } /** * Pause existing requests with the given tag. Use [resumeTag] * to resume requests with the given tag. * * @see [resumeTag] * @see RequestCreator.tag */ fun pauseTag(tag: Any) { dispatcher.dispatchPauseTag(tag) } override fun onStart(owner: LifecycleOwner) { resumeAll() } @JvmName("-resumeAll") internal fun resumeAll() { checkMain() val actions = targetToAction.values.toList() for (i in actions.indices) { dispatcher.dispatchResumeTag(actions[i].tag) } val deferredRequestCreators = targetToDeferredRequestCreator.values.toList() for (i in deferredRequestCreators.indices) { val tag = deferredRequestCreators[i].tag if (tag != null) { dispatcher.dispatchResumeTag(tag) } } } /** * Resume paused requests with the given tag. Use [pauseTag] * to pause requests with the given tag. * * @see [pauseTag] * @see RequestCreator.tag */ fun resumeTag(tag: Any) { dispatcher.dispatchResumeTag(tag) } /** * Start an image request using the specified URI. * * Passing `null` as a [uri] will not trigger any request but will set a placeholder, * if one is specified. * * @see #load(File) * @see #load(String) * @see #load(int) */ fun load(uri: Uri?): RequestCreator { return RequestCreator(this, uri, 0) } /** * Start an image request using the specified path. This is a convenience method for calling * [load]. * * This path may be a remote URL, file resource (prefixed with `file:`), content resource * (prefixed with `content:`), or android resource (prefixed with `android.resource:`. * * Passing `null` as a [path] will not trigger any request but will set a * placeholder, if one is specified. * * @throws IllegalArgumentException if [path] is empty or blank string. * @see #load(Uri) * @see #load(File) * @see #load(int) */ fun load(path: String?): RequestCreator { if (path == null) { return RequestCreator(this, null, 0) } require(path.isNotBlank()) { "Path must not be empty." } return load(Uri.parse(path)) } /** * Start an image request using the specified image file. This is a convenience method for * calling [load]. * * Passing `null` as a [file] will not trigger any request but will set a * placeholder, if one is specified. * * Equivalent to calling [load(Uri.fromFile(file))][load]. * * @see #load(Uri) * @see #load(String) * @see #load(int) */ fun load(file: File?): RequestCreator { return if (file == null) { RequestCreator(this, null, 0) } else { load(Uri.fromFile(file)) } } /** * Start an image request using the specified drawable resource ID. * * @see #load(Uri) * @see #load(String) * @see #load(File) */ fun load(@DrawableRes resourceId: Int): RequestCreator { require(resourceId != 0) { "Resource ID must not be zero." } return RequestCreator(this, null, resourceId) } /** * Clear all the bitmaps from the memory cache. */ fun evictAll() { cache.clear() } /** * Invalidate all memory cached images for the specified [uri]. * * @see #invalidate(String) * @see #invalidate(File) */ fun invalidate(uri: Uri?) { if (uri != null) { cache.clearKeyUri(uri.toString()) } } /** * Invalidate all memory cached images for the specified [path]. You can also pass a * [stable key][RequestCreator.stableKey]. * * @see #invalidate(Uri) * @see #invalidate(File) */ fun invalidate(path: String?) { if (path != null) { invalidate(Uri.parse(path)) } } /** * Invalidate all memory cached images for the specified [file]. * * @see #invalidate(Uri) * @see #invalidate(String) */ fun invalidate(file: File) { invalidate(Uri.fromFile(file)) } /** Stops this instance from accepting further requests. */ fun shutdown() { if (shutdown) { return } cache.clear() close() dispatcher.shutdown() try { closeableCache?.close() } catch (ignored: IOException) { } for (deferredRequestCreator in targetToDeferredRequestCreator.values) { deferredRequestCreator.cancel() } targetToAction.clear() targetToDeferredRequestCreator.clear() shutdown = true } @JvmName("-transformRequest") internal fun transformRequest(request: Request): Request { var nextRequest = request for (i in requestTransformers.indices) { val transformer = requestTransformers[i] nextRequest = transformer.transformRequest(nextRequest) } return nextRequest } @JvmName("-defer") internal fun defer(view: ImageView, request: DeferredRequestCreator) { // If there is already a deferred request, cancel it. if (targetToDeferredRequestCreator.containsKey(view)) { cancelExistingRequest(view) } targetToDeferredRequestCreator[view] = request } @JvmName("-enqueueAndSubmit") internal fun enqueueAndSubmit(action: Action) { val target = action.getTarget() ?: return if (targetToAction[target] !== action) { // This will also check we are on the main thread. cancelExistingRequest(target) targetToAction[target] = action } submit(action) } @JvmName("-submit") internal fun submit(action: Action) { dispatcher.dispatchSubmit(action) } @JvmName("-quickMemoryCacheCheck") internal fun quickMemoryCacheCheck(key: String): Bitmap? { val cached = cache[key] if (cached != null) { cacheHit() } else { cacheMiss() } return cached } @JvmName("-complete") internal fun complete(hunter: BitmapHunter) { val single = hunter.action val joined = hunter.actions val hasMultiple = !joined.isNullOrEmpty() val shouldDeliver = single != null || hasMultiple if (!shouldDeliver) { return } val exception = hunter.exception val result = hunter.result single?.let { deliverAction(result, it, exception) } if (joined != null) { for (i in joined.indices) { deliverAction(result, joined[i], exception) } } if (listener != null && exception != null) { listener.onImageLoadFailed(this, hunter.data.uri, exception) } } @JvmName("-resumeAction") internal fun resumeAction(action: Action) { val bitmap = if (shouldReadFromMemoryCache(action.request.memoryPolicy)) { quickMemoryCacheCheck(action.request.key) } else { null } if (bitmap != null) { // Resumed action is cached, complete immediately. deliverAction(Result.Bitmap(bitmap, MEMORY), action, null) if (isLoggingEnabled) { log( owner = OWNER_MAIN, verb = VERB_COMPLETED, logId = action.request.logId(), extras = "from $MEMORY" ) } } else { // Re-submit the action to the executor. enqueueAndSubmit(action) if (isLoggingEnabled) { log( owner = OWNER_MAIN, verb = VERB_RESUMED, logId = action.request.logId() ) } } } private fun deliverAction(result: Result?, action: Action, e: Exception?) { if (action.cancelled) { return } if (!action.willReplay) { targetToAction.remove(action.getTarget()) } if (result != null) { action.complete(result) if (isLoggingEnabled) { log( owner = OWNER_MAIN, verb = VERB_COMPLETED, logId = action.request.logId(), extras = "from ${result.loadedFrom}" ) } } else if (e != null) { action.error(e) if (isLoggingEnabled) { log( owner = OWNER_MAIN, verb = VERB_ERRORED, logId = action.request.logId(), extras = e.message ) } } } private fun cancelExistingRequest(target: Any) { checkMain() val action = targetToAction.remove(target) if (action != null) { action.cancel() dispatcher.dispatchCancel(action) } if (target is ImageView) { val deferredRequestCreator = targetToDeferredRequestCreator.remove(target) deferredRequestCreator?.cancel() } } fun newBuilder(): Builder = Builder(this) /** Fluent API for creating [Picasso] instances. */ class Builder { private val context: Context private var callFactory: Call.Factory? = null private var service: ExecutorService? = null private var mainContext: CoroutineContext? = null private var backgroundContext: CoroutineContext? = null private var cache: PlatformLruCache? = null private var listener: Listener? = null private val requestTransformers = mutableListOf() private val requestHandlers = mutableListOf() private val eventListeners = mutableListOf() private var defaultBitmapConfig: Config? = null private var indicatorsEnabled = false private var loggingEnabled = false /** Start building a new [Picasso] instance. */ constructor(context: Context) { this.context = context.applicationContext } internal constructor(picasso: Picasso) { context = picasso.context callFactory = picasso.callFactory service = (picasso.dispatcher as? HandlerDispatcher)?.service mainContext = (picasso.dispatcher as? InternalCoroutineDispatcher)?.mainContext backgroundContext = (picasso.dispatcher as? InternalCoroutineDispatcher)?.backgroundContext cache = picasso.cache listener = picasso.listener requestTransformers += picasso.requestTransformers // See Picasso(). Removes internal request handlers added before and after custom handlers. val numRequestHandlers = picasso.requestHandlers.size requestHandlers += picasso.requestHandlers.subList(2, numRequestHandlers - 6) eventListeners += picasso.eventListeners defaultBitmapConfig = picasso.defaultBitmapConfig indicatorsEnabled = picasso.indicatorsEnabled loggingEnabled = picasso.isLoggingEnabled } /** * Specify the default [Bitmap.Config] used when decoding images. This can be overridden * on a per-request basis using [RequestCreator.config]. */ fun defaultBitmapConfig(bitmapConfig: Config) = apply { defaultBitmapConfig = bitmapConfig } /** * Specify the HTTP client to be used for network requests. * * Note: Calling [callFactory] overwrites this value. */ fun client(client: OkHttpClient) = apply { callFactory = client } /** * Specify the call factory to be used for network requests. * * Note: Calling [client] overwrites this value. */ fun callFactory(factory: Call.Factory) = apply { callFactory = factory } /** * Specify the executor service for loading images in the background. * * Note: Calling [Picasso.shutdown] will not shutdown supplied executors. */ fun executor(executorService: ExecutorService) = apply { service = executorService } /** * Specify the memory cache size in bytes to use for the most recent images. * A size of 0 disables in-memory caching. */ fun withCacheSize(maxByteCount: Int) = apply { require(maxByteCount >= 0) { "maxByteCount < 0: $maxByteCount" } cache = PlatformLruCache(maxByteCount) } /** Specify a listener for interesting events. */ fun listener(listener: Listener) = apply { this.listener = listener } /** Add a transformer that observes and potentially modify all incoming requests. */ fun addRequestTransformer(transformer: RequestTransformer) = apply { requestTransformers += transformer } /** Register a [RequestHandler]. */ fun addRequestHandler(requestHandler: RequestHandler) = apply { requestHandlers += requestHandler } /** Register a [EventListener]. */ fun addEventListener(eventListener: EventListener) = apply { eventListeners += eventListener } /** Toggle whether to display debug indicators on images. */ fun indicatorsEnabled(enabled: Boolean) = apply { indicatorsEnabled = enabled } /** * Toggle whether debug logging is enabled. * * **WARNING:** Enabling this will result in excessive object allocation. This should be only * be used for debugging purposes. Do NOT pass `BuildConfig.DEBUG`. */ fun loggingEnabled(enabled: Boolean) = apply { loggingEnabled = enabled } /** * Sets the CoroutineDispatchers used internally */ fun dispatchers( mainContext: CoroutineContext = Dispatchers.Main, backgroundContext: CoroutineContext = Dispatchers.IO ) = apply { this.mainContext = mainContext this.backgroundContext = backgroundContext } /** Create the [Picasso] instance. */ fun build(): Picasso { var unsharedCache: okhttp3.Cache? = null if (callFactory == null) { val cacheDir = createDefaultCacheDir(context) val maxSize = calculateDiskCacheSize(cacheDir) unsharedCache = okhttp3.Cache(cacheDir, maxSize) callFactory = OkHttpClient.Builder() .cache(unsharedCache) .build() } if (cache == null) { cache = PlatformLruCache(calculateMemoryCacheSize(context)) } val dispatcher = if (backgroundContext != null) { InternalCoroutineDispatcher(context, HANDLER, cache!!, mainContext!!, backgroundContext!!) } else { if (service == null) { service = PicassoExecutorService() } HandlerDispatcher(context, service!!, HANDLER, cache!!) } return Picasso( context, dispatcher, callFactory!!, unsharedCache, cache!!, listener, requestTransformers, requestHandlers, eventListeners, defaultBitmapConfig, indicatorsEnabled, loggingEnabled ) } } /** Event listener methods **/ @JvmName("-cacheMaxSize") // Prefix with '-' to hide from Java. internal fun cacheMaxSize(maxSize: Int) { val numListeners = eventListeners.size for (i in 0 until numListeners) { eventListeners[i].cacheMaxSize(maxSize) } } @JvmName("-cacheSize") // Prefix with '-' to hide from Java. internal fun cacheSize(size: Int) { val numListeners = eventListeners.size for (i in 0 until numListeners) { eventListeners[i].cacheSize(size) } } @JvmName("-cacheHit") // Prefix with '-' to hide from Java. internal fun cacheHit() { val numListeners = eventListeners.size for (i in 0 until numListeners) { eventListeners[i].cacheHit() } } @JvmName("-cacheMiss") // Prefix with '-' to hide from Java. internal fun cacheMiss() { val numListeners = eventListeners.size for (i in 0 until numListeners) { eventListeners[i].cacheMiss() } } @JvmName("-downloadFinished") // Prefix with '-' to hide from Java. internal fun downloadFinished(size: Long) { val numListeners = eventListeners.size for (i in 0 until numListeners) { eventListeners[i].downloadFinished(size) } } @JvmName("-bitmapDecoded") // Prefix with '-' to hide from Java. internal fun bitmapDecoded(bitmap: Bitmap) { val numListeners = eventListeners.size for (i in 0 until numListeners) { eventListeners[i].bitmapDecoded(bitmap) } } @JvmName("-bitmapTransformed") // Prefix with '-' to hide from Java. internal fun bitmapTransformed(bitmap: Bitmap) { val numListeners = eventListeners.size for (i in 0 until numListeners) { eventListeners[i].bitmapTransformed(bitmap) } } @JvmName("-close") // Prefix with '-' to hide from Java. internal fun close() { val numListeners = eventListeners.size for (i in 0 until numListeners) { eventListeners[i].close() } } /** Callbacks for Picasso events. */ fun interface Listener { /** * Invoked when an image has failed to load. This is useful for reporting image failures to a * remote analytics service, for example. */ fun onImageLoadFailed(picasso: Picasso, uri: Uri?, exception: Exception) } /** * A transformer that is called immediately before every request is submitted. This can be used to * modify any information about a request. * * For example, if you use a CDN you can change the hostname for the image based on the current * location of the user in order to get faster download speeds. */ fun interface RequestTransformer { /** * Transform a request before it is submitted to be processed. * * @return The original request or a new request to replace it. Must not be null. */ fun transformRequest(request: Request): Request } /** * The priority of a request. * * @see RequestCreator.priority */ enum class Priority { LOW, NORMAL, /** * High priority requests will post to the front of main thread's message queue when * they complete loading and their images need to be rendered. */ HIGH } /** Describes where the image was loaded from. */ enum class LoadedFrom(@get:JvmName("-debugColor") internal val debugColor: Int) { MEMORY(Color.GREEN), DISK(Color.BLUE), NETWORK(Color.RED) } internal companion object { @get:JvmName("-handler") internal val HANDLER = Handler(Looper.getMainLooper()) } } const val TAG = "Picasso" ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/PicassoDrawable.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter import android.graphics.Paint import android.graphics.Path import android.graphics.Rect import android.graphics.drawable.Animatable import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.SystemClock import android.widget.ImageView import com.squareup.picasso3.Picasso.LoadedFrom import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.RequestHandler.Result internal class PicassoDrawable( context: Context, bitmap: Bitmap, placeholder: Drawable?, private val loadedFrom: LoadedFrom, noFade: Boolean, private val debugging: Boolean ) : BitmapDrawable(context.resources, bitmap) { private val density: Float = context.resources.displayMetrics.density var placeholder: Drawable? = null var startTimeMillis: Long = 0 var animating = false private var alpha = 0xFF init { val fade = loadedFrom != MEMORY && !noFade if (fade) { this.placeholder = placeholder animating = true startTimeMillis = SystemClock.uptimeMillis() } } override fun draw(canvas: Canvas) { if (!animating) { super.draw(canvas) } else { val normalized = (SystemClock.uptimeMillis() - startTimeMillis) / FADE_DURATION if (normalized >= 1f) { animating = false placeholder = null super.draw(canvas) } else { if (placeholder != null) { placeholder!!.draw(canvas) } // setAlpha will call invalidateSelf and drive the animation. val partialAlpha = (alpha * normalized).toInt() super.setAlpha(partialAlpha) super.draw(canvas) super.setAlpha(alpha) } } if (debugging) { drawDebugIndicator(canvas) } } override fun setAlpha(alpha: Int) { this.alpha = alpha if (placeholder != null) { placeholder!!.alpha = alpha } super.setAlpha(alpha) } override fun setColorFilter(cf: ColorFilter?) { if (placeholder != null) { placeholder!!.colorFilter = cf } super.setColorFilter(cf) } override fun onBoundsChange(bounds: Rect) { if (placeholder != null) { placeholder!!.bounds = bounds } super.onBoundsChange(bounds) } private fun drawDebugIndicator(canvas: Canvas) { DEBUG_PAINT.color = Color.WHITE var path = getTrianglePath(0, 0, (16 * density).toInt()) canvas.drawPath(path, DEBUG_PAINT) DEBUG_PAINT.color = loadedFrom.debugColor path = getTrianglePath(0, 0, (15 * density).toInt()) canvas.drawPath(path, DEBUG_PAINT) } companion object { // Only accessed from main thread. private val DEBUG_PAINT = Paint() private const val FADE_DURATION = 200f // ms /** * Create or update the drawable on the target [ImageView] to display the supplied bitmap * image. */ fun setResult( target: ImageView, context: Context, result: Result, noFade: Boolean, debugging: Boolean ) { val placeholder = target.drawable if (placeholder is Animatable) { (placeholder as Animatable).stop() } if (result is Result.Bitmap) { val bitmap = result.bitmap val loadedFrom = result.loadedFrom val drawable = PicassoDrawable(context, bitmap, placeholder, loadedFrom, noFade, debugging) target.setImageDrawable(drawable) } else { val drawable = (result as Result.Drawable).drawable target.setImageDrawable(drawable) if (drawable is Animatable) { (drawable as Animatable).start() } } } /** * Create or update the drawable on the target [ImageView] to display the supplied * placeholder image. */ fun setPlaceholder(target: ImageView, placeholderDrawable: Drawable?) { target.setImageDrawable(placeholderDrawable) if (target.drawable is Animatable) { (target.drawable as Animatable).start() } } fun getTrianglePath(x1: Int, y1: Int, width: Int): Path { return Path().apply { moveTo(x1.toFloat(), y1.toFloat()) lineTo((x1 + width).toFloat(), y1.toFloat()) lineTo(x1.toFloat(), (y1 + width).toFloat()) } } } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/PicassoExecutorService.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.os.Process import android.os.Process.THREAD_PRIORITY_BACKGROUND import java.util.concurrent.Future import java.util.concurrent.FutureTask import java.util.concurrent.PriorityBlockingQueue import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit.MILLISECONDS /** * The default [java.util.concurrent.ExecutorService] used for new [Picasso] instances. */ class PicassoExecutorService( threadCount: Int = DEFAULT_THREAD_COUNT, threadFactory: ThreadFactory = PicassoThreadFactory() ) : ThreadPoolExecutor( threadCount, threadCount, 0, MILLISECONDS, PriorityBlockingQueue(), threadFactory ) { override fun submit(task: Runnable): Future<*> { val ftask = PicassoFutureTask(task as BitmapHunter) execute(ftask) return ftask } private class PicassoThreadFactory : ThreadFactory { override fun newThread(r: Runnable): Thread = PicassoThread(r) private class PicassoThread(r: Runnable) : Thread(r) { override fun run() { name = Utils.THREAD_IDLE_NAME Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND) super.run() } } } private class PicassoFutureTask(private val hunter: BitmapHunter) : FutureTask(hunter, null), Comparable { override fun compareTo(other: PicassoFutureTask): Int { val p1 = hunter.priority val p2 = other.hunter.priority // High-priority requests are "lesser" so they are sorted to the front. // Equal priorities are sorted by sequence number to provide FIFO ordering. return if (p1 == p2) hunter.sequence - other.hunter.sequence else p2.ordinal - p1.ordinal } } private companion object { private const val DEFAULT_THREAD_COUNT = 3 } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/PlatformLruCache.kt ================================================ /* * Copyright (C) 2018 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap import android.util.LruCache /** A memory cache which uses a least-recently used eviction policy. */ internal class PlatformLruCache(maxByteCount: Int) { /** Create a cache with a given maximum size in bytes. */ val cache = object : LruCache(if (maxByteCount != 0) maxByteCount else 1) { override fun sizeOf( key: String, value: BitmapAndSize ): Int = value.byteCount } operator fun get(key: String): Bitmap? = cache[key]?.bitmap operator fun set( key: String, bitmap: Bitmap ) { val byteCount = bitmap.allocationByteCount // If the bitmap is too big for the cache, don't even attempt to store it. Doing so will cause // the cache to be cleared. Instead just evict an existing element with the same key if it // exists. if (byteCount > maxSize()) { cache.remove(key) return } cache.put(key, BitmapAndSize(bitmap, byteCount)) } fun size(): Int = cache.size() fun maxSize(): Int = cache.maxSize() fun clear() = cache.evictAll() fun clearKeyUri(uri: String) { // Keys are prefixed with a URI followed by '\n'. for (key in cache.snapshot().keys) { if (key.startsWith(uri) && key.length > uri.length && key[uri.length] == Request.KEY_SEPARATOR ) { cache.remove(key) } } } /** Returns the number of times [get] returned a value. */ fun hitCount(): Int = cache.hitCount() /** Returns the number of times [get] returned `null`. */ fun missCount(): Int = cache.missCount() /** Returns the number of times [set] was called. */ fun putCount(): Int = cache.putCount() /** Returns the number of values that have been evicted. */ fun evictionCount(): Int = cache.evictionCount() internal class BitmapAndSize( val bitmap: Bitmap, val byteCount: Int ) } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/RemoteViewsAction.kt ================================================ /* * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.app.Notification import android.app.NotificationManager import android.appwidget.AppWidgetManager import android.widget.RemoteViews import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import com.squareup.picasso3.RequestHandler.Result import com.squareup.picasso3.RequestHandler.Result.Bitmap internal abstract class RemoteViewsAction( picasso: Picasso, data: Request, @DrawableRes val errorResId: Int, val target: RemoteViewsTarget, var callback: Callback? ) : Action(picasso, data) { override fun complete(result: Result) { if (result is Bitmap) { target.remoteViews.setImageViewBitmap(target.viewId, result.bitmap) update() callback?.onSuccess() } } override fun cancel() { super.cancel() callback = null } override fun error(e: Exception) { if (errorResId != 0) { setImageResource(errorResId) } callback?.onError(e) } fun setImageResource(resId: Int) { target.remoteViews.setImageViewResource(target.viewId, resId) update() } abstract fun update() internal class RemoteViewsTarget( val remoteViews: RemoteViews, val viewId: Int ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false val remoteViewsTarget = other as RemoteViewsTarget return viewId == remoteViewsTarget.viewId && remoteViews == remoteViewsTarget.remoteViews } override fun hashCode(): Int { return 31 * remoteViews.hashCode() + viewId } } internal class AppWidgetAction( picasso: Picasso, data: Request, @DrawableRes errorResId: Int, target: RemoteViewsTarget, private val appWidgetIds: IntArray, callback: Callback? ) : RemoteViewsAction(picasso, data, errorResId, target, callback) { override fun update() { val manager = AppWidgetManager.getInstance(picasso.context) manager.updateAppWidget(appWidgetIds, target.remoteViews) } override fun getTarget(): Any { return target } } internal class NotificationAction( picasso: Picasso, data: Request, @DrawableRes errorResId: Int, target: RemoteViewsTarget, private val notificationId: Int, private val notification: Notification, private val notificationTag: String?, callback: Callback? ) : RemoteViewsAction(picasso, data, errorResId, target, callback) { override fun update() { val manager = ContextCompat.getSystemService( picasso.context, NotificationManager::class.java ) manager?.notify(notificationTag, notificationId, notification) } override fun getTarget(): Any { return target } } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/Request.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap.Config import android.net.Uri import android.os.Looper import android.view.Gravity import androidx.annotation.DrawableRes import androidx.annotation.Px import com.squareup.picasso3.Picasso.Priority import com.squareup.picasso3.Picasso.Priority.NORMAL import okhttp3.Headers import java.util.concurrent.TimeUnit.NANOSECONDS import java.util.concurrent.TimeUnit.SECONDS /** Immutable data about an image and the transformations that will be applied to it. */ class Request internal constructor(builder: Builder) { /** A unique ID for the request. */ @JvmField var id = 0 /** The time that the request was first submitted (in nanos). */ @JvmField var started: Long = 0 /** The [MemoryPolicy] to use for this request. */ @JvmField val memoryPolicy: Int = builder.memoryPolicy /** The [NetworkPolicy] to use for this request. */ @JvmField val networkPolicy: Int = builder.networkPolicy /** HTTP headers for the request */ @JvmField val headers: Headers? = builder.headers /** * The image URI. * * This is mutually exclusive with [.resourceId]. */ @JvmField val uri: Uri? = builder.uri /** * The image resource ID. * * This is mutually exclusive with [.uri]. */ @JvmField val resourceId: Int = builder.resourceId /** * Optional stable key for this request to be used instead of the URI or resource ID when * caching. Two requests with the same value are considered to be for the same resource. */ val stableKey: String? = builder.stableKey /** List of custom transformations to be applied after the built-in transformations. */ @JvmField var transformations: List = if (builder.transformations == null) { emptyList() } else { builder.transformations!!.toList() } /** Target image width for resizing. */ @JvmField val targetWidth: Int = builder.targetWidth /** Target image height for resizing. */ @JvmField val targetHeight: Int = builder.targetHeight /** * True if the final image should use the 'centerCrop' scale technique. * * This is mutually exclusive with [.centerInside]. */ @JvmField val centerCrop: Boolean = builder.centerCrop /** If centerCrop is set, controls alignment of centered image */ @JvmField val centerCropGravity: Int = builder.centerCropGravity /** * True if the final image should use the 'centerInside' scale technique. * * This is mutually exclusive with [.centerCrop]. */ @JvmField val centerInside: Boolean = builder.centerInside @JvmField val onlyScaleDown: Boolean = builder.onlyScaleDown /** Amount to rotate the image in degrees. */ @JvmField val rotationDegrees: Float = builder.rotationDegrees /** Rotation pivot on the X axis. */ @JvmField val rotationPivotX: Float = builder.rotationPivotX /** Rotation pivot on the Y axis. */ @JvmField val rotationPivotY: Float = builder.rotationPivotY /** Whether or not [.rotationPivotX] and [.rotationPivotY] are set. */ @JvmField val hasRotationPivot: Boolean = builder.hasRotationPivot /** Target image config for decoding. */ @JvmField val config: Config? = builder.config /** The priority of this request. */ @JvmField val priority: Priority = checkNotNull(builder.priority) /** The cache key for this request. */ @JvmField var key: String = if (Looper.myLooper() == Looper.getMainLooper()) { createKey() } else { createKey(StringBuilder()) } /** User-provided value to track this request. */ val tag: Any? = builder.tag override fun toString() = buildString { append("Request{") if (resourceId > 0) { append(resourceId) } else { append(uri) } for (transformation in transformations) { append(' ') append(transformation.key()) } if (stableKey != null) { append(" stableKey(") append(stableKey) append(')') } if (targetWidth > 0) { append(" resize(") append(targetWidth) append(',') append(targetHeight) append(')') } if (centerCrop) { append(" centerCrop") } if (centerInside) { append(" centerInside") } if (rotationDegrees != 0f) { append(" rotation(") append(rotationDegrees) if (hasRotationPivot) { append(" @ ") append(rotationPivotX) append(',') append(rotationPivotY) } append(')') } if (config != null) { append(' ') append(config) } append('}') } // TODO make internal fun logId(): String { val delta = System.nanoTime() - started return if (delta > TOO_LONG_LOG) { "${plainId()}+${NANOSECONDS.toSeconds(delta)}s" } else { "${plainId()}+${NANOSECONDS.toMillis(delta)}ms" } } // TODO make internal fun plainId() = "[R$id]" // TODO make internal val name: String get() = uri?.path ?: Integer.toHexString(resourceId) // TODO make internal fun hasSize(): Boolean = targetWidth != 0 || targetHeight != 0 // TODO make internal fun needsMatrixTransform(): Boolean = hasSize() || rotationDegrees != 0f fun newBuilder(): Builder = Builder(this) private fun createKey(): String { val result = createKey(Utils.MAIN_THREAD_KEY_BUILDER) Utils.MAIN_THREAD_KEY_BUILDER.setLength(0) return result } private fun createKey(builder: StringBuilder): String { val data = this if (data.stableKey != null) { builder.ensureCapacity(data.stableKey.length + KEY_PADDING) builder.append(data.stableKey) } else if (data.uri != null) { val path = data.uri.toString() builder.ensureCapacity(path.length + KEY_PADDING) builder.append(path) } else { builder.ensureCapacity(KEY_PADDING) builder.append(data.resourceId) } builder.append(KEY_SEPARATOR) if (data.rotationDegrees != 0f) { builder .append("rotation:") .append(data.rotationDegrees) if (data.hasRotationPivot) { builder .append('@') .append(data.rotationPivotX) .append('x') .append(data.rotationPivotY) } builder.append(KEY_SEPARATOR) } if (data.hasSize()) { builder .append("resize:") .append(data.targetWidth) .append('x') .append(data.targetHeight) builder.append(KEY_SEPARATOR) } if (data.centerCrop) { builder .append("centerCrop:") .append(data.centerCropGravity) .append(KEY_SEPARATOR) } else if (data.centerInside) { builder .append("centerInside") .append(KEY_SEPARATOR) } for (i in data.transformations.indices) { builder.append(data.transformations[i].key()) builder.append(KEY_SEPARATOR) } return builder.toString() } /** Builder for creating [Request] instances. */ class Builder { var uri: Uri? = null var resourceId = 0 var stableKey: String? = null var targetWidth = 0 var targetHeight = 0 var centerCrop = false var centerCropGravity = 0 var centerInside = false var onlyScaleDown = false var rotationDegrees = 0f var rotationPivotX = 0f var rotationPivotY = 0f var hasRotationPivot = false var transformations: MutableList? = null var config: Config? = null var priority: Priority? = null /** Internal use only. Used by [DeferredRequestCreator]. */ var tag: Any? = null var memoryPolicy = 0 var networkPolicy = 0 var headers: Headers? = null /** Start building a request using the specified [Uri]. */ constructor(uri: Uri) { setUri(uri) } /** Start building a request using the specified resource ID. */ constructor(@DrawableRes resourceId: Int) { setResourceId(resourceId) } internal constructor( uri: Uri?, resourceId: Int, bitmapConfig: Config? ) { this.uri = uri this.resourceId = resourceId config = bitmapConfig } internal constructor(request: Request) { uri = request.uri resourceId = request.resourceId stableKey = request.stableKey targetWidth = request.targetWidth targetHeight = request.targetHeight centerCrop = request.centerCrop centerInside = request.centerInside centerCropGravity = request.centerCropGravity rotationDegrees = request.rotationDegrees rotationPivotX = request.rotationPivotX rotationPivotY = request.rotationPivotY hasRotationPivot = request.hasRotationPivot onlyScaleDown = request.onlyScaleDown transformations = request.transformations.toMutableList() config = request.config priority = request.priority memoryPolicy = request.memoryPolicy networkPolicy = request.networkPolicy headers = request.headers } fun hasImage(): Boolean { return uri != null || resourceId != 0 } fun hasSize(): Boolean { return targetWidth != 0 || targetHeight != 0 } fun hasPriority(): Boolean { return priority != null } /** * Set the target image Uri. * * This will clear an image resource ID if one is set. */ fun setUri(uri: Uri) = apply { this.uri = uri resourceId = 0 } /** * Set the target image resource ID. * * This will clear an image Uri if one is set. */ fun setResourceId(@DrawableRes resourceId: Int) = apply { require(resourceId != 0) { "Image resource ID may not be 0." } this.resourceId = resourceId uri = null } /** * Set the stable key to be used instead of the URI or resource ID when caching. * Two requests with the same value are considered to be for the same resource. */ fun stableKey(stableKey: String?) = apply { this.stableKey = stableKey } /** * Assign a tag to this request. */ fun tag(tag: Any) = apply { check(this.tag == null) { "Tag already set." } this.tag = tag } /** Internal use only. Used by [DeferredRequestCreator]. */ fun clearTag() = apply { tag = null } /** * Resize the image to the specified size in pixels. * Use 0 as desired dimension to resize keeping aspect ratio. */ fun resize(@Px targetWidth: Int, @Px targetHeight: Int) = apply { require(targetWidth >= 0) { "Width must be positive number or 0." } require(targetHeight >= 0) { "Height must be positive number or 0." } require( !(targetHeight == 0 && targetWidth == 0) ) { "At least one dimension has to be positive number." } this.targetWidth = targetWidth this.targetHeight = targetHeight } /** Clear the resize transformation, if any. This will also clear center crop/inside if set. */ fun clearResize() = apply { targetWidth = 0 targetHeight = 0 centerCrop = false centerInside = false } /** * Crops an image inside of the bounds specified by [resize] rather than * distorting the aspect ratio. This cropping technique scales the image so that it fills the * requested bounds and then crops the extra. */ @JvmOverloads fun centerCrop(alignGravity: Int = Gravity.CENTER) = apply { check(!centerInside) { "Center crop can not be used after calling centerInside" } centerCrop = true centerCropGravity = alignGravity } /** Clear the center crop transformation flag, if set. */ fun clearCenterCrop() = apply { centerCrop = false centerCropGravity = Gravity.CENTER } /** * Centers an image inside of the bounds specified by [resize]. This scales * the image so that both dimensions are equal to or less than the requested bounds. */ fun centerInside() = apply { check(!centerCrop) { "Center inside can not be used after calling centerCrop" } centerInside = true } /** Clear the center inside transformation flag, if set. */ fun clearCenterInside() = apply { centerInside = false } /** * Only resize an image if the original image size is bigger than the target size * specified by [resize]. */ fun onlyScaleDown() = apply { check(!(targetHeight == 0 && targetWidth == 0)) { "onlyScaleDown can not be applied without resize" } onlyScaleDown = true } /** Clear the onlyScaleUp flag, if set. */ fun clearOnlyScaleDown() = apply { onlyScaleDown = false } /** Rotate the image by the specified degrees. */ fun rotate(degrees: Float) = apply { rotationDegrees = degrees } /** Rotate the image by the specified degrees around a pivot point. */ fun rotate( degrees: Float, pivotX: Float, pivotY: Float ) = apply { rotationDegrees = degrees rotationPivotX = pivotX rotationPivotY = pivotY hasRotationPivot = true } /** Clear the rotation transformation, if any. */ fun clearRotation() = apply { rotationDegrees = 0f rotationPivotX = 0f rotationPivotY = 0f hasRotationPivot = false } /** Decode the image using the specified config. */ fun config(config: Config) = apply { this.config = config } /** Execute request using the specified priority. */ fun priority(priority: Priority) = apply { check(this.priority == null) { "Priority already set." } this.priority = priority } /** * Add a custom transformation to be applied to the image. * * Custom transformations will always be run after the built-in transformations. */ fun transform(transformation: Transformation) = apply { requireNotNull(transformation.key()) { "Transformation key must not be null." } if (transformations == null) { transformations = ArrayList(2) } transformations!!.add(transformation) } /** * Add a list of custom transformations to be applied to the image. * * * Custom transformations will always be run after the built-in transformations. */ fun transform(transformations: List) = apply { for (i in transformations.indices) { transform(transformations[i]) } } /** * Specifies the [MemoryPolicy] to use for this request. You may specify additional policy * options using the varargs parameter. */ fun memoryPolicy( policy: MemoryPolicy, vararg additional: MemoryPolicy ) = apply { memoryPolicy = memoryPolicy or policy.index for (i in additional.indices) { this.memoryPolicy = this.memoryPolicy or additional[i].index } } /** * Specifies the [NetworkPolicy] to use for this request. You may specify additional * policy options using the varargs parameter. */ fun networkPolicy( policy: NetworkPolicy, vararg additional: NetworkPolicy ) = apply { networkPolicy = networkPolicy or policy.index for (i in additional.indices) { this.networkPolicy = this.networkPolicy or additional[i].index } } fun addHeader( name: String, value: String ) = apply { this.headers = (headers?.newBuilder() ?: Headers.Builder()) .add(name, value) .build() } /** Create the immutable [Request] object. */ fun build(): Request { check(!(centerInside && centerCrop)) { "Center crop and center inside can not be used together." } check(!(centerCrop && targetWidth == 0 && targetHeight == 0)) { "Center crop requires calling resize with positive width and height." } check(!(centerInside && targetWidth == 0 && targetHeight == 0)) { "Center inside requires calling resize with positive width and height." } if (priority == null) { priority = NORMAL } return Request(this) } } internal companion object { private val TOO_LONG_LOG = SECONDS.toNanos(5) private const val KEY_PADDING = 50 // Determined by exact science. const val KEY_SEPARATOR = '\n' } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/RequestCreator.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.app.Notification import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri import android.view.Gravity import android.widget.ImageView import android.widget.RemoteViews import androidx.annotation.DimenRes import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.core.content.ContextCompat import com.squareup.picasso3.BitmapHunter.Companion.forRequest import com.squareup.picasso3.MemoryPolicy.Companion.shouldReadFromMemoryCache import com.squareup.picasso3.MemoryPolicy.Companion.shouldWriteToMemoryCache import com.squareup.picasso3.Picasso.LoadedFrom import com.squareup.picasso3.PicassoDrawable.Companion.setPlaceholder import com.squareup.picasso3.PicassoDrawable.Companion.setResult import com.squareup.picasso3.RemoteViewsAction.AppWidgetAction import com.squareup.picasso3.RemoteViewsAction.NotificationAction import com.squareup.picasso3.RemoteViewsAction.RemoteViewsTarget import com.squareup.picasso3.Utils.OWNER_MAIN import com.squareup.picasso3.Utils.VERB_COMPLETED import com.squareup.picasso3.Utils.checkMain import com.squareup.picasso3.Utils.checkNotMain import com.squareup.picasso3.Utils.log import java.io.IOException import java.util.concurrent.atomic.AtomicInteger /** Fluent API for building an image download request. */ class RequestCreator internal constructor( private val picasso: Picasso, uri: Uri?, resourceId: Int ) { private val data = Request.Builder(uri, resourceId, picasso.defaultBitmapConfig) private var noFade = false private var deferred = false private var setPlaceholder = true @DrawableRes private var placeholderResId = 0 @DrawableRes private var errorResId = 0 private var placeholderDrawable: Drawable? = null private var errorDrawable: Drawable? = null /** Internal use only. Used by [DeferredRequestCreator]. */ @get:JvmName("-tag") internal val tag: Any? get() = data.tag init { check(!picasso.shutdown) { "Picasso instance already shut down. Cannot submit new requests." } } /** * Explicitly opt-out to having a placeholder set when calling [into]. * * By default, Picasso will either set a supplied placeholder or clear the target * [ImageView] in order to ensure behavior in situations where views are recycled. This * method will prevent that behavior and retain any already set image. */ fun noPlaceholder(): RequestCreator { check(placeholderResId == 0) { "Placeholder resource already set." } check(placeholderDrawable == null) { "Placeholder image already set." } setPlaceholder = false return this } /** * A placeholder drawable to be used while the image is being loaded. If the requested image is * not immediately available in the memory cache then this resource will be set on the target * [ImageView]. */ fun placeholder(@DrawableRes placeholderResId: Int): RequestCreator { check(setPlaceholder) { "Already explicitly declared as no placeholder." } require(placeholderResId != 0) { "Placeholder image resource invalid." } check(placeholderDrawable == null) { "Placeholder image already set." } this.placeholderResId = placeholderResId return this } /** * A placeholder drawable to be used while the image is being loaded. If the requested image is * not immediately available in the memory cache then this resource will be set on the target * [ImageView]. * * If you are not using a placeholder image but want to clear an existing image (such as when * used in an [adapter][android.widget.Adapter]), pass in `null`. */ fun placeholder(placeholderDrawable: Drawable?): RequestCreator { check(setPlaceholder) { "Already explicitly declared as no placeholder." } check(placeholderResId == 0) { "Placeholder image already set." } this.placeholderDrawable = placeholderDrawable return this } /** An error drawable to be used if the request image could not be loaded. */ fun error(@DrawableRes errorResId: Int): RequestCreator { require(errorResId != 0) { "Error image resource invalid." } check(errorDrawable == null) { "Error image already set." } this.errorResId = errorResId return this } /** An error drawable to be used if the request image could not be loaded. */ fun error(errorDrawable: Drawable): RequestCreator { check(errorResId == 0) { "Error image already set." } this.errorDrawable = errorDrawable return this } /** * Assign a tag to this request. Tags are an easy way to logically associate * related requests that can be managed together e.g. paused, resumed, * or canceled. * * You can either use simple [String] tags or objects that naturally * define the scope of your requests within your app such as a * [android.content.Context], an [android.app.Activity], or a * [android.app.Fragment]. * * **WARNING:**: Picasso will keep a reference to the tag for * as long as this tag is paused and/or has active requests. Look out for * potential leaks. * * @see Picasso.cancelTag * @see Picasso.pauseTag * @see Picasso.resumeTag */ fun tag(tag: Any): RequestCreator { data.tag(tag) return this } /** * Attempt to resize the image to fit exactly into the target [ImageView]'s bounds. This * will result in delayed execution of the request until the [ImageView] has been laid out. * * *Note:* This method works only when your target is an [ImageView]. */ fun fit(): RequestCreator { deferred = true return this } /** Internal use only. Used by [DeferredRequestCreator]. */ @JvmName("-unfit") internal fun unfit(): RequestCreator { deferred = false return this } /** Internal use only. Used by [DeferredRequestCreator]. */ @JvmName("-clearTag") internal fun clearTag(): RequestCreator { data.clearTag() return this } /** * Resize the image to the specified dimension size. * Use 0 as desired dimension to resize keeping aspect ratio. */ fun resizeDimen( @DimenRes targetWidthResId: Int, @DimenRes targetHeightResId: Int ): RequestCreator { val resources = picasso.context.resources val targetWidth = resources.getDimensionPixelSize(targetWidthResId) val targetHeight = resources.getDimensionPixelSize(targetHeightResId) return resize(targetWidth, targetHeight) } /** * Resize the image to the specified size in pixels. * Use 0 as desired dimension to resize keeping aspect ratio. */ fun resize(targetWidth: Int, targetHeight: Int): RequestCreator { data.resize(targetWidth, targetHeight) return this } /** * Crops an image inside of the bounds specified by [resize] rather than * distorting the aspect ratio. This cropping technique scales the image so that it fills the * requested bounds and then crops the extra. */ fun centerCrop(): RequestCreator { data.centerCrop(Gravity.CENTER) return this } /** * Crops an image inside of the bounds specified by [resize] rather than * distorting the aspect ratio. This cropping technique scales the image so that it fills the * requested bounds and then crops the extra, preferring the contents at [alignGravity]. */ fun centerCrop(alignGravity: Int): RequestCreator { data.centerCrop(alignGravity) return this } /** * Centers an image inside of the bounds specified by [resize]. This scales * the image so that both dimensions are equal to or less than the requested bounds. */ fun centerInside(): RequestCreator { data.centerInside() return this } /** * Only resize an image if the original image size is bigger than the target size * specified by [resize]. */ fun onlyScaleDown(): RequestCreator { data.onlyScaleDown() return this } /** Rotate the image by the specified degrees. */ fun rotate(degrees: Float): RequestCreator { data.rotate(degrees) return this } /** Rotate the image by the specified degrees around a pivot point. */ fun rotate(degrees: Float, pivotX: Float, pivotY: Float): RequestCreator { data.rotate(degrees, pivotX, pivotY) return this } /** * Attempt to decode the image using the specified config. * * Note: This value may be ignored by [BitmapFactory]. See * [its documentation][BitmapFactory.Options.inPreferredConfig] for more details. */ fun config(config: Bitmap.Config): RequestCreator { data.config(config) return this } /** * Sets the stable key for this request to be used instead of the URI or resource ID when * caching. Two requests with the same value are considered to be for the same resource. */ fun stableKey(stableKey: String): RequestCreator { data.stableKey(stableKey) return this } /** * Set the priority of this request. * * * This will affect the order in which the requests execute but does not guarantee it. * By default, all requests have [Priority.NORMAL] priority, except for * [fetch] requests, which have [Priority.LOW] priority by default. */ fun priority(priority: Picasso.Priority): RequestCreator { data.priority(priority) return this } /** * Add a custom transformation to be applied to the image. * * Custom transformations will always be run after the built-in transformations. */ // TODO show example of calling resize after a transform in the javadoc fun transform(transformation: Transformation): RequestCreator { data.transform(transformation) return this } /** * Add a list of custom transformations to be applied to the image. * * Custom transformations will always be run after the built-in transformations. */ fun transform(transformations: List): RequestCreator { data.transform(transformations) return this } /** * Specifies the [MemoryPolicy] to use for this request. You may specify additional policy * options using the varargs parameter. */ fun memoryPolicy( policy: MemoryPolicy, vararg additional: MemoryPolicy ): RequestCreator { data.memoryPolicy(policy, *additional) return this } /** * Specifies the [NetworkPolicy] to use for this request. You may specify additional policy * options using the varargs parameter. */ fun networkPolicy( policy: NetworkPolicy, vararg additional: NetworkPolicy ): RequestCreator { data.networkPolicy(policy, *additional) return this } /** * Add custom HTTP headers to the image network request, if desired */ fun addHeader(key: String, value: String): RequestCreator { data.addHeader(key, value) return this } /** Disable brief fade in of images loaded from the disk cache or network. */ fun noFade(): RequestCreator { noFade = true return this } /** * Synchronously fulfill this request. Must not be called from the main thread. */ @Throws(IOException::class) // TODO make non-null and always throw? fun get(): Bitmap? { val started = System.nanoTime() checkNotMain() check(!deferred) { "Fit cannot be used with get." } if (!data.hasImage()) { return null } val request = createRequest(started) val action = GetAction(picasso, request) val result = forRequest(picasso, picasso.dispatcher, picasso.cache, action).hunt() ?: return null val bitmap = result.bitmap if (shouldWriteToMemoryCache(request.memoryPolicy)) { picasso.cache[request.key] = bitmap } return bitmap } /** * Asynchronously fulfills the request without a [ImageView] or [BitmapTarget], * and invokes the target [Callback] with the result. This is useful when you want to warm * up the cache with an image. * * *Note:* The [Callback] param is a strong reference and will prevent your * [android.app.Activity] or [android.app.Fragment] from being garbage collected * until the request is completed. * * *Note:* It is safe to invoke this method from any thread. */ @JvmOverloads fun fetch(callback: Callback? = null) { val started = System.nanoTime() check(!deferred) { "Fit cannot be used with fetch." } if (data.hasImage()) { // Fetch requests have lower priority by default. if (!data.hasPriority()) { data.priority(Picasso.Priority.LOW) } val request = createRequest(started) if (shouldReadFromMemoryCache(request.memoryPolicy)) { val bitmap = picasso.quickMemoryCacheCheck(request.key) if (bitmap != null) { if (picasso.isLoggingEnabled) { log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + LoadedFrom.MEMORY) } callback?.onSuccess() return } } val action = FetchAction(picasso, request, callback) picasso.submit(action) } } /** * Asynchronously fulfills the request into the specified [BitmapTarget]. In most cases, you * should use this when you are dealing with a custom [View][android.view.View] or view * holder which should implement the [BitmapTarget] interface. * * Implementing on a [View][android.view.View]: * ``` * class ProfileView(context: Context) : FrameLayout(context), Target { * override fun onBitmapLoaded(bitmap: Bitmap, from: LoadedFrom) { * setBackgroundDrawable(BitmapDrawable(bitmap)) * } * * override run onBitmapFailed(e: Exception, errorDrawable: Drawable) { * setBackgroundDrawable(errorDrawable) * } * * override fun onPrepareLoad(placeholderDrawable: Drawable) { * setBackgroundDrawable(placeholderDrawable * } * } * ``` */ fun into(target: BitmapTarget) { val started = System.nanoTime() checkMain() check(!deferred) { "Fit cannot be used with a Target." } if (!data.hasImage()) { picasso.cancelRequest(target) target.onPrepareLoad(if (setPlaceholder) getPlaceholderDrawable() else null) return } val request = createRequest(started) if (shouldReadFromMemoryCache(request.memoryPolicy)) { val bitmap = picasso.quickMemoryCacheCheck(request.key) if (bitmap != null) { picasso.cancelRequest(target) target.onBitmapLoaded(bitmap, LoadedFrom.MEMORY) return } } target.onPrepareLoad(if (setPlaceholder) getPlaceholderDrawable() else null) val action = BitmapTargetAction(picasso, target, request, errorDrawable, errorResId) picasso.enqueueAndSubmit(action) } /** * Asynchronously fulfills the request into the specified [DrawableTarget]. In most cases, you * should use this when you are dealing with a custom [View][android.view.View] or view * holder which should implement the [DrawableTarget] interface. */ fun into(target: DrawableTarget) { val started = System.nanoTime() checkMain() check(!deferred) { "Fit cannot be used with a Target." } val placeHolderDrawable = if (setPlaceholder) getPlaceholderDrawable() else null if (!data.hasImage()) { picasso.cancelRequest(target) target.onPrepareLoad(placeHolderDrawable) return } val request = createRequest(started) if (shouldReadFromMemoryCache(request.memoryPolicy)) { val bitmap = picasso.quickMemoryCacheCheck(request.key) if (bitmap != null) { picasso.cancelRequest(target) target.onDrawableLoaded( PicassoDrawable( context = picasso.context, bitmap = bitmap, placeholder = null, loadedFrom = LoadedFrom.MEMORY, noFade = noFade, debugging = picasso.indicatorsEnabled ), LoadedFrom.MEMORY ) return } } target.onPrepareLoad(placeHolderDrawable) val action = DrawableTargetAction(picasso, target, request, noFade, placeHolderDrawable, errorDrawable, errorResId) picasso.enqueueAndSubmit(action) } /** * Asynchronously fulfills the request into the specified [RemoteViews] object with the * given [viewId]. This is used for loading bitmaps into a [Notification]. */ @JvmOverloads fun into( remoteViews: RemoteViews, @IdRes viewId: Int, notificationId: Int, notification: Notification, notificationTag: String? = null, callback: Callback? = null ) { val started = System.nanoTime() check(!deferred) { "Fit cannot be used with RemoteViews." } require(!(placeholderDrawable != null || errorDrawable != null)) { "Cannot use placeholder or error drawables with remote views." } val request = createRequest(started) val action = NotificationAction( picasso, request, errorResId, RemoteViewsTarget(remoteViews, viewId), notificationId, notification, notificationTag, callback ) performRemoteViewInto(request, action) } /** * Asynchronously fulfills the request into the specified [RemoteViews] object with the * given [viewId]. This is used for loading bitmaps into all instances of a widget. */ fun into( remoteViews: RemoteViews, @IdRes viewId: Int, appWidgetId: Int, callback: Callback? = null ) { into(remoteViews, viewId, intArrayOf(appWidgetId), callback) } /** * Asynchronously fulfills the request into the specified [RemoteViews] object with the * given [viewId]. This is used for loading bitmaps into all instances of a widget. */ @JvmOverloads fun into( remoteViews: RemoteViews, @IdRes viewId: Int, appWidgetIds: IntArray, callback: Callback? = null ) { val started = System.nanoTime() check(!deferred) { "Fit cannot be used with remote views." } require(!(placeholderDrawable != null || errorDrawable != null)) { "Cannot use placeholder or error drawables with remote views." } val request = createRequest(started) val action = AppWidgetAction( picasso, request, errorResId, RemoteViewsTarget(remoteViews, viewId), appWidgetIds, callback ) performRemoteViewInto(request, action) } /** * Asynchronously fulfills the request into the specified [ImageView] and invokes the * target [Callback] if it's not `null`. * * *Note:* The [Callback] param is a strong reference and will prevent your * [android.app.Activity] or [android.app.Fragment] from being garbage collected. If * you use this method, it is **strongly** recommended you invoke an adjacent * [Picasso.cancelRequest] call to prevent temporary leaking. * * *Note:* This method will automatically support object recycling. */ @JvmOverloads fun into(target: ImageView, callback: Callback? = null) { val started = System.nanoTime() checkMain() if (!data.hasImage()) { picasso.cancelRequest(target) if (setPlaceholder) { setPlaceholder(target, getPlaceholderDrawable()) } return } if (deferred) { check(!data.hasSize()) { "Fit cannot be used with resize." } val width = target.width val height = target.height if (width == 0 || height == 0) { if (setPlaceholder) { setPlaceholder(target, getPlaceholderDrawable()) } picasso.defer(target, DeferredRequestCreator(this, target, callback)) return } data.resize(width, height) } val request = createRequest(started) if (shouldReadFromMemoryCache(request.memoryPolicy)) { val bitmap = picasso.quickMemoryCacheCheck(request.key) if (bitmap != null) { picasso.cancelRequest(target) val result: RequestHandler.Result = RequestHandler.Result.Bitmap(bitmap, LoadedFrom.MEMORY) setResult(target, picasso.context, result, noFade, picasso.indicatorsEnabled) if (picasso.isLoggingEnabled) { log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + LoadedFrom.MEMORY) } callback?.onSuccess() return } } if (setPlaceholder) { setPlaceholder(target, getPlaceholderDrawable()) } val action = ImageViewAction( picasso, target, request, errorDrawable, errorResId, noFade, callback ) picasso.enqueueAndSubmit(action) } private fun getPlaceholderDrawable(): Drawable? { return if (placeholderResId == 0) { placeholderDrawable } else { ContextCompat.getDrawable(picasso.context, placeholderResId) } } /** Create the request optionally passing it through the request transformer. */ private fun createRequest(started: Long): Request { val id = nextId.getAndIncrement() val request = data.build() request.id = id request.started = started val loggingEnabled = picasso.isLoggingEnabled if (loggingEnabled) { log(OWNER_MAIN, Utils.VERB_CREATED, request.plainId(), request.toString()) } val transformed = picasso.transformRequest(request) if (transformed != request) { // If the request was changed, copy over the id and timestamp from the original. transformed.id = id transformed.started = started if (loggingEnabled) { log(OWNER_MAIN, Utils.VERB_CHANGED, transformed.logId(), "into $transformed") } } return transformed } private fun performRemoteViewInto(request: Request, action: RemoteViewsAction) { if (shouldReadFromMemoryCache(request.memoryPolicy)) { val bitmap = picasso.quickMemoryCacheCheck(action.request.key) if (bitmap != null) { action.complete(RequestHandler.Result.Bitmap(bitmap, LoadedFrom.MEMORY)) return } } if (placeholderResId != 0) { action.setImageResource(placeholderResId) } picasso.enqueueAndSubmit(action) } private companion object { private val nextId = AtomicInteger() } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/RequestHandler.kt ================================================ /* * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.net.NetworkInfo import com.squareup.picasso3.Picasso.LoadedFrom import java.io.IOException /** * `RequestHandler` allows you to extend Picasso to load images in ways that are not * supported by default in the library. * *

Usage

* `RequestHandler` must be subclassed to be used. You will have to override two methods * ([canHandleRequest] and [load]) with your custom logic to load images. * * You should then register your [RequestHandler] using * [Picasso.Builder.addRequestHandler] * * **Note:** This is a beta feature. The API is subject to change in a backwards incompatible * way at any time. * * @see Picasso.Builder.addRequestHandler */ abstract class RequestHandler { /** * [Result] represents the result of a [load] call in a [RequestHandler]. * * @see RequestHandler * @see [load] */ sealed class Result constructor( /** * Returns the resulting [Picasso.LoadedFrom] generated from a [load] call. */ val loadedFrom: LoadedFrom, /** * Returns the resulting EXIF rotation generated from a [load] call. */ val exifRotation: Int = 0 ) { class Bitmap constructor( val bitmap: android.graphics.Bitmap, loadedFrom: LoadedFrom, exifRotation: Int = 0 ) : Result(loadedFrom, exifRotation) class Drawable constructor( val drawable: android.graphics.drawable.Drawable, loadedFrom: LoadedFrom, exifRotation: Int = 0 ) : Result(loadedFrom, exifRotation) } interface Callback { fun onSuccess(result: Result?) fun onError(t: Throwable) } /** * Whether or not this [RequestHandler] can handle a request with the given [Request]. */ abstract fun canHandleRequest(data: Request): Boolean /** * Loads an image for the given [Request]. * @param request the data from which the image should be resolved. */ @Throws(IOException::class) abstract fun load( picasso: Picasso, request: Request, callback: Callback ) open val retryCount = 0 open fun shouldRetry( airplaneMode: Boolean, info: NetworkInfo? ) = false open fun supportsReplay() = false } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/ResourceDrawableRequestHandler.kt ================================================ /* * Copyright (C) 2018 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import androidx.core.content.ContextCompat import com.squareup.picasso3.BitmapUtils.isXmlResource import com.squareup.picasso3.Picasso.LoadedFrom.DISK internal class ResourceDrawableRequestHandler private constructor( private val context: Context, private val loader: DrawableLoader ) : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { return data.resourceId != 0 && isXmlResource(context.resources, data.resourceId) } override fun load( picasso: Picasso, request: Request, callback: Callback ) { val drawable = loader.load(request.resourceId) if (drawable == null) { callback.onError( IllegalArgumentException("invalid resId: ${Integer.toHexString(request.resourceId)}") ) } else { callback.onSuccess(Result.Drawable(drawable, DISK)) } } internal companion object { @JvmName("-create") internal fun create( context: Context, loader: DrawableLoader = DrawableLoader { resId -> ContextCompat.getDrawable(context, resId) } ) = ResourceDrawableRequestHandler(context, loader) } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/ResourceRequestHandler.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.ContentResolver import android.content.Context import com.squareup.picasso3.BitmapUtils.decodeResource import com.squareup.picasso3.BitmapUtils.isXmlResource import com.squareup.picasso3.Picasso.LoadedFrom.DISK internal class ResourceRequestHandler(private val context: Context) : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { return if (data.resourceId != 0 && !isXmlResource(context.resources, data.resourceId)) { true } else { data.uri != null && ContentResolver.SCHEME_ANDROID_RESOURCE == data.uri.scheme } } override fun load( picasso: Picasso, request: Request, callback: Callback ) { var signaledCallback = false try { val bitmap = decodeResource(context, request) signaledCallback = true callback.onSuccess(Result.Bitmap(bitmap, DISK)) } catch (e: Exception) { if (!signaledCallback) { callback.onError(e) } } } } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/Transformation.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import com.squareup.picasso3.RequestHandler.Result /** Image transformation. */ interface Transformation { /** * Transform the source result into a new result. If you create a new bitmap instance, you must * call [android.graphics.Bitmap.recycle] on `source`. You may return the original * if no transformation is required. */ fun transform(source: Result.Bitmap): Result.Bitmap /** * Returns a unique key for the transformation, used for caching purposes. If the transformation * has parameters (e.g. size, scale factor, etc) then these should be part of the key. */ fun key(): String } ================================================ FILE: picasso/src/main/java/com/squareup/picasso3/Utils.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.app.ActivityManager import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.NameNotFoundException import android.content.res.Resources import android.os.Handler import android.os.Looper import android.os.Message import android.os.StatFs import android.provider.Settings.Global import android.util.Log import androidx.core.content.ContextCompat import okio.BufferedSource import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import java.io.File import java.io.FileNotFoundException import kotlin.math.max import kotlin.math.min internal object Utils { const val THREAD_PREFIX = "Picasso-" const val THREAD_IDLE_NAME = THREAD_PREFIX + "Idle" private const val PICASSO_CACHE = "picasso-cache" private const val MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024 // 5MB private const val MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024 // 50MB const val THREAD_LEAK_CLEANING_MS = 1000 /** Thread confined to main thread for key creation. */ val MAIN_THREAD_KEY_BUILDER = StringBuilder() /** Logging */ const val OWNER_MAIN = "Main" const val OWNER_DISPATCHER = "Dispatcher" const val OWNER_HUNTER = "Hunter" const val VERB_CREATED = "created" const val VERB_CHANGED = "changed" const val VERB_IGNORED = "ignored" const val VERB_ENQUEUED = "enqueued" const val VERB_CANCELED = "canceled" const val VERB_RETRYING = "retrying" const val VERB_EXECUTING = "executing" const val VERB_DECODED = "decoded" const val VERB_TRANSFORMED = "transformed" const val VERB_JOINED = "joined" const val VERB_REMOVED = "removed" const val VERB_DELIVERED = "delivered" const val VERB_REPLAYING = "replaying" const val VERB_COMPLETED = "completed" const val VERB_ERRORED = "errored" const val VERB_PAUSED = "paused" const val VERB_RESUMED = "resumed" /* WebP file header 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 'R' | 'I' | 'F' | 'F' | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | File Size | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 'W' | 'E' | 'B' | 'P' | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ private val WEBP_FILE_HEADER_RIFF: ByteString = "RIFF".encodeUtf8() private val WEBP_FILE_HEADER_WEBP: ByteString = "WEBP".encodeUtf8() fun checkNotNull(value: T?, message: String?): T { if (value == null) { throw NullPointerException(message) } return value } fun checkNotMain() { check(!isMain) { "Method call should not happen from the main thread." } } fun checkMain() { check(isMain) { "Method call should happen from the main thread." } } private val isMain: Boolean get() = Looper.getMainLooper().thread === Thread.currentThread() fun getLogIdsForHunter(hunter: BitmapHunter, prefix: String = ""): String { return buildString { append(prefix) val action = hunter.action if (action != null) { append(action.request.logId()) } val actions = hunter.actions if (actions != null) { for (i in actions.indices) { if (i > 0 || action != null) append(", ") append(actions[i].request.logId()) } } } } fun log(owner: String, verb: String, logId: String, extras: String? = "") { Log.d(TAG, String.format("%1$-11s %2$-12s %3\$s %4\$s", owner, verb, logId, extras ?: "")) } fun createDefaultCacheDir(context: Context): File { val cache = File(context.applicationContext.cacheDir, PICASSO_CACHE) if (!cache.exists()) { cache.mkdirs() } return cache } fun calculateDiskCacheSize(dir: File): Long { var size = MIN_DISK_CACHE_SIZE.toLong() try { val statFs = StatFs(dir.absolutePath) val blockCount = statFs.blockCountLong val blockSize = statFs.blockSizeLong val available = blockCount * blockSize // Target 2% of the total space. size = available / 50 } catch (ignored: IllegalArgumentException) { } // Bound inside min/max size for disk cache. return max(min(size, MAX_DISK_CACHE_SIZE.toLong()), MIN_DISK_CACHE_SIZE.toLong()) } fun calculateMemoryCacheSize(context: Context): Int { val am = ContextCompat.getSystemService(context, ActivityManager::class.java) val largeHeap = context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP != 0 val memoryClass = if (largeHeap) am!!.largeMemoryClass else am!!.memoryClass // Target ~15% of the available heap. return (1024L * 1024L * memoryClass / 7).toInt() } fun isAirplaneModeOn(context: Context): Boolean { return try { val contentResolver = context.contentResolver Global.getInt(contentResolver, Global.AIRPLANE_MODE_ON, 0) != 0 } catch (e: NullPointerException) { // https://github.com/square/picasso/issues/761, some devices might crash here, assume that // airplane mode is off. false } catch (e: SecurityException) { // https://github.com/square/picasso/issues/1197 false } } fun hasPermission(context: Context, permission: String): Boolean { return context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED } fun isWebPFile(source: BufferedSource): Boolean { return source.rangeEquals(0, WEBP_FILE_HEADER_RIFF) && source.rangeEquals(8, WEBP_FILE_HEADER_WEBP) } fun getResourceId(resources: Resources, data: Request): Int { if (data.resourceId != 0 || data.uri == null) { return data.resourceId } val pkg = data.uri.authority ?: throw FileNotFoundException("No package provided: " + data.uri) val segments = data.uri.pathSegments return when (segments?.size ?: 0) { 0 -> throw FileNotFoundException("No path segments: " + data.uri) 1 -> { try { segments[0].toInt() } catch (e: NumberFormatException) { throw FileNotFoundException("Last path segment is not a resource ID: " + data.uri) } } 2 -> { val type = segments[0] val name = segments[1] resources.getIdentifier(name, type, pkg) } else -> throw FileNotFoundException("More than two path segments: " + data.uri) } } fun getResources( context: Context, data: Request ): Resources { if (data.resourceId != 0 || data.uri == null) { return context.resources } return try { val pkg = data.uri.authority ?: throw FileNotFoundException("No package provided: " + data.uri) context.packageManager.getResourcesForApplication(pkg) } catch (e: NameNotFoundException) { throw FileNotFoundException("Unable to obtain resources for package: " + data.uri) } } /** * Prior to Android 12, HandlerThread always keeps a stack local reference to the last message * that was sent to it. This method makes sure that stack local reference never stays there * for too long by sending new messages to it every second. * * https://github.com/square/leakcanary/blob/main/plumber-android-core/src/main/java/leakcanary/AndroidLeakFixes.kt#L153 */ fun flushStackLocalLeaks(looper: Looper) { val handler: Handler = object : Handler(looper) { override fun handleMessage(msg: Message) { sendMessageDelayed(obtainMessage(), THREAD_LEAK_CLEANING_MS.toLong()) } } handler.sendMessageDelayed(handler.obtainMessage(), THREAD_LEAK_CLEANING_MS.toLong()) } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/BaseDispatcherTest.kt ================================================ /* * Copyright (C) 2023 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.content.Intent import android.net.ConnectivityManager import com.squareup.picasso3.BaseDispatcher.NetworkBroadcastReceiver import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoInteractions import org.mockito.MockitoAnnotations.initMocks import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class BaseDispatcherTest { @Mock lateinit var context: Context @Before fun setUp() { initMocks(this) } @Test fun nullIntentOnReceiveDoesNothing() { val dispatcher = mock(BaseDispatcher::class.java) val receiver = NetworkBroadcastReceiver(dispatcher) receiver.onReceive(context, null) verifyNoInteractions(dispatcher) } @Test fun nullExtrasOnReceiveConnectivityAreOk() { val connectivityManager = mock(ConnectivityManager::class.java) val networkInfo = TestUtils.mockNetworkInfo() Mockito.`when`(connectivityManager.activeNetworkInfo).thenReturn(networkInfo) Mockito.`when`(context.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(connectivityManager) val dispatcher = mock(BaseDispatcher::class.java) val receiver = NetworkBroadcastReceiver(dispatcher) receiver.onReceive(context, Intent(ConnectivityManager.CONNECTIVITY_ACTION)) verify(dispatcher).dispatchNetworkStateChange(networkInfo) } @Test fun nullExtrasOnReceiveAirplaneDoesNothing() { val dispatcher = mock(BaseDispatcher::class.java) val receiver = NetworkBroadcastReceiver(dispatcher) receiver.onReceive(context, Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED)) verifyNoInteractions(dispatcher) } @Test fun correctExtrasOnReceiveAirplaneDispatches() { setAndVerifyAirplaneMode(false) setAndVerifyAirplaneMode(true) } private fun setAndVerifyAirplaneMode(airplaneOn: Boolean) { val dispatcher = mock(BaseDispatcher::class.java) val receiver = NetworkBroadcastReceiver(dispatcher) val intent = Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED) intent.putExtra(NetworkBroadcastReceiver.EXTRA_AIRPLANE_STATE, airplaneOn) receiver.onReceive(context, intent) verify(dispatcher).dispatchAirplaneModeChange(airplaneOn) } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/BitmapHunterTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.graphics.Bitmap.Config.ARGB_8888 import android.net.NetworkInfo import android.net.Uri import android.os.Looper import android.view.Gravity import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface.ORIENTATION_ROTATE_90 import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.BitmapHunter.Companion.applyTransformations import com.squareup.picasso3.BitmapHunter.Companion.forRequest import com.squareup.picasso3.MatrixTransformation.Companion.transformResult import com.squareup.picasso3.NetworkRequestHandler.ResponseException import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.Picasso.LoadedFrom.NETWORK import com.squareup.picasso3.Picasso.Priority.HIGH import com.squareup.picasso3.Picasso.Priority.LOW import com.squareup.picasso3.Picasso.Priority.NORMAL import com.squareup.picasso3.Request.Companion.KEY_SEPARATOR import com.squareup.picasso3.RequestHandler.Result.Bitmap import com.squareup.picasso3.TestUtils.ASSET_KEY_1 import com.squareup.picasso3.TestUtils.ASSET_URI_1 import com.squareup.picasso3.TestUtils.BITMAP_RESOURCE_VALUE import com.squareup.picasso3.TestUtils.CONTACT_KEY_1 import com.squareup.picasso3.TestUtils.CONTACT_PHOTO_KEY_1 import com.squareup.picasso3.TestUtils.CONTACT_PHOTO_URI_1 import com.squareup.picasso3.TestUtils.CONTACT_URI_1 import com.squareup.picasso3.TestUtils.CONTENT_1_URL import com.squareup.picasso3.TestUtils.CONTENT_KEY_1 import com.squareup.picasso3.TestUtils.CUSTOM_URI import com.squareup.picasso3.TestUtils.CUSTOM_URI_KEY import com.squareup.picasso3.TestUtils.EventRecorder import com.squareup.picasso3.TestUtils.FILE_1_URL import com.squareup.picasso3.TestUtils.FILE_KEY_1 import com.squareup.picasso3.TestUtils.MEDIA_STORE_CONTENT_1_URL import com.squareup.picasso3.TestUtils.MEDIA_STORE_CONTENT_KEY_1 import com.squareup.picasso3.TestUtils.NOOP_REQUEST_HANDLER import com.squareup.picasso3.TestUtils.NO_TRANSFORMERS import com.squareup.picasso3.TestUtils.RESOURCE_ID_1 import com.squareup.picasso3.TestUtils.RESOURCE_ID_KEY_1 import com.squareup.picasso3.TestUtils.RESOURCE_ID_URI import com.squareup.picasso3.TestUtils.RESOURCE_ID_URI_KEY import com.squareup.picasso3.TestUtils.RESOURCE_TYPE_URI import com.squareup.picasso3.TestUtils.RESOURCE_TYPE_URI_KEY import com.squareup.picasso3.TestUtils.UNUSED_CALL_FACTORY import com.squareup.picasso3.TestUtils.URI_1 import com.squareup.picasso3.TestUtils.URI_KEY_1 import com.squareup.picasso3.TestUtils.XML_RESOURCE_VALUE import com.squareup.picasso3.TestUtils.makeBitmap import com.squareup.picasso3.TestUtils.makeLoaderWithDrawable import com.squareup.picasso3.TestUtils.mockAction import com.squareup.picasso3.TestUtils.mockImageViewTarget import com.squareup.picasso3.TestUtils.mockPicasso import com.squareup.picasso3.TestUtils.mockResources import org.junit.Assert.fail import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations.initMocks import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import java.io.File import java.io.IOException import java.util.concurrent.FutureTask @RunWith(RobolectricTestRunner::class) class BitmapHunterTest { @Mock internal lateinit var context: Context @Mock internal lateinit var dispatcher: Dispatcher private lateinit var picasso: Picasso private val cache = PlatformLruCache(2048) private val bitmap = makeBitmap() @Before fun setUp() { initMocks(this) `when`(context.applicationContext).thenReturn(context) picasso = mockPicasso(context, NOOP_REQUEST_HANDLER) } @Test fun nullDecodeResponseIsError() { val action = mockAction(picasso, URI_KEY_1, URI_1) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action, null) hunter.run() verify(dispatcher).dispatchFailed(hunter) } @Test fun runWithResultDispatchComplete() { val action = mockAction(picasso, URI_KEY_1, URI_1) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action, bitmap) hunter.run() verify(dispatcher).dispatchComplete(hunter) } @Test fun runWithNoResultDispatchFailed() { val action = mockAction(picasso, URI_KEY_1, URI_1) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action) hunter.run() verify(dispatcher).dispatchFailed(hunter) } @Test fun responseExceptionDispatchFailed() { val action = mockAction(picasso, URI_KEY_1, URI_1) val hunter = TestableBitmapHunter( picasso, dispatcher, cache, action, null, ResponseException(504, 0) ) hunter.run() verify(dispatcher).dispatchFailed(hunter) } @Test fun runWithIoExceptionDispatchRetry() { val action = mockAction(picasso, URI_KEY_1, URI_1) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action, null, IOException()) hunter.run() verify(dispatcher).dispatchRetry(hunter) } @Test fun huntDecodesWhenNotInCache() { val eventRecorder = EventRecorder() val picasso = picasso.newBuilder().addEventListener(eventRecorder).build() val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action, bitmap) val result = hunter.hunt() assertThat(cache.missCount()).isEqualTo(1) assertThat(result).isNotNull() assertThat(result!!.bitmap).isEqualTo(bitmap) assertThat(result.loadedFrom).isEqualTo(NETWORK) assertThat(eventRecorder.decodedBitmap).isEqualTo(bitmap) } @Test fun huntReturnsWhenResultInCache() { cache[URI_KEY_1 + KEY_SEPARATOR] = bitmap val eventRecorder = EventRecorder() val picasso = picasso.newBuilder().addEventListener(eventRecorder).build() val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action, bitmap) val result = hunter.hunt() assertThat(cache.hitCount()).isEqualTo(1) assertThat(result).isNotNull() assertThat(result!!.bitmap).isEqualTo(bitmap) assertThat(result.loadedFrom).isEqualTo(MEMORY) assertThat(eventRecorder.decodedBitmap).isNull() } @Test fun huntUnrecognizedUri() { val action = mockAction(picasso, CUSTOM_URI_KEY, CUSTOM_URI) val hunter = forRequest(picasso, dispatcher, cache, action) try { hunter.hunt() fail("Unrecognized URI should throw exception.") } catch (ignored: IllegalStateException) { } } @Test fun huntDecodesWithRequestHandler() { val picasso = mockPicasso(context, CustomRequestHandler()) val action = mockAction(picasso, CUSTOM_URI_KEY, CUSTOM_URI) val hunter = forRequest(picasso, dispatcher, cache, action) val result = hunter.hunt() assertThat(result!!.bitmap).isEqualTo(bitmap) } @Test fun attachSingleRequest() { val action1 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action1) assertThat(hunter.action).isEqualTo(action1) hunter.detach(action1) hunter.attach(action1) assertThat(hunter.action).isEqualTo(action1) assertThat(hunter.actions).isNull() } @Test fun attachMultipleRequests() { val action1 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val action2 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action1) assertThat(hunter.actions).isNull() hunter.attach(action2) assertThat(hunter.actions).isNotNull() assertThat(hunter.actions).hasSize(1) } @Test fun detachSingleRequest() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action) assertThat(hunter.action).isNotNull() hunter.detach(action) assertThat(hunter.action).isNull() } @Test fun detachMultipleRequests() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val action2 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action) hunter.attach(action2) hunter.detach(action2) assertThat(hunter.action).isNotNull() assertThat(hunter.actions).isNotNull() assertThat(hunter.actions).isEmpty() hunter.detach(action) assertThat(hunter.action).isNull() } @Test fun cancelSingleRequest() { val action1 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action1) hunter.future = FutureTask(mock(Runnable::class.java), mock(Any::class.java)) assertThat(hunter.isCancelled).isFalse() assertThat(hunter.cancel()).isFalse() hunter.detach(action1) assertThat(hunter.cancel()).isTrue() assertThat(hunter.isCancelled).isTrue() } @Test fun cancelMultipleRequests() { val action1 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val action2 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = TestableBitmapHunter(picasso, dispatcher, cache, action1) hunter.future = FutureTask(mock(Runnable::class.java), mock(Any::class.java)) hunter.attach(action2) assertThat(hunter.isCancelled).isFalse() assertThat(hunter.cancel()).isFalse() hunter.detach(action1) hunter.detach(action2) assertThat(hunter.cancel()).isTrue() assertThat(hunter.isCancelled).isTrue() } // --------------------------------------- @Test fun forContentProviderRequest() { val picasso = mockPicasso(context, ContentStreamRequestHandler(context)) val action = mockAction(picasso, CONTENT_KEY_1, CONTENT_1_URL) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(ContentStreamRequestHandler::class.java) } @Test fun forMediaStoreRequest() { val picasso = mockPicasso(context, MediaStoreRequestHandler(context)) val action = mockAction(picasso, MEDIA_STORE_CONTENT_KEY_1, MEDIA_STORE_CONTENT_1_URL) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(MediaStoreRequestHandler::class.java) } @Test fun forContactsPhotoRequest() { val picasso = mockPicasso(context, ContactsPhotoRequestHandler(context)) val action = mockAction(picasso, CONTACT_KEY_1, CONTACT_URI_1) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(ContactsPhotoRequestHandler::class.java) } @Test fun forContactsThumbnailPhotoRequest() { val picasso = mockPicasso(context, ContactsPhotoRequestHandler(context)) val action = mockAction(picasso, CONTACT_PHOTO_KEY_1, CONTACT_PHOTO_URI_1) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(ContactsPhotoRequestHandler::class.java) } @Test fun forNetworkRequest() { val requestHandler = NetworkRequestHandler(UNUSED_CALL_FACTORY) val picasso = mockPicasso(context, requestHandler) val action = mockAction(picasso, URI_KEY_1, URI_1) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isSameInstanceAs(requestHandler) } @Test fun forFileWithAuthorityRequest() { val picasso = mockPicasso(context, FileRequestHandler(context)) val action = mockAction(picasso, FILE_KEY_1, FILE_1_URL) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(FileRequestHandler::class.java) } @Test fun forAndroidBitmapResourceRequest() { val resources = mockResources(BITMAP_RESOURCE_VALUE) `when`(context.resources).thenReturn(resources) val picasso = mockPicasso(context, ResourceRequestHandler(context)) val action = mockAction(picasso = picasso, key = RESOURCE_ID_KEY_1, resourceId = RESOURCE_ID_1) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(ResourceRequestHandler::class.java) } @Test fun forAndroidBitmapResourceUriWithId() { val picasso = mockPicasso(context, ResourceRequestHandler(context)) val action = mockAction(picasso, RESOURCE_ID_URI_KEY, RESOURCE_ID_URI) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(ResourceRequestHandler::class.java) } @Test fun forAndroidBitmapResourceUriWithType() { val picasso = mockPicasso(context, ResourceRequestHandler(context)) val action = mockAction(picasso, RESOURCE_TYPE_URI_KEY, RESOURCE_TYPE_URI) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(ResourceRequestHandler::class.java) } @Test fun forAndroidXmlResourceRequest() { val resources = mockResources(XML_RESOURCE_VALUE) `when`(context.resources).thenReturn(resources) val requestHandler = ResourceDrawableRequestHandler.create(context, makeLoaderWithDrawable(null)) val picasso = mockPicasso(context, requestHandler) val action = mockAction(picasso = picasso, key = RESOURCE_ID_KEY_1, resourceId = RESOURCE_ID_1) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(ResourceDrawableRequestHandler::class.java) } @Test fun forAssetRequest() { val picasso = mockPicasso(context, AssetRequestHandler(context)) val action = mockAction(picasso, ASSET_KEY_1, ASSET_URI_1) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(AssetRequestHandler::class.java) } @Test fun forFileWithNoPathSegments() { val picasso = mockPicasso(context, FileRequestHandler(context)) val action = mockAction(picasso, "keykeykey", Uri.fromFile(File("/"))) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(FileRequestHandler::class.java) } @Test fun forCustomRequest() { val picasso = mockPicasso(context, CustomRequestHandler()) val action = mockAction(picasso, CUSTOM_URI_KEY, CUSTOM_URI) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isInstanceOf(CustomRequestHandler::class.java) } @Test fun forOverrideRequest() { val handler = AssetRequestHandler(context) // Must use non-mock constructor because that is where Picasso's list of handlers is created. val picasso = Picasso( context, dispatcher, UNUSED_CALL_FACTORY, null, cache, null, NO_TRANSFORMERS, listOf(handler), emptyList(), ARGB_8888, false, false ) val action = mockAction(picasso, ASSET_KEY_1, ASSET_URI_1) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.requestHandler).isEqualTo(handler) } @Test fun sequenceIsIncremented() { val picasso = mockPicasso(context) val action = mockAction(picasso, URI_KEY_1, URI_1) val hunter1 = forRequest(picasso, dispatcher, cache, action) val hunter2 = forRequest(picasso, dispatcher, cache, action) assertThat(hunter2.sequence).isGreaterThan(hunter1.sequence) } @Test fun getPriorityWithNoRequests() { val requestHandler = NetworkRequestHandler(UNUSED_CALL_FACTORY) val picasso = mockPicasso(context, requestHandler) val action = mockAction(picasso, URI_KEY_1, URI_1) val hunter = forRequest(picasso, dispatcher, cache, action) hunter.detach(action) assertThat(hunter.action).isNull() assertThat(hunter.actions).isNull() assertThat(hunter.priority).isEqualTo(LOW) } @Test fun getPriorityWithSingleRequest() { val requestHandler = NetworkRequestHandler(UNUSED_CALL_FACTORY) val picasso = mockPicasso(context, requestHandler) val action = mockAction(picasso = picasso, key = URI_KEY_1, uri = URI_1, priority = HIGH) val hunter = forRequest(picasso, dispatcher, cache, action) assertThat(hunter.action).isEqualTo(action) assertThat(hunter.actions).isNull() assertThat(hunter.priority).isEqualTo(HIGH) } @Test fun getPriorityWithMultipleRequests() { val requestHandler = NetworkRequestHandler(UNUSED_CALL_FACTORY) val picasso = mockPicasso(context, requestHandler) val action1 = mockAction(picasso = picasso, key = URI_KEY_1, uri = URI_1, priority = NORMAL) val action2 = mockAction(picasso = picasso, key = URI_KEY_1, uri = URI_1, priority = HIGH) val hunter = forRequest(picasso, dispatcher, cache, action1) hunter.attach(action2) assertThat(hunter.action).isEqualTo(action1) assertThat(hunter.actions).containsExactly(action2) assertThat(hunter.priority).isEqualTo(HIGH) } @Test fun getPriorityAfterDetach() { val requestHandler = NetworkRequestHandler(UNUSED_CALL_FACTORY) val picasso = mockPicasso(context, requestHandler) val action1 = mockAction(picasso = picasso, key = URI_KEY_1, uri = URI_1, priority = NORMAL) val action2 = mockAction(picasso = picasso, key = URI_KEY_1, uri = URI_1, priority = HIGH) val hunter = forRequest(picasso, dispatcher, cache, action1) hunter.attach(action2) assertThat(hunter.action).isEqualTo(action1) assertThat(hunter.actions).containsExactly(action2) assertThat(hunter.priority).isEqualTo(HIGH) hunter.detach(action2) assertThat(hunter.action).isEqualTo(action1) assertThat(hunter.actions).isEmpty() assertThat(hunter.priority).isEqualTo(NORMAL) } @Test fun exifRotation() { val data = Request.Builder(URI_1).rotate(-45f).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, ORIENTATION_ROTATE_90) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("rotate 90.0") } @Test fun exifRotationSizing() { val data = Request.Builder(URI_1).resize(5, 10).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, ORIENTATION_ROTATE_90) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).contains("scale 1.0 0.5") } @Test fun exifRotationNoSizing() { val data = Request.Builder(URI_1).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, ORIENTATION_ROTATE_90) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).contains("rotate 90.0") } @Test fun rotation90Sizing() { val data = Request.Builder(URI_1).rotate(90f).resize(5, 10).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).contains("scale 1.0 0.5") } @Test fun rotation180Sizing() { val data = Request.Builder(URI_1).rotate(180f).resize(5, 10).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).contains("scale 0.5 1.0") } @Test fun rotation90WithPivotSizing() { val data = Request.Builder(URI_1).rotate(90f, 0f, 10f).resize(5, 10).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).contains("scale 1.0 0.5") } @Test fun exifVerticalFlip() { val data = Request.Builder(URI_1).rotate(-45f).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, ExifInterface.ORIENTATION_FLIP_VERTICAL) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.postOperations).containsExactly("scale -1.0 1.0") assertThat(shadowMatrix.preOperations).containsExactly("rotate 180.0") } @Test fun exifHorizontalFlip() { val data = Request.Builder(URI_1).rotate(-45f).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, ExifInterface.ORIENTATION_FLIP_HORIZONTAL) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.postOperations).containsExactly("scale -1.0 1.0") assertThat(shadowMatrix.preOperations).doesNotContain("rotate 180.0") assertThat(shadowMatrix.preOperations).doesNotContain("rotate 90.0") assertThat(shadowMatrix.preOperations).doesNotContain("rotate 270.0") } @Test fun exifTranspose() { val data = Request.Builder(URI_1).rotate(-45f).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, ExifInterface.ORIENTATION_TRANSPOSE) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.postOperations).containsExactly("scale -1.0 1.0") assertThat(shadowMatrix.preOperations).containsExactly("rotate 90.0") } @Test fun exifTransverse() { val data = Request.Builder(URI_1).rotate(-45f).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, ExifInterface.ORIENTATION_TRANSVERSE) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.postOperations).containsExactly("scale -1.0 1.0") assertThat(shadowMatrix.preOperations).containsExactly("rotate 270.0") } @Test fun keepsAspectRationWhileResizingWhenDesiredWidthIs0() { val request = Request.Builder(URI_1).resize(20, 0).build() val source = android.graphics.Bitmap.createBitmap(40, 20, ARGB_8888) val result = transformResult(request, source, 0) val shadowBitmap = shadowOf(result) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 0.5 0.5") } @Test fun keepsAspectRationWhileResizingWhenDesiredHeightIs0() { val request = Request.Builder(URI_1).resize(0, 10).build() val source = android.graphics.Bitmap.createBitmap(40, 20, ARGB_8888) val result = transformResult(request, source, 0) val shadowBitmap = shadowOf(result) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 0.5 0.5") } @Test fun centerCropResultMatchesTargetSize() { val request = Request.Builder(URI_1).resize(1080, 642).centerCrop().build() val source = android.graphics.Bitmap.createBitmap(640, 640, ARGB_8888) val result = transformResult(request, source, 0) assertThat(result.width).isEqualTo(1080) assertThat(result.height).isEqualTo(642) } @Test fun centerCropResultMatchesTargetSizeWhileDesiredWidthIs0() { val request = Request.Builder(URI_1).resize(0, 642).centerCrop().build() val source = android.graphics.Bitmap.createBitmap(640, 640, ARGB_8888) val result = transformResult(request, source, 0) assertThat(result.width).isEqualTo(642) assertThat(result.height).isEqualTo(642) } @Test fun centerCropResultMatchesTargetSizeWhileDesiredHeightIs0() { val request = Request.Builder(URI_1).resize(1080, 0).centerCrop().build() val source = android.graphics.Bitmap.createBitmap(640, 640, ARGB_8888) val result = transformResult(request, source, 0) assertThat(result.width).isEqualTo(1080) assertThat(result.height).isEqualTo(1080) } @Test fun centerInsideResultMatchesTargetSizeWhileDesiredWidthIs0() { val request = Request.Builder(URI_1).resize(0, 642).centerInside().build() val source = android.graphics.Bitmap.createBitmap(640, 640, ARGB_8888) val result = transformResult(request, source, 0) assertThat(result.width).isEqualTo(642) assertThat(result.height).isEqualTo(642) } @Test fun centerInsideResultMatchesTargetSizeWhileDesiredHeightIs0() { val request = Request.Builder(URI_1).resize(1080, 0).centerInside().build() val source = android.graphics.Bitmap.createBitmap(640, 640, ARGB_8888) val result = transformResult(request, source, 0) assertThat(result.width).isEqualTo(1080) assertThat(result.height).isEqualTo(1080) } @Test fun exifRotationWithManualRotation() { val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val data = Request.Builder(URI_1).rotate(-45f).build() val result = transformResult(data, source, ORIENTATION_ROTATE_90) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("rotate 90.0") assertThat(shadowMatrix.setOperations).containsEntry("rotate", "-45.0") } @Test fun rotation() { val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val data = Request.Builder(URI_1).rotate(-45f).build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.setOperations).containsEntry("rotate", "-45.0") } @Test fun pivotRotation() { val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val data = Request.Builder(URI_1).rotate(-45f, 10f, 10f).build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.setOperations).containsEntry("rotate", "-45.0 10.0 10.0") } @Test fun resize() { val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val data = Request.Builder(URI_1).resize(20, 15).build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 2.0 1.5") } @Test fun centerCropTallTooSmall() { val source = android.graphics.Bitmap.createBitmap(10, 20, ARGB_8888) val data = Request.Builder(URI_1).resize(40, 40).centerCrop().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) assertThat(shadowBitmap.createdFromX).isEqualTo(0) assertThat(shadowBitmap.createdFromY).isEqualTo(5) assertThat(shadowBitmap.createdFromWidth).isEqualTo(10) assertThat(shadowBitmap.createdFromHeight).isEqualTo(10) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 4.0 4.0") } @Test fun centerCropTallTooLarge() { val source = android.graphics.Bitmap.createBitmap(100, 200, ARGB_8888) val data = Request.Builder(URI_1).resize(50, 50).centerCrop().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) assertThat(shadowBitmap.createdFromX).isEqualTo(0) assertThat(shadowBitmap.createdFromY).isEqualTo(50) assertThat(shadowBitmap.createdFromWidth).isEqualTo(100) assertThat(shadowBitmap.createdFromHeight).isEqualTo(100) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 0.5 0.5") } @Test fun centerCropWideTooSmall() { val source = android.graphics.Bitmap.createBitmap(20, 10, ARGB_8888) val data = Request.Builder(URI_1).resize(40, 40).centerCrop().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) assertThat(shadowBitmap.createdFromX).isEqualTo(5) assertThat(shadowBitmap.createdFromY).isEqualTo(0) assertThat(shadowBitmap.createdFromWidth).isEqualTo(10) assertThat(shadowBitmap.createdFromHeight).isEqualTo(10) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 4.0 4.0") } @Test fun centerCropWithGravityHorizontalLeft() { val source = android.graphics.Bitmap.createBitmap(20, 10, ARGB_8888) val data = Request.Builder(URI_1).resize(40, 40).centerCrop(Gravity.LEFT).build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) assertThat(shadowBitmap.createdFromX).isEqualTo(0) assertThat(shadowBitmap.createdFromY).isEqualTo(0) assertThat(shadowBitmap.createdFromWidth).isEqualTo(10) assertThat(shadowBitmap.createdFromHeight).isEqualTo(10) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 4.0 4.0") } @Test fun centerCropWithGravityHorizontalRight() { val source = android.graphics.Bitmap.createBitmap(20, 10, ARGB_8888) val data = Request.Builder(URI_1).resize(40, 40).centerCrop(Gravity.RIGHT).build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) assertThat(shadowBitmap.createdFromX).isEqualTo(10) assertThat(shadowBitmap.createdFromY).isEqualTo(0) assertThat(shadowBitmap.createdFromWidth).isEqualTo(10) assertThat(shadowBitmap.createdFromHeight).isEqualTo(10) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 4.0 4.0") } @Test fun centerCropWithGravityVerticalTop() { val source = android.graphics.Bitmap.createBitmap(10, 20, ARGB_8888) val data = Request.Builder(URI_1).resize(40, 40).centerCrop(Gravity.TOP).build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) assertThat(shadowBitmap.createdFromX).isEqualTo(0) assertThat(shadowBitmap.createdFromY).isEqualTo(0) assertThat(shadowBitmap.createdFromWidth).isEqualTo(10) assertThat(shadowBitmap.createdFromHeight).isEqualTo(10) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 4.0 4.0") } @Test fun centerCropWithGravityVerticalBottom() { val source = android.graphics.Bitmap.createBitmap(10, 20, ARGB_8888) val data = Request.Builder(URI_1).resize(40, 40).centerCrop(Gravity.BOTTOM).build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) assertThat(shadowBitmap.createdFromX).isEqualTo(0) assertThat(shadowBitmap.createdFromY).isEqualTo(10) assertThat(shadowBitmap.createdFromWidth).isEqualTo(10) assertThat(shadowBitmap.createdFromHeight).isEqualTo(10) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 4.0 4.0") } @Test fun centerCropWideTooLarge() { val source = android.graphics.Bitmap.createBitmap(200, 100, ARGB_8888) val data = Request.Builder(URI_1).resize(50, 50).centerCrop().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) assertThat(shadowBitmap.createdFromX).isEqualTo(50) assertThat(shadowBitmap.createdFromY).isEqualTo(0) assertThat(shadowBitmap.createdFromWidth).isEqualTo(100) assertThat(shadowBitmap.createdFromHeight).isEqualTo(100) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 0.5 0.5") } @Test fun centerInsideTallTooSmall() { val source = android.graphics.Bitmap.createBitmap(20, 10, ARGB_8888) val data = Request.Builder(URI_1).resize(50, 50).centerInside().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 2.5 2.5") } @Test fun centerInsideTallTooLarge() { val source = android.graphics.Bitmap.createBitmap(100, 50, ARGB_8888) val data = Request.Builder(URI_1).resize(50, 50).centerInside().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 0.5 0.5") } @Test fun centerInsideWideTooSmall() { val source = android.graphics.Bitmap.createBitmap(10, 20, ARGB_8888) val data = Request.Builder(URI_1).resize(50, 50).centerInside().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 2.5 2.5") } @Test fun centerInsideWideTooLarge() { val source = android.graphics.Bitmap.createBitmap(50, 100, ARGB_8888) val data = Request.Builder(URI_1).resize(50, 50).centerInside().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 0.5 0.5") } @Test fun onlyScaleDownOriginalBigger() { val source = android.graphics.Bitmap.createBitmap(100, 100, ARGB_8888) val data = Request.Builder(URI_1).resize(50, 50).onlyScaleDown().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 0.5 0.5") } @Test fun onlyScaleDownOriginalSmaller() { val source = android.graphics.Bitmap.createBitmap(50, 50, ARGB_8888) val data = Request.Builder(URI_1).resize(100, 100).onlyScaleDown().build() val result = transformResult(data, source, 0) assertThat(result).isSameInstanceAs(source) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isNull() assertThat(shadowBitmap.createdFromBitmap).isNotSameInstanceAs(source) } @Test fun onlyScaleDownOriginalSmallerWidthIs0() { val source = android.graphics.Bitmap.createBitmap(50, 50, ARGB_8888) val data = Request.Builder(URI_1).resize(0, 60).onlyScaleDown().build() val result = transformResult(data, source, 0) assertThat(result).isSameInstanceAs(source) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isNull() } @Test fun onlyScaleDownOriginalSmallerHeightIs0() { val source = android.graphics.Bitmap.createBitmap(50, 50, ARGB_8888) val data = Request.Builder(URI_1).resize(60, 0).onlyScaleDown().build() val result = transformResult(data, source, 0) assertThat(result).isSameInstanceAs(source) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isNull() } @Test fun onlyScaleDownOriginalBiggerWidthIs0() { val source = android.graphics.Bitmap.createBitmap(50, 50, ARGB_8888) val data = Request.Builder(URI_1).resize(0, 40).onlyScaleDown().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 0.8 0.8") } @Test fun onlyScaleDownOriginalBiggerHeightIs0() { val source = android.graphics.Bitmap.createBitmap(50, 50, ARGB_8888) val data = Request.Builder(URI_1).resize(40, 0).onlyScaleDown().build() val result = transformResult(data, source, 0) val shadowBitmap = shadowOf(result) assertThat(shadowBitmap.createdFromBitmap).isSameInstanceAs(source) val matrix = shadowBitmap.createdFromMatrix val shadowMatrix = shadowOf(matrix) assertThat(shadowMatrix.preOperations).containsExactly("scale 0.8 0.8") } @Test fun reusedBitmapIsNotRecycled() { val data = Request.Builder(URI_1).build() val source = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = transformResult(data, source, 0) assertThat(result).isSameInstanceAs(source) assertThat(result.isRecycled).isFalse() } @Test fun crashingOnTransformationThrows() { val badTransformation = object : Transformation { override fun transform(source: Bitmap): Bitmap { throw NullPointerException("hello") } override fun key(): String { return "test" } } val transformations = listOf(badTransformation) val original = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = RequestHandler.Result.Bitmap(original, MEMORY, 0) val data = Request.Builder(URI_1).build() try { applyTransformations(picasso, data, transformations, result) shadowOf(Looper.getMainLooper()).idle() fail("Expected exception to be thrown.") } catch (e: RuntimeException) { assertThat(e) .hasMessageThat() .isEqualTo("Transformation ${badTransformation.key()} crashed with exception.") } } @Test fun recycledTransformationBitmapThrows() { val badTransformation: Transformation = object : Transformation { override fun transform(source: Bitmap): Bitmap { source.bitmap.recycle() return source } override fun key(): String { return "test" } } val transformations = listOf(badTransformation) val original = android.graphics.Bitmap.createBitmap(10, 10, ARGB_8888) val result = RequestHandler.Result.Bitmap(original, MEMORY, 0) val data = Request.Builder(URI_1).build() try { applyTransformations(picasso, data, transformations, result) shadowOf(Looper.getMainLooper()).idle() fail("Expected exception to be thrown.") } catch (e: RuntimeException) { assertThat(e) .hasMessageThat() .isEqualTo("Transformation ${badTransformation.key()} returned a recycled Bitmap.") } } // TODO: fix regression from https://github.com/square/picasso/pull/2137 // @Test public void transformDrawables() { // final AtomicInteger transformationCount = new AtomicInteger(); // Transformation identity = new Transformation() { // @Override public RequestHandler.Result.Bitmap transform(RequestHandler.Result.Bitmap source) { // transformationCount.incrementAndGet(); // return source; // } // // @Override public String key() { // return "test"; // } // }; // List transformations = asList(identity, identity, identity); // Drawable original = new BitmapDrawable(Bitmap.createBitmap(10, 10, ARGB_8888)); // RequestHandler.Result.Bitmap result = new RequestHandler.Result.Bitmap(original, MEMORY); // Request data = new Request.Builder(URI_1).build(); // BitmapHunter.applyTransformations(picasso, data, transformations, result); // assertThat(transformationCount.get()).isEqualTo(3); // } internal class TestableBitmapHunter( picasso: Picasso, dispatcher: Dispatcher, cache: PlatformLruCache, action: Action, result: android.graphics.Bitmap? = null, exception: Exception? = null, shouldRetry: Boolean = false, supportsReplay: Boolean = false ) : BitmapHunter( picasso, dispatcher, cache, action, TestableRequestHandler(result, exception, shouldRetry, supportsReplay) ) private class TestableRequestHandler internal constructor( private val bitmap: android.graphics.Bitmap?, private val exception: Exception?, private val shouldRetry: Boolean, private val supportsReplay: Boolean ) : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { return true } override fun load(picasso: Picasso, request: Request, callback: Callback) { if (exception != null) { callback.onError(exception) } else { callback.onSuccess(Bitmap(bitmap!!, NETWORK)) } } override val retryCount: Int get() = 1 override fun shouldRetry(airplaneMode: Boolean, info: NetworkInfo?): Boolean { return shouldRetry } override fun supportsReplay(): Boolean { return supportsReplay } } private inner class CustomRequestHandler : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { return CUSTOM_URI.scheme == data.uri!!.scheme } override fun load(picasso: Picasso, request: Request, callback: Callback) { callback.onSuccess(Result.Bitmap(bitmap, MEMORY)) } } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/BitmapTargetActionTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.graphics.Bitmap import android.graphics.Bitmap.Config.ARGB_8888 import android.graphics.drawable.Drawable import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.TestUtils.NO_EVENT_LISTENERS import com.squareup.picasso3.TestUtils.NO_HANDLERS import com.squareup.picasso3.TestUtils.NO_TRANSFORMERS import com.squareup.picasso3.TestUtils.RESOURCE_ID_1 import com.squareup.picasso3.TestUtils.SIMPLE_REQUEST import com.squareup.picasso3.TestUtils.UNUSED_CALL_FACTORY import com.squareup.picasso3.TestUtils.makeBitmap import com.squareup.picasso3.TestUtils.mockBitmapTarget import com.squareup.picasso3.TestUtils.mockPicasso import org.junit.Assert.fail import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class BitmapTargetActionTest { @Test fun invokesSuccessIfTargetIsNotNull() { val bitmap = makeBitmap() val target = mockBitmapTarget() val request = BitmapTargetAction( picasso = mockPicasso(RuntimeEnvironment.application), target = target, data = SIMPLE_REQUEST, errorDrawable = null, errorResId = 0 ) request.complete(RequestHandler.Result.Bitmap(bitmap, MEMORY)) verify(target).onBitmapLoaded(bitmap, MEMORY) } @Test fun invokesOnBitmapFailedIfTargetIsNotNullWithErrorDrawable() { val errorDrawable = mock(Drawable::class.java) val target = mockBitmapTarget() val request = BitmapTargetAction( picasso = mockPicasso(RuntimeEnvironment.application), target = target, data = SIMPLE_REQUEST, errorDrawable = errorDrawable, errorResId = 0 ) val e = RuntimeException() request.error(e) verify(target).onBitmapFailed(e, errorDrawable) } @Test fun invokesOnBitmapFailedIfTargetIsNotNullWithErrorResourceId() { val errorDrawable = mock(Drawable::class.java) val target = mockBitmapTarget() val context = mock(Context::class.java) val dispatcher = mock(Dispatcher::class.java) val cache = PlatformLruCache(0) val picasso = Picasso( context, dispatcher, UNUSED_CALL_FACTORY, null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, NO_EVENT_LISTENERS, ARGB_8888, false, false ) val request = BitmapTargetAction( picasso = picasso, target = target, data = SIMPLE_REQUEST, errorDrawable = null, errorResId = RESOURCE_ID_1 ) `when`(context.getDrawable(RESOURCE_ID_1)).thenReturn(errorDrawable) val e = RuntimeException() request.error(e) verify(target).onBitmapFailed(e, errorDrawable) } @Test fun recyclingInSuccessThrowsException() { val picasso = mockPicasso(RuntimeEnvironment.application) val bitmap = makeBitmap() val tr = BitmapTargetAction( picasso = picasso, target = object : BitmapTarget { override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) = bitmap.recycle() override fun onBitmapFailed(e: Exception, errorDrawable: Drawable?) = fail() override fun onPrepareLoad(placeHolderDrawable: Drawable?) = fail() }, data = SIMPLE_REQUEST, errorDrawable = null, errorResId = 0 ) try { tr.complete(RequestHandler.Result.Bitmap(bitmap, MEMORY)) fail() } catch (ignored: IllegalStateException) { } } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/DeferredRequestCreatorTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.TestUtils.argumentCaptor import com.squareup.picasso3.TestUtils.mockCallback import com.squareup.picasso3.TestUtils.mockFitImageViewTarget import com.squareup.picasso3.TestUtils.mockPicasso import com.squareup.picasso3.TestUtils.mockRequestCreator import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class DeferredRequestCreatorTest { private val picasso = mockPicasso(RuntimeEnvironment.application) @Test fun initWhileAttachedAddsAttachAndPreDrawListener() { val target = mockFitImageViewTarget(true) val observer = target.viewTreeObserver val request = DeferredRequestCreator(mockRequestCreator(picasso), target, null) verify(observer).addOnPreDrawListener(request) } @Test fun initWhileDetachedAddsAttachListenerWhichDefersPreDrawListener() { val target = mockFitImageViewTarget(true) `when`(target.windowToken).thenReturn(null) val observer = target.viewTreeObserver val request = DeferredRequestCreator(mockRequestCreator(picasso), target, null) verify(target).addOnAttachStateChangeListener(request) verifyNoMoreInteractions(observer) // Attach and ensure we defer to the pre-draw listener. request.onViewAttachedToWindow(target) verify(observer).addOnPreDrawListener(request) // Detach and ensure we remove the pre-draw listener from the original VTO. request.onViewDetachedFromWindow(target) verify(observer).removeOnPreDrawListener(request) } @Test fun cancelWhileAttachedRemovesAttachListener() { val target = mockFitImageViewTarget(true) val request = DeferredRequestCreator(mockRequestCreator(picasso), target, null) verify(target).addOnAttachStateChangeListener(request) request.cancel() verify(target).removeOnAttachStateChangeListener(request) } @Test fun cancelClearsCallback() { val target = mockFitImageViewTarget(true) val callback = mockCallback() val request = DeferredRequestCreator(mockRequestCreator(picasso), target, callback) assertThat(request.callback).isNotNull() request.cancel() assertThat(request.callback).isNull() } @Test fun cancelClearsTag() { val target = mockFitImageViewTarget(true) val creator = mockRequestCreator(picasso).tag("TAG") val request = DeferredRequestCreator(creator, target, null) assertThat(creator.tag).isNotNull() request.cancel() assertThat(creator.tag).isNull() } @Test fun onLayoutSkipsIfViewIsAttachedAndViewTreeObserverIsDead() { val target = mockFitImageViewTarget(false) val creator = mockRequestCreator(picasso) val request = DeferredRequestCreator(creator, target, null) val viewTreeObserver = target.viewTreeObserver request.onPreDraw() verify(viewTreeObserver).addOnPreDrawListener(request) verify(viewTreeObserver).isAlive verifyNoMoreInteractions(viewTreeObserver) } @Test fun waitsForAnotherLayoutIfWidthOrHeightIsZero() { val target = mockFitImageViewTarget(true) `when`(target.width).thenReturn(0) `when`(target.height).thenReturn(0) val creator = mockRequestCreator(picasso) val request = DeferredRequestCreator(creator, target, null) request.onPreDraw() verify(target.viewTreeObserver, never()).removeOnPreDrawListener(request) } @Test fun cancelSkipsIfViewTreeObserverIsDead() { val target = mockFitImageViewTarget(false) val creator = mockRequestCreator(picasso) val request = DeferredRequestCreator(creator, target, null) request.cancel() verify(target.viewTreeObserver, never()).removeOnPreDrawListener(request) } @Test fun preDrawSubmitsRequestAndCleansUp() { val spyPicasso = spy(picasso) // ugh val creator = RequestCreator(spyPicasso, TestUtils.URI_1, 0) val target = mockFitImageViewTarget(true) `when`(target.width).thenReturn(100) `when`(target.height).thenReturn(100) val observer = target.viewTreeObserver val request = DeferredRequestCreator(creator, target, null) request.onPreDraw() verify(observer).removeOnPreDrawListener(request) val actionCaptor = argumentCaptor() verify(spyPicasso).enqueueAndSubmit(actionCaptor.capture()) val value = actionCaptor.value assertThat(value).isInstanceOf(ImageViewAction::class.java) assertThat(value.request.targetWidth).isEqualTo(100) assertThat(value.request.targetHeight).isEqualTo(100) } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/DrawableTargetActionTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.content.res.Resources import android.graphics.Bitmap.Config.ARGB_8888 import android.graphics.drawable.Drawable import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.Picasso.LoadedFrom.NETWORK import com.squareup.picasso3.TestUtils.argumentCaptor import com.squareup.picasso3.TestUtils.eq import com.squareup.picasso3.TestUtils.makeBitmap import com.squareup.picasso3.TestUtils.mockDrawableTarget import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito import org.mockito.Mockito.mock import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class DrawableTargetActionTest { @Test fun invokesSuccessIfTargetIsNotNull() { val bitmap = makeBitmap() val target = mockDrawableTarget() val drawableCaptor = argumentCaptor() val placeholder = mock(Drawable::class.java) val action = DrawableTargetAction( picasso = TestUtils.mockPicasso(RuntimeEnvironment.application), target = target, data = TestUtils.SIMPLE_REQUEST, noFade = false, placeholderDrawable = placeholder, errorDrawable = null, errorResId = 0 ) action.complete(RequestHandler.Result.Bitmap(bitmap, NETWORK)) Mockito.verify(target).onDrawableLoaded(drawableCaptor.capture(), eq(NETWORK)) with(drawableCaptor.value) { assertThat(this.bitmap).isEqualTo(bitmap) assertThat(this.placeholder).isEqualTo(placeholder) assertThat(this.animating).isTrue() } } @Test fun invokesOnBitmapFailedIfTargetIsNotNullWithErrorDrawable() { val errorDrawable = mock(Drawable::class.java) val target = mockDrawableTarget() val action = DrawableTargetAction( picasso = TestUtils.mockPicasso(RuntimeEnvironment.application), target = target, data = TestUtils.SIMPLE_REQUEST, noFade = true, placeholderDrawable = null, errorDrawable = errorDrawable, errorResId = 0 ) val e = RuntimeException() action.error(e) Mockito.verify(target).onDrawableFailed(e, errorDrawable) } @Test fun invokesOnBitmapFailedIfTargetIsNotNullWithErrorResourceId() { val errorDrawable = mock(Drawable::class.java) val context = mock(Context::class.java) val dispatcher = mock(Dispatcher::class.java) val cache = PlatformLruCache(0) val picasso = Picasso( context, dispatcher, TestUtils.UNUSED_CALL_FACTORY, null, cache, null, TestUtils.NO_TRANSFORMERS, TestUtils.NO_HANDLERS, TestUtils.NO_EVENT_LISTENERS, ARGB_8888, false, false ) val res = mock(Resources::class.java) val target = mockDrawableTarget() val action = DrawableTargetAction( picasso = picasso, target = target, data = TestUtils.SIMPLE_REQUEST, noFade = true, placeholderDrawable = null, errorDrawable = null, errorResId = TestUtils.RESOURCE_ID_1 ) Mockito.`when`(context.getDrawable(TestUtils.RESOURCE_ID_1)).thenReturn(errorDrawable) val e = RuntimeException() action.error(e) Mockito.verify(target).onDrawableFailed(e, errorDrawable) } @Test fun recyclingInSuccessThrowsException() { val picasso = TestUtils.mockPicasso(RuntimeEnvironment.application) val bitmap = makeBitmap() val action = DrawableTargetAction( picasso = picasso, target = object : DrawableTarget { override fun onDrawableLoaded(drawable: Drawable, from: Picasso.LoadedFrom) = (drawable as PicassoDrawable).bitmap.recycle() override fun onDrawableFailed(e: Exception, errorDrawable: Drawable?) = throw AssertionError() override fun onPrepareLoad(placeHolderDrawable: Drawable?) = throw AssertionError() }, data = TestUtils.SIMPLE_REQUEST, noFade = true, placeholderDrawable = null, errorDrawable = null, errorResId = 0 ) try { action.complete(RequestHandler.Result.Bitmap(bitmap, MEMORY)) Assert.fail() } catch (ignored: IllegalStateException) { } } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/HandlerDispatcherTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.content.Context.CONNECTIVITY_SERVICE import android.content.pm.PackageManager.PERMISSION_DENIED import android.content.pm.PackageManager.PERMISSION_GRANTED import android.net.ConnectivityManager import android.net.NetworkInfo import android.os.Handler import android.os.Looper.getMainLooper import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.MemoryPolicy.NO_STORE import com.squareup.picasso3.NetworkRequestHandler.ContentLengthException import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.Picasso.LoadedFrom.NETWORK import com.squareup.picasso3.Request.Builder import com.squareup.picasso3.TestUtils.TestDelegatingService import com.squareup.picasso3.TestUtils.URI_1 import com.squareup.picasso3.TestUtils.URI_2 import com.squareup.picasso3.TestUtils.URI_KEY_1 import com.squareup.picasso3.TestUtils.URI_KEY_2 import com.squareup.picasso3.TestUtils.any import com.squareup.picasso3.TestUtils.makeBitmap import com.squareup.picasso3.TestUtils.mockAction import com.squareup.picasso3.TestUtils.mockBitmapTarget import com.squareup.picasso3.TestUtils.mockCallback import com.squareup.picasso3.TestUtils.mockHunter import com.squareup.picasso3.TestUtils.mockNetworkInfo import com.squareup.picasso3.TestUtils.mockPicasso import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.doReturn import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations.initMocks import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.shadows.ShadowLooper import java.lang.Exception import java.util.concurrent.ExecutorService import java.util.concurrent.Future import java.util.concurrent.FutureTask @RunWith(RobolectricTestRunner::class) class HandlerDispatcherTest { @Mock lateinit var context: Context @Mock lateinit var connectivityManager: ConnectivityManager @Mock lateinit var serviceMock: ExecutorService private lateinit var picasso: Picasso private lateinit var dispatcher: HandlerDispatcher private val executorService = spy(PicassoExecutorService()) private val cache = PlatformLruCache(2048) private val service = TestDelegatingService(executorService) private val bitmap1 = makeBitmap() @Before fun setUp() { initMocks(this) `when`(context.applicationContext).thenReturn(context) doReturn(mock(Future::class.java)).`when`(executorService).submit(any(Runnable::class.java)) picasso = mockPicasso(context) dispatcher = createDispatcher(service) } @Test fun shutdownStopsService() { val service = PicassoExecutorService() dispatcher = createDispatcher(service) dispatcher.shutdown() assertThat(service.isShutdown).isEqualTo(true) } @Test fun shutdownUnregistersReceiver() { dispatcher.shutdown() shadowOf(getMainLooper()).idle() verify(context).unregisterReceiver(dispatcher.receiver) } @Test fun performSubmitWithNewRequestQueuesHunter() { val action = mockAction(picasso, URI_KEY_1, URI_1) dispatcher.performSubmit(action) assertThat(dispatcher.hunterMap).hasSize(1) assertThat(service.submissions).isEqualTo(1) } @Test fun performSubmitWithTwoDifferentRequestsQueuesHunters() { val action1 = mockAction(picasso, URI_KEY_1, URI_1) val action2 = mockAction(picasso, URI_KEY_2, URI_2) dispatcher.performSubmit(action1) dispatcher.performSubmit(action2) assertThat(dispatcher.hunterMap).hasSize(2) assertThat(service.submissions).isEqualTo(2) } @Test fun performSubmitWithExistingRequestAttachesToHunter() { val action1 = mockAction(picasso, URI_KEY_1, URI_1) val action2 = mockAction(picasso, URI_KEY_1, URI_1) dispatcher.performSubmit(action1) assertThat(dispatcher.hunterMap).hasSize(1) assertThat(service.submissions).isEqualTo(1) dispatcher.performSubmit(action2) assertThat(dispatcher.hunterMap).hasSize(1) assertThat(service.submissions).isEqualTo(1) } @Test fun performSubmitWithShutdownServiceIgnoresRequest() { service.shutdown() val action = mockAction(picasso, URI_KEY_1, URI_1) dispatcher.performSubmit(action) assertThat(dispatcher.hunterMap).isEmpty() assertThat(service.submissions).isEqualTo(0) } @Test fun performSubmitWithFetchAction() { val pausedTag = "pausedTag" dispatcher.pausedTags.add(pausedTag) assertThat(dispatcher.pausedActions).isEmpty() val fetchAction1 = FetchAction(picasso, Request.Builder(URI_1).tag(pausedTag).build(), null) val fetchAction2 = FetchAction(picasso, Request.Builder(URI_1).tag(pausedTag).build(), null) dispatcher.performSubmit(fetchAction1) dispatcher.performSubmit(fetchAction2) assertThat(dispatcher.pausedActions).hasSize(2) } @Test fun performCancelWithFetchActionWithCallback() { val pausedTag = "pausedTag" dispatcher.pausedTags.add(pausedTag) assertThat(dispatcher.pausedActions).isEmpty() val callback = mockCallback() val fetchAction1 = FetchAction(picasso, Request.Builder(URI_1).tag(pausedTag).build(), callback) dispatcher.performCancel(fetchAction1) fetchAction1.cancel() assertThat(dispatcher.pausedActions).isEmpty() } @Test fun performCancelDetachesRequestAndCleansUp() { val target = mockBitmapTarget() val action = mockAction(picasso, URI_KEY_1, URI_1, target) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) hunter.future = FutureTask(mock(Runnable::class.java), mock(Any::class.java)) dispatcher.hunterMap[URI_KEY_1 + Request.KEY_SEPARATOR] = hunter dispatcher.failedActions[target] = action dispatcher.performCancel(action) assertThat(hunter.action).isNull() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() } @Test fun performCancelMultipleRequestsDetachesOnly() { val action1 = mockAction(picasso, URI_KEY_1, URI_1) val action2 = mockAction(picasso, URI_KEY_1, URI_1) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action1) hunter.attach(action2) dispatcher.hunterMap[URI_KEY_1 + Request.KEY_SEPARATOR] = hunter dispatcher.performCancel(action1) assertThat(hunter.action).isNull() assertThat(hunter.actions).containsExactly(action2) assertThat(dispatcher.hunterMap).hasSize(1) } @Test fun performCancelUnqueuesAndDetachesPausedRequest() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockBitmapTarget(), tag = "tag") val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) dispatcher.hunterMap[URI_KEY_1 + Request.KEY_SEPARATOR] = hunter dispatcher.pausedTags.add("tag") dispatcher.pausedActions[action.getTarget()] = action dispatcher.performCancel(action) assertThat(hunter.action).isNull() assertThat(dispatcher.pausedTags).containsExactly("tag") assertThat(dispatcher.pausedActions).isEmpty() } @Test fun performCompleteSetsResultInCache() { val data = Request.Builder(URI_1).build() val action = noopAction(data) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) hunter.run() assertThat(cache.size()).isEqualTo(0) dispatcher.performComplete(hunter) assertThat(hunter.result).isInstanceOf(RequestHandler.Result.Bitmap::class.java) val result = hunter.result as RequestHandler.Result.Bitmap assertThat(result.bitmap).isEqualTo(bitmap1) assertThat(result.loadedFrom).isEqualTo(NETWORK) assertThat(cache[hunter.key]).isSameInstanceAs(bitmap1) } @Test fun performCompleteWithNoStoreMemoryPolicy() { val data = Request.Builder(URI_1).memoryPolicy(NO_STORE).build() val action = noopAction(data) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) hunter.run() assertThat(cache.size()).isEqualTo(0) dispatcher.performComplete(hunter) assertThat(dispatcher.hunterMap).isEmpty() assertThat(cache.size()).isEqualTo(0) } @Test fun performCompleteCleansUpAndPostsToMain() { val data = Request.Builder(URI_1).build() var completed = false val action = noopAction(data, onComplete = { completed = true }) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) hunter.run() dispatcher.performComplete(hunter) ShadowLooper.idleMainLooper() assertThat(dispatcher.hunterMap).isEmpty() assertThat(completed).isTrue() } @Test fun performCompleteCleansUpAndDoesNotPostToMainIfCancelled() { val data = Request.Builder(URI_1).build() var completed = false val action = noopAction(data, onComplete = { completed = true }) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) hunter.run() hunter.future = FutureTask(mock(Runnable::class.java), null) hunter.future!!.cancel(false) dispatcher.performComplete(hunter) ShadowLooper.idleMainLooper() assertThat(dispatcher.hunterMap).isEmpty() assertThat(completed).isFalse() } @Test fun performErrorCleansUpAndPostsToMain() { val exception = RuntimeException() val action = mockAction(picasso, URI_KEY_1, URI_1, mockBitmapTarget(), tag = "tag") val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action, exception) dispatcher.hunterMap[hunter.key] = hunter hunter.run() dispatcher.performError(hunter) ShadowLooper.idleMainLooper() assertThat(dispatcher.hunterMap).isEmpty() assertThat(action.errorException).isSameInstanceAs(exception) } @Test fun performErrorCleansUpAndDoesNotPostToMainIfCancelled() { val exception = RuntimeException() val action = mockAction(picasso, URI_KEY_1, URI_1, mockBitmapTarget(), tag = "tag") val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action, exception) hunter.future = FutureTask(mock(Runnable::class.java), mock(Any::class.java)) hunter.future!!.cancel(false) dispatcher.hunterMap[hunter.key] = hunter hunter.run() dispatcher.performError(hunter) ShadowLooper.idleMainLooper() assertThat(dispatcher.hunterMap).isEmpty() assertThat(action.errorException).isNull() assertThat(action.completedResult).isNull() } @Test fun performRetrySkipsIfHunterIsCancelled() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockBitmapTarget(), tag = "tag") val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) hunter.future = FutureTask(mock(Runnable::class.java), mock(Any::class.java)) hunter.future!!.cancel(false) dispatcher.performRetry(hunter) assertThat(hunter.isCancelled).isTrue() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() } @Test fun performRetryForContentLengthResetsNetworkPolicy() { val networkInfo = mockNetworkInfo(true) `when`(connectivityManager.activeNetworkInfo).thenReturn(networkInfo) val action = mockAction(picasso, URI_KEY_2, URI_2) val e = ContentLengthException("304 error") val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action, e, true) hunter.run() dispatcher.performRetry(hunter) assertThat(NetworkPolicy.shouldReadFromDiskCache(hunter.data.networkPolicy)).isFalse() } @Test fun performRetryDoesNotMarkForReplayIfNotSupported() { val networkInfo = mockNetworkInfo(true) val hunter = mockHunter( picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), mockAction(picasso, URI_KEY_1, URI_1) ) `when`(connectivityManager.activeNetworkInfo).thenReturn(networkInfo) dispatcher.performRetry(hunter) assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() assertThat(service.submissions).isEqualTo(0) } @Test fun performRetryDoesNotMarkForReplayIfNoNetworkScanning() { val hunter = mockHunter( picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), mockAction(picasso, URI_KEY_1, URI_1), e = null, shouldRetry = false, supportsReplay = true ) val dispatcher = createDispatcher(false) dispatcher.performRetry(hunter) assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() assertThat(service.submissions).isEqualTo(0) } @Test fun performRetryMarksForReplayIfSupportedScansNetworkChangesAndShouldNotRetry() { val networkInfo = mockNetworkInfo(true) val action = mockAction(picasso, URI_KEY_1, URI_1, mockBitmapTarget()) val hunter = mockHunter( picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action, e = null, shouldRetry = false, supportsReplay = true ) `when`(connectivityManager.activeNetworkInfo).thenReturn(networkInfo) dispatcher.performRetry(hunter) assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).hasSize(1) assertThat(service.submissions).isEqualTo(0) } @Test fun performRetryRetriesIfNoNetworkScanning() { val hunter = mockHunter( picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), mockAction(picasso, URI_KEY_1, URI_1), e = null, shouldRetry = true ) val dispatcher = createDispatcher(false) dispatcher.performRetry(hunter) assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() assertThat(service.submissions).isEqualTo(1) } @Test fun performRetryMarksForReplayIfSupportsReplayAndShouldNotRetry() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockBitmapTarget()) val hunter = mockHunter( picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action, e = null, shouldRetry = false, supportsReplay = true ) dispatcher.performRetry(hunter) assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).hasSize(1) assertThat(service.submissions).isEqualTo(0) } @Test fun performRetryRetriesIfShouldRetry() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockBitmapTarget()) val hunter = mockHunter( picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action, e = null, shouldRetry = true ) dispatcher.performRetry(hunter) assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() assertThat(service.submissions).isEqualTo(1) } @Test fun performRetrySkipIfServiceShutdown() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockBitmapTarget()) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) service.shutdown() dispatcher.performRetry(hunter) assertThat(service.submissions).isEqualTo(0) assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() } @Test fun performAirplaneModeChange() { assertThat(dispatcher.airplaneMode).isFalse() dispatcher.performAirplaneModeChange(true) assertThat(dispatcher.airplaneMode).isTrue() dispatcher.performAirplaneModeChange(false) assertThat(dispatcher.airplaneMode).isFalse() } @Test fun performNetworkStateChangeWithNullInfoIgnores() { val dispatcher = createDispatcher(serviceMock) dispatcher.performNetworkStateChange(null) assertThat(dispatcher.failedActions).isEmpty() } @Test fun performNetworkStateChangeWithDisconnectedInfoIgnores() { val dispatcher = createDispatcher(serviceMock) val info = mockNetworkInfo() `when`(info.isConnectedOrConnecting).thenReturn(false) dispatcher.performNetworkStateChange(info) assertThat(dispatcher.failedActions).isEmpty() } @Test fun performNetworkStateChangeWithConnectedInfoDifferentInstanceIgnores() { val dispatcher = createDispatcher(serviceMock) val info = mockNetworkInfo(true) dispatcher.performNetworkStateChange(info) assertThat(dispatcher.failedActions).isEmpty() } @Test fun performPauseAndResumeUpdatesListOfPausedTags() { dispatcher.performPauseTag("tag") assertThat(dispatcher.pausedTags).containsExactly("tag") dispatcher.performResumeTag("tag") assertThat(dispatcher.pausedTags).isEmpty() } @Test fun performPauseTagIsIdempotent() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockBitmapTarget(), tag = "tag") val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) dispatcher.hunterMap[URI_KEY_1] = hunter assertThat(dispatcher.pausedActions).isEmpty() dispatcher.performPauseTag("tag") assertThat(dispatcher.pausedActions).containsEntry(action.getTarget(), action) dispatcher.performPauseTag("tag") assertThat(dispatcher.pausedActions).containsEntry(action.getTarget(), action) } @Test fun performPauseTagQueuesNewRequestDoesNotSubmit() { dispatcher.performPauseTag("tag") val action = mockAction(picasso = picasso, key = URI_KEY_1, uri = URI_1, tag = "tag") dispatcher.performSubmit(action) assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.pausedActions).hasSize(1) assertThat(dispatcher.pausedActions.containsValue(action)).isTrue() assertThat(service.submissions).isEqualTo(0) } @Test fun performPauseTagDoesNotQueueUnrelatedRequest() { dispatcher.performPauseTag("tag") val action = mockAction(picasso, URI_KEY_1, URI_1, "anothertag") dispatcher.performSubmit(action) assertThat(dispatcher.hunterMap).hasSize(1) assertThat(dispatcher.pausedActions).isEmpty() assertThat(service.submissions).isEqualTo(1) } @Test fun performPauseDetachesRequestAndCancelsHunter() { val action = mockAction( picasso = picasso, key = URI_KEY_1, uri = URI_1, tag = "tag" ) val hunter = mockHunter( picasso = picasso, result = RequestHandler.Result.Bitmap(bitmap1, MEMORY), action = action, dispatcher = dispatcher ) hunter.future = FutureTask(mock(Runnable::class.java), mock(Any::class.java)) dispatcher.hunterMap[URI_KEY_1] = hunter dispatcher.performPauseTag("tag") assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.pausedActions).hasSize(1) assertThat(dispatcher.pausedActions.containsValue(action)).isTrue() assertThat(hunter.action).isNull() } @Test fun performPauseOnlyDetachesPausedRequest() { val action1 = mockAction( picasso = picasso, key = URI_KEY_1, uri = URI_1, target = mockBitmapTarget(), tag = "tag1" ) val action2 = mockAction( picasso = picasso, key = URI_KEY_1, uri = URI_1, target = mockBitmapTarget(), tag = "tag2" ) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action1) hunter.attach(action2) dispatcher.hunterMap[URI_KEY_1] = hunter dispatcher.performPauseTag("tag1") assertThat(dispatcher.hunterMap).hasSize(1) assertThat(dispatcher.hunterMap.containsValue(hunter)).isTrue() assertThat(dispatcher.pausedActions).hasSize(1) assertThat(dispatcher.pausedActions.containsValue(action1)).isTrue() assertThat(hunter.action).isNull() assertThat(hunter.actions).containsExactly(action2) } @Test fun performResumeTagResumesPausedActions() { val action = noopAction(Builder(URI_1).tag("tag").build()) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap1, MEMORY), action) dispatcher.hunterMap[URI_KEY_1] = hunter assertThat(dispatcher.pausedActions).isEmpty() dispatcher.performPauseTag("tag") assertThat(dispatcher.pausedActions).containsEntry(action.getTarget(), action) dispatcher.performResumeTag("tag") assertThat(dispatcher.pausedActions).isEmpty() } @Test fun performNetworkStateChangeFlushesFailedHunters() { val info = mockNetworkInfo(true) val failedAction1 = mockAction(picasso, URI_KEY_1, URI_1) val failedAction2 = mockAction(picasso, URI_KEY_2, URI_2) dispatcher.failedActions[URI_KEY_1] = failedAction1 dispatcher.failedActions[URI_KEY_2] = failedAction2 dispatcher.performNetworkStateChange(info) assertThat(service.submissions).isEqualTo(2) assertThat(dispatcher.failedActions).isEmpty() } private fun createDispatcher(scansNetworkChanges: Boolean): HandlerDispatcher { return createDispatcher(service, scansNetworkChanges) } private fun createDispatcher( service: ExecutorService, scansNetworkChanges: Boolean = true ): HandlerDispatcher { `when`(connectivityManager.activeNetworkInfo).thenReturn( if (scansNetworkChanges) mock(NetworkInfo::class.java) else null ) `when`(context.getSystemService(CONNECTIVITY_SERVICE)).thenReturn(connectivityManager) `when`(context.checkCallingOrSelfPermission(anyString())).thenReturn( if (scansNetworkChanges) PERMISSION_GRANTED else PERMISSION_DENIED ) return HandlerDispatcher(context, service, Handler(getMainLooper()), cache) } private fun noopAction(data: Request, onComplete: () -> Unit = { }): Action { return object : Action(picasso, data) { override fun complete(result: RequestHandler.Result) = onComplete() override fun error(e: Exception) = Unit override fun getTarget(): Any = this } } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/ImageViewActionTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap.Config.ARGB_8888 import android.graphics.drawable.AnimationDrawable import android.graphics.drawable.Drawable import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.RequestHandler.Result.Bitmap import com.squareup.picasso3.TestUtils.NO_EVENT_LISTENERS import com.squareup.picasso3.TestUtils.NO_HANDLERS import com.squareup.picasso3.TestUtils.NO_TRANSFORMERS import com.squareup.picasso3.TestUtils.RESOURCE_ID_1 import com.squareup.picasso3.TestUtils.SIMPLE_REQUEST import com.squareup.picasso3.TestUtils.UNUSED_CALL_FACTORY import com.squareup.picasso3.TestUtils.makeBitmap import com.squareup.picasso3.TestUtils.mockCallback import com.squareup.picasso3.TestUtils.mockImageViewTarget import com.squareup.picasso3.TestUtils.mockPicasso import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class ImageViewActionTest { @Test fun invokesTargetAndCallbackSuccessIfTargetIsNotNull() { val bitmap = makeBitmap() val dispatcher = mock(Dispatcher::class.java) val cache = PlatformLruCache(0) val picasso = Picasso( RuntimeEnvironment.application, dispatcher, UNUSED_CALL_FACTORY, null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, NO_EVENT_LISTENERS, ARGB_8888, false, false ) val target = mockImageViewTarget() val callback = mockCallback() val request = ImageViewAction( picasso = picasso, target = target, data = SIMPLE_REQUEST, errorDrawable = null, errorResId = 0, noFade = false, callback = callback ) request.complete(Bitmap(bitmap, MEMORY)) verify(target).setImageDrawable(any(PicassoDrawable::class.java)) verify(callback).onSuccess() } @Test fun invokesTargetAndCallbackErrorIfTargetIsNotNullWithErrorResourceId() { val target = mockImageViewTarget() val callback = mockCallback() val request = ImageViewAction( picasso = mockPicasso(RuntimeEnvironment.application), target = target, data = SIMPLE_REQUEST, errorDrawable = null, errorResId = RESOURCE_ID_1, noFade = false, callback = callback ) val e = RuntimeException() request.error(e) verify(target).setImageResource(RESOURCE_ID_1) verify(callback).onError(e) } @Test fun invokesErrorIfTargetIsNotNullWithErrorResourceId() { val target = mockImageViewTarget() val callback = mockCallback() val request = ImageViewAction( picasso = mockPicasso(RuntimeEnvironment.application), target = target, data = SIMPLE_REQUEST, errorDrawable = null, errorResId = RESOURCE_ID_1, noFade = false, callback = callback ) val e = RuntimeException() request.error(e) verify(target).setImageResource(RESOURCE_ID_1) verify(callback).onError(e) } @Test fun invokesErrorIfTargetIsNotNullWithErrorDrawable() { val errorDrawable = mock(Drawable::class.java) val target = mockImageViewTarget() val callback = mockCallback() val request = ImageViewAction( picasso = mockPicasso(RuntimeEnvironment.application), target = target, data = SIMPLE_REQUEST, errorDrawable = errorDrawable, errorResId = 0, noFade = false, callback = callback ) val e = RuntimeException() request.error(e) verify(target).setImageDrawable(errorDrawable) verify(callback).onError(e) } @Test fun clearsCallbackOnCancel() { val picasso = mockPicasso(RuntimeEnvironment.application) val target = mockImageViewTarget() val callback = mockCallback() val request = ImageViewAction( picasso = picasso, target = target, data = SIMPLE_REQUEST, errorDrawable = null, errorResId = 0, noFade = false, callback = callback ) request.cancel() assertThat(request.callback).isNull() } @Test fun stopPlaceholderAnimationOnError() { val picasso = mockPicasso(RuntimeEnvironment.application) val placeholder = mock(AnimationDrawable::class.java) val target = mockImageViewTarget() `when`(target.drawable).thenReturn(placeholder) val request = ImageViewAction( picasso = picasso, target = target, data = SIMPLE_REQUEST, errorDrawable = null, errorResId = 0, noFade = false, callback = null ) request.error(RuntimeException()) verify(placeholder).stop() } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/InternalCoroutineDispatcherTest.kt ================================================ /* * Copyright (C) 2023 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.content.pm.PackageManager import android.net.ConnectivityManager import android.net.NetworkInfo import android.os.Handler import android.os.Looper import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.MemoryPolicy.NO_STORE import com.squareup.picasso3.NetworkRequestHandler.ContentLengthException import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.Picasso.LoadedFrom.NETWORK import com.squareup.picasso3.Picasso.Priority.HIGH import com.squareup.picasso3.Request.Builder import com.squareup.picasso3.RequestHandler.Result.Bitmap import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows import java.lang.Exception import java.lang.RuntimeException import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestDispatcher @RunWith(RobolectricTestRunner::class) class InternalCoroutineDispatcherTest { @Mock lateinit var context: Context @Mock lateinit var connectivityManager: ConnectivityManager private lateinit var picasso: Picasso private lateinit var dispatcher: InternalCoroutineDispatcher private lateinit var testDispatcher: TestDispatcher private val cache = PlatformLruCache(2048) private val bitmap1 = TestUtils.makeBitmap() @Before fun setUp() { MockitoAnnotations.initMocks(this) Mockito.`when`(context.applicationContext).thenReturn(context) dispatcher = createDispatcher() } @Test fun shutdownCancelsRunningJob() { createDispatcher(true) val action = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) dispatcher.dispatchSubmit(action) dispatcher.shutdown() testDispatcher.scheduler.runCurrent() assertThat(dispatcher.isShutdown()).isEqualTo(true) assertThat(action.completedResult).isNull() } @Test fun shutdownPreventsFurtherChannelUse() { val dispatcher = createDispatcher(true, backgroundContext = Dispatchers.IO) val action = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) dispatcher.shutdown() dispatcher.dispatchSubmit(action) assertThat(dispatcher.isShutdown()).isEqualTo(true) assertThat(action.completedResult).isNull() } @Test fun shutdownUnregistersReceiver() { dispatcher.shutdown() Shadows.shadowOf(Looper.getMainLooper()).idle() Mockito.verify(context).unregisterReceiver(dispatcher.receiver) } @Test fun dispatchSubmitWithNewRequestQueuesHunter() { val action = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) dispatcher.dispatchSubmit(action) testDispatcher.scheduler.runCurrent() assertThat(action.completedResult).isNotNull() } @Test fun dispatchSubmitWithTwoDifferentRequestsQueuesHunters() { val action1 = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) val action2 = TestUtils.mockAction(picasso, TestUtils.URI_KEY_2, TestUtils.URI_2) dispatcher.dispatchSubmit(action1) dispatcher.dispatchSubmit(action2) testDispatcher.scheduler.runCurrent() assertThat(action1.completedResult).isNotNull() assertThat(action2.completedResult).isNotNull() assertThat(action2.completedResult).isNotEqualTo(action1.completedResult) } @Test fun performSubmitWithExistingRequestAttachesToHunter() { val action1 = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) val action2 = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) dispatcher.dispatchSubmit(action1) dispatcher.dispatchSubmit(action2) testDispatcher.scheduler.runCurrent() assertThat(action1.completedResult).isNotNull() assertThat(action2.completedResult).isEqualTo(action1.completedResult) } @Test fun dispatchSubmitWithShutdownServiceIgnoresRequest() { dispatcher.shutdown() val action = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) dispatcher.dispatchSubmit(action) testDispatcher.scheduler.runCurrent() assertThat(action.completedResult).isNull() } @Test fun dispatchSubmitWithFetchAction() { val pausedTag = "pausedTag" dispatcher.dispatchPauseTag(pausedTag) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedActions).isEmpty() var completed = false val fetchAction1 = noopAction(Request.Builder(TestUtils.URI_1).tag(pausedTag).build(), { completed = true }) val fetchAction2 = noopAction(Request.Builder(TestUtils.URI_1).tag(pausedTag).build(), { completed = true }) dispatcher.dispatchSubmit(fetchAction1) dispatcher.dispatchSubmit(fetchAction2) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedActions).hasSize(2) assertThat(completed).isFalse() } @Test fun dispatchCancelWithFetchActionWithCallback() { val pausedTag = "pausedTag" dispatcher.dispatchPauseTag(pausedTag) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedActions).isEmpty() val callback = TestUtils.mockCallback() val fetchAction1 = FetchAction(picasso, Request.Builder(TestUtils.URI_1).tag(pausedTag).build(), callback) dispatcher.dispatchSubmit(fetchAction1) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedActions).hasSize(1) dispatcher.dispatchCancel(fetchAction1) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedActions).isEmpty() } @Test fun dispatchCancelDetachesRequestAndCleansUp() { val target = TestUtils.mockBitmapTarget() val action = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, target) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action).apply { job = Job() } dispatcher.hunterMap[TestUtils.URI_KEY_1 + Request.KEY_SEPARATOR] = hunter dispatcher.failedActions[target] = action dispatcher.dispatchCancel(action) testDispatcher.scheduler.runCurrent() assertThat(hunter.job!!.isCancelled).isTrue() assertThat(hunter.action).isNull() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() } @Test fun dispatchCancelMultipleRequestsDetachesOnly() { val action1 = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) val action2 = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action1) hunter.attach(action2) dispatcher.hunterMap[TestUtils.URI_KEY_1 + Request.KEY_SEPARATOR] = hunter dispatcher.dispatchCancel(action1) testDispatcher.scheduler.runCurrent() assertThat(hunter.action).isNull() assertThat(hunter.actions).containsExactly(action2) assertThat(dispatcher.hunterMap).hasSize(1) } @Test fun dispatchCancelUnqueuesAndDetachesPausedRequest() { val action = TestUtils.mockAction( picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, TestUtils.mockBitmapTarget(), tag = "tag" ) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action) dispatcher.dispatchSubmit(action) dispatcher.dispatchPauseTag("tag") testDispatcher.scheduler.runCurrent() dispatcher.hunterMap[TestUtils.URI_KEY_1 + Request.KEY_SEPARATOR] = hunter dispatcher.dispatchCancel(action) testDispatcher.scheduler.runCurrent() assertThat(hunter.action).isNull() assertThat(dispatcher.pausedTags).containsExactly("tag") assertThat(dispatcher.pausedActions).isEmpty() } @Test fun dispatchCompleteSetsResultInCache() { val data = Request.Builder(TestUtils.URI_1).build() val action = noopAction(data) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action) hunter.run() assertThat(cache.size()).isEqualTo(0) dispatcher.dispatchComplete(hunter) testDispatcher.scheduler.runCurrent() val result = hunter.result as Bitmap assertThat(result.bitmap).isEqualTo(bitmap1) assertThat(result.loadedFrom).isEqualTo(NETWORK) assertThat(cache[hunter.key]).isSameInstanceAs(bitmap1) } @Test fun dispatchCompleteWithNoStoreMemoryPolicy() { val data = Request.Builder(TestUtils.URI_1).memoryPolicy(NO_STORE).build() val action = noopAction(data) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action) hunter.run() assertThat(cache.size()).isEqualTo(0) dispatcher.dispatchComplete(hunter) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap).isEmpty() assertThat(cache.size()).isEqualTo(0) } @Test fun dispatchCompleteCleansUpAndPostsToMain() { val data = Request.Builder(TestUtils.URI_1).build() var completed = false val action = noopAction(data, onComplete = { completed = true }) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action) hunter.run() dispatcher.dispatchComplete(hunter) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap).isEmpty() assertThat(completed).isTrue() } @Test fun dispatchCompleteCleansUpAndDoesNotPostToMainIfCancelled() { val data = Request.Builder(TestUtils.URI_1).build() var completed = false val action = noopAction(data, onComplete = { completed = true }) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action) hunter.run() hunter.job = Job().apply { cancel() } dispatcher.dispatchComplete(hunter) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap).isEmpty() assertThat(completed).isFalse() } @Test fun dispatchErrorCleansUpAndPostsToMain() { val exception = RuntimeException() val action = TestUtils.mockAction( picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, TestUtils.mockBitmapTarget(), tag = "tag" ) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action, exception) hunter.run() dispatcher.hunterMap[hunter.key] = hunter dispatcher.dispatchFailed(hunter) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap).isEmpty() assertThat(action.errorException).isEqualTo(exception) } @Test fun dispatchErrorCleansUpAndDoesNotPostToMainIfCancelled() { val exception = RuntimeException() val action = TestUtils.mockAction( picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, TestUtils.mockBitmapTarget(), tag = "tag" ) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action, exception) hunter.run() hunter.job = Job().apply { cancel() } dispatcher.hunterMap[hunter.key] = hunter dispatcher.dispatchFailed(hunter) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap).isEmpty() assertThat(action.errorException).isNull() } @Test fun dispatchRetrySkipsIfHunterIsCancelled() { val action = TestUtils.mockAction( picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, TestUtils.mockBitmapTarget(), tag = "tag" ) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action) hunter.job = Job().apply { cancel() } dispatcher.dispatchRetry(hunter) testDispatcher.scheduler.runCurrent() assertThat(hunter.isCancelled).isTrue() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() } @Test fun dispatchRetryForContentLengthResetsNetworkPolicy() { val networkInfo = TestUtils.mockNetworkInfo(true) Mockito.`when`(connectivityManager.activeNetworkInfo).thenReturn(networkInfo) val action = TestUtils.mockAction(picasso, TestUtils.URI_KEY_2, TestUtils.URI_2) val e = ContentLengthException("304 error") val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action, e, true) hunter.run() dispatcher.dispatchRetry(hunter) testDispatcher.scheduler.advanceUntilIdle() assertThat(NetworkPolicy.shouldReadFromDiskCache(hunter.data.networkPolicy)).isFalse() } @Test fun dispatchRetryDoesNotMarkForReplayIfNotSupported() { val networkInfo = TestUtils.mockNetworkInfo(true) val hunter = TestUtils.mockHunter( picasso, Bitmap(bitmap1, MEMORY), TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) ) Mockito.`when`(connectivityManager.activeNetworkInfo).thenReturn(networkInfo) dispatcher.dispatchRetry(hunter) testDispatcher.scheduler.advanceUntilIdle() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() } @Test fun dispatchRetryDoesNotMarkForReplayIfNoNetworkScanning() { val hunter = TestUtils.mockHunter( picasso, Bitmap(bitmap1, MEMORY), TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1), e = null, shouldRetry = false, supportsReplay = true ) val dispatcher = createDispatcher(false) dispatcher.dispatchRetry(hunter) testDispatcher.scheduler.advanceUntilIdle() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() } @Test fun dispatchRetryMarksForReplayIfSupportedScansNetworkChangesAndShouldNotRetry() { val networkInfo = TestUtils.mockNetworkInfo(true) val action = TestUtils.mockAction( picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, TestUtils.mockBitmapTarget() ) val hunter = TestUtils.mockHunter( picasso, Bitmap(bitmap1, MEMORY), action, e = null, shouldRetry = false, supportsReplay = true ) Mockito.`when`(connectivityManager.activeNetworkInfo).thenReturn(networkInfo) dispatcher.dispatchRetry(hunter) testDispatcher.scheduler.advanceUntilIdle() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).hasSize(1) assertThat(action.willReplay).isTrue() } @Test fun dispatchRetryRetriesIfNoNetworkScanning() { val dispatcher = createDispatcher(false) val action = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) val hunter = TestUtils.mockHunter( picasso, Bitmap(bitmap1, MEMORY), action, e = null, shouldRetry = true, dispatcher = dispatcher ) dispatcher.dispatchRetry(hunter) testDispatcher.scheduler.advanceUntilIdle() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() assertThat(action.completedResult).isInstanceOf(Bitmap::class.java) } @Test fun dispatchRetryMarksForReplayIfSupportsReplayAndShouldNotRetry() { val action = TestUtils.mockAction( picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, TestUtils.mockBitmapTarget() ) val hunter = TestUtils.mockHunter( picasso, Bitmap(bitmap1, MEMORY), action, e = null, shouldRetry = false, supportsReplay = true ) dispatcher.dispatchRetry(hunter) testDispatcher.scheduler.advanceUntilIdle() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).hasSize(1) assertThat(action.willReplay).isTrue() } @Test fun dispatchRetryRetriesIfShouldRetry() { val action = TestUtils.mockAction( picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, TestUtils.mockBitmapTarget() ) val hunter = TestUtils.mockHunter( picasso, Bitmap(bitmap1, MEMORY), action, e = null, shouldRetry = true, dispatcher = dispatcher ) dispatcher.dispatchRetry(hunter) testDispatcher.scheduler.advanceUntilIdle() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() assertThat(action.completedResult).isInstanceOf(Bitmap::class.java) } @Test fun dispatchRetrySkipIfServiceShutdown() { val action = TestUtils.mockAction( picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, TestUtils.mockBitmapTarget() ) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action) dispatcher.shutdown() dispatcher.dispatchRetry(hunter) testDispatcher.scheduler.advanceUntilIdle() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.failedActions).isEmpty() assertThat(action.completedResult).isNull() } @Test fun dispatchAirplaneModeChange() { assertThat(dispatcher.airplaneMode).isFalse() dispatcher.dispatchAirplaneModeChange(true) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.airplaneMode).isTrue() dispatcher.dispatchAirplaneModeChange(false) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.airplaneMode).isFalse() } @Test fun dispatchNetworkStateChangeWithDisconnectedInfoIgnores() { val info = TestUtils.mockNetworkInfo() Mockito.`when`(info.isConnectedOrConnecting).thenReturn(false) dispatcher.dispatchNetworkStateChange(info) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.failedActions).isEmpty() } @Test fun dispatchNetworkStateChangeWithConnectedInfoDifferentInstanceIgnores() { val info = TestUtils.mockNetworkInfo(true) dispatcher.dispatchNetworkStateChange(info) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.failedActions).isEmpty() } @Test fun dispatchPauseAndResumeUpdatesListOfPausedTags() { dispatcher.dispatchPauseTag("tag") testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedTags).containsExactly("tag") dispatcher.dispatchResumeTag("tag") testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedTags).isEmpty() } @Test fun dispatchPauseTagIsIdempotent() { val action = TestUtils.mockAction( picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, TestUtils.mockBitmapTarget(), tag = "tag" ) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action) dispatcher.hunterMap[TestUtils.URI_KEY_1] = hunter assertThat(dispatcher.pausedActions).isEmpty() dispatcher.dispatchPauseTag("tag") testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedActions).containsEntry(action.getTarget(), action) dispatcher.dispatchPauseTag("tag") testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedActions).containsEntry(action.getTarget(), action) } @Test fun dispatchPauseTagQueuesNewRequestDoesNotComplete() { dispatcher.dispatchPauseTag("tag") val action = TestUtils.mockAction( picasso = picasso, key = TestUtils.URI_KEY_1, uri = TestUtils.URI_1, tag = "tag" ) dispatcher.dispatchSubmit(action) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.pausedActions).hasSize(1) assertThat(dispatcher.pausedActions.containsValue(action)).isTrue() assertThat(action.completedResult).isNull() } @Test fun dispatchPauseTagDoesNotQueueUnrelatedRequest() { dispatcher.dispatchPauseTag("tag") testDispatcher.scheduler.runCurrent() val action = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1, "anothertag") dispatcher.dispatchSubmit(action) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.pausedActions).isEmpty() assertThat(action.completedResult).isNotNull() } @Test fun dispatchPauseDetachesRequestAndCancelsHunter() { val action = TestUtils.mockAction( picasso = picasso, key = TestUtils.URI_KEY_1, uri = TestUtils.URI_1, tag = "tag" ) val hunter = TestUtils.mockHunter( picasso = picasso, result = Bitmap(bitmap1, MEMORY), action = action, dispatcher = dispatcher ) hunter.job = Job() dispatcher.hunterMap[TestUtils.URI_KEY_1] = hunter dispatcher.dispatchPauseTag("tag") testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap).isEmpty() assertThat(dispatcher.pausedActions).hasSize(1) assertThat(dispatcher.pausedActions.containsValue(action)).isTrue() assertThat(hunter.action).isNull() assertThat(action.completedResult).isNull() } @Test fun dispatchPauseOnlyDetachesPausedRequest() { val action1 = TestUtils.mockAction( picasso = picasso, key = TestUtils.URI_KEY_1, uri = TestUtils.URI_1, target = TestUtils.mockBitmapTarget(), tag = "tag1" ) val action2 = TestUtils.mockAction( picasso = picasso, key = TestUtils.URI_KEY_1, uri = TestUtils.URI_1, target = TestUtils.mockBitmapTarget(), tag = "tag2" ) val hunter = TestUtils.mockHunter(picasso, Bitmap(bitmap1, MEMORY), action1) hunter.attach(action2) dispatcher.hunterMap[TestUtils.URI_KEY_1] = hunter dispatcher.dispatchPauseTag("tag1") testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap).hasSize(1) assertThat(dispatcher.hunterMap.containsValue(hunter)).isTrue() assertThat(dispatcher.pausedActions).hasSize(1) assertThat(dispatcher.pausedActions.containsValue(action1)).isTrue() assertThat(hunter.action).isNull() assertThat(hunter.actions).containsExactly(action2) } @Test fun dispatchResumeTagIsIdempotent() { var completedCount = 0 val action = noopAction(Builder(TestUtils.URI_1).tag("tag").build(), { completedCount++ }) dispatcher.dispatchPauseTag("tag") dispatcher.dispatchSubmit(action) dispatcher.dispatchResumeTag("tag") dispatcher.dispatchResumeTag("tag") testDispatcher.scheduler.runCurrent() assertThat(completedCount).isEqualTo(1) } @Test fun dispatchNetworkStateChangeFlushesFailedHunters() { val info = TestUtils.mockNetworkInfo(true) val failedAction1 = TestUtils.mockAction(picasso, TestUtils.URI_KEY_1, TestUtils.URI_1) val failedAction2 = TestUtils.mockAction(picasso, TestUtils.URI_KEY_2, TestUtils.URI_2) dispatcher.failedActions[TestUtils.URI_KEY_1] = failedAction1 dispatcher.failedActions[TestUtils.URI_KEY_2] = failedAction2 dispatcher.dispatchNetworkStateChange(info) testDispatcher.scheduler.runCurrent() assertThat(failedAction1.completedResult).isNotNull() assertThat(failedAction2.completedResult).isNotNull() assertThat(dispatcher.failedActions).isEmpty() } @Test fun syncCancelWithMainBeforeHunting() { val mainDispatcher = StandardTestDispatcher() val dispatcher = createDispatcher(mainContext = mainDispatcher) var completed = false val action = noopAction(Request.Builder(TestUtils.URI_1).build()) { completed = true } // Submit action, will be gated by main dispatcher.dispatchSubmit(action) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap[action.request.key]).isNotNull() // Cancel action, detaches from hunter but hunter is queued to be submitted dispatcher.dispatchCancel(action) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap[action.request.key]).isNotNull() assertThat(dispatcher.hunterMap[action.request.key]?.action).isNull() // Run main, syncs Dispatcher with main mainDispatcher.scheduler.runCurrent() // Dispatches the submitted hunter to run testDispatcher.scheduler.runCurrent() // It isn't hanging around assertThat(dispatcher.hunterMap[action.request.key]).isNull() // The action is not completed because the hunter never ran mainDispatcher.scheduler.runCurrent() assertThat(completed).isFalse() } @Test fun doesntSyncWithMainIfHighPriorityRequestBeforeHunting() { val mainDispatcher = StandardTestDispatcher() val dispatcher = createDispatcher(mainContext = mainDispatcher) var completed = false val action = noopAction(Request.Builder(TestUtils.URI_1).priority(HIGH).build()) { completed = true } // Submit action dispatcher.dispatchSubmit(action) testDispatcher.scheduler.runCurrent() assertThat(dispatcher.hunterMap[action.request.key]).isNull() // Deliver result to main mainDispatcher.scheduler.runCurrent() assertThat(completed).isTrue() } private fun createDispatcher( scansNetworkChanges: Boolean = true, mainContext: CoroutineContext? = null, backgroundContext: CoroutineContext? = null ): InternalCoroutineDispatcher { Mockito.`when`(connectivityManager.activeNetworkInfo).thenReturn( if (scansNetworkChanges) Mockito.mock(NetworkInfo::class.java) else null ) Mockito.`when`(context.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(connectivityManager) Mockito.`when`(context.checkCallingOrSelfPermission(ArgumentMatchers.anyString())).thenReturn( if (scansNetworkChanges) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED ) testDispatcher = StandardTestDispatcher() picasso = TestUtils.mockPicasso(context).newBuilder().dispatchers(mainContext ?: testDispatcher, testDispatcher).build() return InternalCoroutineDispatcher( context = context, mainThreadHandler = Handler(Looper.getMainLooper()), cache = cache, mainContext = mainContext ?: testDispatcher, backgroundContext = backgroundContext ?: testDispatcher ) } private fun noopAction(data: Request, onComplete: () -> Unit = { }): Action { return object : Action(picasso, data) { override fun complete(result: RequestHandler.Result) = onComplete() override fun error(e: Exception) = Unit override fun getTarget(): Any = this } } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/MediaStoreRequestHandlerTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.graphics.Bitmap import android.graphics.Bitmap.Config.ARGB_8888 import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.MediaStoreRequestHandler.Companion.getPicassoKind import com.squareup.picasso3.MediaStoreRequestHandler.PicassoKind.FULL import com.squareup.picasso3.MediaStoreRequestHandler.PicassoKind.MICRO import com.squareup.picasso3.MediaStoreRequestHandler.PicassoKind.MINI import com.squareup.picasso3.RequestHandler.Callback import com.squareup.picasso3.Shadows.ShadowImageThumbnails import com.squareup.picasso3.Shadows.ShadowVideoThumbnails import com.squareup.picasso3.TestUtils.MEDIA_STORE_CONTENT_1_URL import com.squareup.picasso3.TestUtils.MEDIA_STORE_CONTENT_2_URL import com.squareup.picasso3.TestUtils.MEDIA_STORE_CONTENT_KEY_1 import com.squareup.picasso3.TestUtils.MEDIA_STORE_CONTENT_KEY_2 import com.squareup.picasso3.TestUtils.makeBitmap import com.squareup.picasso3.TestUtils.mockAction import com.squareup.picasso3.TestUtils.mockPicasso import org.junit.Assert.fail import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(shadows = [ShadowVideoThumbnails::class, ShadowImageThumbnails::class]) class MediaStoreRequestHandlerTest { private lateinit var context: Context private lateinit var picasso: Picasso @Before fun setUp() { context = RuntimeEnvironment.getApplication().applicationContext picasso = mockPicasso(context) Robolectric.setupContentProvider(TestContentProvider::class.java, "media") } @Test fun decodesVideoThumbnailWithVideoMimeType() { val bitmap = makeBitmap() val request = Request.Builder( uri = MEDIA_STORE_CONTENT_2_URL, resourceId = 0, bitmapConfig = ARGB_8888 ) .stableKey(MEDIA_STORE_CONTENT_KEY_2) .resize(100, 100) .build() val action = mockAction(picasso, request) val requestHandler = MediaStoreRequestHandler(context) requestHandler.load( picasso = picasso, request = action.request, callback = object : Callback { override fun onSuccess(result: RequestHandler.Result?) = assertBitmapsEqual((result as RequestHandler.Result.Bitmap?)!!.bitmap, bitmap) override fun onError(t: Throwable) = fail(t.message) } ) } @Test fun decodesImageThumbnailWithImageMimeType() { val bitmap = makeBitmap(20, 20) val request = Request.Builder( uri = MEDIA_STORE_CONTENT_1_URL, resourceId = 0, bitmapConfig = ARGB_8888 ) .stableKey(MEDIA_STORE_CONTENT_KEY_1) .resize(100, 100) .build() val action = mockAction(picasso, request) val requestHandler = MediaStoreRequestHandler(context) requestHandler.load( picasso = picasso, request = action.request, callback = object : Callback { override fun onSuccess(result: RequestHandler.Result?) = assertBitmapsEqual((result as RequestHandler.Result.Bitmap?)!!.bitmap, bitmap) override fun onError(t: Throwable) = fail(t.message) } ) } @Test fun getPicassoKindMicro() { assertThat(getPicassoKind(96, 96)).isEqualTo(MICRO) assertThat(getPicassoKind(95, 95)).isEqualTo(MICRO) } @Test fun getPicassoKindMini() { assertThat(getPicassoKind(512, 384)).isEqualTo(MINI) assertThat(getPicassoKind(100, 100)).isEqualTo(MINI) } @Test fun getPicassoKindFull() { assertThat(getPicassoKind(513, 385)).isEqualTo(FULL) assertThat(getPicassoKind(1000, 1000)).isEqualTo(FULL) assertThat(getPicassoKind(1000, 384)).isEqualTo(FULL) assertThat(getPicassoKind(1000, 96)).isEqualTo(FULL) assertThat(getPicassoKind(96, 1000)).isEqualTo(FULL) } private fun assertBitmapsEqual(a: Bitmap, b: Bitmap) { assertThat(a.height).isEqualTo(b.height) assertThat(a.width).isEqualTo(b.width) assertThat(shadowOf(a).description).isEqualTo(shadowOf(b).description) } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/MemoryPolicyTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.MemoryPolicy.NO_CACHE import com.squareup.picasso3.MemoryPolicy.NO_STORE import org.junit.Test class MemoryPolicyTest { @Test fun dontReadFromMemoryCache() { var memoryPolicy = 0 memoryPolicy = memoryPolicy or NO_CACHE.index assertThat(MemoryPolicy.shouldReadFromMemoryCache(memoryPolicy)).isFalse() } @Test fun readFromMemoryCache() { var memoryPolicy = 0 memoryPolicy = memoryPolicy or NO_STORE.index assertThat(MemoryPolicy.shouldReadFromMemoryCache(memoryPolicy)).isTrue() } @Test fun dontWriteToMemoryCache() { var memoryPolicy = 0 memoryPolicy = memoryPolicy or NO_STORE.index assertThat(MemoryPolicy.shouldWriteToMemoryCache(memoryPolicy)).isFalse() } @Test fun writeToMemoryCache() { var memoryPolicy = 0 memoryPolicy = memoryPolicy or NO_CACHE.index assertThat(MemoryPolicy.shouldWriteToMemoryCache(memoryPolicy)).isTrue() } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/NetworkRequestHandlerTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.RequestHandler.Result import com.squareup.picasso3.TestUtils.CUSTOM_HEADER_NAME import com.squareup.picasso3.TestUtils.CUSTOM_HEADER_VALUE import com.squareup.picasso3.TestUtils.EventRecorder import com.squareup.picasso3.TestUtils.PremadeCall import com.squareup.picasso3.TestUtils.URI_1 import com.squareup.picasso3.TestUtils.URI_KEY_1 import com.squareup.picasso3.TestUtils.mockNetworkInfo import com.squareup.picasso3.TestUtils.mockPicasso import okhttp3.CacheControl import okhttp3.MediaType import okhttp3.Protocol.HTTP_1_1 import okhttp3.Response import okhttp3.ResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import okio.BufferedSource import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations.initMocks import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import java.util.concurrent.CountDownLatch import java.util.concurrent.LinkedBlockingDeque import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.atomic.AtomicBoolean @RunWith(RobolectricTestRunner::class) class NetworkRequestHandlerTest { private val responses = LinkedBlockingDeque() private val requests = LinkedBlockingDeque() @Mock internal lateinit var dispatcher: Dispatcher private lateinit var picasso: Picasso private lateinit var networkHandler: NetworkRequestHandler @Before fun setUp() { initMocks(this) picasso = mockPicasso(RuntimeEnvironment.application) networkHandler = NetworkRequestHandler { request -> requests.add(request) try { PremadeCall(request, responses.takeFirst()) } catch (e: InterruptedException) { throw AssertionError(e) } } } @Test fun doesNotForceLocalCacheOnlyWithAirplaneModeOffAndRetryCount() { responses.add(responseOf(ByteArray(10).toResponseBody(null))) val action = TestUtils.mockAction(picasso, URI_KEY_1, URI_1) val latch = CountDownLatch(1) networkHandler.load( picasso = picasso, request = action.request, callback = object : RequestHandler.Callback { override fun onSuccess(result: Result?) { try { assertThat(requests.takeFirst().cacheControl.toString()).isEmpty() latch.countDown() } catch (e: InterruptedException) { throw AssertionError(e) } } override fun onError(t: Throwable): Unit = throw AssertionError(t) } ) assertThat(latch.await(10, SECONDS)).isTrue() } @Test fun withZeroRetryCountForcesLocalCacheOnly() { responses.add(responseOf(ByteArray(10).toResponseBody(null))) val action = TestUtils.mockAction(picasso, URI_KEY_1, URI_1) val cache = PlatformLruCache(0) val hunter = BitmapHunter(picasso, dispatcher, cache, action, networkHandler) hunter.retryCount = 0 hunter.hunt() assertThat(requests.takeFirst().cacheControl.toString()) .isEqualTo(CacheControl.FORCE_CACHE.toString()) } @Test fun shouldRetryTwiceWithAirplaneModeOffAndNoNetworkInfo() { val action = TestUtils.mockAction(picasso, URI_KEY_1, URI_1) val cache = PlatformLruCache(0) val hunter = BitmapHunter(picasso, dispatcher, cache, action, networkHandler) assertThat(hunter.shouldRetry(airplaneMode = false, info = null)).isTrue() assertThat(hunter.shouldRetry(airplaneMode = false, info = null)).isTrue() assertThat(hunter.shouldRetry(airplaneMode = false, info = null)).isFalse() } @Test fun shouldRetryWithUnknownNetworkInfo() { assertThat(networkHandler.shouldRetry(airplaneMode = false, info = null)).isTrue() assertThat(networkHandler.shouldRetry(airplaneMode = true, info = null)).isTrue() } @Test fun shouldRetryWithConnectedNetworkInfo() { val info = mockNetworkInfo() `when`(info.isConnected).thenReturn(true) assertThat(networkHandler.shouldRetry(airplaneMode = false, info = info)).isTrue() assertThat(networkHandler.shouldRetry(airplaneMode = true, info = info)).isTrue() } @Test fun shouldNotRetryWithDisconnectedNetworkInfo() { val info = mockNetworkInfo() `when`(info.isConnectedOrConnecting).thenReturn(false) assertThat(networkHandler.shouldRetry(airplaneMode = false, info = info)).isFalse() assertThat(networkHandler.shouldRetry(airplaneMode = true, info = info)).isFalse() } @Test fun noCacheAndKnownContentLengthDispatchToStats() { val eventRecorder = EventRecorder() val picasso = picasso.newBuilder().addEventListener(eventRecorder).build() val knownContentLengthSize = 10 responses.add(responseOf(ByteArray(knownContentLengthSize).toResponseBody(null))) val action = TestUtils.mockAction(picasso, URI_KEY_1, URI_1) val latch = CountDownLatch(1) networkHandler.load( picasso = picasso, request = action.request, callback = object : RequestHandler.Callback { override fun onSuccess(result: Result?) { assertThat(eventRecorder.downloadSize).isEqualTo(knownContentLengthSize) latch.countDown() } override fun onError(t: Throwable): Unit = throw AssertionError(t) } ) assertThat(latch.await(10, SECONDS)).isTrue() } @Test fun unknownContentLengthFromDiskThrows() { val eventRecorder = EventRecorder() val picasso = picasso.newBuilder().addEventListener(eventRecorder).build() val closed = AtomicBoolean() val body = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 0 override fun source(): BufferedSource = Buffer() override fun close() { closed.set(true) super.close() } } responses += responseOf(body) .newBuilder() .cacheResponse(responseOf(null)) .build() val action = TestUtils.mockAction(picasso, URI_KEY_1, URI_1) val latch = CountDownLatch(1) networkHandler.load( picasso = picasso, request = action.request, callback = object : RequestHandler.Callback { override fun onSuccess(result: Result?): Unit = throw AssertionError() override fun onError(t: Throwable) { assertThat(eventRecorder.downloadSize).isEqualTo(0) assertTrue(closed.get()) latch.countDown() } } ) assertThat(latch.await(10, SECONDS)).isTrue() } @Test fun cachedResponseDoesNotDispatchToStats() { val eventRecorder = EventRecorder() val picasso = picasso.newBuilder().addEventListener(eventRecorder).build() responses += responseOf(ByteArray(10).toResponseBody(null)) .newBuilder() .cacheResponse(responseOf(null)) .build() val action = TestUtils.mockAction(picasso, URI_KEY_1, URI_1) val latch = CountDownLatch(1) networkHandler.load( picasso = picasso, request = action.request, callback = object : RequestHandler.Callback { override fun onSuccess(result: Result?) { assertThat(eventRecorder.downloadSize).isEqualTo(0) latch.countDown() } override fun onError(t: Throwable): Unit = throw AssertionError(t) } ) assertThat(latch.await(10, SECONDS)).isTrue() } @Test fun customHeaders() { responses += responseOf(ByteArray(10).toResponseBody(null)) .newBuilder() .cacheResponse(responseOf(null)) .build() val action = TestUtils.mockAction( picasso, key = URI_KEY_1, uri = URI_1, headers = mapOf(CUSTOM_HEADER_NAME to CUSTOM_HEADER_VALUE) ) val latch = CountDownLatch(1) networkHandler.load( picasso = picasso, request = action.request, callback = object : RequestHandler.Callback { override fun onSuccess(result: Result?) { with(requests.first.headers) { assertThat(names()).containsExactly(CUSTOM_HEADER_NAME) assertThat(values(CUSTOM_HEADER_NAME)).containsExactly(CUSTOM_HEADER_VALUE) } latch.countDown() } override fun onError(t: Throwable): Unit = throw AssertionError(t) } ) assertThat(latch.await(10, SECONDS)).isTrue() } @Test fun shouldHandleSchemeInsensitiveCase() { val schemes = arrayOf("http", "https", "HTTP", "HTTPS", "HTtP") for (scheme in schemes) { val uri = URI_1.buildUpon().scheme(scheme).build() assertThat(networkHandler.canHandleRequest(TestUtils.mockRequest(uri))).isTrue() } } private fun responseOf(body: ResponseBody?) = Response.Builder() .code(200) .protocol(HTTP_1_1) .request(okhttp3.Request.Builder().url("http://example.com").build()) .message("OK") .body(body) .build() } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/PicassoTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.Context import android.graphics.Bitmap.Config.ALPHA_8 import android.graphics.Bitmap.Config.ARGB_8888 import android.net.Uri import android.widget.RemoteViews import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.Picasso.Listener import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.Picasso.LoadedFrom.NETWORK import com.squareup.picasso3.RemoteViewsAction.RemoteViewsTarget import com.squareup.picasso3.RequestHandler.Result import com.squareup.picasso3.RequestHandler.Result.Bitmap import com.squareup.picasso3.TestUtils.EventRecorder import com.squareup.picasso3.TestUtils.FakeAction import com.squareup.picasso3.TestUtils.NO_HANDLERS import com.squareup.picasso3.TestUtils.NO_TRANSFORMERS import com.squareup.picasso3.TestUtils.UNUSED_CALL_FACTORY import com.squareup.picasso3.TestUtils.URI_1 import com.squareup.picasso3.TestUtils.URI_KEY_1 import com.squareup.picasso3.TestUtils.defaultPicasso import com.squareup.picasso3.TestUtils.makeBitmap import com.squareup.picasso3.TestUtils.mockAction import com.squareup.picasso3.TestUtils.mockBitmapTarget import com.squareup.picasso3.TestUtils.mockDeferredRequestCreator import com.squareup.picasso3.TestUtils.mockDrawableTarget import com.squareup.picasso3.TestUtils.mockHunter import com.squareup.picasso3.TestUtils.mockImageViewTarget import com.squareup.picasso3.TestUtils.mockPicasso import com.squareup.picasso3.TestUtils.mockRequestCreator import org.junit.Assert.fail import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoInteractions import org.mockito.MockitoAnnotations.initMocks import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import java.io.File @RunWith(RobolectricTestRunner::class) class PicassoTest { @get:Rule val temporaryFolder = TemporaryFolder() @Mock internal lateinit var context: Context @Mock internal lateinit var dispatcher: Dispatcher @Mock internal lateinit var requestHandler: RequestHandler @Mock internal lateinit var listener: Listener private val cache = PlatformLruCache(2048) private val eventRecorder = EventRecorder() private val bitmap = makeBitmap() private lateinit var picasso: Picasso @Before fun setUp() { initMocks(this) picasso = Picasso( context = context, dispatcher = dispatcher, callFactory = UNUSED_CALL_FACTORY, closeableCache = null, cache = cache, listener = listener, requestTransformers = NO_TRANSFORMERS, extraRequestHandlers = NO_HANDLERS, eventListeners = listOf(eventRecorder), defaultBitmapConfig = ARGB_8888, indicatorsEnabled = false, isLoggingEnabled = false ) } @Test fun submitWithTargetInvokesDispatcher() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) assertThat(picasso.targetToAction).isEmpty() picasso.enqueueAndSubmit(action) assertThat(picasso.targetToAction).hasSize(1) verify(dispatcher).dispatchSubmit(action) } @Test fun submitWithSameActionDoesNotCancel() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) picasso.enqueueAndSubmit(action) verify(dispatcher).dispatchSubmit(action) assertThat(picasso.targetToAction).hasSize(1) assertThat(picasso.targetToAction.containsValue(action)).isTrue() picasso.enqueueAndSubmit(action) assertThat(action.cancelled).isFalse() verify(dispatcher, never()).dispatchCancel(action) } @Test fun quickMemoryCheckReturnsBitmapIfInCache() { cache[URI_KEY_1] = bitmap val cached = picasso.quickMemoryCacheCheck(URI_KEY_1) assertThat(cached).isEqualTo(bitmap) assertThat(eventRecorder.cacheHits).isGreaterThan(0) } @Test fun quickMemoryCheckReturnsNullIfNotInCache() { val cached = picasso.quickMemoryCacheCheck(URI_KEY_1) assertThat(cached).isNull() assertThat(eventRecorder.cacheMisses).isGreaterThan(0) } @Test fun completeInvokesSuccessOnAllSuccessfulRequests() { val action1 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap, MEMORY), action1) val action2 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) hunter.attach(action2) action2.cancelled = true hunter.run() picasso.complete(hunter) verifyActionComplete(action1) assertThat(action2.completedResult).isNull() } @Test fun completeInvokesErrorOnAllFailedRequests() { val action1 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val exception = mock(Exception::class.java) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap, MEMORY), action1, exception) val action2 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) hunter.attach(action2) action2.cancelled = true hunter.run() picasso.complete(hunter) assertThat(action1.errorException).hasCauseThat().isEqualTo(exception) assertThat(action2.errorException).isNull() verify(listener).onImageLoadFailed(picasso, URI_1, action1.errorException!!) } @Test fun completeInvokesErrorOnFailedResourceRequests() { val action = mockAction( picasso = picasso, key = URI_KEY_1, uri = null, resourceId = 123, target = mockImageViewTarget() ) val exception = mock(Exception::class.java) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap, MEMORY), action, exception) hunter.run() picasso.complete(hunter) assertThat(action.errorException).hasCauseThat().isEqualTo(exception) verify(listener).onImageLoadFailed(picasso, null, action.errorException!!) } @Test fun completeDeliversToSingle() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap, MEMORY), action) hunter.run() picasso.complete(hunter) verifyActionComplete(action) } @Test fun completeWithReplayDoesNotRemove() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) action.willReplay = true val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap, MEMORY), action) hunter.run() picasso.enqueueAndSubmit(action) assertThat(picasso.targetToAction).hasSize(1) picasso.complete(hunter) assertThat(picasso.targetToAction).hasSize(1) verifyActionComplete(action) } @Test fun completeDeliversToSingleAndMultiple() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val action2 = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap, MEMORY), action) hunter.attach(action2) hunter.run() picasso.complete(hunter) verifyActionComplete(action) verifyActionComplete(action2) } @Test fun completeSkipsIfNoActions() { val action = mockAction(picasso, URI_KEY_1, URI_1, mockImageViewTarget()) val hunter = mockHunter(picasso, RequestHandler.Result.Bitmap(bitmap, MEMORY), action) hunter.detach(action) hunter.run() picasso.complete(hunter) assertThat(hunter.action).isNull() assertThat(hunter.actions).isNull() } @Test fun resumeActionTriggersSubmitOnPausedAction() { val request = Request.Builder(URI_1, 0, ARGB_8888).build() val action = object : Action(mockPicasso(RuntimeEnvironment.application), request) { override fun complete(result: Result) = fail("Test execution should not call this method") override fun error(e: Exception) = fail("Test execution should not call this method") override fun getTarget(): Any = this } picasso.resumeAction(action) verify(dispatcher).dispatchSubmit(action) } @Test fun resumeActionImmediatelyCompletesCachedRequest() { cache[URI_KEY_1] = bitmap val request = Request.Builder(URI_1, 0, ARGB_8888).build() val action = object : Action(mockPicasso(RuntimeEnvironment.application), request) { override fun complete(result: Result) { assertThat(result).isInstanceOf(Bitmap::class.java) val bitmapResult = result as Bitmap assertThat(bitmapResult.bitmap).isEqualTo(bitmap) assertThat(bitmapResult.loadedFrom).isEqualTo(MEMORY) } override fun error(e: Exception) = fail("Reading from memory cache should not throw an exception") override fun getTarget(): Any = this } picasso.resumeAction(action) } @Test fun cancelExistingRequestWithUnknownTarget() { val target = mockImageViewTarget() val action = mockAction(picasso, URI_KEY_1, URI_1, target) assertThat(action.cancelled).isFalse() picasso.cancelRequest(target) assertThat(action.cancelled).isFalse() verifyNoInteractions(dispatcher) } @Test fun cancelExistingRequestWithImageViewTarget() { val target = mockImageViewTarget() val action = mockAction(picasso, URI_KEY_1, URI_1, target) picasso.enqueueAndSubmit(action) assertThat(picasso.targetToAction).hasSize(1) assertThat(action.cancelled).isFalse() picasso.cancelRequest(target) assertThat(picasso.targetToAction).isEmpty() assertThat(action.cancelled).isTrue() verify(dispatcher).dispatchCancel(action) } @Test fun cancelExistingRequestWithDeferredImageViewTarget() { val target = mockImageViewTarget() val creator = mockRequestCreator(picasso) val deferredRequestCreator = mockDeferredRequestCreator(creator, target) picasso.targetToDeferredRequestCreator[target] = deferredRequestCreator picasso.cancelRequest(target) verify(target).removeOnAttachStateChangeListener(deferredRequestCreator) assertThat(picasso.targetToDeferredRequestCreator).isEmpty() } @Test fun enqueueingDeferredRequestCancelsThePreviousOne() { val target = mockImageViewTarget() val creator = mockRequestCreator(picasso) val firstRequestCreator = mockDeferredRequestCreator(creator, target) picasso.defer(target, firstRequestCreator) assertThat(picasso.targetToDeferredRequestCreator).containsKey(target) val secondRequestCreator = mockDeferredRequestCreator(creator, target) picasso.defer(target, secondRequestCreator) verify(target).removeOnAttachStateChangeListener(firstRequestCreator) assertThat(picasso.targetToDeferredRequestCreator).containsKey(target) } @Test fun cancelExistingRequestWithBitmapTarget() { val target = mockBitmapTarget() val action = mockAction(picasso, URI_KEY_1, URI_1, target) picasso.enqueueAndSubmit(action) assertThat(picasso.targetToAction).hasSize(1) assertThat(action.cancelled).isFalse() picasso.cancelRequest(target) assertThat(picasso.targetToAction).isEmpty() assertThat(action.cancelled).isTrue() verify(dispatcher).dispatchCancel(action) } @Test fun cancelExistingRequestWithDrawableTarget() { val target = mockDrawableTarget() val action = mockAction(picasso, URI_KEY_1, URI_1, target) picasso.enqueueAndSubmit(action) assertThat(picasso.targetToAction).hasSize(1) assertThat(action.cancelled).isFalse() picasso.cancelRequest(target) assertThat(picasso.targetToAction).isEmpty() assertThat(action.cancelled).isTrue() verify(dispatcher).dispatchCancel(action) } @Test fun cancelExistingRequestWithRemoteViewTarget() { val layoutId = 0 val viewId = 1 val remoteViews = RemoteViews("com.squareup.picasso3.test", layoutId) val target = RemoteViewsTarget(remoteViews, viewId) val action = mockAction(picasso, URI_KEY_1, URI_1, target) picasso.enqueueAndSubmit(action) assertThat(picasso.targetToAction).hasSize(1) assertThat(action.cancelled).isFalse() picasso.cancelRequest(remoteViews, viewId) assertThat(picasso.targetToAction).isEmpty() assertThat(action.cancelled).isTrue() verify(dispatcher).dispatchCancel(action) } @Test fun cancelTagAllActions() { val target = mockImageViewTarget() val action = mockAction(picasso, URI_KEY_1, URI_1, target, tag = "TAG") picasso.enqueueAndSubmit(action) assertThat(picasso.targetToAction).hasSize(1) assertThat(action.cancelled).isFalse() picasso.cancelTag("TAG") assertThat(picasso.targetToAction).isEmpty() assertThat(action.cancelled).isTrue() } @Test fun cancelTagAllDeferredRequests() { val target = mockImageViewTarget() val creator = mockRequestCreator(picasso).tag("TAG") val deferredRequestCreator = mockDeferredRequestCreator(creator, target) picasso.defer(target, deferredRequestCreator) picasso.cancelTag("TAG") verify(target).removeOnAttachStateChangeListener(deferredRequestCreator) } @Test fun deferAddsToMap() { val target = mockImageViewTarget() val creator = mockRequestCreator(picasso) val deferredRequestCreator = mockDeferredRequestCreator(creator, target) assertThat(picasso.targetToDeferredRequestCreator).isEmpty() picasso.defer(target, deferredRequestCreator) assertThat(picasso.targetToDeferredRequestCreator).hasSize(1) } @Test fun shutdown() { cache["key"] = makeBitmap(1, 1) assertThat(cache.size()).isEqualTo(1) picasso.shutdown() assertThat(cache.size()).isEqualTo(0) assertThat(eventRecorder.closed).isTrue() verify(dispatcher).shutdown() assertThat(picasso.shutdown).isTrue() } @Test fun shutdownClosesUnsharedCache() { val cache = okhttp3.Cache(temporaryFolder.root, 100) val picasso = Picasso( context, dispatcher, UNUSED_CALL_FACTORY, cache, this.cache, listener, NO_TRANSFORMERS, NO_HANDLERS, listOf(eventRecorder), defaultBitmapConfig = ARGB_8888, indicatorsEnabled = false, isLoggingEnabled = false ) picasso.shutdown() assertThat(cache.isClosed).isTrue() } @Test fun shutdownTwice() { cache["key"] = makeBitmap(1, 1) assertThat(cache.size()).isEqualTo(1) picasso.shutdown() picasso.shutdown() assertThat(cache.size()).isEqualTo(0) assertThat(eventRecorder.closed).isTrue() verify(dispatcher).shutdown() assertThat(picasso.shutdown).isTrue() } @Test fun shutdownClearsTargetsToActions() { picasso.targetToAction[mockImageViewTarget()] = mock(ImageViewAction::class.java) picasso.shutdown() assertThat(picasso.targetToAction).isEmpty() } @Test fun shutdownClearsDeferredRequests() { val target = mockImageViewTarget() val creator = mockRequestCreator(picasso) val deferredRequestCreator = mockDeferredRequestCreator(creator, target) picasso.targetToDeferredRequestCreator[target] = deferredRequestCreator picasso.shutdown() verify(target).removeOnAttachStateChangeListener(deferredRequestCreator) assertThat(picasso.targetToDeferredRequestCreator).isEmpty() } @Test fun loadThrowsWithInvalidInput() { try { picasso.load("") fail("Empty URL should throw exception.") } catch (expected: IllegalArgumentException) { } try { picasso.load(" ") fail("Empty URL should throw exception.") } catch (expected: IllegalArgumentException) { } try { picasso.load(0) fail("Zero resourceId should throw exception.") } catch (expected: IllegalArgumentException) { } } @Test fun builderInvalidCache() { try { Picasso.Builder(RuntimeEnvironment.application).withCacheSize(-1) fail() } catch (expected: IllegalArgumentException) { assertThat(expected).hasMessageThat().isEqualTo("maxByteCount < 0: -1") } } @Test fun builderWithoutRequestHandler() { val picasso = Picasso.Builder(RuntimeEnvironment.application).build() assertThat(picasso.requestHandlers).isNotEmpty() assertThat(picasso.requestHandlers).doesNotContain(requestHandler) } @Test fun builderWithRequestHandler() { val picasso = Picasso.Builder(RuntimeEnvironment.application) .addRequestHandler(requestHandler) .build() assertThat(picasso.requestHandlers).isNotNull() assertThat(picasso.requestHandlers).isNotEmpty() assertThat(picasso.requestHandlers).contains(requestHandler) } @Test fun builderWithDebugIndicators() { val picasso = Picasso.Builder(RuntimeEnvironment.application).indicatorsEnabled(true).build() assertThat(picasso.indicatorsEnabled).isTrue() } @Test fun evictAll() { val picasso = Picasso.Builder(RuntimeEnvironment.application).indicatorsEnabled(true).build() picasso.cache["key"] = android.graphics.Bitmap.createBitmap(1, 1, ALPHA_8) assertThat(picasso.cache.size()).isEqualTo(1) picasso.evictAll() assertThat(picasso.cache.size()).isEqualTo(0) } @Test fun invalidateString() { val request = Request.Builder(Uri.parse("https://example.com")).build() cache[request.key] = makeBitmap(1, 1) assertThat(cache.size()).isEqualTo(1) picasso.invalidate("https://example.com") assertThat(cache.size()).isEqualTo(0) } @Test fun invalidateFile() { val request = Request.Builder(Uri.fromFile(File("/foo/bar/baz"))).build() cache[request.key] = makeBitmap(1, 1) assertThat(cache.size()).isEqualTo(1) picasso.invalidate(File("/foo/bar/baz")) assertThat(cache.size()).isEqualTo(0) } @Test fun invalidateUri() { val request = Request.Builder(URI_1).build() cache[request.key] = makeBitmap(1, 1) assertThat(cache.size()).isEqualTo(1) picasso.invalidate(URI_1) assertThat(cache.size()).isEqualTo(0) } @Test fun clonedRequestHandlersAreIndependent() { val original = defaultPicasso(RuntimeEnvironment.application, false, false) original.newBuilder() .addRequestTransformer(TestUtils.NOOP_TRANSFORMER) .addRequestHandler(TestUtils.NOOP_REQUEST_HANDLER) .build() assertThat(original.requestTransformers).hasSize(NUM_BUILTIN_TRANSFORMERS) assertThat(original.requestHandlers).hasSize(NUM_BUILTIN_HANDLERS) } @Test fun cloneSharesStatefulInstances() { val parent = defaultPicasso(RuntimeEnvironment.application, true, true) val child = parent.newBuilder().build() assertThat(child.context).isEqualTo(parent.context) assertThat(child.callFactory).isEqualTo(parent.callFactory) assertThat((child.dispatcher as HandlerDispatcher).service).isEqualTo((parent.dispatcher as HandlerDispatcher).service) assertThat(child.cache).isEqualTo(parent.cache) assertThat(child.listener).isEqualTo(parent.listener) assertThat(child.requestTransformers).isEqualTo(parent.requestTransformers) assertThat(child.requestHandlers).hasSize(parent.requestHandlers.size) child.requestHandlers.forEachIndexed { index, it -> assertThat(it).isInstanceOf(parent.requestHandlers[index].javaClass) } assertThat(child.defaultBitmapConfig).isEqualTo(parent.defaultBitmapConfig) assertThat(child.indicatorsEnabled).isEqualTo(parent.indicatorsEnabled) assertThat(child.isLoggingEnabled).isEqualTo(parent.isLoggingEnabled) assertThat(child.targetToAction).isEqualTo(parent.targetToAction) assertThat(child.targetToDeferredRequestCreator).isEqualTo( parent.targetToDeferredRequestCreator ) } @Test fun cloneSharesCoroutineDispatchers() { val parent = defaultPicasso(RuntimeEnvironment.application, true, true) .newBuilder() .dispatchers() .build() val child = parent.newBuilder().build() val parentDispatcher = parent.dispatcher as InternalCoroutineDispatcher val childDispatcher = child.dispatcher as InternalCoroutineDispatcher assertThat(childDispatcher.mainContext).isEqualTo(parentDispatcher.mainContext) assertThat(childDispatcher.backgroundContext).isEqualTo(parentDispatcher.backgroundContext) } private fun verifyActionComplete(action: FakeAction) { val result = action.completedResult assertThat(result).isNotNull() assertThat(result).isInstanceOf(RequestHandler.Result.Bitmap::class.java) val bitmapResult = result as RequestHandler.Result.Bitmap assertThat(bitmapResult.bitmap).isEqualTo(bitmap) assertThat(bitmapResult.loadedFrom).isEqualTo(NETWORK) } companion object { private const val NUM_BUILTIN_HANDLERS = 8 private const val NUM_BUILTIN_TRANSFORMERS = 0 } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/RemoteViewsActionTest.kt ================================================ /* * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap.Config.ARGB_8888 import android.widget.RemoteViews import androidx.annotation.DrawableRes import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.Picasso.LoadedFrom.NETWORK import com.squareup.picasso3.RemoteViewsAction.RemoteViewsTarget import com.squareup.picasso3.TestUtils.NO_EVENT_LISTENERS import com.squareup.picasso3.TestUtils.NO_HANDLERS import com.squareup.picasso3.TestUtils.NO_TRANSFORMERS import com.squareup.picasso3.TestUtils.SIMPLE_REQUEST import com.squareup.picasso3.TestUtils.UNUSED_CALL_FACTORY import com.squareup.picasso3.TestUtils.makeBitmap import com.squareup.picasso3.TestUtils.mockCallback import com.squareup.picasso3.TestUtils.mockImageViewTarget import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoInteractions import org.mockito.Mockito.`when` import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class RemoteViewsActionTest { private lateinit var picasso: Picasso private lateinit var remoteViews: RemoteViews @Before fun setUp() { picasso = Picasso( context = RuntimeEnvironment.application, dispatcher = mock(Dispatcher::class.java), callFactory = UNUSED_CALL_FACTORY, closeableCache = null, cache = PlatformLruCache(0), listener = null, requestTransformers = NO_TRANSFORMERS, extraRequestHandlers = NO_HANDLERS, eventListeners = NO_EVENT_LISTENERS, defaultBitmapConfig = ARGB_8888, indicatorsEnabled = false, isLoggingEnabled = false ) remoteViews = mock(RemoteViews::class.java) `when`(remoteViews.layoutId).thenReturn(android.R.layout.list_content) } @Test fun completeSetsBitmapOnRemoteViews() { val callback = mockCallback() val bitmap = makeBitmap() val action = createAction(callback) action.complete(RequestHandler.Result.Bitmap(bitmap, NETWORK)) verify(remoteViews).setImageViewBitmap(1, bitmap) verify(callback).onSuccess() } @Test fun errorWithNoResourceIsNoop() { val callback = mockCallback() val action = createAction(callback) val e = RuntimeException() action.error(e) verifyNoInteractions(remoteViews) verify(callback).onError(e) } @Test fun errorWithResourceSetsResource() { val callback = mockCallback() val action = createAction(callback, 1) val e = RuntimeException() action.error(e) verify(remoteViews).setImageViewResource(1, 1) verify(callback).onError(e) } @Test fun clearsCallbackOnCancel() { val request = ImageViewAction( picasso = picasso, target = mockImageViewTarget(), data = SIMPLE_REQUEST, errorDrawable = null, errorResId = 0, noFade = false, callback = mockCallback() ) request.cancel() assertThat(request.callback).isNull() } private fun createAction(callback: Callback, errorResId: Int = 0): TestableRemoteViewsAction { return TestableRemoteViewsAction( picasso = picasso, data = SIMPLE_REQUEST, errorResId = errorResId, target = RemoteViewsTarget(remoteViews, 1), callback = callback ) } private class TestableRemoteViewsAction( picasso: Picasso, data: Request, @DrawableRes errorResId: Int, target: RemoteViewsTarget, callback: Callback? ) : RemoteViewsAction(picasso, data, errorResId, target, callback) { override fun update() {} override fun getTarget(): Any = target } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/RequestCreatorTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap import android.graphics.Bitmap.Config.ARGB_8888 import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.MemoryPolicy.NO_CACHE import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.Picasso.Priority.HIGH import com.squareup.picasso3.Picasso.Priority.LOW import com.squareup.picasso3.Picasso.Priority.NORMAL import com.squareup.picasso3.RemoteViewsAction.AppWidgetAction import com.squareup.picasso3.RemoteViewsAction.NotificationAction import com.squareup.picasso3.TestUtils.CUSTOM_HEADER_NAME import com.squareup.picasso3.TestUtils.CUSTOM_HEADER_VALUE import com.squareup.picasso3.TestUtils.NO_EVENT_LISTENERS import com.squareup.picasso3.TestUtils.NO_HANDLERS import com.squareup.picasso3.TestUtils.NO_TRANSFORMERS import com.squareup.picasso3.TestUtils.STABLE_1 import com.squareup.picasso3.TestUtils.STABLE_URI_KEY_1 import com.squareup.picasso3.TestUtils.UNUSED_CALL_FACTORY import com.squareup.picasso3.TestUtils.URI_1 import com.squareup.picasso3.TestUtils.URI_KEY_1 import com.squareup.picasso3.TestUtils.any import com.squareup.picasso3.TestUtils.argumentCaptor import com.squareup.picasso3.TestUtils.eq import com.squareup.picasso3.TestUtils.makeBitmap import com.squareup.picasso3.TestUtils.mockBitmapTarget import com.squareup.picasso3.TestUtils.mockCallback import com.squareup.picasso3.TestUtils.mockDrawableTarget import com.squareup.picasso3.TestUtils.mockFitImageViewTarget import com.squareup.picasso3.TestUtils.mockImageViewTarget import com.squareup.picasso3.TestUtils.mockNotification import com.squareup.picasso3.TestUtils.mockPicasso import com.squareup.picasso3.TestUtils.mockRemoteViews import com.squareup.picasso3.TestUtils.mockRequestCreator import org.junit.Assert.fail import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito.doCallRealMethod import org.mockito.Mockito.doReturn import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.Shadows.shadowOf import java.io.IOException import java.util.concurrent.CountDownLatch @RunWith(RobolectricTestRunner::class) class RequestCreatorTest { private val actionCaptor = argumentCaptor() private val picasso = spy(mockPicasso(RuntimeEnvironment.application)) private val bitmap = makeBitmap() @Test fun getOnMainCrashes() { try { RequestCreator(picasso, URI_1, 0).get() fail("Calling get() on main thread should throw exception") } catch (ignored: IllegalStateException) { } } @Test fun loadWithShutdownCrashes() { picasso.shutdown = true try { RequestCreator(picasso, URI_1, 0).fetch() fail("Should have crashed with a shutdown picasso.") } catch (ignored: IllegalStateException) { } } @Test fun getReturnsNullIfNullUriAndResourceId() { val latch = CountDownLatch(1) val result = arrayOfNulls(1) Thread { try { result[0] = RequestCreator(picasso, null, 0).get() } catch (e: IOException) { fail(e.message) } finally { latch.countDown() } }.start() latch.await() assertThat(result[0]).isNull() verify(picasso).defaultBitmapConfig verify(picasso).shutdown verifyNoMoreInteractions(picasso) } @Test fun fetchSubmitsFetchRequest() { RequestCreator(picasso, URI_1, 0).fetch() verify(picasso).submit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(FetchAction::class.java) } @Test fun fetchWithFitThrows() { try { RequestCreator(picasso, URI_1, 0).fit().fetch() fail("Calling fetch() with fit() should throw an exception") } catch (ignored: IllegalStateException) { } } @Test fun fetchWithDefaultPriority() { RequestCreator(picasso, URI_1, 0).fetch() verify(picasso).submit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(LOW) } @Test fun fetchWithCustomPriority() { RequestCreator(picasso, URI_1, 0).priority(HIGH).fetch() verify(picasso).submit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(HIGH) } @Test fun fetchWithCache() { `when`(picasso.quickMemoryCacheCheck(URI_KEY_1)).thenReturn(bitmap) RequestCreator(picasso, URI_1, 0).memoryPolicy(NO_CACHE).fetch() verify(picasso, never()).enqueueAndSubmit(any(Action::class.java)) } @Test fun fetchWithMemoryPolicyNoCache() { RequestCreator(picasso, URI_1, 0).memoryPolicy(NO_CACHE).fetch() verify(picasso, never()).quickMemoryCacheCheck(URI_KEY_1) verify(picasso).submit(actionCaptor.capture()) } @Test fun intoTargetWithFitThrows() { try { RequestCreator(picasso, URI_1, 0).fit().into(mockBitmapTarget()) fail("Calling into() target with fit() should throw exception") } catch (ignored: IllegalStateException) { } } @Test fun intoTargetNoPlaceholderCallsWithNull() { val target = mockBitmapTarget() RequestCreator(picasso, URI_1, 0).noPlaceholder().into(target) verify(target).onPrepareLoad(null) } @Test fun intoTargetWithNullUriAndResourceIdSkipsAndCancels() { val target = mockBitmapTarget() val placeHolderDrawable = mock(Drawable::class.java) RequestCreator(picasso, null, 0).placeholder(placeHolderDrawable).into(target) verify(picasso).defaultBitmapConfig verify(picasso).shutdown verify(picasso).cancelRequest(target) verify(target).onPrepareLoad(placeHolderDrawable) verifyNoMoreInteractions(picasso) } @Test fun intoTargetWithQuickMemoryCacheCheckDoesNotSubmit() { `when`(picasso.quickMemoryCacheCheck(URI_KEY_1)).thenReturn(bitmap) val target = mockBitmapTarget() RequestCreator(picasso, URI_1, 0).into(target) verify(target).onBitmapLoaded(bitmap, MEMORY) verify(picasso).cancelRequest(target) verify(picasso, never()).enqueueAndSubmit(any(Action::class.java)) } @Test fun intoTargetWithSkipMemoryPolicy() { val target = mockBitmapTarget() RequestCreator(picasso, URI_1, 0).memoryPolicy(NO_CACHE).into(target) verify(picasso, never()).quickMemoryCacheCheck(URI_KEY_1) } @Test fun intoTargetAndNotInCacheSubmitsTargetRequest() { val target = mockBitmapTarget() val placeHolderDrawable = mock(Drawable::class.java) RequestCreator(picasso, URI_1, 0).placeholder(placeHolderDrawable).into(target) verify(target).onPrepareLoad(placeHolderDrawable) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(BitmapTargetAction::class.java) } @Test fun targetActionWithDefaultPriority() { RequestCreator(picasso, URI_1, 0).into(mockBitmapTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(NORMAL) } @Test fun targetActionWithCustomPriority() { RequestCreator(picasso, URI_1, 0).priority(HIGH).into(mockBitmapTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(HIGH) } @Test fun targetActionWithDefaultTag() { RequestCreator(picasso, URI_1, 0).into(mockBitmapTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.tag).isEqualTo(actionCaptor.value) } @Test fun targetActionWithCustomTag() { RequestCreator(picasso, URI_1, 0).tag("tag").into(mockBitmapTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.tag).isEqualTo("tag") } @Test fun intoDrawableTargetWithFitThrows() { try { RequestCreator(picasso, URI_1, 0).fit().into(mockDrawableTarget()) fail("Calling into() drawable target with fit() should throw exception") } catch (ignored: IllegalStateException) { } } @Test fun intoDrawableTargetNoPlaceholderCallsWithNull() { val target = mockDrawableTarget() RequestCreator(picasso, URI_1, 0).noPlaceholder().into(target) verify(target).onPrepareLoad(null) } @Test fun intoDrawableTargetWithNullUriAndResourceIdSkipsAndCancels() { val target = mockDrawableTarget() val placeHolderDrawable = mock(Drawable::class.java) RequestCreator(picasso, null, 0).placeholder(placeHolderDrawable).into(target) verify(picasso).defaultBitmapConfig verify(picasso).shutdown verify(picasso).cancelRequest(target) verify(target).onPrepareLoad(placeHolderDrawable) verifyNoMoreInteractions(picasso) } @Test fun intoDrawableTargetWithQuickMemoryCacheCheckDoesNotSubmit() { `when`(picasso.quickMemoryCacheCheck(URI_KEY_1)).thenReturn(bitmap) val target = mockDrawableTarget() RequestCreator(picasso, URI_1, 0).into(target) verify(target).onDrawableLoaded(any(PicassoDrawable::class.java), eq(MEMORY)) verify(picasso).cancelRequest(target) verify(picasso, never()).enqueueAndSubmit(any(Action::class.java)) } @Test fun intoDrawableTargetWithSkipMemoryPolicy() { val target = mockDrawableTarget() RequestCreator(picasso, URI_1, 0).memoryPolicy(NO_CACHE).into(target) verify(picasso, never()).quickMemoryCacheCheck(URI_KEY_1) } @Test fun intoDrawableTargetAndNotInCacheSubmitsTargetRequest() { val target = mockDrawableTarget() val placeHolderDrawable = mock(Drawable::class.java) RequestCreator(picasso, URI_1, 0).placeholder(placeHolderDrawable).into(target) verify(target).onPrepareLoad(placeHolderDrawable) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(DrawableTargetAction::class.java) } @Test fun intoImageViewWithNullUriAndResourceIdSkipsAndCancels() { val target = mockImageViewTarget() RequestCreator(picasso, null, 0).into(target) verify(picasso).cancelRequest(target) verify(picasso, never()).quickMemoryCacheCheck(anyString()) verify(picasso, never()).enqueueAndSubmit(any(Action::class.java)) } @Test fun intoImageViewWithQuickMemoryCacheCheckDoesNotSubmit() { val cache = PlatformLruCache(0) val picasso = spy( Picasso( RuntimeEnvironment.application, mock(Dispatcher::class.java), UNUSED_CALL_FACTORY, null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, NO_EVENT_LISTENERS, ARGB_8888, indicatorsEnabled = false, isLoggingEnabled = false ) ) doReturn(bitmap).`when`(picasso).quickMemoryCacheCheck(URI_KEY_1) val target = mockImageViewTarget() val callback = mockCallback() RequestCreator(picasso, URI_1, 0).into(target, callback) verify(target).setImageDrawable(any(PicassoDrawable::class.java)) verify(callback).onSuccess() verify(picasso).cancelRequest(target) verify(picasso, never()).enqueueAndSubmit(any(Action::class.java)) } @Test fun intoImageViewSetsPlaceholderDrawable() { val cache = PlatformLruCache(0) val picasso = spy( Picasso( RuntimeEnvironment.application, mock(Dispatcher::class.java), UNUSED_CALL_FACTORY, null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, NO_EVENT_LISTENERS, ARGB_8888, false, false ) ) val target = mockImageViewTarget() val placeHolderDrawable = mock(Drawable::class.java) RequestCreator(picasso, URI_1, 0).placeholder(placeHolderDrawable).into(target) verify(target).setImageDrawable(placeHolderDrawable) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(ImageViewAction::class.java) } @Test fun intoImageViewNoPlaceholderDrawable() { val cache = PlatformLruCache(0) val picasso = spy( Picasso( RuntimeEnvironment.application, mock(Dispatcher::class.java), UNUSED_CALL_FACTORY, null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, NO_EVENT_LISTENERS, ARGB_8888, indicatorsEnabled = false, isLoggingEnabled = false ) ) val target = mockImageViewTarget() RequestCreator(picasso, URI_1, 0).noPlaceholder().into(target) verifyNoMoreInteractions(target) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(ImageViewAction::class.java) } @Test fun intoImageViewSetsPlaceholderWithResourceId() { val cache = PlatformLruCache(0) val picasso = spy( Picasso( RuntimeEnvironment.application, mock(Dispatcher::class.java), UNUSED_CALL_FACTORY, null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, NO_EVENT_LISTENERS, ARGB_8888, indicatorsEnabled = false, isLoggingEnabled = false ) ) val target = mockImageViewTarget() RequestCreator(picasso, URI_1, 0).placeholder(android.R.drawable.picture_frame).into(target) val drawableCaptor = ArgumentCaptor.forClass(Drawable::class.java) verify(target).setImageDrawable(drawableCaptor.capture()) assertThat(shadowOf(drawableCaptor.value).createdFromResId) .isEqualTo(android.R.drawable.picture_frame) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(ImageViewAction::class.java) } @Test fun cancelNotOnMainThreadCrashes() { doCallRealMethod().`when`(picasso).cancelRequest(any(BitmapTarget::class.java)) val latch = CountDownLatch(1) Thread { try { RequestCreator(picasso, null, 0).into(mockBitmapTarget()) fail("Should have thrown IllegalStateException") } catch (ignored: IllegalStateException) { } finally { latch.countDown() } }.start() latch.await() } @Test fun intoNotOnMainThreadCrashes() { doCallRealMethod().`when`(picasso).enqueueAndSubmit(any(Action::class.java)) val latch = CountDownLatch(1) Thread { try { RequestCreator(picasso, URI_1, 0).into(mockImageViewTarget()) fail("Should have thrown IllegalStateException") } catch (ignored: IllegalStateException) { } finally { latch.countDown() } }.start() latch.await() } @Test fun intoImageViewAndNotInCacheSubmitsImageViewRequest() { val target = mockImageViewTarget() RequestCreator(picasso, URI_1, 0).into(target) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(ImageViewAction::class.java) } @Test fun intoImageViewWithFitAndNoDimensionsQueuesDeferredImageViewRequest() { val target = mockFitImageViewTarget(true) `when`(target.width).thenReturn(0) `when`(target.height).thenReturn(0) RequestCreator(picasso, URI_1, 0).fit().into(target) verify(picasso, never()).enqueueAndSubmit(any(Action::class.java)) verify(picasso).defer(eq(target), any(DeferredRequestCreator::class.java)) } @Test fun intoImageViewWithFitAndDimensionsQueuesImageViewRequest() { val target = mockFitImageViewTarget(true) `when`(target.width).thenReturn(100) `when`(target.height).thenReturn(100) RequestCreator(picasso, URI_1, 0).fit().into(target) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(ImageViewAction::class.java) } @Test fun intoImageViewWithSkipMemoryCachePolicy() { val target = mockImageViewTarget() RequestCreator(picasso, URI_1, 0).memoryPolicy(NO_CACHE).into(target) verify(picasso, never()).quickMemoryCacheCheck(URI_KEY_1) } @Test fun intoImageViewWithFitAndResizeThrows() { try { val target = mockImageViewTarget() RequestCreator(picasso, URI_1, 0).fit().resize(10, 10).into(target) fail("Calling into() ImageView with fit() and resize() should throw exception") } catch (ignored: IllegalStateException) { } } @Test fun imageViewActionWithDefaultPriority() { RequestCreator(picasso, URI_1, 0).into(mockImageViewTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(NORMAL) } @Test fun imageViewActionWithCustomPriority() { RequestCreator(picasso, URI_1, 0).priority(HIGH).into(mockImageViewTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(HIGH) } @Test fun imageViewActionWithDefaultTag() { RequestCreator(picasso, URI_1, 0).into(mockImageViewTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.tag).isEqualTo(actionCaptor.value) } @Test fun imageViewActionWithCustomTag() { RequestCreator(picasso, URI_1, 0).tag("tag").into(mockImageViewTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.tag).isEqualTo("tag") } @Test fun intoRemoteViewsWidgetQueuesAppWidgetAction() { RequestCreator(picasso, URI_1, 0).into(mockRemoteViews(), 0, intArrayOf(1, 2, 3)) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(AppWidgetAction::class.java) } @Test fun intoRemoteViewsNotificationQueuesNotificationAction() { RequestCreator(picasso, URI_1, 0).into(mockRemoteViews(), 0, 0, mockNotification()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value).isInstanceOf(NotificationAction::class.java) } @Test fun intoRemoteViewsWidgetWithPlaceholderDrawableThrows() { try { RequestCreator(picasso, URI_1, 0).placeholder(ColorDrawable(0)) .into(mockRemoteViews(), 0, intArrayOf(1, 2, 3)) fail("Calling into() with placeholder drawable should throw exception") } catch (ignored: IllegalArgumentException) { } } @Test fun intoRemoteViewsWidgetWithErrorDrawableThrows() { try { RequestCreator(picasso, URI_1, 0).error(ColorDrawable(0)) .into(mockRemoteViews(), 0, intArrayOf(1, 2, 3)) fail("Calling into() with error drawable should throw exception") } catch (ignored: IllegalArgumentException) { } } @Test fun intoRemoteViewsNotificationWithPlaceholderDrawableThrows() { try { RequestCreator(picasso, URI_1, 0).placeholder(ColorDrawable(0)) .into(mockRemoteViews(), 0, 0, mockNotification()) fail("Calling into() with error drawable should throw exception") } catch (ignored: IllegalArgumentException) { } } @Test fun intoRemoteViewsNotificationWithErrorDrawableThrows() { try { RequestCreator(picasso, URI_1, 0).error(ColorDrawable(0)) .into(mockRemoteViews(), 0, 0, mockNotification()) fail("Calling into() with error drawable should throw exception") } catch (ignored: IllegalArgumentException) { } } @Test fun intoRemoteViewsWidgetWithFitThrows() { try { val remoteViews = mockRemoteViews() RequestCreator(picasso, URI_1, 0).fit().into(remoteViews, 1, intArrayOf(1, 2, 3)) fail("Calling fit() into remote views should throw exception") } catch (ignored: IllegalStateException) { } } @Test fun intoRemoteViewsNotificationWithFitThrows() { try { val remoteViews = mockRemoteViews() RequestCreator(picasso, URI_1, 0).fit().into(remoteViews, 1, 1, mockNotification()) fail("Calling fit() into remote views should throw exception") } catch (ignored: IllegalStateException) { } } @Test fun intoTargetNoResizeWithCenterInsideOrCenterCropThrows() { try { RequestCreator(picasso, URI_1, 0).centerInside().into(mockBitmapTarget()) fail("Center inside with unknown width should throw exception.") } catch (ignored: IllegalStateException) { } try { RequestCreator(picasso, URI_1, 0).centerCrop().into(mockBitmapTarget()) fail("Center inside with unknown height should throw exception.") } catch (ignored: IllegalStateException) { } } @Test fun appWidgetActionWithDefaultPriority() { RequestCreator(picasso, URI_1, 0).into(mockRemoteViews(), 0, intArrayOf(1, 2, 3)) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(NORMAL) } @Test fun appWidgetActionWithCustomPriority() { RequestCreator(picasso, URI_1, 0).priority(HIGH).into(mockRemoteViews(), 0, intArrayOf(1, 2, 3)) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(HIGH) } @Test fun notificationActionWithDefaultPriority() { RequestCreator(picasso, URI_1, 0).into(mockRemoteViews(), 0, 0, mockNotification()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(NORMAL) } @Test fun notificationActionWithCustomPriority() { RequestCreator(picasso, URI_1, 0).priority(HIGH) .into(mockRemoteViews(), 0, 0, mockNotification()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.priority).isEqualTo(HIGH) } @Test fun appWidgetActionWithDefaultTag() { RequestCreator(picasso, URI_1, 0).into(mockRemoteViews(), 0, intArrayOf(1, 2, 3)) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.tag).isEqualTo(actionCaptor.value) } @Test fun appWidgetActionWithCustomTag() { RequestCreator(picasso, URI_1, 0).tag("tag") .into(remoteViews = mockRemoteViews(), viewId = 0, appWidgetIds = intArrayOf(1, 2, 3)) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.tag).isEqualTo("tag") } @Test fun notificationActionWithDefaultTag() { RequestCreator(picasso, URI_1, 0) .into( remoteViews = mockRemoteViews(), viewId = 0, notificationId = 0, notification = mockNotification() ) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.tag).isEqualTo(actionCaptor.value) } @Test fun notificationActionWithCustomTag() { RequestCreator(picasso, URI_1, 0).tag("tag") .into(mockRemoteViews(), 0, 0, mockNotification()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.tag).isEqualTo("tag") } @Test fun invalidResize() { try { mockRequestCreator(picasso).resize(-1, 10) fail("Negative width should throw exception.") } catch (ignored: IllegalArgumentException) { } try { mockRequestCreator(picasso).resize(10, -1) fail("Negative height should throw exception.") } catch (ignored: IllegalArgumentException) { } try { mockRequestCreator(picasso).resize(0, 0) fail("Zero dimensions should throw exception.") } catch (ignored: IllegalArgumentException) { } } @Test fun invalidCenterCrop() { try { mockRequestCreator(picasso).resize(10, 10).centerInside().centerCrop() fail("Calling center crop after center inside should throw exception.") } catch (ignored: IllegalStateException) { } } @Test fun invalidCenterInside() { try { mockRequestCreator(picasso).resize(10, 10).centerCrop().centerInside() fail("Calling center inside after center crop should throw exception.") } catch (ignored: IllegalStateException) { } } @Test fun invalidPlaceholderImage() { try { mockRequestCreator(picasso).placeholder(0) fail("Resource ID of zero should throw exception.") } catch (ignored: IllegalArgumentException) { } try { mockRequestCreator(picasso).placeholder(1).placeholder(ColorDrawable(0)) fail("Two placeholders should throw exception.") } catch (ignored: IllegalStateException) { } try { mockRequestCreator(picasso).placeholder(ColorDrawable(0)).placeholder(1) fail("Two placeholders should throw exception.") } catch (ignored: IllegalStateException) { } } @Test fun invalidNoPlaceholder() { try { mockRequestCreator(picasso).noPlaceholder().placeholder(ColorDrawable(0)) fail("Placeholder after no placeholder should throw exception.") } catch (ignored: IllegalStateException) { } try { mockRequestCreator(picasso).noPlaceholder().placeholder(1) fail("Placeholder after no placeholder should throw exception.") } catch (ignored: IllegalStateException) { } try { mockRequestCreator(picasso).placeholder(1).noPlaceholder() fail("No placeholder after placeholder should throw exception.") } catch (ignored: IllegalStateException) { } try { mockRequestCreator(picasso).placeholder(ColorDrawable(0)).noPlaceholder() fail("No placeholder after placeholder should throw exception.") } catch (ignored: IllegalStateException) { } } @Test fun invalidErrorImage() { try { mockRequestCreator(picasso).error(0) fail("Resource ID of zero should throw exception.") } catch (ignored: IllegalArgumentException) { } try { mockRequestCreator(picasso).error(1).error(ColorDrawable(0)) fail("Two error placeholders should throw exception.") } catch (ignored: IllegalStateException) { } try { mockRequestCreator(picasso).error(ColorDrawable(0)).error(1) fail("Two error placeholders should throw exception.") } catch (ignored: IllegalStateException) { } } @Test fun invalidPriority() { try { mockRequestCreator(picasso).priority(LOW).priority(HIGH) fail("Two priorities should throw exception.") } catch (ignored: IllegalStateException) { } } @Test fun alreadySetTagThrows() { try { mockRequestCreator(picasso).tag("tag1").tag("tag2") fail("Two tags should throw exception.") } catch (ignored: IllegalStateException) { } } @Test fun transformationListImplementationValid() { val transformations = listOf(TestTransformation("test")) mockRequestCreator(picasso).transform(transformations) // TODO verify something! } @Test fun imageViewActionWithStableKey() { RequestCreator(picasso, URI_1, 0).stableKey(STABLE_1).into(mockImageViewTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.key).isEqualTo(STABLE_URI_KEY_1) } @Test fun imageViewActionWithCustomHeaders() { RequestCreator(picasso, URI_1, 0) .addHeader(CUSTOM_HEADER_NAME, CUSTOM_HEADER_VALUE) .into(mockImageViewTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) assertThat(actionCaptor.value.request.headers!![CUSTOM_HEADER_NAME]) .isEqualTo(CUSTOM_HEADER_VALUE) } @Test fun imageViewActionWithCustomHeadersCopiesHeaders() { RequestCreator(picasso, URI_1, 0) .addHeader(CUSTOM_HEADER_NAME, CUSTOM_HEADER_VALUE) .into(mockImageViewTarget()) verify(picasso).enqueueAndSubmit(actionCaptor.capture()) val newRequest = actionCaptor.value.request.newBuilder().build() assertThat(newRequest.headers!![CUSTOM_HEADER_NAME]).isEqualTo(CUSTOM_HEADER_VALUE) } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/Shadows.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.ContentResolver import android.graphics.Bitmap import android.graphics.BitmapFactory import android.provider.MediaStore import com.squareup.picasso3.TestUtils.makeBitmap import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implements object Shadows { @Implements(MediaStore.Video.Thumbnails::class) object ShadowVideoThumbnails { @Implementation @JvmStatic fun getThumbnail( cr: ContentResolver, origId: Long, kind: Int, options: BitmapFactory.Options ): Bitmap = makeBitmap() } @Implements(MediaStore.Images.Thumbnails::class) object ShadowImageThumbnails { @Implementation @JvmStatic fun getThumbnail( cr: ContentResolver, origId: Long, kind: Int, options: BitmapFactory.Options ): Bitmap = makeBitmap(20, 20) } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/TestContentProvider.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.content.ContentProvider import android.content.ContentValues import android.database.Cursor import android.net.Uri class TestContentProvider : ContentProvider() { override fun onCreate(): Boolean = true override fun query( uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String? ): Cursor? = null override fun getType(uri: Uri): String? { val path = uri.path return when { path == null -> null path.contains("video") -> "video/" path.contains("images") -> "image/png" else -> throw IllegalArgumentException() } } override fun insert( uri: Uri, values: ContentValues? ): Uri = TODO("Not yet implemented") override fun delete( uri: Uri, selection: String?, selectionArgs: Array? ): Int = TODO("Not yet implemented") override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array? ): Int = TODO("Not yet implemented") } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/TestTransformation.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.graphics.Bitmap import android.graphics.Bitmap.Config.ARGB_8888 internal class TestTransformation( private val key: String, private val result: Bitmap? = Bitmap.createBitmap(10, 10, ARGB_8888) ) : Transformation { override fun transform(source: RequestHandler.Result.Bitmap): RequestHandler.Result.Bitmap { val bitmap = source.bitmap bitmap.recycle() return RequestHandler.Result.Bitmap(result!!, source.loadedFrom, source.exifRotation) } override fun key(): String = key } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/TestUtils.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import android.app.Notification import android.content.ContentResolver import android.content.Context import android.content.pm.PackageManager import android.content.pm.PackageManager.NameNotFoundException import android.content.res.Resources import android.graphics.Bitmap.Config.ALPHA_8 import android.graphics.Bitmap.Config.ARGB_8888 import android.graphics.drawable.Drawable import android.net.NetworkInfo import android.net.Uri import android.os.IBinder import android.provider.ContactsContract.Contacts.CONTENT_URI import android.provider.ContactsContract.Contacts.Photo import android.provider.MediaStore.Images import android.provider.MediaStore.Video import android.util.TypedValue import android.view.ViewTreeObserver import android.widget.ImageView import android.widget.RemoteViews import com.squareup.picasso3.BitmapHunterTest.TestableBitmapHunter import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.Picasso.Priority import com.squareup.picasso3.Picasso.RequestTransformer import com.squareup.picasso3.RequestHandler.Result import com.squareup.picasso3.RequestHandler.Result.Bitmap import okhttp3.Call import okhttp3.Response import okio.Timeout import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito import org.mockito.Mockito.doAnswer import org.mockito.Mockito.doReturn import org.mockito.Mockito.mock import org.mockito.Mockito.`when` import org.mockito.invocation.InvocationOnMock import java.io.File import java.io.IOException import java.util.concurrent.Callable import java.util.concurrent.ExecutorService import java.util.concurrent.Future import java.util.concurrent.TimeUnit internal object TestUtils { val URI_1: Uri = Uri.parse("http://example.com/1.png") val URI_2: Uri = Uri.parse("http://example.com/2.png") const val STABLE_1 = "stableExampleKey1" val SIMPLE_REQUEST: Request = Request.Builder(URI_1).build() val URI_KEY_1: String = SIMPLE_REQUEST.key val URI_KEY_2: String = Request.Builder(URI_2).build().key val STABLE_URI_KEY_1: String = Request.Builder(URI_1).stableKey(STABLE_1).build().key private val FILE_1 = File("C:\\windows\\system32\\logo.exe") val FILE_KEY_1: String = Request.Builder(Uri.fromFile(FILE_1)).build().key val FILE_1_URL: Uri = Uri.parse("file:///" + FILE_1.path) val FILE_1_URL_NO_AUTHORITY: Uri = Uri.parse("file:/" + FILE_1.parent) val MEDIA_STORE_CONTENT_1_URL: Uri = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().appendPath("1").build() val MEDIA_STORE_CONTENT_2_URL: Uri = Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendPath("1").build() val MEDIA_STORE_CONTENT_KEY_1: String = Request.Builder(MEDIA_STORE_CONTENT_1_URL).build().key val MEDIA_STORE_CONTENT_KEY_2: String = Request.Builder(MEDIA_STORE_CONTENT_2_URL).build().key val CONTENT_1_URL: Uri = Uri.parse("content://zip/zap/zoop.jpg") val CONTENT_KEY_1: String = Request.Builder(CONTENT_1_URL).build().key val CONTACT_URI_1: Uri = CONTENT_URI.buildUpon().appendPath("1234").build() val CONTACT_KEY_1: String = Request.Builder(CONTACT_URI_1).build().key val CONTACT_PHOTO_URI_1: Uri = CONTENT_URI.buildUpon().appendPath("1234").appendPath(Photo.CONTENT_DIRECTORY).build() val CONTACT_PHOTO_KEY_1: String = Request.Builder(CONTACT_PHOTO_URI_1).build().key const val RESOURCE_ID_1 = 1 val RESOURCE_ID_KEY_1: String = Request.Builder(RESOURCE_ID_1).build().key val ASSET_URI_1: Uri = Uri.parse("file:///android_asset/foo/bar.png") val ASSET_KEY_1: String = Request.Builder(ASSET_URI_1).build().key private const val RESOURCE_PACKAGE = "com.squareup.picasso3" private const val RESOURCE_TYPE = "drawable" private const val RESOURCE_NAME = "foo" val RESOURCE_ID_URI: Uri = Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(RESOURCE_PACKAGE) .appendPath(RESOURCE_ID_1.toString()) .build() val RESOURCE_ID_URI_KEY: String = Request.Builder(RESOURCE_ID_URI).build().key val RESOURCE_TYPE_URI: Uri = Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(RESOURCE_PACKAGE) .appendPath(RESOURCE_TYPE) .appendPath(RESOURCE_NAME) .build() val RESOURCE_TYPE_URI_KEY: String = Request.Builder(RESOURCE_TYPE_URI).build().key val CUSTOM_URI: Uri = Uri.parse("foo://bar") val CUSTOM_URI_KEY: String = Request.Builder(CUSTOM_URI).build().key const val BITMAP_RESOURCE_VALUE = "foo.png" const val XML_RESOURCE_VALUE = "foo.xml" private val DEFAULT_CONFIG = ARGB_8888 private const val DEFAULT_CACHE_SIZE = 123 const val CUSTOM_HEADER_NAME = "Cache-Control" const val CUSTOM_HEADER_VALUE = "no-cache" fun mockPackageResourceContext(): Context { val context = mock(Context::class.java) val pm = mock(PackageManager::class.java) val res = mock(Resources::class.java) doReturn(pm).`when`(context).packageManager try { doReturn(res).`when`(pm).getResourcesForApplication(RESOURCE_PACKAGE) } catch (e: NameNotFoundException) { throw RuntimeException(e) } doReturn(RESOURCE_ID_1).`when`(res) .getIdentifier(RESOURCE_NAME, RESOURCE_TYPE, RESOURCE_PACKAGE) return context } fun mockResources(resValueString: String): Resources { val resources = mock(Resources::class.java) doAnswer { invocation: InvocationOnMock -> val args = invocation.arguments (args[1] as TypedValue).string = resValueString null }.`when`(resources).getValue(anyInt(), any(TypedValue::class.java), anyBoolean()) return resources } fun mockRequest(uri: Uri): Request = Request.Builder(uri).build() fun mockAction( picasso: Picasso, key: String, uri: Uri? = null, target: Any = mockBitmapTarget(), resourceId: Int = 0, priority: Priority? = null, tag: String? = null, headers: Map = emptyMap() ): FakeAction { val builder = Request.Builder(uri, resourceId, DEFAULT_CONFIG).stableKey(key) if (priority != null) { builder.priority(priority) } if (tag != null) { builder.tag(tag) } headers.forEach { (key, value) -> builder.addHeader(key, value) } val request = builder.build() return mockAction(picasso, request, target) } fun mockAction(picasso: Picasso, request: Request, target: Any = mockBitmapTarget()) = FakeAction(picasso, request, target) fun mockImageViewTarget(): ImageView = mock(ImageView::class.java) fun mockRemoteViews(): RemoteViews = mock(RemoteViews::class.java) fun mockNotification(): Notification = mock(Notification::class.java) fun mockFitImageViewTarget(alive: Boolean): ImageView { val observer = mock(ViewTreeObserver::class.java) `when`(observer.isAlive).thenReturn(alive) val mock = mock(ImageView::class.java) `when`(mock.windowToken).thenReturn(mock(IBinder::class.java)) `when`(mock.viewTreeObserver).thenReturn(observer) return mock } fun mockBitmapTarget(): BitmapTarget = mock(BitmapTarget::class.java) fun mockDrawableTarget(): DrawableTarget = mock(DrawableTarget::class.java) fun mockCallback(): Callback = mock(Callback::class.java) fun mockDeferredRequestCreator( creator: RequestCreator?, target: ImageView ): DeferredRequestCreator { val observer = mock(ViewTreeObserver::class.java) `when`(target.viewTreeObserver).thenReturn(observer) return DeferredRequestCreator(creator!!, target, null) } fun mockRequestCreator(picasso: Picasso) = RequestCreator(picasso, null, 0) fun mockNetworkInfo(isConnected: Boolean = false): NetworkInfo { val mock = mock(NetworkInfo::class.java) `when`(mock.isConnected).thenReturn(isConnected) `when`(mock.isConnectedOrConnecting).thenReturn(isConnected) return mock } fun mockHunter( picasso: Picasso, result: Result, action: Action, e: Exception? = null, shouldRetry: Boolean = false, supportsReplay: Boolean = false, dispatcher: Dispatcher = mock(Dispatcher::class.java) ): BitmapHunter = TestableBitmapHunter( picasso = picasso, dispatcher = dispatcher, cache = PlatformLruCache(0), action = action, result = (result as Bitmap).bitmap, exception = e, shouldRetry = shouldRetry, supportsReplay = supportsReplay ) fun mockPicasso(context: Context): Picasso { // Inject a RequestHandler that can handle any request. val requestHandler: RequestHandler = object : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { return true } override fun load(picasso: Picasso, request: Request, callback: Callback) { val defaultResult = makeBitmap() val result = RequestHandler.Result.Bitmap(defaultResult, MEMORY) callback.onSuccess(result) } } return mockPicasso(context, requestHandler) } fun mockPicasso(context: Context, requestHandler: RequestHandler): Picasso { return Picasso.Builder(context) .callFactory(UNUSED_CALL_FACTORY) .withCacheSize(0) .addRequestHandler(requestHandler) .build() } fun makeBitmap( width: Int = 10, height: Int = 10 ): android.graphics.Bitmap = android.graphics.Bitmap.createBitmap(width, height, ALPHA_8) fun makeLoaderWithDrawable(drawable: Drawable?): DrawableLoader = DrawableLoader { drawable } internal class FakeAction( picasso: Picasso, request: Request, private val target: Any ) : Action(picasso, request) { var completedResult: Result? = null var errorException: Exception? = null override fun complete(result: Result) { completedResult = result } override fun error(e: Exception) { errorException = e } override fun getTarget(): Any = target } val UNUSED_CALL_FACTORY = Call.Factory { throw AssertionError() } val NOOP_REQUEST_HANDLER: RequestHandler = object : RequestHandler() { override fun canHandleRequest(data: Request): Boolean = false override fun load(picasso: Picasso, request: Request, callback: Callback) = Unit } val NOOP_TRANSFORMER = RequestTransformer { Request.Builder(0).build() } private val NOOP_LISTENER = Picasso.Listener { _: Picasso, _: Uri?, _: Exception -> } val NO_TRANSFORMERS: List = emptyList() val NO_HANDLERS: List = emptyList() val NO_EVENT_LISTENERS: List = emptyList() fun defaultPicasso( context: Context, hasRequestHandlers: Boolean, hasTransformers: Boolean ): Picasso { val builder = Picasso.Builder(context) if (hasRequestHandlers) { builder.addRequestHandler(NOOP_REQUEST_HANDLER) } if (hasTransformers) { builder.addRequestTransformer(NOOP_TRANSFORMER) } return builder .callFactory(UNUSED_CALL_FACTORY) .defaultBitmapConfig(DEFAULT_CONFIG) .executor(PicassoExecutorService()) .indicatorsEnabled(true) .listener(NOOP_LISTENER) .loggingEnabled(true) .withCacheSize(DEFAULT_CACHE_SIZE) .build() } internal class EventRecorder : EventListener { var maxCacheSize = 0 var cacheSize = 0 var cacheHits = 0 var cacheMisses = 0 var downloadSize: Long = 0 var decodedBitmap: android.graphics.Bitmap? = null var transformedBitmap: android.graphics.Bitmap? = null var closed = false override fun cacheMaxSize(maxSize: Int) { maxCacheSize = maxSize } override fun cacheSize(size: Int) { cacheSize = size } override fun cacheHit() { cacheHits++ } override fun cacheMiss() { cacheMisses++ } override fun downloadFinished(size: Long) { downloadSize = size } override fun bitmapDecoded(bitmap: android.graphics.Bitmap) { decodedBitmap = bitmap } override fun bitmapTransformed(bitmap: android.graphics.Bitmap) { transformedBitmap = bitmap } override fun close() { closed = true } } internal class PremadeCall( private val request: okhttp3.Request, private val response: Response ) : Call { override fun request(): okhttp3.Request = request override fun execute(): Response = response override fun enqueue(responseCallback: okhttp3.Callback) { try { responseCallback.onResponse(this, response) } catch (e: IOException) { throw AssertionError(e) } } override fun cancel(): Unit = throw AssertionError() override fun isExecuted(): Boolean = throw AssertionError() override fun isCanceled(): Boolean = throw AssertionError() override fun clone(): Call = throw AssertionError() override fun timeout(): Timeout = throw AssertionError() } class TestDelegatingService(private val delegate: ExecutorService) : ExecutorService { var submissions = 0 override fun shutdown() = delegate.shutdown() override fun shutdownNow(): List = throw AssertionError("Not implemented.") override fun isShutdown(): Boolean = delegate.isShutdown override fun isTerminated(): Boolean = throw AssertionError("Not implemented.") override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean = delegate.awaitTermination(timeout, unit) override fun submit(task: Callable): Future = throw AssertionError("Not implemented.") override fun submit(task: Runnable, result: T): Future = throw AssertionError("Not implemented.") override fun submit(task: Runnable): Future<*> { submissions++ return delegate.submit(task) } override fun invokeAll(tasks: Collection?>): List> = throw AssertionError("Not implemented.") override fun invokeAll( tasks: Collection?>, timeout: Long, unit: TimeUnit ): List> = throw AssertionError("Not implemented.") override fun invokeAny(tasks: Collection?>): T = throw AssertionError("Not implemented.") override fun invokeAny(tasks: Collection?>, timeout: Long, unit: TimeUnit): T = throw AssertionError("Not implemented.") override fun execute(command: Runnable) = delegate.execute(command) } fun any(type: Class): T = Mockito.any(type) fun eq(value: T): T = Mockito.eq(value) ?: value inline fun argumentCaptor(): KArgumentCaptor { return KArgumentCaptor(ArgumentCaptor.forClass(T::class.java)) } class KArgumentCaptor( private val captor: ArgumentCaptor ) { val value: T get() = captor.value fun capture(): T = captor.capture() } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/UtilsTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3 import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.TestUtils.RESOURCE_ID_1 import com.squareup.picasso3.TestUtils.RESOURCE_ID_URI import com.squareup.picasso3.TestUtils.RESOURCE_TYPE_URI import com.squareup.picasso3.TestUtils.URI_1 import com.squareup.picasso3.TestUtils.mockPackageResourceContext import com.squareup.picasso3.Utils.isWebPFile import okio.Buffer import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class UtilsTest { @Test fun matchingRequestsHaveSameKey() { val request = Request.Builder(URI_1).build() val request2 = Request.Builder(URI_1).build() assertThat(request.key).isEqualTo(request2.key) val t1 = TestTransformation("foo", null) val t2 = TestTransformation("foo", null) val requestTransform1 = Request.Builder(URI_1).transform(t1).build() val requestTransform2 = Request.Builder(URI_1).transform(t2).build() assertThat(requestTransform1.key).isEqualTo(requestTransform2.key) val t3 = TestTransformation("foo", null) val t4 = TestTransformation("bar", null) val requestTransform3 = Request.Builder(URI_1).transform(t3).transform(t4).build() val requestTransform4 = Request.Builder(URI_1).transform(t3).transform(t4).build() assertThat(requestTransform3.key).isEqualTo(requestTransform4.key) val t5 = TestTransformation("foo", null) val t6 = TestTransformation("bar", null) val requestTransform5 = Request.Builder(URI_1).transform(t5).transform(t6).build() val requestTransform6 = Request.Builder(URI_1).transform(t6).transform(t5).build() assertThat(requestTransform5.key).isNotEqualTo(requestTransform6.key) } @Test fun detectedWebPFile() { assertThat(isWebPFile(Buffer().writeUtf8("RIFFxxxxWEBP"))).isTrue() assertThat(isWebPFile(Buffer().writeUtf8("RIFFxxxxxWEBP"))).isFalse() assertThat(isWebPFile(Buffer().writeUtf8("ABCDxxxxWEBP"))).isFalse() assertThat(isWebPFile(Buffer().writeUtf8("RIFFxxxxABCD"))).isFalse() assertThat(isWebPFile(Buffer().writeUtf8("RIFFxxWEBP"))).isFalse() } @Test fun ensureBuilderIsCleared() { Request.Builder(RESOURCE_ID_URI).build() assertThat(Utils.MAIN_THREAD_KEY_BUILDER.length).isEqualTo(0) Request.Builder(URI_1).build() assertThat(Utils.MAIN_THREAD_KEY_BUILDER.length).isEqualTo(0) } @Test fun getResourceById() { val request = Request.Builder(RESOURCE_ID_URI).build() val res = Utils.getResources(mockPackageResourceContext(), request) val id = Utils.getResourceId(res, request) assertThat(id).isEqualTo(RESOURCE_ID_1) } @Test fun getResourceByTypeAndName() { val request = Request.Builder(RESOURCE_TYPE_URI).build() val res = Utils.getResources(mockPackageResourceContext(), request) val id = Utils.getResourceId(res, request) assertThat(id).isEqualTo(RESOURCE_ID_1) } } ================================================ FILE: picasso/src/test/java/com/squareup/picasso3/_JavaConsumerIdeCheck.java ================================================ package com.squareup.picasso3; import android.content.Context; import android.graphics.Bitmap; import org.junit.Ignore; import org.junit.Test; import org.mockito.Mock; import static android.graphics.Bitmap.Config.ALPHA_8; import static com.squareup.picasso3.Picasso.LoadedFrom.DISK; public class _JavaConsumerIdeCheck { @Mock Context context; @Test @Ignore("Quick IDE check for compile-time access to Kotlin internal methods from Java callers") public void name() { Picasso picasso = new Picasso.Builder(context).build(); picasso.setLoggingEnabled(true); RequestCreator requestCreator = picasso.load(""); requestCreator.fit(); AssetRequestHandler assetRequestHandler = new AssetRequestHandler(context); assetRequestHandler.getRetryCount(); Request.Builder requestBuilder = new Request.Builder(0); requestBuilder.getRotationDegrees(); Request request = requestBuilder.build(); MatrixTransformation matrixTransformation = new MatrixTransformation(request); Bitmap bitmap = Bitmap.createBitmap(0, 0, ALPHA_8); RequestHandler.Result.Bitmap transform = matrixTransformation.transform(new RequestHandler.Result.Bitmap(bitmap, DISK, 0)); Picasso.LoadedFrom loadedFrom = transform.getLoadedFrom(); Picasso.RequestTransformer requestTransformer = request1 -> request1; Dispatcher.Companion companion1 = Dispatcher.Companion; MatrixTransformation.Companion companion = MatrixTransformation.Companion; Picasso.Companion companion2 = Picasso.Companion; ResourceDrawableRequestHandler.Companion companion3 = ResourceDrawableRequestHandler.Companion; } } ================================================ FILE: picasso/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker ================================================ mock-maker-inline ================================================ FILE: picasso/src/test/resources/robolectric.properties ================================================ sdk: 21 constants: com.squareup.picasso3.BuildConfig manifest: --default ================================================ FILE: picasso-compose/README.md ================================================ Picasso Compose Ui ==================================== A [Painter] which wraps a [RequestCreator] Usage ----- Create a `Painter` using the rememberPainter extension on a Picasso instance. ```kotlin val picasso = Picasso.Builder(context).build() val painter = picasso.rememberPainter(key = url) { it.load(url).placeholder(placeholderDrawable).error(errorDrawable) } ``` ================================================ FILE: picasso-compose/api/picasso-compose.api ================================================ public final class com/squareup/picasso3/compose/PicassoPainterKt { public static final fun rememberPainter (Lcom/squareup/picasso3/Picasso;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/graphics/painter/Painter; } ================================================ FILE: picasso-compose/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'org.jetbrains.kotlin.plugin.compose' apply plugin: 'com.vanniktech.maven.publish' android { namespace 'com.squareup.picasso3.compose' compileSdkVersion libs.versions.compileSdk.get() as int defaultConfig { minSdkVersion libs.versions.minSdk.get() as int testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } buildFeatures { compose true } compileOptions { sourceCompatibility libs.versions.javaTarget.get() targetCompatibility libs.versions.javaTarget.get() } kotlinOptions { jvmTarget = libs.versions.javaTarget.get() } lintOptions { textOutput 'stdout' textReport true lintConfig rootProject.file('lint.xml') } } dependencies { api projects.picasso implementation libs.drawablePainter implementation libs.composeUi implementation libs.composeUi.foundation implementation libs.composeRuntime debugImplementation libs.composeUi.testManifest androidTestImplementation libs.composeUi.test androidTestImplementation libs.truth compileOnly libs.androidx.annotations } ================================================ FILE: picasso-compose/gradle.properties ================================================ POM_ARTIFACT_ID=picasso-compose POM_NAME=Picasso Compose POM_DESCRIPTION=Compose UI support for Picasso. POM_PACKAGING=aar ================================================ FILE: picasso-compose/src/androidTest/java/com/squareup/picasso3/compose/PicassoPainterTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3.compose import android.graphics.Bitmap import android.graphics.Bitmap.Config.ARGB_8888 import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.requiredSize import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.Picasso import com.squareup.picasso3.Picasso.LoadedFrom import com.squareup.picasso3.Request import com.squareup.picasso3.RequestHandler import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import kotlinx.coroutines.Dispatchers @RunWith(AndroidJUnit4::class) class PicassoPainterTest { @get:Rule val rule = createComposeRule() @Test fun firstFrameConsumesStateFromLayout() { lateinit var lastRequest: Request val context = InstrumentationRegistry.getInstrumentation().targetContext val picasso = Picasso.Builder(context) .callFactory { throw RuntimeException() } .dispatchers(Dispatchers.Unconfined, Dispatchers.Unconfined) .addRequestHandler(object : RequestHandler() { override fun canHandleRequest(data: Request): Boolean = true override fun load(picasso: Picasso, request: Request, callback: Callback) { lastRequest = request callback.onSuccess(Result.Bitmap(Bitmap.createBitmap(1, 1, ARGB_8888), LoadedFrom.MEMORY)) } }) .build() var size: IntSize by mutableStateOf(IntSize.Zero) var drawn = false rule.setContent { CompositionLocalProvider(LocalDensity provides Density(1f)) { val painter = picasso.rememberPainter { it.load("http://example.com/") // Headers are not part of a cache key, using a stable key to break cache .stableKey("http://example.com/$size") .addHeader("width", size.width.toString()) .addHeader("height", size.height.toString()) } Canvas( Modifier .requiredSize(9.dp) .onSizeChanged { size = it } ) { val canvasSize = this.size with(painter) { draw(canvasSize) } drawn = true } } } rule.waitUntil { drawn } // Draw triggers request was made with size. assertThat(lastRequest.headers?.toMultimap()).containsAtLeastEntriesIn( mapOf( "width" to listOf("9"), "height" to listOf("9") ) ) } @Test fun redrawDoesNotReexecuteUnchangedRequest() { var requestCount = 0 val context = InstrumentationRegistry.getInstrumentation().targetContext val picasso = Picasso.Builder(context) .callFactory { throw RuntimeException() } .dispatchers(Dispatchers.Unconfined, Dispatchers.Unconfined) .addRequestHandler(object : RequestHandler() { override fun canHandleRequest(data: Request): Boolean = true override fun load(picasso: Picasso, request: Request, callback: Callback) { requestCount++ callback.onSuccess(Result.Bitmap(Bitmap.createBitmap(1, 1, ARGB_8888), LoadedFrom.MEMORY)) } }) .build() var drawInvalidator by mutableStateOf(0) var drawCount = 0 rule.setContent { CompositionLocalProvider(LocalDensity provides Density(1f)) { val painter = picasso.rememberPainter { it.load("http://example.com/") } Canvas(Modifier.fillMaxSize()) { drawCount++ drawInvalidator = 1 val canvasSize = this.size with(painter) { draw(canvasSize) } } } } rule.waitUntil { drawCount == 2 } assertThat(requestCount).isEqualTo(1) } @Test fun newRequestLoaded_whenRequestDependenciesChangedAfterFirstFrame() { var lastRequest: Request? = null val context = InstrumentationRegistry.getInstrumentation().targetContext val picasso = Picasso.Builder(context) .callFactory { throw RuntimeException() } .dispatchers(Dispatchers.Unconfined, Dispatchers.Unconfined) .addRequestHandler(object : RequestHandler() { override fun canHandleRequest(data: Request): Boolean = true override fun load(picasso: Picasso, request: Request, callback: Callback) { lastRequest = request callback.onSuccess(Result.Bitmap(Bitmap.createBitmap(1, 1, ARGB_8888), LoadedFrom.MEMORY)) } }) .build() var testHeader by mutableStateOf("one") rule.setContent { CompositionLocalProvider(LocalDensity provides Density(1f)) { val painter = picasso.rememberPainter { it.load("http://example.com/") // Headers are not part of a cache key, using a stable key to break cache .stableKey("http://example.com/$testHeader") .addHeader("testHeader", testHeader) } Canvas(Modifier.fillMaxSize()) { val canvasSize = this.size with(painter) { draw(canvasSize) } } } } rule.waitUntil { lastRequest != null } assertThat(lastRequest!!.headers?.get("testHeader")).isEqualTo("one") var currentRequest = lastRequest testHeader = "two" // On API 21 runOnIdle runs before the composition recomposes :-( // Waiting until the request updates, then asserting rule.waitUntil { currentRequest != lastRequest } assertThat(lastRequest!!.headers?.get("testHeader")).isEqualTo("two") currentRequest = lastRequest testHeader = "three" rule.waitUntil { currentRequest != lastRequest } assertThat(lastRequest!!.headers?.get("testHeader")).isEqualTo("three") } } ================================================ FILE: picasso-compose/src/main/java/com/squareup/picasso3/compose/PicassoPainter.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3.compose import android.graphics.drawable.Drawable import androidx.compose.runtime.Composable import androidx.compose.runtime.RememberObserver import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.painter.Painter import com.google.accompanist.drawablepainter.DrawablePainter import com.squareup.picasso3.DrawableTarget import com.squareup.picasso3.Picasso import com.squareup.picasso3.Picasso.LoadedFrom import com.squareup.picasso3.RequestCreator @Composable fun Picasso.rememberPainter( key: Any? = null, onError: ((Exception) -> Unit)? = null, request: (Picasso) -> RequestCreator ): Painter { return remember(key) { PicassoPainter(this, request, onError) } } internal class PicassoPainter( private val picasso: Picasso, private val request: (Picasso) -> RequestCreator, private val onError: ((Exception) -> Unit)? = null ) : Painter(), RememberObserver, DrawableTarget { private var lastRequestCreator: RequestCreator? by mutableStateOf(null) private val requestCreator: RequestCreator by derivedStateOf { request(picasso) } private var painter: Painter by mutableStateOf(EmptyPainter) private var alpha: Float by mutableStateOf(DefaultAlpha) private var colorFilter: ColorFilter? by mutableStateOf(null) override val intrinsicSize: Size get() { // Make sure we're using the latest request. If the request function reads any state, it will // invalidate whatever scope this property is being read from. load() return painter.intrinsicSize } override fun applyAlpha(alpha: Float): Boolean { this.alpha = alpha return true } override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { this.colorFilter = colorFilter return true } override fun DrawScope.onDraw() { // Make sure we're using the latest request. If the request function reads any state, it will // invalidate this draw scope when it changes. load() with(painter) { draw(size, alpha, colorFilter) } } override fun onRemembered() { // This is called from composition, but if the request provider function reads any state we // don't want that to invalidate composition. It will invalidate draw, later. Snapshot.withoutReadObservation { load() } } override fun onAbandoned() { (painter as? RememberObserver)?.onAbandoned() painter = EmptyPainter picasso.cancelRequest(this) } override fun onForgotten() { (painter as? RememberObserver)?.onForgotten() painter = EmptyPainter picasso.cancelRequest(this) } override fun onPrepareLoad(placeHolderDrawable: Drawable?) { placeHolderDrawable?.let(::setPainter) } override fun onDrawableLoaded(drawable: Drawable, from: LoadedFrom) { setPainter(drawable) } override fun onDrawableFailed(e: Exception, errorDrawable: Drawable?) { onError?.invoke(e) errorDrawable?.let(::setPainter) } private fun load() { // This derived state read will return the same instance of RequestCreator if one has been // cached and none of the state dependencies have since changed. val requestCreator = requestCreator // lastRequestCreator is just used for diffing, we don't want it to invalidate anything. val lastRequestCreator = Snapshot.withoutReadObservation { lastRequestCreator } // Only launch a new request if anything has actually changed. RequestCreator does not // currently implement an equals method, relying here on reference equality, future improvement // will be to implement equals which can prevent further re-requests. if (requestCreator != lastRequestCreator) { this.lastRequestCreator = requestCreator requestCreator.into(this) } } private fun setPainter(drawable: Drawable) { (painter as? RememberObserver)?.onForgotten() painter = DrawablePainter(drawable).apply(DrawablePainter::onRemembered) } } private object EmptyPainter : Painter() { override val intrinsicSize = Size.Unspecified override fun DrawScope.onDraw() = Unit } ================================================ FILE: picasso-paparazzi-sample/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'app.cash.paparazzi' android { namespace 'com.example.picasso.paparazzi' compileSdkVersion libs.versions.compileSdk.get() as int defaultConfig { minSdkVersion libs.versions.minSdk.get() as int } compileOptions { sourceCompatibility libs.versions.javaTarget.get() targetCompatibility libs.versions.javaTarget.get() } kotlinOptions { jvmTarget = libs.versions.javaTarget.get() } lintOptions { textOutput 'stdout' textReport true lintConfig rootProject.file('lint.xml') } testOptions { unitTests { includeAndroidResources = true } } } dependencies { testImplementation libs.junit testImplementation projects.picasso } // https://github.com/diffplug/spotless/issues/1572 tasks.withType(com.diffplug.gradle.spotless.SpotlessTask).configureEach { dependsOn(tasks.withType(Test)) } ================================================ FILE: picasso-paparazzi-sample/src/test/java/com/example/picasso/paparazzi/PicassoPaparazziTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso.paparazzi import android.graphics.BitmapFactory import android.widget.ImageView import android.widget.ImageView.ScaleType.CENTER import app.cash.paparazzi.Paparazzi import com.squareup.picasso3.Picasso import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.Request import com.squareup.picasso3.RequestHandler import org.junit.Rule import org.junit.Test import kotlinx.coroutines.Dispatchers class PicassoPaparazziTest { @get:Rule val paparazzi = Paparazzi() @Test fun loadsUrlIntoImageView() { val picasso = Picasso.Builder(paparazzi.context) .callFactory { throw AssertionError() } // Removes network .dispatchers( mainContext = Dispatchers.Unconfined, backgroundContext = Dispatchers.Unconfined ) .addRequestHandler(FakeRequestHandler()) .build() paparazzi.snapshot( ImageView(paparazzi.context).apply { scaleType = CENTER picasso.load("fake:///zkaAooq.png") .resize(200, 200) .centerInside() .onlyScaleDown() .into(this) } ) } class FakeRequestHandler : RequestHandler() { override fun canHandleRequest(data: Request): Boolean { return "fake" == data.uri!!.scheme } override fun load(picasso: Picasso, request: Request, callback: Callback) { val imagePath = request.uri!!.lastPathSegment!! callback.onSuccess(Result.Bitmap(loadBitmap(imagePath)!!, MEMORY)) } private fun loadBitmap(imagePath: String): android.graphics.Bitmap? { val resourceAsStream = javaClass.classLoader!!.getResourceAsStream(imagePath) return BitmapFactory.decodeStream(resourceAsStream) } } } ================================================ FILE: picasso-pollexor/README.md ================================================ Picasso Pollexor Request Transformer ==================================== A request transformer which uses a remote [Thumbor][1] install to perform image transformation on the server. Usage ----- Create a `PollexorRequestTransformer` using the remote host and optional encryption key. ```java RequestTransformer transformer = new PollexorRequestTransformer("http://example.com", "secretpassword"); ``` Pass the transformer when creating a `Picasso` instance. ```java Picasso p = new Picasso.Builder(context) .requestTransformer(transformer) .build(); ``` _Note: This can only be used with an instance you create yourself. You cannot set a request transformer on the global singleton instance (`Picasso.get`)._ [1]: https://github.com/globocom/thumbor ================================================ FILE: picasso-pollexor/api/picasso-pollexor.api ================================================ public final class com/squareup/picasso3/pollexor/PollexorRequestTransformer : com/squareup/picasso3/Picasso$RequestTransformer { public static final field Companion Lcom/squareup/picasso3/pollexor/PollexorRequestTransformer$Companion; public fun (Lcom/squareup/pollexor/Thumbor;)V public fun (Lcom/squareup/pollexor/Thumbor;Lcom/squareup/picasso3/pollexor/PollexorRequestTransformer$Callback;)V public fun (Lcom/squareup/pollexor/Thumbor;Z)V public fun (Lcom/squareup/pollexor/Thumbor;ZLcom/squareup/picasso3/pollexor/PollexorRequestTransformer$Callback;)V public synthetic fun (Lcom/squareup/pollexor/Thumbor;ZLcom/squareup/picasso3/pollexor/PollexorRequestTransformer$Callback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun transformRequest (Lcom/squareup/picasso3/Request;)Lcom/squareup/picasso3/Request; } public abstract interface class com/squareup/picasso3/pollexor/PollexorRequestTransformer$Callback { public abstract fun configure (Lcom/squareup/pollexor/ThumborUrlBuilder;)V } public final class com/squareup/picasso3/pollexor/PollexorRequestTransformer$Companion { } ================================================ FILE: picasso-pollexor/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'com.vanniktech.maven.publish' android { namespace 'com.squareup.picasso3.pollexor' compileSdkVersion libs.versions.compileSdk.get() as int defaultConfig { minSdkVersion libs.versions.minSdk.get() as int } compileOptions { sourceCompatibility libs.versions.javaTarget.get() targetCompatibility libs.versions.javaTarget.get() } kotlinOptions { jvmTarget = libs.versions.javaTarget.get() } lintOptions { textOutput 'stdout' textReport true lintConfig rootProject.file('lint.xml') } } dependencies { api projects.picasso api libs.pollexor compileOnly libs.androidx.annotations testImplementation libs.junit testImplementation libs.robolectric testImplementation libs.truth testImplementation libs.pollexor } ================================================ FILE: picasso-pollexor/gradle.properties ================================================ POM_ARTIFACT_ID=picasso-pollexor POM_NAME=Picasso Pollexor Transformer POM_DESCRIPTION=A request transformer which uses a remote Thumbor install to perform image transformation on the server. POM_PACKAGING=aar ================================================ FILE: picasso-pollexor/src/main/java/com/squareup/picasso3/pollexor/PollexorRequestTransformer.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3.pollexor import android.net.Uri import com.squareup.picasso3.Picasso.RequestTransformer import com.squareup.picasso3.Request import com.squareup.picasso3.pollexor.PollexorRequestTransformer.Callback import com.squareup.pollexor.Thumbor import com.squareup.pollexor.ThumborUrlBuilder import com.squareup.pollexor.ThumborUrlBuilder.ImageFormat.WEBP /** * A [RequestTransformer] that changes requests to use [Thumbor] for some remote * transformations. * By default images are only transformed with Thumbor if they have a size set, * unless alwaysTransform is set to true */ class PollexorRequestTransformer @JvmOverloads constructor( private val thumbor: Thumbor, private val alwaysTransform: Boolean = false, private val callback: Callback = NONE ) : RequestTransformer { constructor(thumbor: Thumbor, callback: Callback) : this(thumbor, false, callback) override fun transformRequest(request: Request): Request { if (request.resourceId != 0) { return request // Don't transform resource requests. } val uri = requireNotNull(request.uri) { "Null uri passed to ${javaClass.canonicalName}" } val scheme = uri.scheme if ("https" != scheme && "http" != scheme) { return request // Thumbor only supports remote images. } // Only transform requests that have resizes unless `alwaysTransform` is set. if (!request.hasSize() && !alwaysTransform) { return request } // Start building a new request for us to mutate. val newRequest = request.newBuilder() // Create the url builder to use. val urlBuilder = thumbor.buildImage(uri.toString()) callback.configure(urlBuilder) // Resize the image to the target size if it has a size. if (request.hasSize()) { urlBuilder.resize(request.targetWidth, request.targetHeight) newRequest.clearResize() } // If the center inside flag is set, perform that with Thumbor as well. if (request.centerInside) { urlBuilder.fitIn() newRequest.clearCenterInside() } // Use WebP for downloading. urlBuilder.filter(ThumborUrlBuilder.format(WEBP)) // Update the request with the completed Thumbor URL. newRequest.setUri(Uri.parse(urlBuilder.toUrl())) return newRequest.build() } fun interface Callback { fun configure(builder: ThumborUrlBuilder) } companion object { private val NONE = Callback { } } } ================================================ FILE: picasso-pollexor/src/test/java/com/squareup/picasso3/pollexor/PollexorRequestTransformerTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.picasso3.pollexor import android.net.Uri import com.google.common.truth.Truth.assertThat import com.squareup.picasso3.Request.Builder import com.squareup.pollexor.Thumbor import com.squareup.pollexor.ThumborUrlBuilder import com.squareup.pollexor.ThumborUrlBuilder.ImageFormat.WEBP import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class PollexorRequestTransformerTest { private val transformer = PollexorRequestTransformer(Thumbor.create(HOST)) private val secureTransformer = PollexorRequestTransformer(Thumbor.create(HOST, KEY)) private val alwaysResizeTransformer = PollexorRequestTransformer( Thumbor.create(HOST), alwaysTransform = true ) private val callbackTransformer = PollexorRequestTransformer( Thumbor.create(HOST), callback = { it.filter("custom") } ) @Test fun resourceIdRequestsAreNotTransformed() { val input = Builder(12).build() val output = transformer.transformRequest(input) assertThat(output).isSameInstanceAs(input) } @Test fun resourceIdRequestsAreNotTransformedWhenAlwaysTransformIsTrue() { val input = Builder(12).build() val output = alwaysResizeTransformer.transformRequest(input) assertThat(output).isSameInstanceAs(input) } @Test fun nonHttpRequestsAreNotTransformed() { val input = Builder(IMAGE_URI).build() val output = transformer.transformRequest(input) assertThat(output).isSameInstanceAs(input) } @Test fun nonResizedRequestsAreNotTransformed() { val input = Builder(IMAGE_URI).build() val output = transformer.transformRequest(input) assertThat(output).isSameInstanceAs(input) } @Test fun nonResizedRequestsAreTransformedWhenAlwaysTransformIsSet() { val input = Builder(IMAGE_URI).build() val output = alwaysResizeTransformer.transformRequest(input) assertThat(output).isNotSameInstanceAs(input) assertThat(output.hasSize()).isFalse() val expected = Thumbor.create(HOST) .buildImage(IMAGE) .filter(ThumborUrlBuilder.format(WEBP)) .toUrl() assertThat(output.uri.toString()).isEqualTo(expected) } @Test fun simpleResize() { val input = Builder(IMAGE_URI).resize(50, 50).build() val output = transformer.transformRequest(input) assertThat(output).isNotSameInstanceAs(input) assertThat(output.hasSize()).isFalse() val expected = Thumbor.create(HOST) .buildImage(IMAGE) .resize(50, 50) .filter(ThumborUrlBuilder.format(WEBP)) .toUrl() assertThat(output.uri.toString()).isEqualTo(expected) } @Test fun simpleResizeWithCenterCrop() { val input = Builder(IMAGE_URI).resize(50, 50).centerCrop().build() val output = transformer.transformRequest(input) assertThat(output).isNotSameInstanceAs(input) assertThat(output.hasSize()).isFalse() assertThat(output.centerCrop).isFalse() val expected = Thumbor.create(HOST) .buildImage(IMAGE) .resize(50, 50) .filter(ThumborUrlBuilder.format(WEBP)) .toUrl() assertThat(output.uri.toString()).isEqualTo(expected) } @Test fun simpleResizeWithCenterInside() { val input = Builder(IMAGE_URI).resize(50, 50).centerInside().build() val output = transformer.transformRequest(input) assertThat(output).isNotSameInstanceAs(input) assertThat(output.hasSize()).isFalse() assertThat(output.centerInside).isFalse() val expected = Thumbor.create(HOST) .buildImage(IMAGE) .resize(50, 50) .filter(ThumborUrlBuilder.format(WEBP)) .fitIn() .toUrl() assertThat(output.uri.toString()).isEqualTo(expected) } @Test fun simpleResizeWithEncryption() { val input = Builder(IMAGE_URI).resize(50, 50).build() val output = secureTransformer.transformRequest(input) assertThat(output).isNotSameInstanceAs(input) assertThat(output.hasSize()).isFalse() val expected = Thumbor.create(HOST, KEY) .buildImage(IMAGE) .resize(50, 50) .filter(ThumborUrlBuilder.format(WEBP)) .toUrl() assertThat(output.uri.toString()).isEqualTo(expected) } @Test fun simpleResizeWithCenterInsideAndEncryption() { val input = Builder(IMAGE_URI).resize(50, 50).centerInside().build() val output = secureTransformer.transformRequest(input) assertThat(output).isNotSameInstanceAs(input) assertThat(output.hasSize()).isFalse() assertThat(output.centerInside).isFalse() val expected = Thumbor.create(HOST, KEY) .buildImage(IMAGE) .resize(50, 50) .filter(ThumborUrlBuilder.format(WEBP)) .fitIn() .toUrl() assertThat(output.uri.toString()).isEqualTo(expected) } @Test fun configureCallback() { val input = Builder(IMAGE_URI).resize(50, 50).build() val output = callbackTransformer.transformRequest(input) assertThat(output).isNotSameInstanceAs(input) assertThat(output.hasSize()).isFalse() val expected = Thumbor.create(HOST) .buildImage(IMAGE) .resize(50, 50) .filter("custom") .filter(ThumborUrlBuilder.format(WEBP)) .toUrl() assertThat(output.uri.toString()).isEqualTo(expected) } companion object { private const val HOST = "http://example.com/" private const val KEY = "omgsecretpassword" private const val IMAGE = "http://google.com/logo.png" private val IMAGE_URI = Uri.parse(IMAGE) } } ================================================ FILE: picasso-sample/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'org.jetbrains.kotlin.plugin.compose' android { namespace 'com.example.picasso' compileSdkVersion libs.versions.compileSdk.get() as int defaultConfig { minSdkVersion libs.versions.minSdk.get() as int applicationId 'com.example.picasso' } buildFeatures { compose true } compileOptions { sourceCompatibility libs.versions.javaTarget.get() targetCompatibility libs.versions.javaTarget.get() } kotlinOptions { jvmTarget = libs.versions.javaTarget.get() } lintOptions { lintConfig file('lint.xml') textOutput 'stdout' textReport true // https://github.com/square/okhttp/issues/896 ignore 'InvalidPackage' } } dependencies { compileOnly libs.androidx.annotations implementation libs.androidx.core implementation libs.androidx.cursorAdapter implementation libs.androidx.fragment implementation libs.androidx.startup implementation libs.drawablePainter implementation libs.composeUi implementation libs.composeRuntime implementation libs.composeUi.foundation implementation libs.composeUi.material implementation libs.composeUi.uiTooling implementation projects.picasso implementation projects.picassoStats implementation projects.picassoCompose } ================================================ FILE: picasso-sample/lint.xml ================================================ ================================================ FILE: picasso-sample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/Data.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso internal object Data { private const val BASE = "https://i.imgur.com/" private const val EXT = ".jpg" @JvmField val URLS = arrayOf( BASE + "CqmBjo5" + EXT, BASE + "zkaAooq" + EXT, BASE + "0gqnEaY" + EXT, BASE + "9gbQ7YR" + EXT, BASE + "aFhEEby" + EXT, BASE + "0E2tgV7" + EXT, BASE + "P5JLfjk" + EXT, BASE + "nz67a4F" + EXT, BASE + "dFH34N5" + EXT, BASE + "FI49ftb" + EXT, BASE + "DvpvklR" + EXT, BASE + "DNKnbG8" + EXT, BASE + "yAdbrLp" + EXT, BASE + "55w5Km7" + EXT, BASE + "NIwNTMR" + EXT, BASE + "DAl0KB8" + EXT, BASE + "xZLIYFV" + EXT, BASE + "HvTyeh3" + EXT, BASE + "Ig9oHCM" + EXT, BASE + "7GUv9qa" + EXT, BASE + "i5vXmXp" + EXT, BASE + "glyvuXg" + EXT, BASE + "u6JF6JZ" + EXT, BASE + "ExwR7ap" + EXT, BASE + "Q54zMKT" + EXT, BASE + "9t6hLbm" + EXT, BASE + "F8n3Ic6" + EXT, BASE + "P5ZRSvT" + EXT, BASE + "jbemFzr" + EXT, BASE + "8B7haIK" + EXT, BASE + "aSeTYQr" + EXT, BASE + "OKvWoTh" + EXT, BASE + "zD3gT4Z" + EXT, BASE + "z77CaIt" + EXT ) } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/GrayscaleTransformation.kt ================================================ /* * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.graphics.Bitmap.createBitmap import android.graphics.BitmapShader import android.graphics.Canvas import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.graphics.Paint import android.graphics.Paint.ANTI_ALIAS_FLAG import android.graphics.PorterDuff.Mode.MULTIPLY import android.graphics.PorterDuffXfermode import android.graphics.Shader.TileMode.REPEAT import com.squareup.picasso3.Picasso import com.squareup.picasso3.RequestHandler.Result import com.squareup.picasso3.Transformation import java.io.IOException class GrayscaleTransformation(private val picasso: Picasso) : Transformation { override fun transform(source: Result.Bitmap): Result.Bitmap { val bitmap = source.bitmap val result = createBitmap(bitmap.width, bitmap.height, bitmap.config) val noise = try { picasso.load(R.drawable.noise).get()!! } catch (e: IOException) { throw RuntimeException("Failed to apply transformation! Missing resource.") } val colorMatrix = ColorMatrix().apply { setSaturation(0f) } val paint = Paint(ANTI_ALIAS_FLAG).apply { colorFilter = ColorMatrixColorFilter(colorMatrix) } val canvas = Canvas(result) canvas.drawBitmap(bitmap, 0f, 0f, paint) paint.apply { colorFilter = null shader = BitmapShader(noise, REPEAT, REPEAT) xfermode = PorterDuffXfermode(MULTIPLY) } canvas.drawRect(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), paint) bitmap.recycle() noise.recycle() return Result.Bitmap(result, source.loadedFrom, source.exifRotation) } override fun key() = "grayscaleTransformation()" } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/PicassoInitializer.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.content.Context import androidx.startup.Initializer import com.squareup.picasso3.Picasso import com.squareup.picasso3.stats.StatsEventListener class PicassoInitializer : Initializer { override fun create(context: Context) { appContext = context } override fun dependencies() = emptyList>>() companion object { private lateinit var appContext: Context private val instance: Picasso by lazy { Picasso .Builder(appContext) .addEventListener(StatsEventListener()) .build() } fun get() = instance } } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/PicassoSampleActivity.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.os.Bundle import android.view.View import android.view.ViewGroup.LayoutParams import android.widget.AdapterView.OnItemClickListener import android.widget.FrameLayout import android.widget.ListView import android.widget.ToggleButton import androidx.fragment.app.FragmentActivity abstract class PicassoSampleActivity : FragmentActivity() { private lateinit var sampleContent: FrameLayout private lateinit var showHide: ToggleButton override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) super.setContentView(R.layout.picasso_sample_activity) sampleContent = findViewById(R.id.sample_content) val activityList = findViewById(R.id.activity_list) val adapter = PicassoSampleAdapter(this) activityList.adapter = adapter activityList.onItemClickListener = OnItemClickListener { _, _, position, _ -> adapter.getItem(position).launch(this@PicassoSampleActivity) } showHide = findViewById(R.id.faux_action_bar_control) showHide.setOnCheckedChangeListener { _, checked -> activityList.visibility = if (checked) View.VISIBLE else View.GONE } lifecycle.addObserver(PicassoInitializer.get()) } override fun onBackPressed() { if (showHide.isChecked) { showHide.isChecked = false } else { super.onBackPressed() } } override fun setContentView(layoutResID: Int) { layoutInflater.inflate(layoutResID, sampleContent) } override fun setContentView(view: View) { sampleContent.addView(view) } override fun setContentView(view: View, params: LayoutParams) { sampleContent.addView(view, params) } } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/PicassoSampleAdapter.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.Manifest.permission.POST_NOTIFICATIONS import android.app.Activity import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build.VERSION import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES.TIRAMISU import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.RemoteViews import android.widget.TextView import androidx.core.app.ActivityCompat.checkSelfPermission import androidx.core.app.ActivityCompat.requestPermissions import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import java.util.Random internal class PicassoSampleAdapter(context: Context?) : BaseAdapter() { internal enum class Sample( val label: String, private val activityClass: Class? ) { GRID_VIEW("Image Grid View", SampleGridViewActivity::class.java), COMPOSE_UI("Compose UI", SampleComposeActivity::class.java), GALLERY("Load from Gallery", SampleGalleryActivity::class.java), CONTACTS("Contact Photos", SampleContactsActivity::class.java), LIST_DETAIL("List / Detail View", SampleListDetailActivity::class.java), SHOW_NOTIFICATION("Sample Notification", null) { override fun launch(activity: Activity) { val remoteViews = RemoteViews(activity.packageName, R.layout.notification_view) val intent = Intent(activity, SampleGridViewActivity::class.java) val flags = if (VERSION.SDK_INT >= VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 val notification = NotificationCompat.Builder(activity, CHANNEL_ID) .setSmallIcon(R.drawable.icon) .setContentIntent(PendingIntent.getActivity(activity, -1, intent, flags)) .setContent(remoteViews) .setAutoCancel(true) .setChannelId(CHANNEL_ID) .build() val notificationManager = NotificationManagerCompat.from(activity) val channel = NotificationChannelCompat .Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT) .setName("Picasso Notification Channel") notificationManager.createNotificationChannel(channel.build()) if (VERSION.SDK_INT >= TIRAMISU && checkSelfPermission(activity, POST_NOTIFICATIONS) != PERMISSION_GRANTED ) { requestPermissions(activity, arrayOf(POST_NOTIFICATIONS), 200) return } notificationManager.notify(NOTIFICATION_ID, notification) // Now load an image for this notification. PicassoInitializer.get() .load(Data.URLS[Random().nextInt(Data.URLS.size)]) .resizeDimen( R.dimen.notification_icon_width_height, R.dimen.notification_icon_width_height ) .into(remoteViews, R.id.photo, NOTIFICATION_ID, notification) } }; open fun launch(activity: Activity) { activity.startActivity(Intent(activity, activityClass)) activity.finish() } } private val inflater: LayoutInflater = LayoutInflater.from(context) override fun getCount(): Int = Sample.values().size override fun getItem(position: Int): Sample = Sample.values()[position] override fun getItemId(position: Int): Long = position.toLong() override fun getView( position: Int, convertView: View?, parent: ViewGroup ): View { val view = if (convertView == null) { inflater.inflate(R.layout.picasso_sample_activity_item, parent, false) as TextView } else { convertView as TextView } view.text = getItem(position).label return view } companion object { private const val NOTIFICATION_ID = 666 private const val CHANNEL_ID = "channel-id" } } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleComposeActivity.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.graphics.Bitmap import android.graphics.Bitmap.Config import android.graphics.Canvas import android.os.Bundle import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells.Adaptive import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import com.squareup.picasso3.Picasso import com.squareup.picasso3.Picasso.LoadedFrom.MEMORY import com.squareup.picasso3.Request import com.squareup.picasso3.RequestHandler import com.squareup.picasso3.compose.rememberPainter import kotlinx.coroutines.Dispatchers class SampleComposeActivity : PicassoSampleActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val composeView = ComposeView(this) val urls = Data.URLS.toMutableList().shuffled() + Data.URLS.toMutableList().shuffled() + Data.URLS.toMutableList().shuffled() composeView.setContent { Content(urls) } setContentView(composeView) } } @Composable fun Content(urls: List, picasso: Picasso = PicassoInitializer.get()) { var contentScale by remember { mutableStateOf(ContentScale.Inside) } var alignment by remember { mutableStateOf(Alignment.Center) } Column { ImageGrid( modifier = Modifier.weight(1F), urls = urls, contentScale = contentScale, alignment = alignment, picasso = picasso ) Options( modifier = Modifier .background(Color.DarkGray) .padding(vertical = 4.dp), onContentScaleSelected = { contentScale = it }, onAlignmentSelected = { alignment = it } ) } } @Composable fun ImageGrid( modifier: Modifier = Modifier, urls: List, contentScale: ContentScale, alignment: Alignment, picasso: Picasso = PicassoInitializer.get() ) { LazyVerticalGrid( columns = Adaptive(150.dp), modifier = modifier ) { items(urls.size) { val url = urls[it] Image( painter = picasso.rememberPainter(key = url) { it.load(url).placeholder(R.drawable.placeholder).error(R.drawable.error) }, contentDescription = null, contentScale = contentScale, alignment = alignment, modifier = Modifier .fillMaxWidth() .aspectRatio(1f) ) } } } @Composable fun Options( modifier: Modifier = Modifier, onContentScaleSelected: (ContentScale) -> Unit, onAlignmentSelected: (Alignment) -> Unit ) { var contentScaleKey by remember { mutableStateOf("Inside") } var alignmentKey by remember { mutableStateOf("Center") } Column(modifier = modifier) { CONTENT_SCALES.entries.chunked(4).forEach { entries -> Row( modifier = Modifier .padding(2.dp) .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { entries.forEach { (key, value) -> OptionText( modifier = Modifier.weight(1F), key = key, selected = contentScaleKey == key, onClick = { contentScaleKey = key onContentScaleSelected(value) } ) } } } Spacer(modifier = Modifier.height(8.dp)) ALIGNMENTS.entries.chunked(3).forEach { entries -> Row( modifier = Modifier .padding(2.dp) .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { entries.forEach { (key, value) -> OptionText( modifier = Modifier.weight(1F), key = key, selected = alignmentKey == key, onClick = { alignmentKey = key onAlignmentSelected(value) } ) } } } } } @Composable private fun OptionText(modifier: Modifier, key: String, selected: Boolean, onClick: () -> Unit) { Box(modifier = modifier) { BasicText( text = key, modifier = Modifier .align(Alignment.Center) .clip(RoundedCornerShape(8.dp)) .clickable(onClick = onClick) .background(if (selected) Color.Blue else Color.White) .padding(horizontal = 8.dp, vertical = 4.dp) ) } } private val CONTENT_SCALES = mapOf( Pair("Crop", ContentScale.Crop), Pair("Fit", ContentScale.Fit), Pair("Inside", ContentScale.Inside), Pair("Fill Width", ContentScale.FillWidth), Pair("Fill Height", ContentScale.FillHeight), Pair("Fill Bounds", ContentScale.FillBounds), Pair("None", ContentScale.None) ) private val ALIGNMENTS = mapOf( Pair("TopStart", Alignment.TopStart), Pair("TopCenter", Alignment.TopCenter), Pair("TopEnd", Alignment.TopEnd), Pair("CenterStart", Alignment.CenterStart), Pair("Center", Alignment.Center), Pair("CenterEnd", Alignment.CenterEnd), Pair("BottomStart", Alignment.BottomStart), Pair("BottomCenter", Alignment.BottomCenter), Pair("BottomEnd", Alignment.BottomEnd) ) @Preview @Composable private fun ContentPreview() { val images = listOf( Color.Blue.toArgb() to IntSize(200, 100), Color.Red.toArgb() to IntSize(100, 200), Color.Green.toArgb() to IntSize(100, 100), Color.Yellow.toArgb() to IntSize(300, 100), Color.Black.toArgb() to IntSize(100, 300), Color.LightGray.toArgb() to IntSize(400, 100), Color.Cyan.toArgb() to IntSize(100, 100), Color.White.toArgb() to IntSize(100, 400) ).associateBy { (color) -> "https://cash.app/$color.png" } val context = LocalContext.current Content( urls = images.keys.toList(), picasso = remember { Picasso.Builder(context) .callFactory { throw AssertionError() } // Removes network .dispatchers( mainContext = Dispatchers.Unconfined, backgroundContext = Dispatchers.Unconfined ) .addRequestHandler( object : RequestHandler() { override fun canHandleRequest(data: Request) = data.uri?.toString()?.run(images::containsKey) == true override fun load(picasso: Picasso, request: Request, callback: Callback) { val (color, size) = images[request.uri!!.toString()]!! val bitmap = Bitmap.createBitmap(size.width, size.height, Config.ARGB_8888).apply { Canvas(this).apply { drawColor(color) } } callback.onSuccess(Result.Bitmap(bitmap, MEMORY)) } } ) .build() } ) } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleContactsActivity.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.Manifest.permission.READ_CONTACTS import android.content.pm.PackageManager.PERMISSION_GRANTED import android.database.Cursor import android.net.Uri import android.os.Bundle import android.provider.ContactsContract.Contacts import android.widget.ListView import android.widget.Toast import androidx.core.app.ActivityCompat.checkSelfPermission import androidx.core.app.ActivityCompat.requestPermissions import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager.LoaderCallbacks import androidx.loader.content.CursorLoader import androidx.loader.content.Loader class SampleContactsActivity : PicassoSampleActivity(), LoaderCallbacks { private lateinit var adapter: SampleContactsAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.sample_contacts_activity) adapter = SampleContactsAdapter(this) findViewById(android.R.id.list).apply { adapter = this@SampleContactsActivity.adapter setOnScrollListener(SampleScrollListener(this@SampleContactsActivity)) } if (checkSelfPermission(this, READ_CONTACTS) == PERMISSION_GRANTED) { loadContacts() } else { requestPermissions(this, arrayOf(READ_CONTACTS), REQUEST_READ_CONTACTS) } } private fun loadContacts() { LoaderManager.getInstance(this).initLoader(ContactsQuery.QUERY_ID, null, this) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { if (requestCode == REQUEST_READ_CONTACTS) { if (grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) { loadContacts() } else { Toast .makeText(this, "Read contacts permission denied", Toast.LENGTH_LONG) .show() finish() } } else { super.onRequestPermissionsResult(requestCode, permissions, grantResults) } } override fun onCreateLoader( id: Int, args: Bundle? ): Loader { return if (id == ContactsQuery.QUERY_ID) { CursorLoader( this, ContactsQuery.CONTENT_URI, ContactsQuery.PROJECTION, ContactsQuery.SELECTION, null, ContactsQuery.SORT_ORDER ) } else { throw RuntimeException("this shouldn't happen") } } override fun onLoadFinished( loader: Loader, data: Cursor ) { adapter.swapCursor(data) } override fun onLoaderReset(loader: Loader) { adapter.swapCursor(null) } internal interface ContactsQuery { companion object { const val QUERY_ID = 1 val CONTENT_URI: Uri = Contacts.CONTENT_URI const val SELECTION = "${Contacts.DISPLAY_NAME_PRIMARY}<>'' AND ${Contacts.IN_VISIBLE_GROUP}=1" const val SORT_ORDER = Contacts.SORT_KEY_PRIMARY val PROJECTION = arrayOf( Contacts._ID, Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME_PRIMARY, Contacts.PHOTO_THUMBNAIL_URI, SORT_ORDER ) const val ID = 0 const val LOOKUP_KEY = 1 const val DISPLAY_NAME = 2 } } companion object { private const val REQUEST_READ_CONTACTS = 123 } } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleContactsAdapter.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.content.Context import android.database.Cursor import android.provider.ContactsContract.Contacts import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.QuickContactBadge import android.widget.TextView import androidx.cursoradapter.widget.CursorAdapter import com.example.picasso.SampleContactsActivity.ContactsQuery internal class SampleContactsAdapter(context: Context) : CursorAdapter(context, null, 0) { private val inflater = LayoutInflater.from(context) override fun newView( context: Context, cursor: Cursor, viewGroup: ViewGroup ): View { val itemLayout = inflater.inflate(R.layout.sample_contacts_activity_item, viewGroup, false) itemLayout.tag = ViewHolder( text1 = itemLayout.findViewById(android.R.id.text1), icon = itemLayout.findViewById(android.R.id.icon) ) return itemLayout } override fun bindView( view: View, context: Context, cursor: Cursor ) { val contactUri = Contacts.getLookupUri( cursor.getLong(ContactsQuery.ID), cursor.getString(ContactsQuery.LOOKUP_KEY) ) val holder = (view.tag as ViewHolder).apply { text1.text = cursor.getString(ContactsQuery.DISPLAY_NAME) icon.assignContactUri(contactUri) } PicassoInitializer.get() .load(contactUri) .placeholder(R.drawable.contact_picture_placeholder) .tag(context) .into(holder.icon) } override fun getCount(): Int { return if (cursor == null) 0 else super.getCount() } private class ViewHolder( val text1: TextView, val icon: QuickContactBadge ) } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleGalleryActivity.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.app.Activity import android.content.Intent import android.os.Bundle import android.provider.MediaStore.Images.Media import android.view.View import android.widget.ImageView import android.widget.ViewAnimator import com.squareup.picasso3.Callback.EmptyCallback class SampleGalleryActivity : PicassoSampleActivity() { private lateinit var imageView: ImageView lateinit var animator: ViewAnimator private var image: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.sample_gallery_activity) animator = findViewById(R.id.animator) imageView = findViewById(R.id.image) findViewById(R.id.go).setOnClickListener { val gallery = Intent(Intent.ACTION_PICK, Media.EXTERNAL_CONTENT_URI) startActivityForResult(gallery, GALLERY_REQUEST) } if (savedInstanceState != null) { image = savedInstanceState.getString(KEY_IMAGE) if (image != null) { loadImage() } } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(KEY_IMAGE, image) } override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ) { if (requestCode == GALLERY_REQUEST && resultCode == Activity.RESULT_OK && data != null) { image = data.data.toString() loadImage() } else { super.onActivityResult(requestCode, resultCode, data) } } private fun loadImage() { // Index 1 is the progress bar. Show it while we're loading the image. animator.displayedChild = 1 PicassoInitializer.get() .load(image) .fit() .centerInside() .into( imageView, object : EmptyCallback() { override fun onSuccess() { // Index 0 is the image view. animator.displayedChild = 0 } } ) } companion object { private const val GALLERY_REQUEST = 9391 private const val KEY_IMAGE = "com.example.picasso:image" } } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleGridViewActivity.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.os.Bundle import android.widget.GridView class SampleGridViewActivity : PicassoSampleActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.sample_gridview_activity) findViewById(R.id.grid_view).apply { adapter = SampleGridViewAdapter(this@SampleGridViewActivity) setOnScrollListener(SampleScrollListener(this@SampleGridViewActivity)) } } } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleGridViewAdapter.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.content.Context import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ImageView.ScaleType.CENTER_CROP internal class SampleGridViewAdapter(private val context: Context) : BaseAdapter() { private val urls: List init { // Ensure we get a different ordering of images on each run. val tmpList = Data.URLS.toMutableList() tmpList.shuffle() // Triple up the list. urls = listOf(tmpList, tmpList, tmpList).flatten() } override fun getView( position: Int, convertView: View?, parent: ViewGroup ): View { val view = convertView as? SquaredImageView ?: SquaredImageView(context).apply { scaleType = CENTER_CROP } // Get the image URL for the current position. val url = getItem(position) // Trigger the download of the URL asynchronously into the image view. PicassoInitializer.get() .load(url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .tag(context) .into(view) return view } override fun getCount(): Int = urls.size override fun getItem(position: Int): String = urls[position] override fun getItemId(position: Int): Long = position.toLong() } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleListDetailActivity.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView.OnItemClickListener import android.widget.ImageView import android.widget.ListView import android.widget.TextView import androidx.fragment.app.Fragment class SampleListDetailActivity : PicassoSampleActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { supportFragmentManager .beginTransaction() .add(R.id.sample_content, ListFragment.newInstance()) .commit() } } fun showDetails(url: String) { supportFragmentManager .beginTransaction() .replace(R.id.sample_content, DetailFragment.newInstance(url)) .addToBackStack(null) .commit() } class ListFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val activity = activity as SampleListDetailActivity val adapter = SampleListDetailAdapter(activity) val listView = LayoutInflater.from(activity) .inflate(R.layout.sample_list_detail_list, container, false) as ListView listView.adapter = adapter listView.setOnScrollListener(SampleScrollListener(activity)) listView.onItemClickListener = OnItemClickListener { _, _, position, _ -> val url = adapter.getItem(position) activity.showDetails(url) } return listView } companion object { fun newInstance(): ListFragment { return ListFragment() } } } class DetailFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val activity = activity as SampleListDetailActivity val view = LayoutInflater.from(activity) .inflate(R.layout.sample_list_detail_detail, container, false) val urlView = view.findViewById(R.id.url) val imageView = view.findViewById(R.id.photo) val url = requireArguments().getString(KEY_URL) urlView.text = url PicassoInitializer.get() .load(url) .fit() .tag(activity) .into(imageView) return view } companion object { private const val KEY_URL = "picasso:url" fun newInstance(url: String): DetailFragment { return DetailFragment().apply { arguments = Bundle().apply { putString(KEY_URL, url) } } } } } } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleListDetailAdapter.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ImageView import android.widget.TextView internal class SampleListDetailAdapter(private val context: Context) : BaseAdapter() { private val layoutInflater = LayoutInflater.from(context) private val urls = Data.URLS.toList() override fun getView( position: Int, view: View?, parent: ViewGroup ): View { val newView: View val holder: ViewHolder if (view == null) { newView = layoutInflater.inflate(R.layout.sample_list_detail_item, parent, false) holder = ViewHolder( image = newView.findViewById(R.id.photo), text = newView.findViewById(R.id.url) ) newView.tag = holder } else { newView = view holder = newView.tag as ViewHolder } // Get the image URL for the current position. val url = getItem(position) holder.text.text = url // Trigger the download of the URL asynchronously into the image view. PicassoInitializer.get() .load(url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .resizeDimen(R.dimen.list_detail_image_size, R.dimen.list_detail_image_size) .centerInside() .tag(context) .into(holder.image) return newView } override fun getCount(): Int = urls.size override fun getItem(position: Int): String = urls[position] override fun getItemId(position: Int): Long = position.toLong() internal class ViewHolder( val image: ImageView, val text: TextView ) } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleScrollListener.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.content.Context import android.widget.AbsListView import android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE import android.widget.AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL class SampleScrollListener(private val context: Context) : AbsListView.OnScrollListener { override fun onScrollStateChanged( view: AbsListView, scrollState: Int ) { val picasso = PicassoInitializer.get() when (scrollState) { SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL -> picasso.resumeTag(context) else -> picasso.pauseTag(context) } } override fun onScroll( view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int ) = Unit } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SampleWidgetProvider.kt ================================================ /* * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.Context import android.widget.RemoteViews import java.util.Random class SampleWidgetProvider : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { val updateViews = RemoteViews(context.packageName, R.layout.sample_widget) // Load image for all appWidgetIds. val picasso = PicassoInitializer.get() picasso.load(Data.URLS[Random().nextInt(Data.URLS.size)]) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .transform(GrayscaleTransformation(picasso)) .into(updateViews, R.id.image, appWidgetIds) } } ================================================ FILE: picasso-sample/src/main/java/com/example/picasso/SquaredImageView.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.picasso import android.content.Context import android.util.AttributeSet import android.widget.ImageView /** An image view which always remains square with respect to its width. */ class SquaredImageView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : ImageView(context, attrs) { override fun onMeasure( widthMeasureSpec: Int, heightMeasureSpec: Int ) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) setMeasuredDimension(measuredWidth, measuredWidth) } } ================================================ FILE: picasso-sample/src/main/res/drawable/button_selector.xml ================================================ ================================================ FILE: picasso-sample/src/main/res/drawable/list_selector.xml ================================================ ================================================ FILE: picasso-sample/src/main/res/drawable/overlay_selector.xml ================================================ ================================================ FILE: picasso-sample/src/main/res/layout/notification_view.xml ================================================ ================================================ FILE: picasso-sample/src/main/res/layout/picasso_sample_activity.xml ================================================ ================================================ FILE: picasso-sample/src/main/res/layout/picasso_sample_activity_item.xml ================================================ ================================================ FILE: picasso-sample/src/main/res/layout/sample_contacts_activity.xml ================================================ ================================================ FILE: picasso-sample/src/main/res/layout/sample_contacts_activity_item.xml ================================================ ================================================ FILE: picasso-sample/src/main/res/layout/sample_gallery_activity.xml ================================================