From a690769e11dc187df602435f3c2aa8f38ea5077c Mon Sep 17 00:00:00 2001 From: Ibrahim Ulukaya Date: Wed, 15 Oct 2014 11:35:59 -0400 Subject: [PATCH] Move project to Android Studio --- .classpath | 9 + .gitignore | 6 + .idea/.name | 1 + .idea/compiler.xml | 23 + .idea/copyright/profiles_settings.xml | 3 + .idea/encodings.xml | 5 + .idea/gradle.xml | 18 + .idea/misc.xml | 26 + .idea/modules.xml | 10 + .idea/scopes/scope_settings.xml | 5 + .idea/vcs.xml | 7 + CONTRIBUTING.md | 82 ++ LICENSE-2.0.txt | 202 ++++ README.md | 26 + YouTubeDirectLiteforAndroid.iml | 19 + app/.gitignore | 1 + app/app.iml | 103 ++ app/build.gradle | 32 + app/libs/YouTubeAndroidPlayerApi.jar | Bin 0 -> 106239 bytes app/proguard-rules.pro | 17 + .../java/com/google/ytdl/ApplicationTest.java | 13 + app/src/main/AndroidManifest.xml | 55 + app/src/main/java/com/google/ytdl/Auth.java | 25 + .../main/java/com/google/ytdl/Constants.java | 29 + .../java/com/google/ytdl/MainActivity.java | 644 ++++++++++++ .../java/com/google/ytdl/PlayActivity.java | 205 ++++ .../java/com/google/ytdl/ResumableUpload.java | 276 +++++ .../java/com/google/ytdl/ReviewActivity.java | 97 ++ .../java/com/google/ytdl/UploadService.java | 191 ++++ .../com/google/ytdl/UploadsListFragment.java | 232 +++++ .../java/com/google/ytdl/util/AsyncTask.java | 671 ++++++++++++ .../com/google/ytdl/util/DiskLruCache.java | 967 ++++++++++++++++++ .../java/com/google/ytdl/util/ImageCache.java | 681 ++++++++++++ .../com/google/ytdl/util/ImageFetcher.java | 311 ++++++ .../com/google/ytdl/util/ImageResizer.java | 265 +++++ .../com/google/ytdl/util/ImageWorker.java | 474 +++++++++ .../ytdl/util/RecyclingBitmapDrawable.java | 102 ++ .../java/com/google/ytdl/util/Upload.java | 33 + .../main/java/com/google/ytdl/util/Utils.java | 118 +++ .../java/com/google/ytdl/util/VideoData.java | 65 ++ .../main/res/drawable-hdpi/ic_av_upload.png | Bin 0 -> 907 bytes .../res/drawable-hdpi/ic_content_picture.png | Bin 0 -> 917 bytes .../drawable-hdpi/ic_device_access_video.png | Bin 0 -> 749 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 3375 bytes .../ic_stat_device_access_video.png | Bin 0 -> 446 bytes .../main/res/drawable-mdpi/ic_av_upload.png | Bin 0 -> 610 bytes .../res/drawable-mdpi/ic_content_picture.png | Bin 0 -> 623 bytes .../drawable-mdpi/ic_device_access_video.png | Bin 0 -> 514 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2089 bytes .../drawable-mdpi/ic_mailboxes_accounts.png | Bin 0 -> 2099 bytes .../res/drawable-mdpi/ic_menu_refresh.png | Bin 0 -> 2093 bytes .../ic_stat_device_access_video.png | Bin 0 -> 327 bytes .../main/res/drawable-xhdpi/ic_av_upload.png | Bin 0 -> 1269 bytes .../res/drawable-xhdpi/ic_content_picture.png | Bin 0 -> 1278 bytes .../drawable-xhdpi/ic_device_access_video.png | Bin 0 -> 1007 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 4686 bytes .../res/drawable-xhdpi/ic_menu_refresh.png | Bin 0 -> 6524 bytes .../ic_stat_device_access_video.png | Bin 0 -> 561 bytes .../drawable-xhdpi/list_divider_holo_dark.png | Bin 0 -> 158 bytes .../main/res/drawable-xxhdpi/ic_av_upload.png | Bin 0 -> 2109 bytes .../drawable-xxhdpi/ic_content_picture.png | Bin 0 -> 2231 bytes .../ic_device_access_video.png | Bin 0 -> 1939 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 8072 bytes .../ic_stat_device_access_video.png | Bin 0 -> 834 bytes .../list_divider_horizontal_inset.xml | 22 + app/src/main/res/layout/activity_main.xml | 61 ++ app/src/main/res/layout/activity_play.xml | 47 + app/src/main/res/layout/activity_review.xml | 49 + .../res/layout/developer_setup_required.xml | 23 + app/src/main/res/layout/list_fragment.xml | 87 ++ app/src/main/res/layout/list_item.xml | 53 + app/src/main/res/menu/activity_main.xml | 10 + app/src/main/res/menu/play.xml | 9 + app/src/main/res/menu/review.xml | 9 + app/src/main/res/values/dimens.xml | 12 + app/src/main/res/values/strings.xml | 48 + app/src/main/res/values/styles.xml | 20 + build.gradle | 19 + gradle.properties | 18 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++ gradlew.bat | 90 ++ settings.gradle | 1 + 84 files changed, 6797 insertions(+) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/scopes/scope_settings.xml create mode 100644 .idea/vcs.xml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE-2.0.txt create mode 100644 README.md create mode 100644 YouTubeDirectLiteforAndroid.iml create mode 100644 app/.gitignore create mode 100644 app/app.iml create mode 100644 app/build.gradle create mode 100644 app/libs/YouTubeAndroidPlayerApi.jar create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/google/ytdl/ApplicationTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/google/ytdl/Auth.java create mode 100644 app/src/main/java/com/google/ytdl/Constants.java create mode 100644 app/src/main/java/com/google/ytdl/MainActivity.java create mode 100644 app/src/main/java/com/google/ytdl/PlayActivity.java create mode 100644 app/src/main/java/com/google/ytdl/ResumableUpload.java create mode 100644 app/src/main/java/com/google/ytdl/ReviewActivity.java create mode 100644 app/src/main/java/com/google/ytdl/UploadService.java create mode 100644 app/src/main/java/com/google/ytdl/UploadsListFragment.java create mode 100644 app/src/main/java/com/google/ytdl/util/AsyncTask.java create mode 100644 app/src/main/java/com/google/ytdl/util/DiskLruCache.java create mode 100644 app/src/main/java/com/google/ytdl/util/ImageCache.java create mode 100644 app/src/main/java/com/google/ytdl/util/ImageFetcher.java create mode 100644 app/src/main/java/com/google/ytdl/util/ImageResizer.java create mode 100644 app/src/main/java/com/google/ytdl/util/ImageWorker.java create mode 100644 app/src/main/java/com/google/ytdl/util/RecyclingBitmapDrawable.java create mode 100644 app/src/main/java/com/google/ytdl/util/Upload.java create mode 100644 app/src/main/java/com/google/ytdl/util/Utils.java create mode 100644 app/src/main/java/com/google/ytdl/util/VideoData.java create mode 100644 app/src/main/res/drawable-hdpi/ic_av_upload.png create mode 100644 app/src/main/res/drawable-hdpi/ic_content_picture.png create mode 100644 app/src/main/res/drawable-hdpi/ic_device_access_video.png create mode 100644 app/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-hdpi/ic_stat_device_access_video.png create mode 100644 app/src/main/res/drawable-mdpi/ic_av_upload.png create mode 100644 app/src/main/res/drawable-mdpi/ic_content_picture.png create mode 100644 app/src/main/res/drawable-mdpi/ic_device_access_video.png create mode 100644 app/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-mdpi/ic_mailboxes_accounts.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_refresh.png create mode 100644 app/src/main/res/drawable-mdpi/ic_stat_device_access_video.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_av_upload.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_content_picture.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_device_access_video.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_refresh.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_device_access_video.png create mode 100644 app/src/main/res/drawable-xhdpi/list_divider_holo_dark.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_av_upload.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_content_picture.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_device_access_video.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_stat_device_access_video.png create mode 100644 app/src/main/res/drawable/list_divider_horizontal_inset.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_play.xml create mode 100644 app/src/main/res/layout/activity_review.xml create mode 100644 app/src/main/res/layout/developer_setup_required.xml create mode 100644 app/src/main/res/layout/list_fragment.xml create mode 100644 app/src/main/res/layout/list_item.xml create mode 100644 app/src/main/res/menu/activity_main.xml create mode 100644 app/src/main/res/menu/play.xml create mode 100644 app/src/main/res/menu/review.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..7bc01d9 --- /dev/null +++ b/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..afbdab3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..3c27d33 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +YouTube Direct Lite for Android \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..217af47 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..736c7b5 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8d0c7fa --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + 1.7 + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9330c6f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c80f219 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..812e19e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,82 @@ +# How to contribute # + +We'd love to accept your patches and contributions to this project. There are +a just a few small guidelines you need to follow. + + +## Contributor License Agreement ## + +Contributions to any Google project must be accompanied by a Contributor +License Agreement. This is not a copyright **assignment**, it simply gives +Google permission to use and redistribute your contributions as part of the +project. + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual + CLA][]. + + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA][]. + +You generally only need to submit a CLA once, so if you've already submitted +one (even if it was for a different project), you probably don't need to do it +again. + +[individual CLA]: https://developers.google.com/open-source/cla/individual +[corporate CLA]: https://developers.google.com/open-source/cla/corporate + + +## Submitting a patch ## + + 1. It's generally best to start by opening a new issue describing the bug or + feature you're intending to fix. Even if you think it's relatively minor, + it's helpful to know what people are working on. Mention in the initial + issue that you are planning to work on that bug or feature so that it can + be assigned to you. + + 1. Follow the normal process of [forking][] the project, and setup a new + branch to work in. It's important that each group of changes be done in + separate branches in order to ensure that a pull request only includes the + commits related to that bug or feature. + + 1. Go makes it very simple to ensure properly formatted code, so always run + `go fmt` on your code before committing it. You should also run + [golint][] over your code. As noted in the [golint readme][], it's not + strictly necessary that your code be completely "lint-free", but this will + help you find common style issues. + + 1. Any significant changes should almost always be accompanied by tests. The + project already has good test coverage, so look at some of the existing + tests if you're unsure how to go about it. [gocov][] and [gocov-html][] + are invaluable tools for seeing which parts of your code aren't being + exercised by your tests. + + 1. Do your best to have [well-formed commit messages][] for each change. + This provides consistency throughout the project, and ensures that commit + messages are able to be formatted properly by various git tools. + + 1. Finally, push the commits to your fork and submit a [pull request][]. + +[forking]: https://help.github.com/articles/fork-a-repo +[golint]: https://github.com/golang/lint +[golint readme]: https://github.com/golang/lint/blob/master/README +[gocov]: https://github.com/axw/gocov +[gocov-html]: https://github.com/matm/gocov-html +[well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html +[squash]: http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits +[pull request]: https://help.github.com/articles/creating-a-pull-request + + +## Other notes on code organization ## + +Currently, everything is defined in the main `github` package, with API methods +broken into separate service objects. These services map directly to how +the [GitHub API documentation][] is organized, so use that as your guide for +where to put new methods. + +Sub-service (e.g. [Repo Hooks][]) implementations are split into separate files +based on the APIs they provide. These files are named service_api.go (e.g. +repos_hooks.go) to describe the API to service mappings. + +[GitHub API documentation]: http://developer.github.com/v3/ +[Repo Hooks]: http://developer.github.com/v3/repos/hooks/ diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..58394c0 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +YouTube Direct Lite for Android +=========== + +The code is a reference implementation for an Android OS application that captures video, uploads it to YouTube, and submits the video to a [YouTube Direct Lite](http://code.google.com/p/youtube-direct-lite/) instance. + +For more information, you can read the [Youtube API blog post](http://apiblog.youtube.com/2013/08/heres-my-playlist-so-submit-video-maybe.html). + +This application utilizes [YouTube Data API v3](https://developers.google.com/youtube/v3/) , [YouTube Android Player API](https://developers.google.com/youtube/android/player/), [YouTube Resumable Uploads](https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol?hl=en), [Google Play Services](https://developer.android.com/google/play-services/index.html) and [Plus API](https://developers.google.com/+/mobile/android/Google). + +To use this application, + +1) [Register your Android app](https://developers.google.com/youtube/android/player/register) + +2) Enable the YouTube Data API v3 and Google+ API in your [API Console](https://code.google.com/apis/console/). + +3) Include the [Google Play Services library](http://developer.android.com/google/play-services/setup.html) in your project to build this application. + +4) Plug in your Playlist Id into Contants.java and Android API Key into Auth.java + +![alt tag](https://ytd-android.googlecode.com/files/YTDL.png) + +![alt tag](https://ytd-android.googlecode.com/files/YTDL-review.png) + +![alt tag](https://ytd-android.googlecode.com/files/YTDL-upload.png) + +![alt tag](https://ytd-android.googlecode.com/files/YTDL-watch.png) diff --git a/YouTubeDirectLiteforAndroid.iml b/YouTubeDirectLiteforAndroid.iml new file mode 100644 index 0000000..0bb6048 --- /dev/null +++ b/YouTubeDirectLiteforAndroid.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..212df4e --- /dev/null +++ b/app/app.iml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..a0dce10 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 19 + buildToolsVersion "19.1.0" + + defaultConfig { + applicationId "com.google.ytdl" + minSdkVersion 16 + targetSdkVersion 19 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.google.android.gms:play-services:+' + compile 'com.android.support:support-v13:20.0.0' + compile 'com.google.apis:google-api-services-youtube:v3-rev120-1.19.0' + compile 'com.google.http-client:google-http-client-android:+' + compile 'com.google.api-client:google-api-client-android:+' + compile 'com.google.api-client:google-api-client-gson:+' + compile 'com.google.code.gson:gson:2.2.4' + compile files('libs/YouTubeAndroidPlayerApi.jar') +} diff --git a/app/libs/YouTubeAndroidPlayerApi.jar b/app/libs/YouTubeAndroidPlayerApi.jar new file mode 100644 index 0000000000000000000000000000000000000000..c7825a757161dc7735d028d2fcafa3b79e9e3899 GIT binary patch literal 106239 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 literal 0 HcmV?d00001 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..6df0d69 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/ulukaya/android-sdks/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/com/google/ytdl/ApplicationTest.java b/app/src/androidTest/java/com/google/ytdl/ApplicationTest.java new file mode 100644 index 0000000..09cc036 --- /dev/null +++ b/app/src/androidTest/java/com/google/ytdl/ApplicationTest.java @@ -0,0 +1,13 @@ +package com.google.ytdl; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a29ddbe --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/google/ytdl/Auth.java b/app/src/main/java/com/google/ytdl/Auth.java new file mode 100644 index 0000000..b1eda24 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/Auth.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl; + +import com.google.android.gms.common.Scopes; +import com.google.api.services.youtube.YouTubeScopes; + +public class Auth { + // Register an API key here: https://code.google.com/apis/console + public static final String KEY = "AIzaSyAEks36Xz1EuzVozpF-bP4gUuZoUi9akbE"; + + public static final String[] SCOPES = {Scopes.PROFILE, YouTubeScopes.YOUTUBE}; +} diff --git a/app/src/main/java/com/google/ytdl/Constants.java b/app/src/main/java/com/google/ytdl/Constants.java new file mode 100644 index 0000000..8466244 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/Constants.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl; + +/** + * @author Ibrahim Ulukaya + *

+ * This class hold constants. + */ +public class Constants { + public static final int MAX_KEYWORD_LENGTH = 30; + public static final String DEFAULT_KEYWORD = "ytdl"; + // A playlist ID is a string that begins with PL. You must replace this string with the correct + // playlist ID for the app to work + public static final String UPLOAD_PLAYLIST = "PLpZ720-WRTJz-LFGDm1yzAoClUE6795ft"; + public static final String APP_NAME = "ytd-android"; +} diff --git a/app/src/main/java/com/google/ytdl/MainActivity.java b/app/src/main/java/com/google/ytdl/MainActivity.java new file mode 100644 index 0000000..62d1cbf --- /dev/null +++ b/app/src/main/java/com/google/ytdl/MainActivity.java @@ -0,0 +1,644 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl; + +import android.accounts.AccountManager; +import android.app.Activity; +import android.app.Dialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.provider.MediaStore; +import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.ArrayAdapter; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.api.client.extensions.android.http.AndroidHttp; +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; +import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException; +import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.ChannelListResponse; +import com.google.api.services.youtube.model.PlaylistItem; +import com.google.api.services.youtube.model.PlaylistItemListResponse; +import com.google.api.services.youtube.model.Video; +import com.google.api.services.youtube.model.VideoListResponse; +import com.google.api.services.youtube.model.VideoSnippet; +import com.google.ytdl.util.ImageFetcher; +import com.google.ytdl.util.Upload; +import com.google.ytdl.util.Utils; +import com.google.ytdl.util.VideoData; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * @author Ibrahim Ulukaya + *

+ * Main activity class which handles authorization and intents. + */ +public class MainActivity extends Activity implements + UploadsListFragment.Callbacks { + // private static final int MEDIA_TYPE_VIDEO = 7; + public static final String ACCOUNT_KEY = "accountName"; + public static final String MESSAGE_KEY = "message"; + public static final String YOUTUBE_ID = "youtubeId"; + public static final String YOUTUBE_WATCH_URL_PREFIX = "http://www.youtube.com/watch?v="; + static final String REQUEST_AUTHORIZATION_INTENT = "com.google.example.yt.RequestAuth"; + static final String REQUEST_AUTHORIZATION_INTENT_PARAM = "com.google.example.yt.RequestAuth.param"; + private static final int REQUEST_GOOGLE_PLAY_SERVICES = 0; + private static final int REQUEST_GMS_ERROR_DIALOG = 1; + private static final int REQUEST_ACCOUNT_PICKER = 2; + private static final int REQUEST_AUTHORIZATION = 3; + private static final int RESULT_PICK_IMAGE_CROP = 4; + private static final int RESULT_VIDEO_CAP = 5; + private static final int REQUEST_DIRECT_TAG = 6; + private static final String TAG = "MainActivity"; + final HttpTransport transport = AndroidHttp.newCompatibleTransport(); + final JsonFactory jsonFactory = new GsonFactory(); + GoogleAccountCredential credential; + private ImageFetcher mImageFetcher; + private String mChosenAccountName; + private Uri mFileURI = null; + private VideoData mVideoData; + private UploadBroadcastReceiver broadcastReceiver; + private UploadsListFragment mUploadsListFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + super.onCreate(savedInstanceState); + + // Check to see if the proper keys and playlist IDs have been set up + if (!isCorrectlyConfigured()) { + setContentView(R.layout.developer_setup_required); + showMissingConfigurations(); + } else { + setContentView(R.layout.activity_main); + + ensureFetcher(); + + credential = GoogleAccountCredential.usingOAuth2( + getApplicationContext(), Arrays.asList(Auth.SCOPES)); + // set exponential backoff policy + credential.setBackOff(new ExponentialBackOff()); + + if (savedInstanceState != null) { + mChosenAccountName = savedInstanceState.getString(ACCOUNT_KEY); + } else { + loadAccount(); + } + + credential.setSelectedAccountName(mChosenAccountName); + + mUploadsListFragment = (UploadsListFragment) getFragmentManager() + .findFragmentById(R.id.list_fragment); + } + } + + /** + * This method checks various internal states to figure out at startup time + * whether certain elements have been configured correctly by the developer. + * Checks that: + *

    + *
  • the API key has been configured
  • + *
  • the playlist ID has been configured
  • + *
+ * + * @return true if the application is correctly configured for use, false if + * not + */ + private boolean isCorrectlyConfigured() { + // This isn't going to internationalize well, but we only really need + // this for the sample app. + // Real applications will remove this section of code and ensure that + // all of these values are configured. + if (Auth.KEY.startsWith("Replace")) { + return false; + } + if (Constants.UPLOAD_PLAYLIST.startsWith("Replace")) { + return false; + } + return true; + } + + /** + * This method renders the ListView explaining what the configurations the + * developer of this application has to complete. Typically, these are + * static variables defined in {@link Auth} and {@link Constants}. + */ + private void showMissingConfigurations() { + List missingConfigs = new ArrayList(); + + // Make sure an API key is registered + if (Auth.KEY.startsWith("Replace")) { + missingConfigs + .add(new MissingConfig( + "API key not configured", + "KEY constant in Auth.java must be configured with your Simple API key from the Google API Console")); + } + + // Make sure a playlist ID is registered + if (Constants.UPLOAD_PLAYLIST.startsWith("Replace")) { + missingConfigs + .add(new MissingConfig( + "Playlist ID not configured", + "UPLOAD_PLAYLIST constant in Constants.java must be configured with a Playlist ID to submit to. (The playlist ID typically has a prexix of PL)")); + } + + // Renders a simple_list_item_2, which consists of a title and a body + // element + ListAdapter adapter = new ArrayAdapter(this, + android.R.layout.simple_list_item_2, missingConfigs) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row; + if (convertView == null) { + LayoutInflater inflater = (LayoutInflater) getApplicationContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + row = inflater.inflate(android.R.layout.simple_list_item_2, + null); + } else { + row = convertView; + } + + TextView titleView = (TextView) row + .findViewById(android.R.id.text1); + TextView bodyView = (TextView) row + .findViewById(android.R.id.text2); + MissingConfig config = getItem(position); + titleView.setText(config.title); + bodyView.setText(config.body); + return row; + } + }; + + // Wire the data adapter up to the view + ListView missingConfigList = (ListView) findViewById(R.id.missing_config_list); + missingConfigList.setAdapter(adapter); + } + + @Override + protected void onResume() { + super.onResume(); + if (broadcastReceiver == null) + broadcastReceiver = new UploadBroadcastReceiver(); + IntentFilter intentFilter = new IntentFilter( + REQUEST_AUTHORIZATION_INTENT); + LocalBroadcastManager.getInstance(this).registerReceiver( + broadcastReceiver, intentFilter); + } + + private void ensureFetcher() { + if (mImageFetcher == null) { + mImageFetcher = new ImageFetcher(this, 512, 512); + mImageFetcher.addImageCache(getFragmentManager(), + new com.google.ytdl.util.ImageCache.ImageCacheParams(this, + "cache")); + } + } + + private void loadAccount() { + SharedPreferences sp = PreferenceManager + .getDefaultSharedPreferences(this); + mChosenAccountName = sp.getString(ACCOUNT_KEY, null); + invalidateOptionsMenu(); + } + + private void saveAccount() { + SharedPreferences sp = PreferenceManager + .getDefaultSharedPreferences(this); + sp.edit().putString(ACCOUNT_KEY, mChosenAccountName).commit(); + } + + private void loadData() { + if (mChosenAccountName == null) { + return; + } + + loadUploadedVideos(); + } + + @Override + protected void onPause() { + super.onPause(); + if (broadcastReceiver != null) { + LocalBroadcastManager.getInstance(this).unregisterReceiver( + broadcastReceiver); + } + if (isFinishing()) { + // mHandler.removeCallbacksAndMessages(null); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.activity_main, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_refresh: + loadData(); + break; + case R.id.menu_accounts: + chooseAccount(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + switch (requestCode) { + case REQUEST_GMS_ERROR_DIALOG: + break; + case RESULT_PICK_IMAGE_CROP: + if (resultCode == RESULT_OK) { + mFileURI = data.getData(); + if (mFileURI != null) { + Intent intent = new Intent(this, ReviewActivity.class); + intent.setData(mFileURI); + startActivity(intent); + } + } + break; + + case RESULT_VIDEO_CAP: + if (resultCode == RESULT_OK) { + mFileURI = data.getData(); + if (mFileURI != null) { + Intent intent = new Intent(this, ReviewActivity.class); + intent.setData(mFileURI); + startActivity(intent); + } + } + break; + case REQUEST_GOOGLE_PLAY_SERVICES: + if (resultCode == Activity.RESULT_OK) { + haveGooglePlayServices(); + } else { + checkGooglePlayServicesAvailable(); + } + break; + case REQUEST_AUTHORIZATION: + if (resultCode != Activity.RESULT_OK) { + chooseAccount(); + } + break; + case REQUEST_ACCOUNT_PICKER: + if (resultCode == Activity.RESULT_OK && data != null + && data.getExtras() != null) { + String accountName = data.getExtras().getString( + AccountManager.KEY_ACCOUNT_NAME); + if (accountName != null) { + mChosenAccountName = accountName; + credential.setSelectedAccountName(accountName); + saveAccount(); + } + } + break; + case REQUEST_DIRECT_TAG: + if (resultCode == Activity.RESULT_OK && data != null + && data.getExtras() != null) { + String youtubeId = data.getStringExtra(YOUTUBE_ID); + if (youtubeId.equals(mVideoData.getYouTubeId())) { + directTag(mVideoData); + } + } + break; + } + } + + private void directTag(final VideoData video) { + final Video updateVideo = new Video(); + VideoSnippet snippet = video + .addTags(Arrays.asList( + Constants.DEFAULT_KEYWORD, + Upload.generateKeywordFromPlaylistId(Constants.UPLOAD_PLAYLIST))); + updateVideo.setSnippet(snippet); + updateVideo.setId(video.getYouTubeId()); + + new AsyncTask() { + @Override + protected Void doInBackground(Void... voids) { + + YouTube youtube = new YouTube.Builder(transport, jsonFactory, + credential).setApplicationName(Constants.APP_NAME) + .build(); + try { + youtube.videos().update("snippet", updateVideo).execute(); + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + } + return null; + } + + }.execute((Void) null); + Toast.makeText(this, + R.string.video_submitted_to_ytdl, Toast.LENGTH_LONG) + .show(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(ACCOUNT_KEY, mChosenAccountName); + } + + private void loadUploadedVideos() { + if (mChosenAccountName == null) { + return; + } + + setProgressBarIndeterminateVisibility(true); + new AsyncTask>() { + @Override + protected List doInBackground(Void... voids) { + + YouTube youtube = new YouTube.Builder(transport, jsonFactory, + credential).setApplicationName(Constants.APP_NAME) + .build(); + + try { + /* + * Now that the user is authenticated, the app makes a + * channels list request to get the authenticated user's + * channel. Returned with that data is the playlist id for + * the uploaded videos. + * https://developers.google.com/youtube + * /v3/docs/channels/list + */ + ChannelListResponse clr = youtube.channels() + .list("contentDetails").setMine(true).execute(); + + // Get the user's uploads playlist's id from channel list + // response + String uploadsPlaylistId = clr.getItems().get(0) + .getContentDetails().getRelatedPlaylists() + .getUploads(); + + List videos = new ArrayList(); + + // Get videos from user's upload playlist with a playlist + // items list request + PlaylistItemListResponse pilr = youtube.playlistItems() + .list("id,contentDetails") + .setPlaylistId(uploadsPlaylistId) + .setMaxResults(20l).execute(); + List videoIds = new ArrayList(); + + // Iterate over playlist item list response to get uploaded + // videos' ids. + for (PlaylistItem item : pilr.getItems()) { + videoIds.add(item.getContentDetails().getVideoId()); + } + + // Get details of uploaded videos with a videos list + // request. + VideoListResponse vlr = youtube.videos() + .list("id,snippet,status") + .setId(TextUtils.join(",", videoIds)).execute(); + + // Add only the public videos to the local videos list. + for (Video video : vlr.getItems()) { + if ("public".equals(video.getStatus() + .getPrivacyStatus())) { + VideoData videoData = new VideoData(); + videoData.setVideo(video); + videos.add(videoData); + } + } + + // Sort videos by title + Collections.sort(videos, new Comparator() { + @Override + public int compare(VideoData videoData, + VideoData videoData2) { + return videoData.getTitle().compareTo( + videoData2.getTitle()); + } + }); + + return videos; + + } catch (final GooglePlayServicesAvailabilityIOException availabilityException) { + showGooglePlayServicesAvailabilityErrorDialog(availabilityException + .getConnectionStatusCode()); + } catch (UserRecoverableAuthIOException userRecoverableException) { + startActivityForResult( + userRecoverableException.getIntent(), + REQUEST_AUTHORIZATION); + } catch (IOException e) { + Utils.logAndShow(MainActivity.this, Constants.APP_NAME, e); + } + return null; + } + + @Override + protected void onPostExecute(List videos) { + setProgressBarIndeterminateVisibility(false); + + if (videos == null) { + return; + } + + mUploadsListFragment.setVideos(videos); + } + + }.execute((Void) null); + } + + @Override + public void onBackPressed() { + // if (mDirectFragment.popPlayerFromBackStack()) { + // super.onBackPressed(); + // } + } + + @Override + public ImageFetcher onGetImageFetcher() { + ensureFetcher(); + return mImageFetcher; + } + + @Override + public void onVideoSelected(VideoData video) { + mVideoData = video; + Intent intent = new Intent(this, PlayActivity.class); + intent.putExtra(YOUTUBE_ID, video.getYouTubeId()); + startActivityForResult(intent, REQUEST_DIRECT_TAG); + } + + @Override + public void onConnected(String connectedAccountName) { + // Make API requests only when the user has successfully signed in. + loadData(); + } + + public void pickFile(View view) { + Intent intent = new Intent(Intent.ACTION_PICK); + intent.setType("video/*"); + startActivityForResult(intent, RESULT_PICK_IMAGE_CROP); + } + + public void recordVideo(View view) { + Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + + // Workaround for Nexus 7 Android 4.3 Intent Returning Null problem + // create a file to save the video in specific folder (this works for + // video only) + // mFileURI = getOutputMediaFile(MEDIA_TYPE_VIDEO); + // intent.putExtra(MediaStore.EXTRA_OUTPUT, mFileURI); + + // set the video image quality to high + intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); + + // start the Video Capture Intent + startActivityForResult(intent, RESULT_VIDEO_CAP); + } + + public void showGooglePlayServicesAvailabilityErrorDialog( + final int connectionStatusCode) { + runOnUiThread(new Runnable() { + public void run() { + Dialog dialog = GooglePlayServicesUtil.getErrorDialog( + connectionStatusCode, MainActivity.this, + REQUEST_GOOGLE_PLAY_SERVICES); + dialog.show(); + } + }); + } + + /** + * Check that Google Play services APK is installed and up to date. + */ + private boolean checkGooglePlayServicesAvailable() { + final int connectionStatusCode = GooglePlayServicesUtil + .isGooglePlayServicesAvailable(this); + if (GooglePlayServicesUtil.isUserRecoverableError(connectionStatusCode)) { + showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode); + return false; + } + return true; + } + + private void haveGooglePlayServices() { + // check if there is already an account selected + if (credential.getSelectedAccountName() == null) { + // ask user to choose account + chooseAccount(); + } + } + + private void chooseAccount() { + startActivityForResult(credential.newChooseAccountIntent(), + REQUEST_ACCOUNT_PICKER); + } + + /** + * Private class representing a missing configuration and what the developer + * can do to fix the issue. + */ + private class MissingConfig { + + public final String title; + public final String body; + + public MissingConfig(String title, String body) { + this.title = title; + this.body = body; + } + } + + // public Uri getOutputMediaFile(int type) + // { + // // To be safe, you should check that the SDCard is mounted + // if(Environment.getExternalStorageState() != null) { + // // this works for Android 2.2 and above + // File mediaStorageDir = new + // File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), + // "SMW_VIDEO"); + // + // // This location works best if you want the created images to be shared + // // between applications and persist after your app has been uninstalled. + // + // // Create the storage directory if it does not exist + // if (! mediaStorageDir.exists()) { + // if (! mediaStorageDir.mkdirs()) { + // Log.d(TAG, "failed to create directory"); + // return null; + // } + // } + // + // // Create a media file name + // String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", + // Locale.getDefault()).format(new Date()); + // File mediaFile; + // if(type == MEDIA_TYPE_VIDEO) { + // mediaFile = new File(mediaStorageDir.getPath() + File.separator + + // "VID_"+ timeStamp + ".mp4"); + // } else { + // return null; + // } + // + // return Uri.fromFile(mediaFile); + // } + // + // return null; + // } + + private class UploadBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(REQUEST_AUTHORIZATION_INTENT)) { + Log.d(TAG, "Request auth received - executing the intent"); + Intent toRun = intent + .getParcelableExtra(REQUEST_AUTHORIZATION_INTENT_PARAM); + startActivityForResult(toRun, REQUEST_AUTHORIZATION); + } + } + } +} diff --git a/app/src/main/java/com/google/ytdl/PlayActivity.java b/app/src/main/java/com/google/ytdl/PlayActivity.java new file mode 100644 index 0000000..a1916c3 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/PlayActivity.java @@ -0,0 +1,205 @@ +package com.google.ytdl; + +import android.app.Activity; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import com.google.android.youtube.player.YouTubeInitializationResult; +import com.google.android.youtube.player.YouTubePlayer; +import com.google.android.youtube.player.YouTubePlayer.OnFullscreenListener; +import com.google.android.youtube.player.YouTubePlayer.PlayerStateChangeListener; +import com.google.android.youtube.player.YouTubePlayerFragment; +import com.google.api.client.extensions.android.http.AndroidHttp; +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.gson.GsonFactory; +import com.google.ytdl.util.ImageFetcher; +import com.google.ytdl.util.VideoData; + +/* + * Copyright (c) 2013 Google 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. + */ + +/** + * @author Ibrahim Ulukaya + *

+ * Main fragment showing YouTube Direct Lite upload options and having + * YT Android Player. + */ +public class PlayActivity extends Activity implements + PlayerStateChangeListener, OnFullscreenListener { + + private static final String YOUTUBE_FRAGMENT_TAG = "youtube"; + final HttpTransport transport = AndroidHttp.newCompatibleTransport(); + final JsonFactory jsonFactory = new GsonFactory(); + GoogleAccountCredential credential; + private YouTubePlayer mYouTubePlayer; + private boolean mIsFullScreen = false; + private Intent intent; + + public PlayActivity() { + } + + @Override + public void onStart() { + super.onStart(); + + } + + @Override + public void onStop() { + super.onStop(); + } + + public void directLite(View view) { + this.setResult(RESULT_OK, intent); + finish(); + } + + public void panToVideo(final String youtubeId) { + popPlayerFromBackStack(); + YouTubePlayerFragment playerFragment = YouTubePlayerFragment + .newInstance(); + getFragmentManager() + .beginTransaction() + .replace(R.id.detail_container, playerFragment, + YOUTUBE_FRAGMENT_TAG) + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + .addToBackStack(null).commit(); + playerFragment.initialize(Auth.KEY, + new YouTubePlayer.OnInitializedListener() { + @Override + public void onInitializationSuccess( + YouTubePlayer.Provider provider, + YouTubePlayer youTubePlayer, boolean b) { + youTubePlayer.loadVideo(youtubeId); + mYouTubePlayer = youTubePlayer; + youTubePlayer + .setPlayerStateChangeListener(PlayActivity.this); + youTubePlayer + .setOnFullscreenListener(PlayActivity.this); + } + + @Override + public void onInitializationFailure( + YouTubePlayer.Provider provider, + YouTubeInitializationResult result) { + showErrorToast(result.toString()); + } + }); + } + + public boolean popPlayerFromBackStack() { + if (mIsFullScreen) { + mYouTubePlayer.setFullscreen(false); + return false; + } + if (getFragmentManager().findFragmentByTag(YOUTUBE_FRAGMENT_TAG) != null) { + getFragmentManager().popBackStack(); + return false; + } + return true; + } + + @Override + public void onAdStarted() { + } + + @Override + public void onError(YouTubePlayer.ErrorReason errorReason) { + showErrorToast(errorReason.toString()); + } + + private void showErrorToast(String message) { + Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT) + .show(); + } + + @Override + public void onLoaded(String arg0) { + } + + @Override + public void onLoading() { + } + + @Override + public void onVideoEnded() { + // popPlayerFromBackStack(); + } + + @Override + public void onVideoStarted() { + } + + @Override + public void onFullscreen(boolean fullScreen) { + mIsFullScreen = fullScreen; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.activity_play); + intent = getIntent(); + Button submitButton = (Button) findViewById(R.id.submit_button); + if (Intent.ACTION_VIEW.equals(intent.getAction())) { + submitButton.setVisibility(View.GONE); + setTitle(R.string.playing_uploaded_video); + } + String youtubeId = intent.getStringExtra(MainActivity.YOUTUBE_ID); + panToVideo(youtubeId); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.play, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + // Respond to the action bar's Up/Home button + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + NavUtils.navigateUpFromSameTask(this); + } + + public interface Callbacks { + public ImageFetcher onGetImageFetcher(); + + public void onVideoSelected(VideoData video); + + public void onResume(); + + } +} diff --git a/app/src/main/java/com/google/ytdl/ResumableUpload.java b/app/src/main/java/com/google/ytdl/ResumableUpload.java new file mode 100644 index 0000000..c2afc8d --- /dev/null +++ b/app/src/main/java/com/google/ytdl/ResumableUpload.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.ThumbnailUtils; +import android.net.Uri; +import android.provider.MediaStore.Video.Thumbnails; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException; +import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; +import com.google.api.client.googleapis.media.MediaHttpUploader; +import com.google.api.client.googleapis.media.MediaHttpUploaderProgressListener; +import com.google.api.client.http.InputStreamContent; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.model.Video; +import com.google.api.services.youtube.model.VideoListResponse; +import com.google.api.services.youtube.model.VideoSnippet; +import com.google.api.services.youtube.model.VideoStatus; +import com.google.ytdl.util.Upload; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; + + +/** + * @author Ibrahim Ulukaya + *

+ * YouTube Resumable Upload controller class. + */ +public class ResumableUpload { + /** + * Assigned to the upload + */ + public static final String[] DEFAULT_KEYWORDS = {"MultiSquash", "Game"}; + /** + * Indicates that the video is fully processed, see https://www.googleapis.com/discovery/v1/apis/youtube/v3/rpc + */ + private static final String SUCCEEDED = "succeeded"; + private static final String TAG = "UploadingActivity"; + private static int UPLOAD_NOTIFICATION_ID = 1001; + private static int PLAYBACK_NOTIFICATION_ID = 1002; + /* + * Global instance of the format used for the video being uploaded (MIME type). + */ + private static String VIDEO_FILE_FORMAT = "video/*"; + + /** + * Uploads user selected video in the project folder to the user's YouTube account using OAuth2 + * for authentication. + */ + + public static String upload(YouTube youtube, final InputStream fileInputStream, + final long fileSize, final Uri mFileUri, final String path, final Context context) { + final NotificationManager notifyManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); + + Intent notificationIntent = new Intent(context, ReviewActivity.class); + notificationIntent.setData(mFileUri); + notificationIntent.setAction(Intent.ACTION_VIEW); + Bitmap thumbnail = ThumbnailUtils.createVideoThumbnail(path, Thumbnails.MICRO_KIND); + PendingIntent contentIntent = PendingIntent.getActivity(context, + 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); + builder.setContentTitle(context.getString(R.string.youtube_upload)) + .setContentText(context.getString(R.string.youtube_upload_started)) + .setSmallIcon(R.drawable.ic_stat_device_access_video).setContentIntent(contentIntent).setStyle(new NotificationCompat.BigPictureStyle().bigPicture(thumbnail)); + notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); + + String videoId = null; + try { + // Add extra information to the video before uploading. + Video videoObjectDefiningMetadata = new Video(); + + /* + * Set the video to public, so it is available to everyone (what most people want). This is + * actually the default, but I wanted you to see what it looked like in case you need to set + * it to "unlisted" or "private" via API. + */ + VideoStatus status = new VideoStatus(); + status.setPrivacyStatus("public"); + videoObjectDefiningMetadata.setStatus(status); + + // We set a majority of the metadata with the VideoSnippet object. + VideoSnippet snippet = new VideoSnippet(); + + /* + * The Calendar instance is used to create a unique name and description for test purposes, so + * you can see multiple files being uploaded. You will want to remove this from your project + * and use your own standard names. + */ + Calendar cal = Calendar.getInstance(); + snippet.setTitle("Test Upload via Java on " + cal.getTime()); + snippet.setDescription("Video uploaded via YouTube Data API V3 using the Java library " + + "on " + cal.getTime()); + + // Set your keywords. + snippet.setTags(Arrays.asList(Constants.DEFAULT_KEYWORD, Upload.generateKeywordFromPlaylistId(Constants.UPLOAD_PLAYLIST))); + + // Set completed snippet to the video object. + videoObjectDefiningMetadata.setSnippet(snippet); + + InputStreamContent mediaContent = + new InputStreamContent(VIDEO_FILE_FORMAT, new BufferedInputStream(fileInputStream)); + mediaContent.setLength(fileSize); + + /* + * The upload command includes: 1. Information we want returned after file is successfully + * uploaded. 2. Metadata we want associated with the uploaded video. 3. Video file itself. + */ + YouTube.Videos.Insert videoInsert = + youtube.videos().insert("snippet,statistics,status", videoObjectDefiningMetadata, + mediaContent); + + // Set the upload type and add event listener. + MediaHttpUploader uploader = videoInsert.getMediaHttpUploader(); + + /* + * Sets whether direct media upload is enabled or disabled. True = whole media content is + * uploaded in a single request. False (default) = resumable media upload protocol to upload + * in data chunks. + */ + uploader.setDirectUploadEnabled(false); + + MediaHttpUploaderProgressListener progressListener = new MediaHttpUploaderProgressListener() { + public void progressChanged(MediaHttpUploader uploader) throws IOException { + switch (uploader.getUploadState()) { + case INITIATION_STARTED: + builder.setContentText(context.getString(R.string.initiation_started)).setProgress((int) fileSize, + (int) uploader.getNumBytesUploaded(), false); + notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); + break; + case INITIATION_COMPLETE: + builder.setContentText(context.getString(R.string.initiation_completed)).setProgress((int) fileSize, + (int) uploader.getNumBytesUploaded(), false); + notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); + break; + case MEDIA_IN_PROGRESS: + builder + .setContentTitle(context.getString(R.string.youtube_upload) + + (int) (uploader.getProgress() * 100) + "%") + .setContentText(context.getString(R.string.upload_in_progress)) + .setProgress((int) fileSize, (int) uploader.getNumBytesUploaded(), false); + notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); + break; + case MEDIA_COMPLETE: + builder.setContentTitle(context.getString(R.string.yt_upload_completed)) + .setContentText(context.getString(R.string.upload_completed)) + // Removes the progress bar + .setProgress(0, 0, false); + notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); + case NOT_STARTED: + Log.d(this.getClass().getSimpleName(), context.getString(R.string.upload_not_started)); + break; + } + } + }; + uploader.setProgressListener(progressListener); + + // Execute upload. + Video returnedVideo = videoInsert.execute(); + Log.d(TAG, "Video upload completed"); + videoId = returnedVideo.getId(); + Log.d(TAG, String.format("videoId = [%s]", videoId)); + } catch (final GooglePlayServicesAvailabilityIOException availabilityException) { + Log.e(TAG, "GooglePlayServicesAvailabilityIOException", availabilityException); + notifyFailedUpload(context, context.getString(R.string.cant_access_play), notifyManager, builder); + } catch (UserRecoverableAuthIOException userRecoverableException) { + Log.i(TAG, String.format("UserRecoverableAuthIOException: %s", + userRecoverableException.getMessage())); + requestAuth(context, userRecoverableException); + } catch (IOException e) { + Log.e(TAG, "IOException", e); + notifyFailedUpload(context, context.getString(R.string.please_try_again), notifyManager, builder); + } + return videoId; + } + + private static void requestAuth(Context context, + UserRecoverableAuthIOException userRecoverableException) { + LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); + Intent authIntent = userRecoverableException.getIntent(); + Intent runReqAuthIntent = new Intent(MainActivity.REQUEST_AUTHORIZATION_INTENT); + runReqAuthIntent.putExtra(MainActivity.REQUEST_AUTHORIZATION_INTENT_PARAM, authIntent); + manager.sendBroadcast(runReqAuthIntent); + Log.d(TAG, String.format("Sent broadcast %s", MainActivity.REQUEST_AUTHORIZATION_INTENT)); + } + + private static void notifyFailedUpload(Context context, String message, NotificationManager notifyManager, + NotificationCompat.Builder builder) { + builder.setContentTitle(context.getString(R.string.yt_upload_failed)) + .setContentText(message); + notifyManager.notify(UPLOAD_NOTIFICATION_ID, builder.build()); + Log.e(ResumableUpload.class.getSimpleName(), message); + } + + public static void showSelectableNotification(String videoId, Context context) { + Log.d(TAG, String.format("Posting selectable notification for video ID [%s]", videoId)); + final NotificationManager notifyManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); + Intent notificationIntent = new Intent(context, PlayActivity.class); + notificationIntent.putExtra(MainActivity.YOUTUBE_ID, videoId); + notificationIntent.setAction(Intent.ACTION_VIEW); + + URL url; + try { + url = new URL("https://i1.ytimg.com/vi/" + videoId + "/mqdefault.jpg"); + Bitmap thumbnail = BitmapFactory.decodeStream(url.openConnection().getInputStream()); + PendingIntent contentIntent = PendingIntent.getActivity(context, + 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); + builder.setContentTitle(context.getString(R.string.watch_your_video)) + .setContentText(context.getString(R.string.see_the_newly_uploaded_video)).setContentIntent(contentIntent).setSmallIcon(R.drawable.ic_stat_device_access_video).setStyle(new NotificationCompat.BigPictureStyle().bigPicture(thumbnail)); + notifyManager.notify(PLAYBACK_NOTIFICATION_ID, builder.build()); + Log.d(TAG, String.format("Selectable notification for video ID [%s] posted", videoId)); + } catch (MalformedURLException e) { + Log.e(TAG, e.getMessage()); + } catch (IOException e) { + Log.e(TAG, e.getMessage()); + } + } + + + /** + * @return url of thumbnail if the video is fully processed + */ + public static boolean checkIfProcessed(String videoId, YouTube youtube) { + try { + YouTube.Videos.List list = youtube.videos().list("processingDetails"); + list.setId(videoId); + VideoListResponse listResponse = list.execute(); + List

+ * Intent service to handle uploads. + */ +public class UploadService extends IntentService { + + /** + * defines how long we'll wait for a video to finish processing + */ + private static final int PROCESSING_TIMEOUT_SEC = 60 * 20; // 20 minutes + + /** + * controls how often to poll for video processing status + */ + private static final int PROCESSING_POLL_INTERVAL_SEC = 60; + /** + * how long to wait before re-trying the upload + */ + private static final int UPLOAD_REATTEMPT_DELAY_SEC = 60; + /** + * max number of retry attempts + */ + private static final int MAX_RETRY = 3; + private static final String TAG = "UploadService"; + /** + * processing start time + */ + private static long mStartTime; + final HttpTransport transport = AndroidHttp.newCompatibleTransport(); + final JsonFactory jsonFactory = new GsonFactory(); + GoogleAccountCredential credential; + /** + * tracks the number of upload attempts + */ + private int mUploadAttemptCount; + + public UploadService() { + super("YTUploadService"); + } + + private static void zzz(int duration) throws InterruptedException { + Log.d(TAG, String.format("Sleeping for [%d] ms ...", duration)); + Thread.sleep(duration); + Log.d(TAG, String.format("Sleeping for [%d] ms ... done", duration)); + } + + private static boolean timeoutExpired(long startTime, int timeoutSeconds) { + long currTime = System.currentTimeMillis(); + long elapsed = currTime - startTime; + if (elapsed >= timeoutSeconds * 1000) { + return true; + } else { + return false; + } + } + + @Override + protected void onHandleIntent(Intent intent) { + Uri fileUri = intent.getData(); + String chosenAccountName = intent.getStringExtra(MainActivity.ACCOUNT_KEY); + + credential = + GoogleAccountCredential.usingOAuth2(getApplicationContext(), Lists.newArrayList(Auth.SCOPES)); + credential.setSelectedAccountName(chosenAccountName); + credential.setBackOff(new ExponentialBackOff()); + + String appName = getResources().getString(R.string.app_name); + final YouTube youtube = + new YouTube.Builder(transport, jsonFactory, credential).setApplicationName( + appName).build(); + + + try { + tryUploadAndShowSelectableNotification(fileUri, youtube); + } catch (InterruptedException e) { + // ignore + } + } + + private void tryUploadAndShowSelectableNotification(final Uri fileUri, final YouTube youtube) throws InterruptedException { + while (true) { + Log.i(TAG, String.format("Uploading [%s] to YouTube", fileUri.toString())); + String videoId = tryUpload(fileUri, youtube); + if (videoId != null) { + Log.i(TAG, String.format("Uploaded video with ID: %s", videoId)); + tryShowSelectableNotification(videoId, youtube); + return; + } else { + Log.e(TAG, String.format("Failed to upload %s", fileUri.toString())); + if (mUploadAttemptCount++ < MAX_RETRY) { + Log.i(TAG, String.format("Will retry to upload the video ([%d] out of [%d] reattempts)", + mUploadAttemptCount, MAX_RETRY)); + zzz(UPLOAD_REATTEMPT_DELAY_SEC * 1000); + } else { + Log.e(TAG, String.format("Giving up on trying to upload %s after %d attempts", + fileUri.toString(), mUploadAttemptCount)); + return; + } + } + } + } + + private void tryShowSelectableNotification(final String videoId, final YouTube youtube) + throws InterruptedException { + mStartTime = System.currentTimeMillis(); + boolean processed = false; + while (!processed) { + processed = ResumableUpload.checkIfProcessed(videoId, youtube); + if (!processed) { + // wait a while + Log.d(TAG, String.format("Video [%s] is not processed yet, will retry after [%d] seconds", + videoId, PROCESSING_POLL_INTERVAL_SEC)); + if (!timeoutExpired(mStartTime, PROCESSING_TIMEOUT_SEC)) { + zzz(PROCESSING_POLL_INTERVAL_SEC * 1000); + } else { + Log.d(TAG, String.format("Bailing out polling for processing status after [%d] seconds", + PROCESSING_TIMEOUT_SEC)); + return; + } + } else { + ResumableUpload.showSelectableNotification(videoId, getApplicationContext()); + return; + } + } + } + + private String tryUpload(Uri mFileUri, YouTube youtube) { + long fileSize; + InputStream fileInputStream = null; + String videoId = null; + try { + fileSize = getContentResolver().openFileDescriptor(mFileUri, "r").getStatSize(); + fileInputStream = getContentResolver().openInputStream(mFileUri); + String[] proj = {MediaStore.Images.Media.DATA}; + Cursor cursor = getContentResolver().query(mFileUri, proj, null, null, null); + int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + cursor.moveToFirst(); + + videoId = ResumableUpload.upload(youtube, fileInputStream, fileSize, mFileUri, cursor.getString(column_index), getApplicationContext()); + + + } catch (FileNotFoundException e) { + Log.e(getApplicationContext().toString(), e.getMessage()); + } finally { + try { + fileInputStream.close(); + } catch (IOException e) { + // ignore + } + } + return videoId; + } + +} diff --git a/app/src/main/java/com/google/ytdl/UploadsListFragment.java b/app/src/main/java/com/google/ytdl/UploadsListFragment.java new file mode 100644 index 0000000..b7ead4d --- /dev/null +++ b/app/src/main/java/com/google/ytdl/UploadsListFragment.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl; + +import android.app.Activity; +import android.app.Fragment; +import android.content.IntentSender; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks; +import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener; +import com.google.android.gms.plus.PlusClient; +import com.google.android.gms.plus.PlusOneButton; +import com.google.android.gms.plus.model.people.Person; +import com.google.ytdl.util.ImageFetcher; +import com.google.ytdl.util.ImageWorker; +import com.google.ytdl.util.VideoData; + +import java.util.List; + +/** + * @author Ibrahim Ulukaya + *

+ * Left side fragment showing user's uploaded YouTube videos. + */ +public class UploadsListFragment extends Fragment implements ConnectionCallbacks, + OnConnectionFailedListener { + + private static final String TAG = UploadsListFragment.class.getName(); + private Callbacks mCallbacks; + private ImageWorker mImageFetcher; + private PlusClient mPlusClient; + private GridView mGridView; + + public UploadsListFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mPlusClient = new PlusClient.Builder(getActivity(), this, this) + .setScopes(Auth.SCOPES) + .build(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View listView = inflater.inflate(R.layout.list_fragment, container, false); + mGridView = (GridView) listView.findViewById(R.id.grid_view); + TextView emptyView = (TextView) listView.findViewById(android.R.id.empty); + mGridView.setEmptyView(emptyView); + return listView; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setProfileInfo(); + } + + public void setVideos(List videos) { + if (!isAdded()) { + return; + } + + mGridView.setAdapter(new UploadedVideoAdapter(videos)); + } + + public void setProfileInfo() { + if (!mPlusClient.isConnected() || mPlusClient.getCurrentPerson() == null) { + ((ImageView) getView().findViewById(R.id.avatar)) + .setImageDrawable(null); + ((TextView) getView().findViewById(R.id.display_name)) + .setText(R.string.not_signed_in); + } else { + Person currentPerson = mPlusClient.getCurrentPerson(); + if (currentPerson.hasImage()) { + mImageFetcher.loadImage(currentPerson.getImage().getUrl(), + ((ImageView) getView().findViewById(R.id.avatar))); + } + if (currentPerson.hasDisplayName()) { + ((TextView) getView().findViewById(R.id.display_name)) + .setText(currentPerson.getDisplayName()); + } + } + } + + @Override + public void onResume() { + super.onResume(); + mPlusClient.connect(); + } + + @Override + public void onPause() { + super.onPause(); + mPlusClient.disconnect(); + } + + @Override + public void onConnected(Bundle bundle) { + if (mGridView.getAdapter() != null) { + ((UploadedVideoAdapter) mGridView.getAdapter()).notifyDataSetChanged(); + } + + setProfileInfo(); + mCallbacks.onConnected(mPlusClient.getAccountName()); + } + + @Override + public void onDisconnected() { + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + if (connectionResult.hasResolution()) { + Toast.makeText(getActivity(), + R.string.connection_to_google_play_failed, Toast.LENGTH_SHORT) + .show(); + + Log.e(TAG, + String.format( + "Connection to Play Services Failed, error: %d, reason: %s", + connectionResult.getErrorCode(), + connectionResult.toString())); + try { + connectionResult.startResolutionForResult(getActivity(), 0); + } catch (IntentSender.SendIntentException e) { + Log.e(TAG, e.toString(), e); + } + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (!(activity instanceof Callbacks)) { + throw new ClassCastException("Activity must implement callbacks."); + } + + mCallbacks = (Callbacks) activity; + mImageFetcher = mCallbacks.onGetImageFetcher(); + } + + @Override + public void onDetach() { + super.onDetach(); + mCallbacks = null; + mImageFetcher = null; + } + + public interface Callbacks { + public ImageFetcher onGetImageFetcher(); + + public void onVideoSelected(VideoData video); + + public void onConnected(String connectedAccountName); + } + + private class UploadedVideoAdapter extends BaseAdapter { + private List mVideos; + + private UploadedVideoAdapter(List videos) { + mVideos = videos; + } + + @Override + public int getCount() { + return mVideos.size(); + } + + @Override + public Object getItem(int i) { + return mVideos.get(i); + } + + @Override + public long getItemId(int i) { + return mVideos.get(i).getYouTubeId().hashCode(); + } + + @Override + public View getView(final int position, View convertView, + ViewGroup container) { + if (convertView == null) { + convertView = LayoutInflater.from(getActivity()).inflate( + R.layout.list_item, container, false); + } + + VideoData video = mVideos.get(position); + ((TextView) convertView.findViewById(android.R.id.text1)) + .setText(video.getTitle()); + mImageFetcher.loadImage(video.getThumbUri(), + (ImageView) convertView.findViewById(R.id.thumbnail)); + if (mPlusClient.isConnected()) { + ((PlusOneButton) convertView.findViewById(R.id.plus_button)) + .initialize(video.getWatchUri(), null); + } + convertView.findViewById(R.id.main_target).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + mCallbacks.onVideoSelected(mVideos.get(position)); + } + }); + return convertView; + } + } +} diff --git a/app/src/main/java/com/google/ytdl/util/AsyncTask.java b/app/src/main/java/com/google/ytdl/util/AsyncTask.java new file mode 100644 index 0000000..011f748 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/AsyncTask.java @@ -0,0 +1,671 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * 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.google.ytdl.util; + +import android.annotation.TargetApi; +import android.os.Handler; +import android.os.Message; +import android.os.Process; + +import java.util.ArrayDeque; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + + +/** + * ************************************* + * Copied from JB release framework: + * https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java + *

+ * so that threading behavior on all OS versions is the same and we can tweak behavior by using + * executeOnExecutor() if needed. + *

+ * There are 3 changes in this copy of AsyncTask: + * -pre-HC a single thread executor is used for serial operation + * (Executors.newSingleThreadExecutor) and is the default + * -the default THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy + * -a new fixed thread pool called DUAL_THREAD_EXECUTOR was added + * ************************************* + *

+ *

AsyncTask enables proper and easy use of the UI thread. This class allows to + * perform background operations and publish results on the UI thread without + * having to manipulate threads and/or handlers.

+ *

+ *

AsyncTask is designed to be a helper class around {@link Thread} and {@link android.os.Handler} + * and does not constitute a generic threading framework. AsyncTasks should ideally be + * used for short operations (a few seconds at the most.) If you need to keep threads + * running for long periods of time, it is highly recommended you use the various APIs + * provided by the java.util.concurrent pacakge such as {@link java.util.concurrent.Executor}, + * {@link java.util.concurrent.ThreadPoolExecutor} and {@link java.util.concurrent.FutureTask}.

+ *

+ *

An asynchronous task is defined by a computation that runs on a background thread and + * whose result is published on the UI thread. An asynchronous task is defined by 3 generic + * types, called Params, Progress and Result, + * and 4 steps, called onPreExecute, doInBackground, + * onProgressUpdate and onPostExecute.

+ *

+ *

+ *

Developer Guides

+ *

For more information about using tasks and threads, read the + * Processes and + * Threads developer guide.

+ *
+ *

+ *

Usage

+ *

AsyncTask must be subclassed to be used. The subclass will override at least + * one method ({@link #doInBackground}), and most often will override a + * second one ({@link #onPostExecute}.)

+ *

+ *

Here is an example of subclassing:

+ *
+ * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
+ * protected Long doInBackground(URL... urls) {
+ * int count = urls.length;
+ * long totalSize = 0;
+ * for (int i = 0; i < count; i++) {
+ * totalSize += Downloader.downloadFile(urls[i]);
+ * publishProgress((int) ((i / (float) count) * 100));
+ * // Escape early if cancel() is called
+ * if (isCancelled()) break;
+ * }
+ * return totalSize;
+ * }
+ * 

+ * protected void onProgressUpdate(Integer... progress) { + * setProgressPercent(progress[0]); + * } + *

+ * protected void onPostExecute(Long result) { + * showDialog("Downloaded " + result + " bytes"); + * } + * } + *

+ *

+ *

Once created, a task is executed very simply:

+ *
+ * new DownloadFilesTask().execute(url1, url2, url3);
+ * 
+ *

+ *

AsyncTask's generic types

+ *

The three types used by an asynchronous task are the following:

+ *
    + *
  1. Params, the type of the parameters sent to the task upon + * execution.
  2. + *
  3. Progress, the type of the progress units published during + * the background computation.
  4. + *
  5. Result, the type of the result of the background + * computation.
  6. + *
+ *

Not all types are always used by an asynchronous task. To mark a type as unused, + * simply use the type {@link Void}:

+ *
+ * private class MyTask extends AsyncTask<Void, Void, Void> { ... }
+ * 
+ *

+ *

The 4 steps

+ *

When an asynchronous task is executed, the task goes through 4 steps:

+ *
    + *
  1. {@link #onPreExecute()}, invoked on the UI thread immediately after the task + * is executed. This step is normally used to setup the task, for instance by + * showing a progress bar in the user interface.
  2. + *
  3. {@link #doInBackground}, invoked on the background thread + * immediately after {@link #onPreExecute()} finishes executing. This step is used + * to perform background computation that can take a long time. The parameters + * of the asynchronous task are passed to this step. The result of the computation must + * be returned by this step and will be passed back to the last step. This step + * can also use {@link #publishProgress} to publish one or more units + * of progress. These values are published on the UI thread, in the + * {@link #onProgressUpdate} step.
  4. + *
  5. {@link #onProgressUpdate}, invoked on the UI thread after a + * call to {@link #publishProgress}. The timing of the execution is + * undefined. This method is used to display any form of progress in the user + * interface while the background computation is still executing. For instance, + * it can be used to animate a progress bar or show logs in a text field.
  6. + *
  7. {@link #onPostExecute}, invoked on the UI thread after the background + * computation finishes. The result of the background computation is passed to + * this step as a parameter.
  8. + *
+ *

+ *

Cancelling a task

+ *

A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking + * this method will cause subsequent calls to {@link #isCancelled()} to return true. + * After invoking this method, {@link #onCancelled(Object)}, instead of + * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} + * returns. To ensure that a task is cancelled as quickly as possible, you should always + * check the return value of {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)

+ *

+ *

Threading rules

+ *

There are a few threading rules that must be followed for this class to + * work properly:

+ *
    + *
  • The AsyncTask class must be loaded on the UI thread. This is done + * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
  • + *
  • The task instance must be created on the UI thread.
  • + *
  • {@link #execute} must be invoked on the UI thread.
  • + *
  • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, + * {@link #doInBackground}, {@link #onProgressUpdate} manually.
  • + *
  • The task can be executed only once (an exception will be thrown if + * a second execution is attempted.)
  • + *
+ *

+ *

Memory observability

+ *

AsyncTask guarantees that all callback calls are synchronized in such a way that the following + * operations are safe without explicit synchronizations.

+ *
    + *
  • Set member fields in the constructor or {@link #onPreExecute}, and refer to them + * in {@link #doInBackground}. + *
  • Set member fields in {@link #doInBackground}, and refer to them in + * {@link #onProgressUpdate} and {@link #onPostExecute}. + *
+ *

+ *

Order of execution

+ *

When first introduced, AsyncTasks were executed serially on a single background + * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. Starting with + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single + * thread to avoid common application errors caused by parallel execution.

+ *

If you truly want parallel execution, you can invoke + * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with + * {@link #THREAD_POOL_EXECUTOR}.

+ */ +public abstract class AsyncTask { + private static final String LOG_TAG = "AsyncTask"; + + private static final int CORE_POOL_SIZE = 5; + private static final int MAXIMUM_POOL_SIZE = 128; + private static final int KEEP_ALIVE = 1; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); + } + }; + /** + * An {@link java.util.concurrent.Executor} that executes tasks one at a time in serial + * order. This serialization is global to a particular process. + */ + public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() : + Executors.newSingleThreadExecutor(sThreadFactory); + private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; + public static final Executor DUAL_THREAD_EXECUTOR = + Executors.newFixedThreadPool(2, sThreadFactory); + private static final BlockingQueue sPoolWorkQueue = + new LinkedBlockingQueue(10); + /** + * An {@link java.util.concurrent.Executor} that can be used to execute tasks in parallel. + */ + public static final Executor THREAD_POOL_EXECUTOR + = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory, + new ThreadPoolExecutor.DiscardOldestPolicy()); + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + private static final InternalHandler sHandler = new InternalHandler(); + private final WorkerRunnable mWorker; + private final FutureTask mFuture; + private final AtomicBoolean mCancelled = new AtomicBoolean(); + private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); + private volatile Status mStatus = Status.PENDING; + + /** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ + public AsyncTask() { + mWorker = new WorkerRunnable() { + public Result call() throws Exception { + mTaskInvoked.set(true); + + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + //noinspection unchecked + return postResult(doInBackground(mParams)); + } + }; + + mFuture = new FutureTask(mWorker) { + @Override + protected void done() { + try { + postResultIfNotInvoked(get()); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + postResultIfNotInvoked(null); + } + } + }; + } + + /** + * @hide Used to force static handler to be created. + */ + public static void init() { + sHandler.getLooper(); + } + + /** + * @hide + */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + + /** + * Convenience version of {@link #execute(Object...)} for use with + * a simple Runnable object. See {@link #execute(Object[])} for more + * information on the order of execution. + * + * @see #execute(Object[]) + * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) + */ + public static void execute(Runnable runnable) { + sDefaultExecutor.execute(runnable); + } + + private void postResultIfNotInvoked(Result result) { + final boolean wasTaskInvoked = mTaskInvoked.get(); + if (!wasTaskInvoked) { + postResult(result); + } + } + + private Result postResult(Result result) { + @SuppressWarnings("unchecked") + Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskResult(this, result)); + message.sendToTarget(); + return result; + } + + /** + * Returns the current status of this task. + * + * @return The current status. + */ + public final Status getStatus() { + return mStatus; + } + + /** + * Override this method to perform a computation on a background thread. The + * specified parameters are the parameters passed to {@link #execute} + * by the caller of this task. + *

+ * This method can call {@link #publishProgress} to publish updates + * on the UI thread. + * + * @param params The parameters of the task. + * @return A result, defined by the subclass of this task. + * @see #onPreExecute() + * @see #onPostExecute + * @see #publishProgress + */ + protected abstract Result doInBackground(Params... params); + + /** + * Runs on the UI thread before {@link #doInBackground}. + * + * @see #onPostExecute + * @see #doInBackground + */ + protected void onPreExecute() { + } + + /** + *

Runs on the UI thread after {@link #doInBackground}. The + * specified result is the value returned by {@link #doInBackground}.

+ *

+ *

This method won't be invoked if the task was cancelled.

+ * + * @param result The result of the operation computed by {@link #doInBackground}. + * @see #onPreExecute + * @see #doInBackground + * @see #onCancelled(Object) + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onPostExecute(Result result) { + } + + /** + * Runs on the UI thread after {@link #publishProgress} is invoked. + * The specified values are the values passed to {@link #publishProgress}. + * + * @param values The values indicating progress. + * @see #publishProgress + * @see #doInBackground + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onProgressUpdate(Progress... values) { + } + + /** + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ *

+ *

The default implementation simply invokes {@link #onCancelled()} and + * ignores the result. If you write your own implementation, do not call + * super.onCancelled(result).

+ * + * @param result The result, if any, computed in + * {@link #doInBackground(Object[])}, can be null + * @see #cancel(boolean) + * @see #isCancelled() + */ + @SuppressWarnings({"UnusedParameters"}) + protected void onCancelled(Result result) { + onCancelled(); + } + + /** + *

Applications should preferably override {@link #onCancelled(Object)}. + * This method is invoked by the default implementation of + * {@link #onCancelled(Object)}.

+ *

+ *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + * @see #onCancelled(Object) + * @see #cancel(boolean) + * @see #isCancelled() + */ + protected void onCancelled() { + } + + /** + * Returns true if this task was cancelled before it completed + * normally. If you are calling {@link #cancel(boolean)} on the task, + * the value returned by this method should be checked periodically from + * {@link #doInBackground(Object[])} to end the task as soon as possible. + * + * @return true if task was cancelled before it completed + * @see #cancel(boolean) + */ + public final boolean isCancelled() { + return mCancelled.get(); + } + + /** + *

Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when cancel is called, + * this task should never run. If the task has already started, + * then the mayInterruptIfRunning parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task.

+ *

+ *

Calling this method will result in {@link #onCancelled(Object)} being + * invoked on the UI thread after {@link #doInBackground(Object[])} + * returns. Calling this method guarantees that {@link #onPostExecute(Object)} + * is never invoked. After invoking this method, you should check the + * value returned by {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])} to finish the task as early as + * possible.

+ * + * @param mayInterruptIfRunning true if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + * @return false if the task could not be cancelled, + * typically because it has already completed normally; + * true otherwise + * @see #isCancelled() + * @see #onCancelled(Object) + */ + public final boolean cancel(boolean mayInterruptIfRunning) { + mCancelled.set(true); + return mFuture.cancel(mayInterruptIfRunning); + } + + /** + * Waits if necessary for the computation to complete, and then + * retrieves its result. + * + * @return The computed result. + * @throws java.util.concurrent.CancellationException If the computation was cancelled. + * @throws java.util.concurrent.ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + */ + public final Result get() throws InterruptedException, ExecutionException { + return mFuture.get(); + } + + /** + * Waits if necessary for at most the given time for the computation + * to complete, and then retrieves its result. + * + * @param timeout Time to wait before cancelling the operation. + * @param unit The time unit for the timeout. + * @return The computed result. + * @throws java.util.concurrent.CancellationException If the computation was cancelled. + * @throws java.util.concurrent.ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + * @throws java.util.concurrent.TimeoutException If the wait timed out. + */ + public final Result get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return mFuture.get(timeout, unit); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + *

+ *

Note: this function schedules the task on a queue for a single background + * thread or pool of threads depending on the platform version. When first + * introduced, AsyncTasks were executed serially on a single background thread. + * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. Starting + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being + * executed on a single thread to avoid common application errors caused + * by parallel execution. If you truly want parallel execution, you can use + * the {@link #executeOnExecutor} version of this method + * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings + * on its use. + *

+ *

This method must be invoked on the UI thread. + * + * @param params The parameters of the task. + * @return This instance of AsyncTask. + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) + * @see #execute(Runnable) + */ + public final AsyncTask execute(Params... params) { + return executeOnExecutor(sDefaultExecutor, params); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + *

+ *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to + * allow multiple tasks to run in parallel on a pool of threads managed by + * AsyncTask, however you can also use your own {@link java.util.concurrent.Executor} for custom + * behavior. + *

+ *

Warning: Allowing multiple tasks to run in parallel from + * a thread pool is generally not what one wants, because the order + * of their operation is not defined. For example, if these tasks are used + * to modify any state in common (such as writing a file due to a button click), + * there are no guarantees on the order of the modifications. + * Without careful work it is possible in rare cases for the newer version + * of the data to be over-written by an older one, leading to obscure data + * loss and stability issues. Such changes are best + * executed in serial; to guarantee such work is serialized regardless of + * platform version you can use this function with {@link #SERIAL_EXECUTOR}. + *

+ *

This method must be invoked on the UI thread. + * + * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a + * convenient process-wide thread pool for tasks that are loosely coupled. + * @param params The parameters of the task. + * @return This instance of AsyncTask. + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * @see #execute(Object[]) + */ + public final AsyncTask executeOnExecutor(Executor exec, + Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + } + } + + mStatus = Status.RUNNING; + + onPreExecute(); + + mWorker.mParams = params; + exec.execute(mFuture); + + return this; + } + + /** + * This method can be invoked from {@link #doInBackground} to + * publish updates on the UI thread while the background computation is + * still running. Each call to this method will trigger the execution of + * {@link #onProgressUpdate} on the UI thread. + *

+ * {@link #onProgressUpdate} will note be called if the task has been + * canceled. + * + * @param values The progress values to update the UI with. + * @see #onProgressUpdate + * @see #doInBackground + */ + protected final void publishProgress(Progress... values) { + if (!isCancelled()) { + sHandler.obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult(this, values)).sendToTarget(); + } + } + + private void finish(Result result) { + if (isCancelled()) { + onCancelled(result); + } else { + onPostExecute(result); + } + mStatus = Status.FINISHED; + } + + /** + * Indicates the current status of the task. Each status will be set only once + * during the lifetime of a task. + */ + public enum Status { + /** + * Indicates that the task has not been executed yet. + */ + PENDING, + /** + * Indicates that the task is running. + */ + RUNNING, + /** + * Indicates that {@link AsyncTask#onPostExecute} has finished. + */ + FINISHED, + } + + @TargetApi(11) + private static class SerialExecutor implements Executor { + final ArrayDeque mTasks = new ArrayDeque(); + Runnable mActive; + + public synchronized void execute(final Runnable r) { + mTasks.offer(new Runnable() { + public void run() { + try { + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (mActive == null) { + scheduleNext(); + } + } + + protected synchronized void scheduleNext() { + if ((mActive = mTasks.poll()) != null) { + THREAD_POOL_EXECUTOR.execute(mActive); + } + } + } + + private static class InternalHandler extends Handler { + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) + @Override + public void handleMessage(Message msg) { + AsyncTaskResult result = (AsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + } + } + } + + private static abstract class WorkerRunnable implements Callable { + Params[] mParams; + } + + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class AsyncTaskResult { + final AsyncTask mTask; + final Data[] mData; + + AsyncTaskResult(AsyncTask task, Data... data) { + mTask = task; + mData = data; + } + } +} diff --git a/app/src/main/java/com/google/ytdl/util/DiskLruCache.java b/app/src/main/java/com/google/ytdl/util/DiskLruCache.java new file mode 100644 index 0000000..997234c --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/DiskLruCache.java @@ -0,0 +1,967 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.google.ytdl.util; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * ***************************************************************************** + * Taken from the JB source code, can be found in: + * libcore/luni/src/main/java/libcore/io/DiskLruCache.java + * or direct link: + * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java + * ***************************************************************************** + *

+ * A cache that uses a bounded amount of space on a filesystem. Each cache + * entry has a string key and a fixed number of values. Values are byte + * sequences, accessible as streams or files. Each value must be between {@code + * 0} and {@code Integer.MAX_VALUE} bytes in length. + *

+ *

The cache stores its data in a directory on the filesystem. This + * directory must be exclusive to the cache; the cache may delete or overwrite + * files from its directory. It is an error for multiple processes to use the + * same cache directory at the same time. + *

+ *

This cache limits the number of bytes that it will store on the + * filesystem. When the number of stored bytes exceeds the limit, the cache will + * remove entries in the background until the limit is satisfied. The limit is + * not strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache + * journal so space-sensitive applications should set a conservative limit. + *

+ *

Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + *

    + *
  • When an entry is being created it is necessary to + * supply a full set of values; the empty value should be used as a + * placeholder if necessary. + *
  • When an entry is being edited, it is not necessary + * to supply data for every value; values default to their previous + * value. + *
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + *

+ *

Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + *

+ *

This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If + * an error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. + */ +public final class DiskLruCache implements Closeable { + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TMP = "journal.tmp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private static final int IO_BUFFER_SIZE = 8 * 1024; + + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 + * + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 + * DIRTY 335c4c6028171cfddfbaae1a9c313c52 + * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 + * REMOVE 335c4c6028171cfddfbaae1a9c313c52 + * DIRTY 1ab96a171faeeee38496d8b330771a7a + * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 + * READ 335c4c6028171cfddfbaae1a9c313c52 + * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. + */ + + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final int appVersion; + private final long maxSize; + private final int valueCount; + private final LinkedHashMap lruEntries + = new LinkedHashMap(0, 0.75f, true); + /** + * This cache uses a single background thread to evict entries. + */ + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private long size = 0; + private Writer journalWriter; + private int redundantOpCount; + private final Callable cleanupCallable = new Callable() { + @Override + public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // closed + } + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /* From java.util.Arrays */ + @SuppressWarnings("unchecked") + private static T[] copyOfRange(T[] original, int start, int end) { + final int originalLength = original.length; // For exception priority compatibility. + if (start > end) { + throw new IllegalArgumentException(); + } + if (start < 0 || start > originalLength) { + throw new ArrayIndexOutOfBoundsException(); + } + final int resultLength = end - start; + final int copyLength = Math.min(resultLength, originalLength - start); + final T[] result = (T[]) Array + .newInstance(original.getClass().getComponentType(), resultLength); + System.arraycopy(original, start, result, 0, copyLength); + return result; + } + + /** + * Returns the remainder of 'reader' as a string, closing it when done. + */ + public static String readFully(Reader reader) throws IOException { + try { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); + } + return writer.toString(); + } finally { + reader.close(); + } + } + + /** + * Returns the ASCII characters up to but not including the next "\r\n", or + * "\n". + * + * @throws java.io.EOFException if the stream is exhausted before the next newline + * character. + */ + public static String readAsciiLine(InputStream in) throws IOException { + // TODO: support UTF-8 here instead + + StringBuilder result = new StringBuilder(80); + while (true) { + int c = in.read(); + if (c == -1) { + throw new EOFException(); + } else if (c == '\n') { + break; + } + + result.append((char) c); + } + int length = result.length(); + if (length > 0 && result.charAt(length - 1) == '\r') { + result.setLength(length - 1); + } + return result.toString(); + } + + /** + * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Recursively delete everything in {@code dir}. + */ + // TODO: this should specify paths as Strings rather than as Files + public static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalArgumentException("not a directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param appVersion + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws java.io.IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // prefer to pick up where we left off + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), + IO_BUFFER_SIZE); + return cache; + } catch (IOException journalIsCorrupt) { +// System.logW("DiskLruCache " + directory + " is corrupt: " +// + journalIsCorrupt.getMessage() + ", removing"); + cache.delete(); + } + } + + // create a new empty cache + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private static void deleteIfExists(File file) throws IOException { +// try { +// Libcore.os.remove(file.getPath()); +// } catch (ErrnoException errnoException) { +// if (errnoException.errno != OsConstants.ENOENT) { +// throw errnoException.rethrowAsIOException(); +// } +// } + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + private static String inputStreamToString(InputStream in) throws IOException { + return readFully(new InputStreamReader(in, UTF_8)); + } + + private void readJournal() throws IOException { + InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE); + try { + String magic = readAsciiLine(in); + String version = readAsciiLine(in); + String appVersionString = readAsciiLine(in); + String valueCountString = readAsciiLine(in); + String blank = readAsciiLine(in); + if (!MAGIC.equals(magic) + || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + + magic + ", " + version + ", " + valueCountString + ", " + blank + "]"); + } + + while (true) { + try { + readJournalLine(readAsciiLine(in)); + } catch (EOFException endOfJournal) { + break; + } + } + } finally { + closeQuietly(in); + } + } + + private void readJournalLine(String line) throws IOException { + String[] parts = line.split(" "); + if (parts.length < 2) { + throw new IOException("unexpected journal line: " + line); + } + + String key = parts[1]; + if (parts[0].equals(REMOVE) && parts.length == 2) { + lruEntries.remove(key); + return; + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(copyOfRange(parts, 2, parts.length)); + } else if (parts[0].equals(DIRTY) && parts.length == 2) { + entry.currentEditor = new Editor(entry); + } else if (parts[0].equals(READ) && parts.length == 2) { + // this work was already done by calling lruEntries.get() + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE); + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } + } + + writer.close(); + journalFileTmp.renameTo(journalFile); + journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE); + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + /* + * Open all streams eagerly to guarantee that we see a single published + * snapshot. If we opened streams lazily then the streams could come + * from different edits. + */ + InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (FileNotFoundException e) { + // a file must have been deleted manually! + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER + && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // snapshot is stale + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // another edit is in progress + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // flush the journal before creating files to prevent file leaks + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** + * Returns the directory where this cache stores its data. + */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public long maxSize() { + return maxSize; + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // if this edit is creating the entry for the first time, every index must have a value + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + throw new IllegalStateException("edit didn't create file " + i); + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000; + return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (!file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** + * Returns true if this cache has been closed. + */ + public boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** + * Force buffered operations to the filesystem. + */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** + * Closes this cache. Stored values will remain on the filesystem. + */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // already closed + } + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { +// Map.Entry toEvict = lruEntries.eldest(); + final Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + deleteContents(directory); + } + + private void validateKey(String key) { + if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { + throw new IllegalArgumentException( + "keys must not contain spaces or newlines: \"" + key + "\""); + } + } + + /** + * A snapshot of the values for an entry. + */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final InputStream[] ins; + + private Snapshot(String key, long sequenceNumber, InputStream[] ins) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** + * Returns the unbuffered stream with the value for {@code index}. + */ + public InputStream getInputStream(int index) { + return ins[index]; + } + + /** + * Returns the string value for {@code index}. + */ + public String getString(int index) throws IOException { + return inputStreamToString(getInputStream(index)); + } + + @Override + public void close() { + for (InputStream in : ins) { + closeQuietly(in); + } + } + } + + /** + * Edits the values for an entry. + */ + public final class Editor { + private final Entry entry; + private boolean hasErrors; + + private Editor(Entry entry) { + this.entry = entry; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + return new FileInputStream(entry.getCleanFile(index)); + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors + * when writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); + } + } + + /** + * Sets the value at {@code index} to {@code value}. + */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), UTF_8); + writer.write(value); + } finally { + closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // the previous entry is stale + } else { + completeEdit(this, true); + } + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + private class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int oneByte) { + try { + out.write(oneByte); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void write(byte[] buffer, int offset, int length) { + try { + out.write(buffer, offset, length); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void close() { + try { + out.close(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void flush() { + try { + out.flush(); + } catch (IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** + * Lengths of this entry's files. + */ + private final long[] lengths; + + /** + * True if this entry has ever been published + */ + private boolean readable; + + /** + * The ongoing edit or null if this entry is not being edited. + */ + private Editor currentEditor; + + /** + * The sequence number of the most recently committed edit to this entry. + */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** + * Set lengths using decimal numbers like "10123". + */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + Arrays.toString(strings)); + } + + public File getCleanFile(int i) { + return new File(directory, key + "." + i); + } + + public File getDirtyFile(int i) { + return new File(directory, key + "." + i + ".tmp"); + } + } +} diff --git a/app/src/main/java/com/google/ytdl/util/ImageCache.java b/app/src/main/java/com/google/ytdl/util/ImageCache.java new file mode 100644 index 0000000..f0374a7 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/ImageCache.java @@ -0,0 +1,681 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl.util; + +import android.annotation.TargetApi; +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.os.Bundle; +import android.os.Environment; +import android.os.StatFs; +import android.support.v4.util.LruCache; +import android.util.Log; + +import com.google.ytdl.BuildConfig; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.SoftReference; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashSet; +import java.util.Iterator; + + +/** + * This class holds our bitmap caches (memory and disk). + */ +public class ImageCache { + private static final String TAG = "ImageCache"; + + // Default memory cache size in kilobytes + private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5MB + + // Default disk cache size in bytes + private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB + + // Compression settings when writing images to disk cache + private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG; + private static final int DEFAULT_COMPRESS_QUALITY = 70; + private static final int DISK_CACHE_INDEX = 0; + + // Constants to easily toggle various caches + private static final boolean DEFAULT_MEM_CACHE_ENABLED = true; + private static final boolean DEFAULT_DISK_CACHE_ENABLED = true; + private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false; + private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false; + private final Object mDiskCacheLock = new Object(); + private DiskLruCache mDiskLruCache; + private LruCache mMemoryCache; + private ImageCacheParams mCacheParams; + private boolean mDiskCacheStarting = true; + + private HashSet> mReusableBitmaps; + + /** + * Creating a new ImageCache object using the specified parameters. + * + * @param cacheParams The cache parameters to use to initialize the cache + */ + public ImageCache(ImageCacheParams cacheParams) { + init(cacheParams); + } + + /** + * Creating a new ImageCache object using the default parameters. + * + * @param context The context to use + * @param uniqueName A unique name that will be appended to the cache directory + */ + public ImageCache(Context context, String uniqueName) { + init(new ImageCacheParams(context, uniqueName)); + } + + /** + * Find and return an existing ImageCache stored in a {@link RetainFragment}, if not found a new + * one is created using the supplied params and saved to a {@link RetainFragment}. + * + * @param fragmentManager The fragment manager to use when dealing with the retained fragment. + * @param cacheParams The cache parameters to use if creating the ImageCache + * @return An existing retained ImageCache object or a new one if one did not exist + */ + public static ImageCache findOrCreateCache( + FragmentManager fragmentManager, ImageCacheParams cacheParams) { + + // Search for, or create an instance of the non-UI RetainFragment + final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager); + + // See if we already have an ImageCache stored in RetainFragment + ImageCache imageCache = (ImageCache) mRetainFragment.getObject(); + + // No existing ImageCache, create one and store it in RetainFragment + if (imageCache == null) { + imageCache = new ImageCache(cacheParams); + mRetainFragment.setObject(imageCache); + } + + return imageCache; + } + + /** + * @param candidate - Bitmap to check + * @param targetOptions - Options that have the out* value populated + * @return true if candidate can be used for inBitmap re-use with + * targetOptions + */ + private static boolean canUseForInBitmap( + Bitmap candidate, BitmapFactory.Options targetOptions) { + int width = targetOptions.outWidth / targetOptions.inSampleSize; + int height = targetOptions.outHeight / targetOptions.inSampleSize; + + return candidate.getWidth() == width && candidate.getHeight() == height; + } + + /** + * Get a usable cache directory (external if available, internal otherwise). + * + * @param context The context to use + * @param uniqueName A unique directory name to append to the cache dir + * @return The cache dir + */ + public static File getDiskCacheDir(Context context, String uniqueName) { + // Check if media is mounted or storage is built-in, if so, try and use external cache dir + // otherwise use internal cache dir + final String cachePath = + Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || + !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : + context.getCacheDir().getPath(); + + return new File(cachePath + File.separator + uniqueName); + } + + /** + * A hashing method that changes a string (like a URL) into a hash suitable for using as a + * disk filename. + */ + public static String hashKeyForDisk(String key) { + String cacheKey; + try { + final MessageDigest mDigest = MessageDigest.getInstance("MD5"); + mDigest.update(key.getBytes()); + cacheKey = bytesToHexString(mDigest.digest()); + } catch (NoSuchAlgorithmException e) { + cacheKey = String.valueOf(key.hashCode()); + } + return cacheKey; + } + + private static String bytesToHexString(byte[] bytes) { + // http://stackoverflow.com/questions/332079 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + sb.append('0'); + } + sb.append(hex); + } + return sb.toString(); + } + + /** + * Get the size in bytes of a bitmap in a BitmapDrawable. + * + * @param value + * @return size in bytes + */ + @TargetApi(12) + public static int getBitmapSize(BitmapDrawable value) { + Bitmap bitmap = value.getBitmap(); + + if (Utils.hasHoneycombMR1()) { + return bitmap.getByteCount(); + } + // Pre HC-MR1 + return bitmap.getRowBytes() * bitmap.getHeight(); + } + + /** + * Check if external storage is built-in or removable. + * + * @return True if external storage is removable (like an SD card), false + * otherwise. + */ + @TargetApi(9) + public static boolean isExternalStorageRemovable() { + if (Utils.hasGingerbread()) { + return Environment.isExternalStorageRemovable(); + } + return true; + } + + /** + * Get the external app cache directory. + * + * @param context The context to use + * @return The external cache dir + */ + @TargetApi(8) + public static File getExternalCacheDir(Context context) { + if (Utils.hasFroyo()) { + return context.getExternalCacheDir(); + } + + // Before Froyo we need to construct the external cache dir ourselves + final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/"; + return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir); + } + + /** + * Check how much usable space is available at a given path. + * + * @param path The path to check + * @return The space available in bytes + */ + @TargetApi(9) + public static long getUsableSpace(File path) { + if (Utils.hasGingerbread()) { + return path.getUsableSpace(); + } + final StatFs stats = new StatFs(path.getPath()); + return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks(); + } + + /** + * Locate an existing instance of this Fragment or if not found, create and + * add it using FragmentManager. + * + * @param fm The FragmentManager manager to use. + * @return The existing instance of the Fragment or the new instance if just + * created. + */ + public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { + // Check to see if we have retained the worker fragment. + RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG); + + // If not retained (or first time running), we need to create and add it. + if (mRetainFragment == null) { + mRetainFragment = new RetainFragment(); + fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss(); + } + + return mRetainFragment; + } + + /** + * Initialize the cache, providing all parameters. + * + * @param cacheParams The cache parameters to initialize the cache + */ + private void init(ImageCacheParams cacheParams) { + mCacheParams = cacheParams; + + // Set up memory cache + if (mCacheParams.memoryCacheEnabled) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")"); + } + + // If we're running on Honeycomb or newer, then + if (Utils.hasHoneycomb()) { + mReusableBitmaps = new HashSet>(); + } + + mMemoryCache = new LruCache(mCacheParams.memCacheSize) { + + /** + * Notify the removed entry that is no longer being cached + */ + @Override + protected void entryRemoved(boolean evicted, String key, + BitmapDrawable oldValue, BitmapDrawable newValue) { + if (RecyclingBitmapDrawable.class.isInstance(oldValue)) { + // The removed entry is a recycling drawable, so notify it + // that it has been removed from the memory cache + ((RecyclingBitmapDrawable) oldValue).setIsCached(false); + } else { + // The removed entry is a standard BitmapDrawable + + if (Utils.hasHoneycomb()) { + // We're running on Honeycomb or later, so add the bitmap + // to a SoftRefrence set for possible use with inBitmap later + mReusableBitmaps.add(new SoftReference(oldValue.getBitmap())); + } + } + } + + /** + * Measure item size in kilobytes rather than units which is more practical + * for a bitmap cache + */ + @Override + protected int sizeOf(String key, BitmapDrawable value) { + final int bitmapSize = getBitmapSize(value) / 1024; + return bitmapSize == 0 ? 1 : bitmapSize; + } + }; + } + + // By default the disk cache is not initialized here as it should be initialized + // on a separate thread due to disk access. + if (cacheParams.initDiskCacheOnCreate) { + // Set up disk cache + initDiskCache(); + } + } + + /** + * Initializes the disk cache. Note that this includes disk access so this should not be + * executed on the main/UI thread. By default an ImageCache does not initialize the disk + * cache when it is created, instead you should call initDiskCache() to initialize it on a + * background thread. + */ + public void initDiskCache() { + // Set up disk cache + synchronized (mDiskCacheLock) { + if (mDiskLruCache == null || mDiskLruCache.isClosed()) { + File diskCacheDir = mCacheParams.diskCacheDir; + if (mCacheParams.diskCacheEnabled && diskCacheDir != null) { + if (!diskCacheDir.exists()) { + diskCacheDir.mkdirs(); + } + if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) { + try { + mDiskLruCache = DiskLruCache.open( + diskCacheDir, 1, 1, mCacheParams.diskCacheSize); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache initialized"); + } + } catch (final IOException e) { + mCacheParams.diskCacheDir = null; + Log.e(TAG, "initDiskCache - " + e); + } + } + } + } + mDiskCacheStarting = false; + mDiskCacheLock.notifyAll(); + } + } + + /** + * Adds a bitmap to both memory and disk cache. + * + * @param data Unique identifier for the bitmap to store + * @param value The bitmap drawable to store + */ + public void addBitmapToCache(String data, BitmapDrawable value) { + if (data == null || value == null) { + return; + } + + // Add to memory cache + if (mMemoryCache != null) { + if (RecyclingBitmapDrawable.class.isInstance(value)) { + // The removed entry is a recycling drawable, so notify it + // that it has been added into the memory cache + ((RecyclingBitmapDrawable) value).setIsCached(true); + } + mMemoryCache.put(data, value); + } + + synchronized (mDiskCacheLock) { + // Add to disk cache + if (mDiskLruCache != null) { + final String key = hashKeyForDisk(data); + OutputStream out = null; + try { + DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); + if (snapshot == null) { + final DiskLruCache.Editor editor = mDiskLruCache.edit(key); + if (editor != null) { + out = editor.newOutputStream(DISK_CACHE_INDEX); + value.getBitmap().compress( + mCacheParams.compressFormat, mCacheParams.compressQuality, out); + editor.commit(); + out.close(); + } + } else { + snapshot.getInputStream(DISK_CACHE_INDEX).close(); + } + } catch (final IOException e) { + Log.e(TAG, "addBitmapToCache - " + e); + } catch (Exception e) { + Log.e(TAG, "addBitmapToCache - " + e); + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException e) { + } + } + } + } + } + + /** + * Get from memory cache. + * + * @param data Unique identifier for which item to get + * @return The bitmap drawable if found in cache, null otherwise + */ + public BitmapDrawable getBitmapFromMemCache(String data) { + BitmapDrawable memValue = null; + + if (mMemoryCache != null) { + memValue = mMemoryCache.get(data); + } + + if (BuildConfig.DEBUG && memValue != null) { + Log.d(TAG, "Memory cache hit"); + } + + return memValue; + } + + /** + * Get from disk cache. + * + * @param data Unique identifier for which item to get + * @return The bitmap if found in cache, null otherwise + */ + public Bitmap getBitmapFromDiskCache(String data) { + final String key = hashKeyForDisk(data); + Bitmap bitmap = null; + + synchronized (mDiskCacheLock) { + while (mDiskCacheStarting) { + try { + mDiskCacheLock.wait(); + } catch (InterruptedException e) { + } + } + if (mDiskLruCache != null) { + InputStream inputStream = null; + try { + final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); + if (snapshot != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache hit"); + } + inputStream = snapshot.getInputStream(DISK_CACHE_INDEX); + if (inputStream != null) { + FileDescriptor fd = ((FileInputStream) inputStream).getFD(); + + // Decode bitmap, but we don't want to sample so give + // MAX_VALUE as the target dimensions + bitmap = ImageResizer.decodeSampledBitmapFromDescriptor( + fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this); + } + } + } catch (final IOException e) { + Log.e(TAG, "getBitmapFromDiskCache - " + e); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + } + } + } + return bitmap; + } + } + + /** + * @param options - BitmapFactory.Options with out* options populated + * @return Bitmap that case be used for inBitmap + */ + protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) { + Bitmap bitmap = null; + + if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) { + final Iterator> iterator = mReusableBitmaps.iterator(); + Bitmap item; + + while (iterator.hasNext()) { + item = iterator.next().get(); + + if (null != item && item.isMutable()) { + // Check to see it the item can be used for inBitmap + if (canUseForInBitmap(item, options)) { + bitmap = item; + + // Remove from reusable set so it can't be used again + iterator.remove(); + break; + } + } else { + // Remove from the set if the reference has been cleared. + iterator.remove(); + } + } + } + + return bitmap; + } + + /** + * Clears both the memory and disk cache associated with this ImageCache object. Note that + * this includes disk access so this should not be executed on the main/UI thread. + */ + public void clearCache() { + if (mMemoryCache != null) { + mMemoryCache.evictAll(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Memory cache cleared"); + } + } + + synchronized (mDiskCacheLock) { + mDiskCacheStarting = true; + if (mDiskLruCache != null && !mDiskLruCache.isClosed()) { + try { + mDiskLruCache.delete(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache cleared"); + } + } catch (IOException e) { + Log.e(TAG, "clearCache - " + e); + } + mDiskLruCache = null; + initDiskCache(); + } + } + } + + /** + * Flushes the disk cache associated with this ImageCache object. Note that this includes + * disk access so this should not be executed on the main/UI thread. + */ + public void flush() { + synchronized (mDiskCacheLock) { + if (mDiskLruCache != null) { + try { + mDiskLruCache.flush(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache flushed"); + } + } catch (IOException e) { + Log.e(TAG, "flush - " + e); + } + } + } + } + + /** + * Closes the disk cache associated with this ImageCache object. Note that this includes + * disk access so this should not be executed on the main/UI thread. + */ + public void close() { + synchronized (mDiskCacheLock) { + if (mDiskLruCache != null) { + try { + if (!mDiskLruCache.isClosed()) { + mDiskLruCache.close(); + mDiskLruCache = null; + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache closed"); + } + } + } catch (IOException e) { + Log.e(TAG, "close - " + e); + } + } + } + } + + /** + * A holder class that contains cache parameters. + */ + public static class ImageCacheParams { + public File diskCacheDir; + + public ImageCacheParams(Context context, String uniqueName) { + diskCacheDir = getDiskCacheDir(context, uniqueName); + } + + public int memCacheSize = DEFAULT_MEM_CACHE_SIZE; + + public ImageCacheParams(File diskCacheDir) { + this.diskCacheDir = diskCacheDir; + } + + /** + * Sets the memory cache size based on a percentage of the max available VM memory. + * Eg. setting percent to 0.2 would set the memory cache to one fifth of the available + * memory. Throws {@link IllegalArgumentException} if percent is < 0.05 or > .8. + * memCacheSize is stored in kilobytes instead of bytes as this will eventually be passed + * to construct a LruCache which takes an int in its constructor. + *

+ * This value should be chosen carefully based on a number of factors + * Refer to the corresponding Android Training class for more discussion: + * http://developer.android.com/training/displaying-bitmaps/ + * + * @param percent Percent of available app memory to use to size memory cache + */ + public void setMemCacheSizePercent(float percent) { + if (percent < 0.05f || percent > 0.8f) { + throw new IllegalArgumentException("setMemCacheSizePercent - percent must be " + + "between 0.05 and 0.8 (inclusive)"); + } + memCacheSize = Math.round(percent * Runtime.getRuntime().maxMemory() / 1024); + } + + public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE; + + + public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT; + public int compressQuality = DEFAULT_COMPRESS_QUALITY; + public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED; + public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED; + public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START; + public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE; + + + } + + /** + * A simple non-UI Fragment that stores a single Object and is retained over configuration + * changes. It will be used to retain the ImageCache object. + */ + public static class RetainFragment extends Fragment { + private Object mObject; + + /** + * Empty constructor as per the Fragment documentation + */ + public RetainFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Make sure this Fragment is retained over a configuration change + setRetainInstance(true); + } + + /** + * Get the stored object. + * + * @return The stored object + */ + public Object getObject() { + return mObject; + } + + /** + * Store a single object in this Fragment. + * + * @param object The object to store + */ + public void setObject(Object object) { + mObject = object; + } + } + +} diff --git a/app/src/main/java/com/google/ytdl/util/ImageFetcher.java b/app/src/main/java/com/google/ytdl/util/ImageFetcher.java new file mode 100644 index 0000000..71e4343 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/ImageFetcher.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build; +import android.util.Log; + +import com.google.ytdl.BuildConfig; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + + +/** + * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL. + */ +public class ImageFetcher extends ImageResizer { + private static final String TAG = "ImageFetcher"; + private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB + private static final String HTTP_CACHE_DIR = "http"; + private static final int IO_BUFFER_SIZE = 8 * 1024; + private static final int DISK_CACHE_INDEX = 0; + private final Object mHttpDiskCacheLock = new Object(); + private DiskLruCache mHttpDiskCache; + private File mHttpCacheDir; + private boolean mHttpDiskCacheStarting = true; + + /** + * Initialize providing a target image width and height for the processing images. + * + * @param context + * @param imageWidth + * @param imageHeight + */ + public ImageFetcher(Context context, int imageWidth, int imageHeight) { + super(context, imageWidth, imageHeight); + init(context); + } + + /** + * Initialize providing a single target image size (used for both width and height); + * + * @param context + * @param imageSize + */ + public ImageFetcher(Context context, int imageSize) { + super(context, imageSize); + init(context); + } + + /** + * Workaround for bug pre-Froyo, see here for more info: + * http://android-developers.blogspot.com/2011/09/androids-http-clients.html + */ + public static void disableConnectionReuseIfNecessary() { + // HTTP connection reuse which was buggy pre-froyo + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { + System.setProperty("http.keepAlive", "false"); + } + } + + private void init(Context context) { + checkConnection(context); + mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR); + } + + @Override + protected void initDiskCacheInternal() { + super.initDiskCacheInternal(); + initHttpDiskCache(); + } + + private void initHttpDiskCache() { + if (!mHttpCacheDir.exists()) { + mHttpCacheDir.mkdirs(); + } + synchronized (mHttpDiskCacheLock) { + if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) { + try { + mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE); + if (BuildConfig.DEBUG) { + Log.d(TAG, "HTTP cache initialized"); + } + } catch (IOException e) { + mHttpDiskCache = null; + } + } + mHttpDiskCacheStarting = false; + mHttpDiskCacheLock.notifyAll(); + } + } + + @Override + protected void clearCacheInternal() { + super.clearCacheInternal(); + synchronized (mHttpDiskCacheLock) { + if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) { + try { + mHttpDiskCache.delete(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "HTTP cache cleared"); + } + } catch (IOException e) { + Log.e(TAG, "clearCacheInternal - " + e); + } + mHttpDiskCache = null; + mHttpDiskCacheStarting = true; + initHttpDiskCache(); + } + } + } + + @Override + protected void flushCacheInternal() { + super.flushCacheInternal(); + synchronized (mHttpDiskCacheLock) { + if (mHttpDiskCache != null) { + try { + mHttpDiskCache.flush(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "HTTP cache flushed"); + } + } catch (IOException e) { + Log.e(TAG, "flush - " + e); + } + } + } + } + + @Override + protected void closeCacheInternal() { + super.closeCacheInternal(); + synchronized (mHttpDiskCacheLock) { + if (mHttpDiskCache != null) { + try { + if (!mHttpDiskCache.isClosed()) { + mHttpDiskCache.close(); + mHttpDiskCache = null; + if (BuildConfig.DEBUG) { + Log.d(TAG, "HTTP cache closed"); + } + } + } catch (IOException e) { + Log.e(TAG, "closeCacheInternal - " + e); + } + } + } + } + + /** + * Simple network connection check. + * + * @param context + */ + private void checkConnection(Context context) { + final ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) { + //Toast.makeText(context, R.string.no_network_connection_toast, Toast.LENGTH_LONG).show(); + Log.e(TAG, "checkConnection - no connection found"); + } + } + + /** + * The main process method, which will be called by the ImageWorker in the AsyncTask background + * thread. + * + * @param data The data to load the bitmap, in this case, a regular http URL + * @return The downloaded and resized bitmap + */ + private Bitmap processBitmap(String data) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "processBitmap - " + data); + } + + final String key = ImageCache.hashKeyForDisk(data); + FileDescriptor fileDescriptor = null; + FileInputStream fileInputStream = null; + DiskLruCache.Snapshot snapshot; + synchronized (mHttpDiskCacheLock) { + // Wait for disk cache to initialize + while (mHttpDiskCacheStarting) { + try { + mHttpDiskCacheLock.wait(); + } catch (InterruptedException e) { + } + } + + if (mHttpDiskCache != null) { + try { + snapshot = mHttpDiskCache.get(key); + if (snapshot == null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "processBitmap, not found in http cache, downloading..."); + } + DiskLruCache.Editor editor = mHttpDiskCache.edit(key); + if (editor != null) { + if (downloadUrlToStream(data, + editor.newOutputStream(DISK_CACHE_INDEX))) { + editor.commit(); + } else { + editor.abort(); + } + } + snapshot = mHttpDiskCache.get(key); + } + if (snapshot != null) { + fileInputStream = + (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX); + fileDescriptor = fileInputStream.getFD(); + } + } catch (IOException e) { + Log.e(TAG, "processBitmap - " + e); + } catch (IllegalStateException e) { + Log.e(TAG, "processBitmap - " + e); + } finally { + if (fileDescriptor == null && fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + } + } + } + } + } + + Bitmap bitmap = null; + if (fileDescriptor != null) { + bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, + mImageHeight, getImageCache()); + } + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + } + } + return bitmap; + } + + @Override + protected Bitmap processBitmap(Object data) { + return processBitmap(String.valueOf(data)); + } + + /** + * Download a bitmap from a URL and write the content to an output stream. + * + * @param urlString The URL to fetch + * @return true if successful, false otherwise + */ + public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { + disableConnectionReuseIfNecessary(); + HttpURLConnection urlConnection = null; + BufferedOutputStream out = null; + BufferedInputStream in = null; + + try { + final URL url = new URL(urlString); + urlConnection = (HttpURLConnection) url.openConnection(); + in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); + out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); + + int b; + while ((b = in.read()) != -1) { + out.write(b); + } + return true; + } catch (final IOException e) { + Log.e(TAG, "Error in downloadBitmap - " + e); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (final IOException e) { + } + } + return false; + } +} diff --git a/app/src/main/java/com/google/ytdl/util/ImageResizer.java b/app/src/main/java/com/google/ytdl/util/ImageResizer.java new file mode 100644 index 0000000..d31c1a7 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/ImageResizer.java @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.util.Log; + +import com.google.ytdl.BuildConfig; + +import java.io.FileDescriptor; + + +/** + * A simple subclass of {@link ImageWorker} that resizes images from resources given a target width + * and height. Useful for when the input images might be too large to simply load directly into + * memory. + */ +public class ImageResizer extends ImageWorker { + private static final String TAG = "ImageResizer"; + protected int mImageWidth; + protected int mImageHeight; + + /** + * Initialize providing a single target image size (used for both width and height); + * + * @param context + * @param imageWidth + * @param imageHeight + */ + public ImageResizer(Context context, int imageWidth, int imageHeight) { + super(context); + setImageSize(imageWidth, imageHeight); + } + + /** + * Initialize providing a single target image size (used for both width and height); + * + * @param context + * @param imageSize + */ + public ImageResizer(Context context, int imageSize) { + super(context); + setImageSize(imageSize); + } + + /** + * Decode and sample down a bitmap from resources to the requested width and height. + * + * @param res The resources object containing the image data + * @param resId The resource id of the image data + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, + int reqWidth, int reqHeight, ImageCache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, options); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + return BitmapFactory.decodeResource(res, resId, options); + } + + /** + * Decode and sample down a bitmap from a file to the requested width and height. + * + * @param filename The full path of the file to decode + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromFile(String filename, + int reqWidth, int reqHeight, ImageCache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filename, options); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(filename, options); + } + + /** + * Decode and sample down a bitmap from a file input stream to the requested width and height. + * + * @param fileDescriptor The file descriptor to read from + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromDescriptor( + FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) { + // inBitmap only works with mutable bitmaps so force the decoder to + // return mutable bitmaps. + options.inMutable = true; + + if (cache != null) { + // Try and find a bitmap to use for inBitmap + Bitmap inBitmap = cache.getBitmapFromReusableSet(options); + + if (inBitmap != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Found bitmap to use for inBitmap"); + } + options.inBitmap = inBitmap; + } + } + } + + /** + * Calculate an inSampleSize for use in a {@link android.graphics.BitmapFactory.Options} object when decoding + * bitmaps using the decode* methods from {@link android.graphics.BitmapFactory}. This implementation calculates + * the closest inSampleSize that will result in the final decoded bitmap having a width and + * height equal to or larger than the requested width and height. This implementation does not + * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but + * results in a larger bitmap which isn't as useful for caching purposes. + * + * @param options An options object with out* params already populated (run through a decode* + * method with inJustDecodeBounds==true + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @return The value to be used for inSampleSize + */ + public static int calculateInSampleSize(BitmapFactory.Options options, + int reqWidth, int reqHeight) { + // Raw height and width of image + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + + // Calculate ratios of height and width to requested height and width + final int heightRatio = Math.round((float) height / (float) reqHeight); + final int widthRatio = Math.round((float) width / (float) reqWidth); + + // Choose the smallest ratio as inSampleSize value, this will guarantee a final image + // with both dimensions larger than or equal to the requested height and width. + inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; + + // This offers some additional logic in case the image has a strange + // aspect ratio. For example, a panorama may have a much larger + // width than height. In these cases the total pixels might still + // end up being too large to fit comfortably in memory, so we should + // be more aggressive with sample down the image (=larger inSampleSize). + + final float totalPixels = width * height; + + // Anything more than 2x the requested pixels we'll sample down further + final float totalReqPixelsCap = reqWidth * reqHeight * 2; + + while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { + inSampleSize++; + } + } + return inSampleSize; + } + + /** + * Set the target image width and height. + * + * @param width + * @param height + */ + public void setImageSize(int width, int height) { + mImageWidth = width; + mImageHeight = height; + } + + /** + * Set the target image size (width and height will be the same). + * + * @param size + */ + public void setImageSize(int size) { + setImageSize(size, size); + } + + /** + * The main processing method. This happens in a background task. In this case we are just + * sampling down the bitmap and returning it from a resource. + * + * @param resId + * @return + */ + private Bitmap processBitmap(int resId) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "processBitmap - " + resId); + } + return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, + mImageHeight, getImageCache()); + } + + @Override + protected Bitmap processBitmap(Object data) { + return processBitmap(Integer.parseInt(String.valueOf(data))); + } +} diff --git a/app/src/main/java/com/google/ytdl/util/ImageWorker.java b/app/src/main/java/com/google/ytdl/util/ImageWorker.java new file mode 100644 index 0000000..20e9d0b --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/ImageWorker.java @@ -0,0 +1,474 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl.util; + +import android.app.FragmentManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.util.Log; +import android.widget.ImageView; + +import com.google.ytdl.BuildConfig; + +import java.lang.ref.WeakReference; + + +/** + * This class wraps up completing some arbitrary long running work when loading a bitmap to an + * ImageView. It handles things like using a memory and disk cache, running the work in a background + * thread and setting a placeholder image. + */ +public abstract class ImageWorker { + private static final String TAG = "ImageWorker"; + private static final int FADE_IN_TIME = 200; + private static final int MESSAGE_CLEAR = 0; + private static final int MESSAGE_INIT_DISK_CACHE = 1; + private static final int MESSAGE_FLUSH = 2; + private static final int MESSAGE_CLOSE = 3; + private final Object mPauseWorkLock = new Object(); + protected boolean mPauseWork = false; + protected Resources mResources; + private ImageCache mImageCache; + private ImageCache.ImageCacheParams mImageCacheParams; + private Bitmap mLoadingBitmap; + private boolean mFadeInBitmap = true; + private boolean mExitTasksEarly = false; + + protected ImageWorker(Context context) { + mResources = context.getResources(); + } + + /** + * Cancels any pending work attached to the provided ImageView. + * + * @param imageView + */ + public static void cancelWork(ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + if (bitmapWorkerTask != null) { + bitmapWorkerTask.cancel(true); + if (BuildConfig.DEBUG) { + final Object bitmapData = bitmapWorkerTask.data; + Log.d(TAG, "cancelWork - cancelled work for " + bitmapData); + } + } + } + + /** + * Returns true if the current work has been canceled or if there was no work in + * progress on this image view. + * Returns false if the work in progress deals with the same data. The work is not + * stopped in that case. + */ + public static boolean cancelPotentialWork(Object data, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Object bitmapData = bitmapWorkerTask.data; + if (bitmapData == null || !bitmapData.equals(data)) { + bitmapWorkerTask.cancel(true); + if (BuildConfig.DEBUG) { + Log.d(TAG, "cancelPotentialWork - cancelled work for " + data); + } + } else { + // The same work is already in progress. + return false; + } + } + return true; + } + + /** + * @param imageView Any imageView + * @return Retrieve the currently active work task (if any) associated with this imageView. + * null if there is no such task. + */ + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + /** + * Load an image specified by the data parameter into an ImageView (override + * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and disk + * cache will be used if an {@link ImageCache} has been set using + * {@link ImageWorker#setImageCache(ImageCache)}. If the image is found in the memory cache, it + * is set immediately, otherwise an {@link AsyncTask} will be created to asynchronously load the + * bitmap. + * + * @param data The URL of the image to download. + * @param imageView The ImageView to bind the downloaded image to. + */ + public void loadImage(Object data, ImageView imageView) { + if (data == null) { + return; + } + + BitmapDrawable value = null; + + if (mImageCache != null) { + value = mImageCache.getBitmapFromMemCache(String.valueOf(data)); + } + + if (value != null) { + // Bitmap found in memory cache + imageView.setImageDrawable(value); + } else if (cancelPotentialWork(data, imageView)) { + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = + new AsyncDrawable(mResources, mLoadingBitmap, task); + imageView.setImageDrawable(asyncDrawable); + + // NOTE: This uses a custom version of AsyncTask that has been pulled from the + // framework and slightly modified. Refer to the docs at the top of the class + // for more info on what was changed. + task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR, data); + } + } + + /** + * Set placeholder bitmap that shows when the the background thread is running. + * + * @param bitmap + */ + public void setLoadingImage(Bitmap bitmap) { + mLoadingBitmap = bitmap; + } + + /** + * Set placeholder bitmap that shows when the the background thread is running. + * + * @param resId + */ + public void setLoadingImage(int resId) { + mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId); + } + + /** + * Adds an {@link ImageCache} to this worker in the background (to prevent disk access on UI + * thread). + * + * @param fragmentManager + * @param cacheParams + */ + public void addImageCache(FragmentManager fragmentManager, + ImageCache.ImageCacheParams cacheParams) { + mImageCacheParams = cacheParams; + setImageCache(ImageCache.findOrCreateCache(fragmentManager, mImageCacheParams)); + new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); + } + + /** + * If set to true, the image will fade-in once it has been loaded by the background thread. + */ + public void setImageFadeIn(boolean fadeIn) { + mFadeInBitmap = fadeIn; + } + + public void setExitTasksEarly(boolean exitTasksEarly) { + mExitTasksEarly = exitTasksEarly; + setPauseWork(false); + } + + /** + * Subclasses should override this to define any processing or work that must happen to produce + * the final bitmap. This will be executed in a background thread and be long running. For + * example, you could resize a large bitmap here, or pull down an image from the network. + * + * @param data The data to identify which image to process, as provided by + * {@link ImageWorker#loadImage(Object, android.widget.ImageView)} + * @return The processed bitmap + */ + protected abstract Bitmap processBitmap(Object data); + + /** + * @return The {@link ImageCache} object currently being used by this ImageWorker. + */ + protected ImageCache getImageCache() { + return mImageCache; + } + + /** + * Sets the {@link ImageCache} object to use with this ImageWorker. Usually you will not need + * to call this directly, instead use {@link ImageWorker#addImageCache} which will create and + * add the {@link ImageCache} object in a background thread (to ensure no disk access on the + * main/UI thread). + * + * @param imageCache + */ + public void setImageCache(ImageCache imageCache) { + mImageCache = imageCache; + } + + /** + * Called when the processing is complete and the final drawable should be + * set on the ImageView. + * + * @param imageView + * @param drawable + */ + private void setImageDrawable(ImageView imageView, Drawable drawable) { + if (mFadeInBitmap) { + // Transition drawable with a transparent drawable and the final drawable + final TransitionDrawable td = + new TransitionDrawable(new Drawable[]{ + new ColorDrawable(android.R.color.transparent), + drawable + }); + // Set background to loading bitmap + imageView.setBackgroundDrawable( + new BitmapDrawable(mResources, mLoadingBitmap)); + + imageView.setImageDrawable(td); + td.startTransition(FADE_IN_TIME); + } else { + imageView.setImageDrawable(drawable); + } + } + + /** + * Pause any ongoing background work. This can be used as a temporary + * measure to improve performance. For example background work could + * be paused when a ListView or GridView is being scrolled using a + * {@link android.widget.AbsListView.OnScrollListener} to keep + * scrolling smooth. + *

+ * If work is paused, be sure setPauseWork(false) is called again + * before your fragment or activity is destroyed (for example during + * {@link android.app.Activity#onPause()}), or there is a risk the + * background thread will never finish. + */ + public void setPauseWork(boolean pauseWork) { + synchronized (mPauseWorkLock) { + mPauseWork = pauseWork; + if (!mPauseWork) { + mPauseWorkLock.notifyAll(); + } + } + } + + protected void initDiskCacheInternal() { + if (mImageCache != null) { + mImageCache.initDiskCache(); + } + } + + protected void clearCacheInternal() { + if (mImageCache != null) { + mImageCache.clearCache(); + } + } + + protected void flushCacheInternal() { + if (mImageCache != null) { + mImageCache.flush(); + } + } + + protected void closeCacheInternal() { + if (mImageCache != null) { + mImageCache.close(); + mImageCache = null; + } + } + + public void clearCache() { + new CacheAsyncTask().execute(MESSAGE_CLEAR); + } + + public void flushCache() { + new CacheAsyncTask().execute(MESSAGE_FLUSH); + } + + public void closeCache() { + new CacheAsyncTask().execute(MESSAGE_CLOSE); + } + + /** + * A custom Drawable that will be attached to the imageView while the work is in progress. + * Contains a reference to the actual worker task, so that it can be stopped if a new binding is + * required, and makes sure that only the last started worker process can bind its result, + * independently of the finish order. + */ + private static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = + new WeakReference(bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + + /** + * The actual AsyncTask that will asynchronously process the image. + */ + private class BitmapWorkerTask extends AsyncTask { + private final WeakReference imageViewReference; + private Object data; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference(imageView); + } + + /** + * Background processing. + */ + @Override + protected BitmapDrawable doInBackground(Object... params) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "doInBackground - starting work"); + } + + data = params[0]; + final String dataString = String.valueOf(data); + Bitmap bitmap = null; + BitmapDrawable drawable = null; + + // Wait here if work is paused and the task is not cancelled + synchronized (mPauseWorkLock) { + while (mPauseWork && !isCancelled()) { + try { + mPauseWorkLock.wait(); + } catch (InterruptedException e) { + } + } + } + + // If the image cache is available and this task has not been cancelled by another + // thread and the ImageView that was originally bound to this task is still bound back + // to this task and our "exit early" flag is not set then try and fetch the bitmap from + // the cache + if (mImageCache != null && !isCancelled() && getAttachedImageView() != null + && !mExitTasksEarly) { + bitmap = mImageCache.getBitmapFromDiskCache(dataString); + } + + // If the bitmap was not found in the cache and this task has not been cancelled by + // another thread and the ImageView that was originally bound to this task is still + // bound back to this task and our "exit early" flag is not set, then call the main + // process method (as implemented by a subclass) + if (bitmap == null && !isCancelled() && getAttachedImageView() != null + && !mExitTasksEarly) { + bitmap = processBitmap(params[0]); + } + + // If the bitmap was processed and the image cache is available, then add the processed + // bitmap to the cache for future use. Note we don't check if the task was cancelled + // here, if it was, and the thread is still running, we may as well add the processed + // bitmap to our cache as it might be used again in the future + if (bitmap != null) { + if (Utils.hasHoneycomb()) { + // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable + drawable = new BitmapDrawable(mResources, bitmap); + } else { + // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable + // which will recycle automagically + drawable = new RecyclingBitmapDrawable(mResources, bitmap); + } + + if (mImageCache != null) { + mImageCache.addBitmapToCache(dataString, drawable); + } + } + + if (BuildConfig.DEBUG) { + Log.d(TAG, "doInBackground - finished work"); + } + + return drawable; + } + + /** + * Once the image is processed, associates it to the imageView + */ + @Override + protected void onPostExecute(BitmapDrawable value) { + // if cancel was called on this task or the "exit early" flag is set then we're done + if (isCancelled() || mExitTasksEarly) { + value = null; + } + + final ImageView imageView = getAttachedImageView(); + if (value != null && imageView != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "onPostExecute - setting bitmap"); + } + setImageDrawable(imageView, value); + } + } + + @Override + protected void onCancelled(BitmapDrawable value) { + super.onCancelled(value); + synchronized (mPauseWorkLock) { + mPauseWorkLock.notifyAll(); + } + } + + /** + * Returns the ImageView associated with this task as long as the ImageView's task still + * points to this task as well. Returns null otherwise. + */ + private ImageView getAttachedImageView() { + final ImageView imageView = imageViewReference.get(); + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (this == bitmapWorkerTask) { + return imageView; + } + + return null; + } + } + + protected class CacheAsyncTask extends AsyncTask { + + @Override + protected Void doInBackground(Object... params) { + switch ((Integer) params[0]) { + case MESSAGE_CLEAR: + clearCacheInternal(); + break; + case MESSAGE_INIT_DISK_CACHE: + initDiskCacheInternal(); + break; + case MESSAGE_FLUSH: + flushCacheInternal(); + break; + case MESSAGE_CLOSE: + closeCacheInternal(); + break; + } + return null; + } + } +} diff --git a/app/src/main/java/com/google/ytdl/util/RecyclingBitmapDrawable.java b/app/src/main/java/com/google/ytdl/util/RecyclingBitmapDrawable.java new file mode 100644 index 0000000..89efdc5 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/RecyclingBitmapDrawable.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl.util; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.util.Log; + +import com.google.ytdl.BuildConfig; + + +/** + * A BitmapDrawable that keeps track of whether it is being displayed or cached. + * When the drawable is no longer being displayed or cached, + * {@link android.graphics.Bitmap#recycle() recycle()} will be called on this drawable's bitmap. + */ +public class RecyclingBitmapDrawable extends BitmapDrawable { + + static final String LOG_TAG = "CountingBitmapDrawable"; + + private int mCacheRefCount = 0; + private int mDisplayRefCount = 0; + + private boolean mHasBeenDisplayed; + + public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + /** + * Notify the drawable that the displayed state has changed. Internally a + * count is kept so that the drawable knows when it is no longer being + * displayed. + * + * @param isDisplayed - Whether the drawable is being displayed or not + */ + public void setIsDisplayed(boolean isDisplayed) { + synchronized (this) { + if (isDisplayed) { + mDisplayRefCount++; + mHasBeenDisplayed = true; + } else { + mDisplayRefCount--; + } + } + + // Check to see if recycle() can be called + checkState(); + } + + /** + * Notify the drawable that the cache state has changed. Internally a count + * is kept so that the drawable knows when it is no longer being cached. + * + * @param isCached - Whether the drawable is being cached or not + */ + public void setIsCached(boolean isCached) { + synchronized (this) { + if (isCached) { + mCacheRefCount++; + } else { + mCacheRefCount--; + } + } + + // Check to see if recycle() can be called + checkState(); + } + + private synchronized void checkState() { + // If the drawable cache and display ref counts = 0, and this drawable + // has been displayed, then recycle + if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed + && hasValidBitmap()) { + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "No longer being used or cached so recycling. " + + toString()); + } + + getBitmap().recycle(); + } + } + + private synchronized boolean hasValidBitmap() { + Bitmap bitmap = getBitmap(); + return bitmap != null && !bitmap.isRecycled(); + } + +} diff --git a/app/src/main/java/com/google/ytdl/util/Upload.java b/app/src/main/java/com/google/ytdl/util/Upload.java new file mode 100644 index 0000000..fb76ac4 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/Upload.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl.util; + +import com.google.ytdl.Constants; + +public class Upload { + public static String generateKeywordFromPlaylistId(String playlistId) { + if (playlistId == null) playlistId = ""; + if (playlistId.indexOf("PL") == 0) { + playlistId = playlistId.substring(2); + } + playlistId = playlistId.replaceAll("\\W", ""); + String keyword = Constants.DEFAULT_KEYWORD.concat(playlistId); + if (keyword.length() > Constants.MAX_KEYWORD_LENGTH) { + keyword = keyword.substring(0, Constants.MAX_KEYWORD_LENGTH); + } + return keyword; + } + +} diff --git a/app/src/main/java/com/google/ytdl/util/Utils.java b/app/src/main/java/com/google/ytdl/util/Utils.java new file mode 100644 index 0000000..9dbe8d7 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/Utils.java @@ -0,0 +1,118 @@ +/* Copyright (c) 2013 Google 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.google.ytdl.util; + +import android.app.Activity; +import android.content.res.Resources; +import android.os.Build; +import android.util.Log; +import android.widget.Toast; + +import com.google.android.gms.auth.GoogleAuthException; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.ytdl.R; + +/** + * Class containing some static utility methods. + */ +public class Utils { + private Utils() { + } + + public static boolean hasFroyo() { + // Can use static final constants like FROYO, declared in later versions + // of the OS since they are inlined at compile time. This is guaranteed behavior. + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; + } + + public static boolean hasGingerbread() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD; + } + + public static boolean hasHoneycomb() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; + } + + public static boolean hasHoneycombMR1() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1; + } + + public static boolean hasJellyBean() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + } + + /** + * Logs the given throwable and shows an error alert dialog with its message. + * + * @param activity activity + * @param tag log tag to use + * @param t throwable to log and show + */ + public static void logAndShow(Activity activity, String tag, Throwable t) { + Log.e(tag, "Error", t); + String message = t.getMessage(); + if (t instanceof GoogleJsonResponseException) { + GoogleJsonError details = ((GoogleJsonResponseException) t).getDetails(); + if (details != null) { + message = details.getMessage(); + } + } else if (t.getCause() instanceof GoogleAuthException) { + message = ((GoogleAuthException) t.getCause()).getMessage(); + } + showError(activity, message); + } + + /** + * Logs the given message and shows an error alert dialog with it. + * + * @param activity activity + * @param tag log tag to use + * @param message message to log and show or {@code null} for none + */ + public static void logAndShowError(Activity activity, String tag, String message) { + String errorMessage = getErrorMessage(activity, message); + Log.e(tag, errorMessage); + showErrorInternal(activity, errorMessage); + } + + /** + * Shows an error alert dialog with the given message. + * + * @param activity activity + * @param message message to show or {@code null} for none + */ + public static void showError(Activity activity, String message) { + String errorMessage = getErrorMessage(activity, message); + showErrorInternal(activity, errorMessage); + } + + private static void showErrorInternal(final Activity activity, final String errorMessage) { + activity.runOnUiThread(new Runnable() { + public void run() { + Toast.makeText(activity, errorMessage, Toast.LENGTH_LONG).show(); + } + }); + } + + private static String getErrorMessage(Activity activity, String message) { + Resources resources = activity.getResources(); + if (message == null) { + return resources.getString(R.string.error); + } + return resources.getString(R.string.error_format, message); + } +} diff --git a/app/src/main/java/com/google/ytdl/util/VideoData.java b/app/src/main/java/com/google/ytdl/util/VideoData.java new file mode 100644 index 0000000..b401f68 --- /dev/null +++ b/app/src/main/java/com/google/ytdl/util/VideoData.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2013 Google 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.google.ytdl.util; + +import com.google.api.services.youtube.model.Video; +import com.google.api.services.youtube.model.VideoSnippet; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Ibrahim Ulukaya + *

+ * Helper class to handle YouTube videos. + */ +public class VideoData { + private Video mVideo; + + public Video getVideo() { + return mVideo; + } + + public void setVideo(Video video) { + mVideo = video; + } + + public String getYouTubeId() { + return mVideo.getId(); + } + + public String getTitle() { + return mVideo.getSnippet().getTitle(); + } + + public VideoSnippet addTags(Collection tags) { + VideoSnippet mSnippet = mVideo.getSnippet(); + List mTags = mSnippet.getTags(); + if (mTags == null) { + mTags = new ArrayList(2); + } + mTags.addAll(tags); + return mSnippet; + } + + public String getThumbUri() { + return mVideo.getSnippet().getThumbnails().getDefault().getUrl(); + } + + public String getWatchUri() { + return "http://www.youtube.com/watch?v=" + getYouTubeId(); + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_av_upload.png b/app/src/main/res/drawable-hdpi/ic_av_upload.png new file mode 100644 index 0000000000000000000000000000000000000000..19d96fc468608a6168ede99008675f3076f28814 GIT binary patch literal 907 zcmeAS@N?(olHy`uVBq!ia0vp^At21b1|(&&1r9PWFb8?MIEGZjy`Amf(HkgnZ2oDN ze>zigA~qa#`lq%%C23M?j!y2B)*BvWUY&E7t#Qj)Hbu5;)6SMjPwpE?TW<;y6BB7Q zwAsG%<*_?E-+!O`{NDRJReopF-c`?gUt9lq&i6a-f1kVZKv6Yd@~TK4HZ8NpMyVAu z8Y0f|nFbg#Zk!{VwIGQp$zaMP(}*yEr?Zz6oF!6&5&sb0M3Sn!>cr&b%Q$ z4m#=WV2ybSGQW6kY!sMrhoMIFy8iUjaxbzIu8Mq^F*W`^)0|1d0^Di5Ul4%CFv%U>Y2SQm9w|%WvrjR zzxvRU?*%{Wg^MP5rgCmso4@W|z{`wve@(0x{Qq*+@7&34^V8VB96jS#r?`R5{tWvo z>BjGkH}h+-JDPY~S?Jmj5){i{J!H+3y{q>vbc(N`AXs z%=-L`jt%#MJ3F&uUWLy*vruSi-SauMEvLRk|2@H^ktnCwrTs^9_W9_0zE`(1%dd8e z+QtxhPx!#D%mEfUTlMhUqf53K{fALcOZGDUPnC_jkE+lSY z(1Ms(i+%p4{QUBN_RpE$?ri0Ku-DOavC)E<$Ltj!rFCY{-d*CeVLspGPw`7uId1f7 z*vEA3&#X$7?M5@wFK+O@^R-`e?vjr6TrZ5&_Ad%%Y_UCe{#CK)iMq-%jvZ#vJkR}$ zo2!mLlDKw1kn=*G>Za{x62_NQ-&w?3T=QLhf4=mJ89N%KUc@uSeF`+*(eCES`k2Rr zLrW@cL15593ufM?t~SYxDIo^u9GVv_N@SB#740#;ps*riMk8G5H7k{4#yrJ+hIYBT zgs;8{kD12r{wuUbe_ir|q*ZL-hl>v_uy%gDQxt(_iUSo1PW>>a(y#0OuiN0kSuQy!poT5@R^Yr0;;=%t9 vt4L<(85Es7>~OT6lbOt92~W>jY`@q~GffXQ6#E?u%w!Cnu6{1-oD!M<5mum1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_content_picture.png b/app/src/main/res/drawable-hdpi/ic_content_picture.png new file mode 100644 index 0000000000000000000000000000000000000000..b5254c8529618667d614616a271c1eb9fb688057 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^At21b1|(&&1r9PWFvobhIEGZjy}f<9J1CKX^}|KU z$qP8tJUAq^T9_O^2<}{waXs07^(|J%|9;(E=4UUt`kZdM>BFk4G+*J1L6yS9*=jLM z4DGD$KWN~6T=CiZ`kpE_mh?T}Yrg-p`5yc{u9o}PLarYxGP$A!G&-6AROj1u2Y0GD{bBx;&b7{<*d9 zy=|SxzE-ayL-s2ZcFVl z^^JA{=O3JiGE4R~ej$Cvb*5*-jXuHt)`ibp68m$WYUDk0*yyZY==WUBZ^~Xh5w7#HngZgpR4*J2(JI|~`|Z7k^9%P~ zW1=Z4_>Fcpw&4nfLzQtl*trt7_MVtu|F}d{r3`vuQHhEiHq7 zIfgLTV&kJpAD7==F^{#^ZLxdP!3SCmmv;2-i8}iJdwsUqY<}&jr^?L(Z8^J~|K$7^ z;Qy<9PVCm4gJ0eJ);mS^xF~sF&HB3ia-j`p7x&3`j|Jk3c1>C+Y(YeRCN5<#Bkyf36GX9=|`B=zHX`@0PN~7c+8rX0tt$ zys~4ae{g(3^ue?5pG+`2Jwx2WhVgYqMdf>|&wQ`06x@G$;l}og2ZqnCuiwqhaiyyy z{ps7X-L>mu^!6L?=eQzw#phV|jP7{ODGfL4ONBK~7OA`Ore<+XS)&=yDAl-sA8S+p zyz(D9kJZbp=AI2&`Q_f`^`9QU-}O7Y>Qn8C-#U-`6)t>y`z`je@PZ#_{xHA)YB%Bg z-KiFT9;-C{_mDMzc=^v&m+-QCZ-FVGbvc(v@x@XZyX7Z-TU@m837%iL>gme23zW}0 u+~PCH2PTz2J>1|FNkrO!C1efBFZ?Ro?V27dT(kjZHwI5vKbLh*2~7YVF{jo5 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_device_access_video.png b/app/src/main/res/drawable-hdpi/ic_device_access_video.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e80ff60d089d45b3435246209d190336ade5ee GIT binary patch literal 749 zcmeAS@N?(olHy`uVBq!ia0vp^At21b1|(&&1r9PWFm3U4aSW-5dwa`1Tey(n*hlp# zTA|$*5gM{CstO|8^SB;eIC^khpH-KI<&F+6ub12!?_z6(m3IAX*V^%u%S$}h@gx7Q zm^}_XFTeWCU*`FnSH}0n-QxWvPj0go8cV|Xk?v}?H z8VeE|3uJg%O>_kM7B6gQV?9)8B64AagGFB(lav_8@rwZthnXH)Na4}6nCVy0EE}D< zJpJ2^&b|F%qrR5EX#U;bUFKibmb_a2<;FqodpDmn%)cYtd{63w-Tzy$A|;`-ek$62 zmYY@mWyv@37uuOO*I$S;TO#%%`{f_~mkW>I`f}g*|K2BQ_5M>7t_AR~`RMlRh2vaL zo;Bxtye{xx+NgiYHvRs$q9u2ZFXzn63j1~Kxn22l-S-FOJ|$eAQ)gqi!OpmWsf4zC-gsh?nv`@U=X$F5w4yL?yH+B`3v zS3Pr8;R}W>Y#ATT>I0cKN3aGsWQVD4h#dS+LlEHOQgr-s@H8k96;Q z9oc^^*TV7i!M-CZcdcKpNPm*Ii^cbG-Q5ZAVy4+7?*1J8COOzpWZA^#`Odf3>)Myx zs61V*_91vqY?E!q#*4`>91p+#TEob@oq=#nk{12E&Hvbb2AvCSUA*82Fc~v=y85}S Ib4q9e03z*Gc>n+a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3343573b5d8ab891432706b2f47274a9e978537b GIT binary patch literal 3375 zcmV+~4bbw5P)H%F0prTQWmNh(o^1C^Ju|ZA)kt1q^`ke#&9N5F% zYidLT`2*7hyv)EcfF($lA3^>nujw-fmQ9)v1t}6x@5!517Ui5B zhlN*wrRm=~j|2fFQ%ce`^9xAjP?yR-yqLfR6_x5iWnkrv zauj3@Pb-F$C*kRrfcsN51*^)-MvQlh6asW}*-Jlf)A;;YJfWZ%r6>h~NrAbky{Rm! zgn-g@3Z4N;rlsm^(&IRhIZ+;CYO3NAf(AhqR^~-;)YH(c)E>=Isnfnrh!4*-T)kuR zO?UC~gaA$6exOGAnXtJ?2wE|&x|DLlP>ssed0AY2Z~C1H5Lp;igkS_qt4E?6bOsInP9x4wBkzf#zJjeC{ zD3x|_Y63u{0!u|uGp-JV6;hd_E&@${h~Ow?8OQIMnlUX3<)8$3%Th5JAca)I)Im^4 zW!5pN2lKS_vr>Q^=TD7v&MrNkE2`W7R;f|ehWCZ^g#Zo7#3Ty98LCT5$N>t`}K>PXXa08ym# zD(KSy6x9m16=ZjUc~rBy0YQs7B67vFhIy-6ni*^BWKz&&1r%y^OFw2uum4#EMLFqu zAH>=q^-(Ngn77P?buZAr^k|bWhJL<<=-xVCVq8K+Oy*5RFo_yn&0>s9odvz!f6TzH5+a~i%L_nk00ifEBNte!rL3<~x z)A-H0QRz|86R#f=+ul|_sHh-U+zpBu@R{av=p}9|KU^h7t^21H{iIXZe#<*(-w^~R3J71~0bS~L`814>14le|QxUw)`cjFbxE{X5gkKkS>;=Op^YWP0!T0kaYSB%_#~ z;t2-gA#DRH2*PF>w#7S-iRS?j291TcbxK^MeE?|0T8v`k+?9EeqK^6iwVzWSnmDBK zp^Yg5v}os1f!z%>`9}w32k6^tI3T-;aBPN0LG1`ogo-we=_at@m4@xnKO&$?Y4VE( zX9sBbn)gi$oT&#LeuWQE+u5ifHeI7l4Tn5_@tyPlS_pu){G}3JSb;fN{N3pRH2-%;#K!%{ z0Z0w);Sp7^ne7tv5P7#{;O@^u2)(av{BdfZ&a#VVm=t!(#ou<7&Mj{JW(` zk+}CO?V$78D7JWS0iau-c{j8ei&l=DdC&u>DFT8=Sj$u(0pr^Cd#;j_PQy0)*N4P9 z4Tx5qHP6o#llm8naWM3epJ7trtpjJo00g9GZLI}bkXGdbR5%Sy%q)=$*788wixN;e zhFoX8a!9N}Kyq4$E!BhLYuXox>EA37{X2)tihyoeK|aX74t>zsxqah(P{GtaU}AiM zlUZo64WiifU7MysKw{?02MP^xl{ z+2i7e)01hE=INKbt|ktnzPaE|JG9nO1L6xrl>*hBy?}D3?A9h`8jA{+`z?m@Q?}aJ zDte|uiOpTSplQWHBN()>9PgtRSgAAWH{Tn_%fZ-r3=C=>SW$4kM?nn`P*6Gj$$(>W zAt*7Rl%~u=aSnukE0#$={F^Zj%>JDWj8AP+Nl5_lN(J9lcp!0=kds$w_VO54F>&1k zt{koy^;;gB<#R7O+NCv4T&YB7~Jim}s_ z^1^BCsYwBaD$2~oRg(*fOe+c&6(yU9M*c=sEE1h0C_@ z<*QsK6Z2h{`24bA9zYW|eyV_?3RW9Wbjb@v1zmBc2hg~+m#r*`-|FSexN3|CkPt1P zJXgXW4n8LtHpClZzS;=mQdS*bH2xn=lDu%tR{?+T!(SyYR8*yK3IHd+a0V_(rDccy zQr7e$?kyYJ39iRH=isjj{?7XVCAi=JPnNX4`P96D{{pkX5bo|PmOTIf002ovPDHLk FV1m0}Fn0g| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_stat_device_access_video.png b/app/src/main/res/drawable-hdpi/ic_stat_device_access_video.png new file mode 100644 index 0000000000000000000000000000000000000000..ad36a698338a1307bac14f1de1f78b38783b994c GIT binary patch literal 446 zcmV;v0YUzWP)t5w*Xu zLXn8UX($ARVTN($fA9aCHxh2o581Om*952}+#qTYRT8LNZ~&*sw(VO%5N3ps^Gd%Z z3s$SuQNQ2++V+|_j`JP_!A%^;b@jP+yR90A@tF&_u6x>QwVskBxge9t#Dln8TOAwo z0Q32rUoMvq5Pw9{Cs7npVp*0CJQAXD=(#$*q9`hoyyAKOISj+@dcDqdvH(azVsYSk zo~miu2eLjZpr&Lz9y2@eeP0Ep1(NI(2xJa0Db>Z3?n@m2(*oId3ObaGMk8hilu}ie zSM5PW`Kk|=*S0t(z4oimUF9JxCbPOF6?H(<^<0vsr)58Q)q2YL{gTY`^ z$;09B4u$rD;U(H8z#9f#mOA(yAk~>pr!wYx80rKOuTgfcFsND&w1~@%VLQ5h((+qgy4U7SR51w9alhF)NzA+ z;faI35;-0q4mxg-FFbM3SE2v`m%t1ay-5O30n&^@8|L6SkYKSq(Oz>-4+AA5mWRM1ZL-fPi+Q1iPi|_fOrDfXZ^GfhE=+_`R@qw zt(l>ESq0$5FCaAvFeUt9#lZ_;j8Km`TNEKbu$7bJKtVBHIV^O7B-hF6C!BrCC@O(F z+&UHsi0`EEh7E$dgtwprkzkfca$6_2UQVb4&w!3*V6M6t?dan5x(9$Fi?7t&FYq%i z7p9A$67*7$```dc-g*4?#)mAOx$G;%R-03ihk2XDRNU4lb*%9mqk|MO}&$f-bqCgGC(FQO%_sanL0< wbg+nnI;y#pBM!Rch7J~SP)9YFaw6~G53$m5Ipqyr3jhEB07*qoM6N<$f&vu`kN^Mx literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_content_picture.png b/app/src/main/res/drawable-mdpi/ic_content_picture.png new file mode 100644 index 0000000000000000000000000000000000000000..afb2e0f671ffb2cad624618c7d37d0035ea46df9 GIT binary patch literal 623 zcmV-#0+9WQP)bkMxz;2vHtsFpqeZE?;sjbo4%l(pJ!x35go{QS$8G{8ck zu%@_bZ z0cfiSIw}fsh^>4h5jQB`OO&>iM8IA$_He-UNPxrI$?*0dPmOkMNx}b;7WzyE3T_8b zh4~h&rtVItDXj;YBMoUtfFc(e_u~uvy@nKp_x5>|>ts~lCU}DrWP`#wBbj0u+`?NsdTCNr@LWEh#80m69Bhf|3$1Y}&*t_zQO5dRJeH=T86t002ov JPDHLkV1l>q4uAjv literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_device_access_video.png b/app/src/main/res/drawable-mdpi/ic_device_access_video.png new file mode 100644 index 0000000000000000000000000000000000000000..19b8d41c8ebcf4869ebc3b13a2ba049654d57110 GIT binary patch literal 514 zcmV+d0{#7oP)ikoAxOHC1B>^Fh;K($^+i2ULzd#-&cnLOV?Cs@Qa;RH1oLuYI=`Xl-Z?Dqoa6gn2= zHIZEOXTWeDu*t9A8buDJgyw%u6g8^xU7w`5WK>%ybW|&H*iMA8-qP1hI#I~5{U5(e z7r;P2Fv2nJiO2X^^Bq%P^hxrle|Rb%p6vy8lkb5^55RluFLn#Q5($XpgOS$C;D24*L1Vs+D3d?Zc2o!wqC_x1Qp;SN+ z0R{CF-z=*z0wq^~gg{UPfn;;XWaE4Nx_5eJXTo6?iBp;M?DX!;|M!mWo;NONxcgzfPc(;8=KSwrnu2HZnq2EP=~Esw)h#5JuVlxaCf_~PaeDQdt?ndkAI?7TU}ip z;2-YT;z{}@9S6nE$F<@ZAc+5h*dGiAWlNv&JoZ(4VmFoUoIO>K8j}aovv+OrLHR@y z?7uA0>P-Q z$JS;|QrFk-Ktm9o-y~) zcNEatA#FT#3A%?4(6sHU<5gsNFDj;;1gm4wVyEgSIO?ZxvV!W~(5-G*n?+ODkYGBl z5uX8S@E;wko%%z^vj6u5J)-cs)mDIL1gP*@MGF`N5#r5Y0z!I>`Y`fjm~m(sIY1$! zVjKkv;Vd{LUJ9i?hA@O1K>Yp7^LxbLwJXB_+6e$du*v!oj6~}mVh`xyrt(Gukg=^i zAjFg9r3VaaE6ZENYe4kdLI;4D_N$MV8=}L*N5KhC*o2cxdOasp?CZ&D7dFE3%IcOd zGI<005Fi$9QepK#y#P&6uw7m#>nl$f5QYah`8YWZ%k!~ay#IZ-03CD>2*DOFsPMrt zK)`4@q4GhERy0;o9ktN@U4uf((EDemoJUV6m>bZMT02O0vv7?C&^l;zcMdEW#4 zryi&;i5o)>#3DeM-6+&7z6o@183WN?3JYUBMB}~~|J7_P)<8HH$l-zVK+Lg}z-kF0 zlewfi2swpip~>qGN1-^pT)A}=io7`nQ7kD4M++Zp*(>1#cXC_`z|4IYO=}?vzu^xj!i3(bkQBon zpSAxY{BR~ zvuM-(W+3E`xeP#SBpQbQZ~}M$0eG#$Vbs5q4?Y+c84sLCfN6UR(9&L*oRI?k9`cD* zt#zA$kl*JPi)99{q3Ynu!gPBNv|2#`yf9M$9}bTcKqXAwa}j1g)dGfe@jH6?H3B$O zEXDI|eRYvNKz!Qrqb0n~XDU2=`kl^i-jQN0kt=lH~^$9 z{k25Rtds)-F*Y}jjEo0zuT{d>9r>`}nU-ScblRruxd7h@0OJ9uXsH80(h>uJ@?dZm zA8Z=cJ|ZD>1_83Z$b&`CwuH<_k{s_bdAAjy4g$-1*@uvSF##AAh{5QA&Cf>$;Pll> z7>xi+5MX3CXMl;AC+q>^S)z2gJwQUj!khrrbQHvpu7241LZkpHVC42Zn1eYmzE_H) z2gdKp7XVxv;=(}Lie5haL5Xz~^2II4DOPnMY6)Q5=nfGHp;K2YV8pibV!j#MGa05m z)(nzk!`ECd?aYU72|%k0Rjlky6GtEnAZGqA2w)Vjo9stICnFj|>at?^^;JIoifTjq*wNHjfjrT&%tgg1M{kK#nu2jH) z&1YqKQI!q306<}t)jbK?r$oc&IoII?!Yc7%K3J7uA41-_M??te5Hy*6iX)L9*XRwo z{e}SjHszXxsak2s+Vbg57cYtgs~rH`a|nP6rn0&wJ<$T=^U*f35Jl6p^pHp(@m)?O zYdzEnmO_Ap2C8b|@c9y9aV1<7cteF=a{*YDCPkTMjXi+hm3^d$tIZ5y9CN*&&vW^l zT61Y#qR17ozAz?Xw9(B)D@7|9t8n!mf%C$stEws=tjVCMyO>R9Y>AM4G;dI9tcJ6YW>d_rCbjY7ti9K)nOig)`u08Br0+W;mQEoZ~iR}-uKNO|Ix z*0%e?`a!k;N$|?v8+UcVd;OzNGL!L^!q~dOl3?6wlRs T)0DUJ00000NkvXXu0mjfIo!xh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_mailboxes_accounts.png b/app/src/main/res/drawable-mdpi/ic_mailboxes_accounts.png new file mode 100644 index 0000000000000000000000000000000000000000..a1a494f776ee23f67d47e78fb016306d64d9cb67 GIT binary patch literal 2099 zcmV-32+a41P)&bV*a z#>9|F1;qtyjSp4qOARpXLlXr;jD5HzMbic{8XuaFc?Vn`8XztqHiIT6gv^UcNyHbI z;BquX-@45f^zyJUK<=im^0s-Z79%kh;A5j2N z08s!@0RQi>Omp89G>wcv(42{^K#07s0hok$ct2KrhP$yJa{_Y=(grbf91>tGk;ob!ANR$^#`bEO){dXU!YChj z!ZbxILFH3^#(44K#jo7YWD3t{l!xivwryL{(W6Iih)olYA3t95?Af#5jmq--{ob6M zoWf(rj@=Lj@rC07S}0+7c=)G%`}R5BzJ04Rd zT+S2TOc)p#_$@Cl&pA3eS~8t1i^ZbA^13Vla6fwV$brk>V2*@KA#yer8(8n&y{m26 zvc(B7niT`e(JCKp$cgpbu%)Z3OG`*d;H`K;p;#zmg(4+cSy@g{EF4)KV`F0-kX1Tw z-nht-?b$ubNPD*dRrN^ftkmYSNX zUl*R_rZK!mS$FQ-(blb7*M#+x$`T-SS$%zdrOC<3iZDiu(K=uvEtV`ZXObS$urFZY>zo3B_%g1xZOsiU9sW>ZGitOPA`hon6XS3MBF$dBSV4Pl{atR*xtQ+=UKRLp$Aqgh2U=7uYdjewH5bOuH!S@ zFf`HdIzicNHeJ@MSFconxdnQ3KA~#AgA#qQ?nhA);0yK$q$~Fqz!6K0eizD_7Fpw%fOFYYZ@WLs`6dv4Zb* zDAarFsTBN{8l;wgUywpKZ z$WSHLD`2}uHqc>P#_wV1YojZ5#E7XTPSlkx+yyQQvOUuF=`ZZbTWJU?C$hg;?o15FR zW5u=vW#$_98M3l^F*z^J%X zj?@UvO#B|KJX56r*icnfHS zL5L611}W0JcJ10Sq+kWg*Fcta!GZ-QBWDJqM@ln;i)ob2*yYQYYr)-{DJdxqUMC%7 z<(z6#d1f;_=gG{>{NCkq9YB9nO+Zjy_CqEkRy8!J1OsmQ^5r^+D_5>GqRV8Cx6`6j zPOb}`1j+mCflzHL&YwT8p{1~+GP61>qVCM?p|aSvhIJYuB#j)1s^sGOI|% zxt@bEXDGEuYnw>t&lU=Wiu~Ey_UvY%h?FO0QqPeqhg@J!PPQi^|Gs zO9eqM@3LjfD%P)G-*WNd#Rj$#j3=%WhnHy^w^;`uIn53eAUr#F?rb@A>XZg$n-CXH zSl3Ic;djU0y?YO~wY8}T*c4oM<9wl@((B4IBT40rzAd*zlD{R5WI#dz&#qm&Dr#$M zYuGa(xbqR{^(7@G2a#$s;EBQ0r%%_Q@MS%D@}&6Cp+jY8=oEm|Q?NW<$~mV1g6Tu$ zd5qk;b!)}hvuA5i&{T1Du<}@ms#*l@_kdnm1j{S~-djPT+QP!ZGSrS+=zXfGGz;Q? zLLq5+SdTw-%p7)uIuz0YDbxgft^y#DSi@E?l_46nr0F9kPx@;sK)o zCMHuJ6wsBI$3jkiXKG{%E-LQ(NiRW$UPstIND#7)Ox63(6aalYPDNRSsC->roo;zq zfMka$gOOn6D-xL^W#IFC43AhZUAm-$KtMYP0AQ!={bve5KO;b)=A4|I3bYdMEl=gC zs7!^E1!U-TdDf;UBnSYi!t4?~P*}lWGQhbQ(O#z_INR{t7m^y0R8TqC$s&*yFGhBD zcCp=V{|lGjiT9xsvzdkbqC*_qzJ2?T0|Nu!!s_Xm=5SvXPL&r>Lne@&`Vr`Vi`R$a zbK!yDgHD{7mIzwzMXe0k4lDow002ovPDHLkV1hM6?REeF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_menu_refresh.png b/app/src/main/res/drawable-mdpi/ic_menu_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..83ac7ae47f31fb04daad5e2fab4a03a615271980 GIT binary patch literal 2093 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 aF)!V;uP{S*b1twrWAJqKb6Mw<&;$UFFh`~U literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_content_picture.png b/app/src/main/res/drawable-xhdpi/ic_content_picture.png new file mode 100644 index 0000000000000000000000000000000000000000..02308540aa4005b6106018d18e433cf43fc331fb GIT binary patch literal 1278 zcmeAS@N?(olHy`uVBq!ia0vp^1t8491|*L?_~OmLz;e{n#WAEJ?(JOPY~etWw$H6f znVU-$y^dy>3;l9j$u;%Saify?A$*{=I&1 z`JsjK{%ps6uS)FZ%kv9uD0#bY-@eT&3cHw=dU8Kq?x46~ z*`=TbnwOkPH3B%+tlT1!(X}N&mP>>wTT7R9mXdBrJ5%d{Szg|a%RIQRE~KsU1HK2C z{G{?j>)Th?)^UIQK6e_Ew1Us_%eRA9UdcRR-FWx&jbjQQ+w*TRacUIEUYR#<-nt5# zI#%(cZ`QnTI?+GXE43sq@7>-8=6Frb5Aypgp2#-uF`lq}G2_+sSF=iwn01P91mC@T zckaoQPeyfTOeER;ALV+~yv@!r(c|0ez+lnRY2AM~WtHfD#Xv6Qr*BrEb32{v+<|#N*d5UEqB4 z_N{IE=}G<0afifrZaq5V$n?{vPn(O2i=XcLnbc4(5H48$Kjpp_-tBXri8aLdWb`6-kMlrLEPRhi+M{)3YjnLBE3=luQkS7e1(PIHAvMRV-- z4?BMB4D?W&?5QTy>G($a(6-f%XY`*`^kmj8GCC`=!gIlNHsP{{KS$#Xd8 zFjj5+x52CNnaYo~Pi*DRO+Wp#y4C5Sx2+`WucKLYe^vbCe1aBKe%PfIv_NkXBN|FI`Ts$0m)@>45(VgN@c}Q2_z0Ga`fePK!!n!RD z!gtm?{dS7ucI}?j@-AeS>zl1xoEoDH*rvBIH7BoMS`^Lw<=*lp`wqPf#neopJI9!J z^!|SJcI#F4`iN68TXeZZj%v@J!}L_LJFFm9!JffLsPH?pg6sQRKL4gSyb5(Vk|xQ> owZ*3+%NiKeXEs&PpIy%|kLB*#7@;@nz;cbj)78&qol`;+0MQagQ@Z8<0T;y}9!ag@;Fk2eeV0qu z)tfop^2a?#}f2D$RW5Qh~?cXo*OvcbQst(>;SzXRc$o z7vLQ3RjwBN_ZGu_mz*C`g~1Q}R=@r7Fz4k(`39+l=^!eGqC1$ABdg2EZ8di$_2IqQfgCoJekh2 zuQ%em<)cMXyOtC$b8KeFNeDaV-Oey|m#o)aX3_iC7BMnxGtfP*>)h~bLs1ax<>~yn zd<@$Pbr0Jz$e3RIYS_fW=fPxfcuzp!qRIJ53>*iF_;l8vkvbu@;|0Tfr`*R1J2JIO)FgUK_&m%W1TiDA;GfQOECo7{8PD>Nnx&8Xx4BUBg}t$yzHre(7y zRPyCmYyJPQFEv{0%kBqr+;jcXt(1?SV@$i^r)kOH@Hvrz_6e16_ONGI Ye)~#cS#zNvFe5W~y85}Sb4q9e07bODCjbBd literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..135debcfa8e53248f5e4e829ad4def4d23ee7c27 GIT binary patch literal 4686 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/list_divider_holo_dark.png b/app/src/main/res/drawable-xhdpi/list_divider_holo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..387a95b70328b615ff3abce8e1a7d7cdef8ba210 GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm1|(O0oL2{=I14-?iy0WWg+Z8+Vb&Z8prAss zN02WAL!LSVL-BeBMz&2143)YJ41RAI7+FsKQy;{an^LB{Ts5okJ_? literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_av_upload.png b/app/src/main/res/drawable-xxhdpi/ic_av_upload.png new file mode 100644 index 0000000000000000000000000000000000000000..bef0fc4065e6447c8c6f8d2e2fff6b7c4ad194fa GIT binary patch literal 2109 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_access_video.png b/app/src/main/res/drawable-xxhdpi/ic_device_access_video.png new file mode 100644 index 0000000000000000000000000000000000000000..a255fb525ccdc089ec1fd93074e0987e0d7169ba GIT binary patch literal 1939 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 + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..f62bc38 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,61 @@ + + + + + + +