[
  {
    "path": ".github/contributing.md",
    "content": "# Contributing to Novoda's Open Source projects\n\nWe encourage everyone inside and outside Novoda to contribute to the projects using Github's pull requests.\n\n## Issuing a pull request\n\nGithub makes it really easy to create a pull request (PR) against a repo. Just fork it, implement your changes and create a pull request back to the original repo.\n\nThe PR should follow this format:\n\n  * The title of the PR should be a short sentence explaining the fix\n  * The PR description must contain at least two sections:\n    1. **The problem**: Explain what's the bug you're trying to solve or the missing feature you're trying to add with this PR.\n    2. **The solution**: Explain the fix or feature you've implemented in the PR. If this PR caused any UI change, then you should include screenshots or gifs showing how it looked before and after the change. See https://guides.github.com/features/mastering-markdown/ to create great looking markdown tables for showing your UI changes.\n    3. Feel free to include funny memes or gifs ;)\n\n\n## Writing tests\n\nWhen you issue a PR, please take some time to consider writing tests for the issue. For example if you're solving a bug, you could write the test that reproduces the bug first, then fix the issue. This makes sure the bug doesn't come back later.\n\nAlso make sure the project builds and all the tests pass before creating the PR.\n\nA non tested PR will not be merged back.\n\n\n## Code formatting\n\nWe use a pretty standard Java code formatting:\n\n  * Use spaces instead of tabs.\n  * Indenting as explained here: http://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS\n  * Use curly braces for everything, even for one line `if`, `for`, etc. statements.\n  * One line of white spaces between methods.\n  * One space before parenthesis, curly braces, equals, etc. Such as:\n    ```java\n    if (value == 2) {\n        value = 3;\n    }\n    ```\n\nTo make it easier we've made public the IDE settings we use internally so that you can import them if you want: https://github.com/novoda/novoda/tree/master/ide-settings\n"
  },
  {
    "path": ".github/issue_template.md",
    "content": "#### Problem\n\n_Explain the problem that requires addressing_\n\n#### Potential Solution\n\n_Explain high level any potential solutions to the problem as you see it_\n\n#### Impact\n\n_Explain high level what is the impact of fixing (or not fixing) this issue:\n- Are there any other components affected?\n- Any future feature can benefit from this?\n- Is it slowing down something?\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Problem\n\n_Explain what is requested to do, or the problem you had to fix_\n\n## Solution\n\n_Explain high level how have you implemented the task, add any quirks or interesting information_\n\n### Test(s) added \n\n_Explain what you did test and what you didn't, and why_\n\n### Screenshots\n\n| Before | After |\n| ------ | ----- |\n| gif/png _before_ | gif/png _after_ |\n\n### Paired with \n\n_Specify @github-handle @or-handles, or write \"Nobody\" if you did not pair_\n"
  },
  {
    "path": ".github/workflows/pull-request-builder.yml",
    "content": "name: Pull Request Builder\n\non:\n  pull_request:\n\nenv:\n  GRADLE_USER_HOME: .gradle\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout source-code\n        uses: actions/checkout@v1\n        with:\n          fetch-depth: 1\n\n      - name: Set up JDK 1.8\n        uses: actions/setup-java@v1\n        with:\n          java-version: 1.8\n\n      - name: Synchronise cache 1/2\n        uses: actions/cache@v1\n        with:\n          path: .gradle/wrapper\n          key: gradle-cache-wrapper-${{ hashFiles('core/build.gradle') }}\n\n      - name: Synchronise cache 2/2\n        uses: actions/cache@v1\n        with:\n          path: .gradle/caches\n          key: gradle-cache-caches-${{ hashFiles('core/build.gradle') }}\n\n      - name: Build\n        run: ./gradlew --no-daemon evaluateViolations lint test\n\n      - name: Gather results\n        if: success() || failure()\n        run: |\n          mkdir -p artifacts/core |\n          mkdir -p artifacts/demo |\n          cp -r core/build/reports/* artifacts/core |\n          cp -r demo/build/reports/* artifacts/demo\n\n      - name: Upload results\n        uses: actions/upload-artifact@v1.0.0\n        if: success() || failure()\n        with:\n          name: results\n          path: artifacts\n"
  },
  {
    "path": ".gitignore",
    "content": "# Built application files\n*.apk\n*.ap_\n\n# Files for the Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# IDEA/Android Studio ignores\n*.iml\n*.ipr\n*.iws\n**/.idea/\n\n# IDEA/Android Studio Ignore exceptions\n!/.idea/copyright/\n!/.idea/fileTemplates/\n!/.idea/inspectionProfiles/\n!/.idea/scopes/\n!/.idea/codeStyleSettings.xml\n!/.idea/compiler.xml\n!/.idea/encodings.xml\n!/.idea/vcs.xml\n\n# OSX\n*.DS_Store\n\n# Heap dump captures\ncaptures/\n"
  },
  {
    "path": ".idea/codeStyleSettings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectCodeStyleSettingsManager\">\n    <option name=\"PER_PROJECT_SETTINGS\">\n      <value>\n        <option name=\"CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND\" value=\"99\" />\n        <option name=\"NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND\" value=\"99\" />\n        <option name=\"IMPORT_LAYOUT_TABLE\">\n          <value>\n            <package name=\"android\" withSubpackages=\"true\" static=\"false\" />\n            <emptyLine />\n            <package name=\"com\" withSubpackages=\"true\" static=\"false\" />\n            <emptyLine />\n            <package name=\"javax\" withSubpackages=\"true\" static=\"false\" />\n            <package name=\"java\" withSubpackages=\"true\" static=\"false\" />\n            <emptyLine />\n            <package name=\"net\" withSubpackages=\"true\" static=\"false\" />\n            <emptyLine />\n            <package name=\"org\" withSubpackages=\"true\" static=\"false\" />\n            <emptyLine />\n            <package name=\"\" withSubpackages=\"true\" static=\"false\" />\n            <emptyLine />\n            <package name=\"\" withSubpackages=\"true\" static=\"true\" />\n          </value>\n        </option>\n        <option name=\"RIGHT_MARGIN\" value=\"150\" />\n        <AndroidXmlCodeStyleSettings>\n          <option name=\"USE_CUSTOM_SETTINGS\" value=\"true\" />\n          <option name=\"VALUE_RESOURCE_FILE_SETTINGS\">\n            <value>\n              <option name=\"WRAP_ATTRIBUTES\" value=\"4\" />\n            </value>\n          </option>\n          <option name=\"OTHER_SETTINGS\">\n            <value>\n              <option name=\"WRAP_ATTRIBUTES\" value=\"4\" />\n            </value>\n          </option>\n        </AndroidXmlCodeStyleSettings>\n        <Objective-C-extensions>\n          <file>\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Import\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Macro\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Typedef\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Enum\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Constant\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Global\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Struct\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"FunctionPredecl\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Function\" />\n          </file>\n          <class>\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Property\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"Synthesize\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"InitMethod\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"StaticMethod\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"InstanceMethod\" />\n            <option name=\"com.jetbrains.cidr.lang.util.OCDeclarationKind\" value=\"DeallocMethod\" />\n          </class>\n          <extensions>\n            <pair source=\"cpp\" header=\"h\" />\n            <pair source=\"c\" header=\"h\" />\n          </extensions>\n        </Objective-C-extensions>\n        <XML>\n          <option name=\"XML_ATTRIBUTE_WRAP\" value=\"2\" />\n          <option name=\"XML_KEEP_LINE_BREAKS\" value=\"false\" />\n          <option name=\"XML_KEEP_BLANK_LINES\" value=\"1\" />\n          <option name=\"XML_ALIGN_ATTRIBUTES\" value=\"false\" />\n          <option name=\"XML_SPACE_INSIDE_EMPTY_TAG\" value=\"true\" />\n          <option name=\"XML_LEGACY_SETTINGS_IMPORTED\" value=\"true\" />\n        </XML>\n        <editorconfig>\n          <option name=\"ENABLED\" value=\"false\" />\n        </editorconfig>\n        <codeStyleSettings language=\"JAVA\">\n          <option name=\"KEEP_BLANK_LINES_IN_DECLARATIONS\" value=\"1\" />\n          <option name=\"KEEP_BLANK_LINES_IN_CODE\" value=\"1\" />\n          <option name=\"KEEP_BLANK_LINES_BEFORE_RBRACE\" value=\"0\" />\n          <option name=\"BLANK_LINES_AFTER_CLASS_HEADER\" value=\"1\" />\n          <option name=\"IF_BRACE_FORCE\" value=\"3\" />\n          <option name=\"DOWHILE_BRACE_FORCE\" value=\"3\" />\n          <option name=\"WHILE_BRACE_FORCE\" value=\"3\" />\n          <option name=\"FOR_BRACE_FORCE\" value=\"3\" />\n        </codeStyleSettings>\n        <codeStyleSettings language=\"XML\">\n          <indentOptions>\n            <option name=\"INDENT_SIZE\" value=\"2\" />\n            <option name=\"CONTINUATION_INDENT_SIZE\" value=\"2\" />\n            <option name=\"TAB_SIZE\" value=\"2\" />\n          </indentOptions>\n          <arrangement>\n            <rules>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>xmlns:android</NAME>\n                      <XML_NAMESPACE>^$</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>xmlns:.*</NAME>\n                      <XML_NAMESPACE>^$</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                  <order>BY_NAME</order>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*:id</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*:name</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>name</NAME>\n                      <XML_NAMESPACE>^$</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>class</NAME>\n                      <XML_NAMESPACE>^$</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>layout</NAME>\n                      <XML_NAMESPACE>^$</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <NAME>style</NAME>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*:theme</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*:layout_width</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*:layout_height</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*:layout_.*</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*:layout_.*</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <NAME>.*</NAME>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*:layout_.*</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n              <section>\n                <rule>\n                  <match>\n                    <AND>\n                      <NAME>.*</NAME>\n                      <XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>\n                    </AND>\n                  </match>\n                </rule>\n              </section>\n            </rules>\n          </arrangement>\n        </codeStyleSettings>\n      </value>\n    </option>\n    <option name=\"PREFERRED_PROJECT_CODE_STYLE\" value=\"Novoda-v2\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/compiler.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"CompilerConfiguration\">\n    <resourceExtensions />\n    <wildcardResourcePatterns />\n    <annotationProcessing>\n      <profile default=\"true\" name=\"Default\" enabled=\"false\">\n        <outputRelativeToContentRoot value=\"true\" />\n        <processorPath useClasspath=\"true\" />\n      </profile>\n    </annotationProcessing>\n  </component>\n</project>\n"
  },
  {
    "path": ".idea/encodings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"Encoding\">\n    <file url=\"PROJECT\" charset=\"UTF-8\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "NOTICE",
    "content": "   Copyright 2017 Novoda\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# This project is no longer under maintenance - 13/7/2020\nUpgrades to latests version of `ExoPlayer` requires a significative amount of changes. The project is no longer maintained from our end.\n\n-----\n\n![noplayer](art/noplayer-header.png)\n\n[![CI status](https://github.com/novoda/no-player/workflows/Production%20Builder/badge.svg)](https://github.com/novoda/no-player/actions?query=workflow%3A%22Production+Builder%22) [![Download from Bintray](https://api.bintray.com/packages/novoda/maven/no-player/images/download.svg)](https://bintray.com/novoda/maven/no-player/_latestVersion) ![Tests](https://img.shields.io/jenkins/t/https/ci.novoda.com/view/Open%20source/job/no-player.svg) ![Coverage](https://img.shields.io/jenkins/j/https/ci.novoda.com/view/Open%20source/job/no-player.svg) [![Apache 2.0 Licence](https://img.shields.io/github/license/novoda/no-player.svg)](https://github.com/novoda/no-player/blob/master/LICENSE)\n\nA simplified Android `Player` wrapper for [MediaPlayer](https://developer.android.com/reference/android/media/MediaPlayer.html) and [ExoPlayer](https://google.github.io/ExoPlayer/).\n\n## Description\n\nSome of the benefits are:\n\n- Unified playback interface and event listeners for ExoPlayer and MediaPlayer\n- `MediaPlayer` buffering\n- `ExoPlayer` local, streaming and provisioning WideVine Modular DRM\n- Maintains video Aspect Ratio by default\n- Player selection based on `ContentType` and DRM\n\nExperimental Features, use with caution:\n- Support for TextureView\n\n## Adding to your project\n\nTo start using this library, add these lines to the `build.gradle` of your project:\n\n```groovy\nrepositories {\n    jcenter()\n}\n\ndependencies {\n    implementation 'com.novoda:no-player:<latest-version>'\n}\n```\n\nFrom no-player 4.5.0 this is also needed in the android section of your `build.gradle`\n\n```groovy\ncompileOptions {\n        targetCompatibility JavaVersion.VERSION_1_8\n}\n```\n\n### Simple usage\n\n 1. Create a `Player`:\n\n    ```java\n    Player player = new PlayerBuilder()\n      .withPriority(PlayerType.EXO_PLAYER)\n      .withWidevineModularStreamingDrm(drmHandler)\n      .build(this);\n    ```\n\n 2. Create the `PlayerView`:\n  \n    ```xml\n    <com.novoda.noplayer.NoPlayerView\n      android:id=\"@+id/player_view\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:layout_gravity=\"center\" />\n    ```\n\n 3. Attach to a `PlayerView`:\n\n    ```java\n    PlayerView playerView = findViewById(R.id.player_view);\n    player.attach(playerView);\n    ```\n\n\n 4. Play some content:\n\n    ```java\n    player.getListeners().addPreparedListener(playerState -> player.play());\n    \n    Uri uri = Uri.parse(mpdUrl);\n    player.loadVideo(uri, ContentType.DASH);\n    ```\n\n## Snapshots\n\n[![CI status](https://github.com/novoda/no-player/workflows/Snapshot%20Builder/badge.svg)](https://github.com/novoda/no-player/actions?query=workflow%3A%22Snapshot+Builder%22) [![Download from Bintray](https://api.bintray.com/packages/novoda-oss/snapshots/no-player/images/download.svg)](https://bintray.com/novoda-oss/snapshots/no-player/_latestVersion)\n\nSnapshot builds from [`develop`](https://github.com/novoda/no-player/compare/master...develop) are automatically deployed to a [repository](https://bintray.com/novoda-oss/snapshots/no-player/_latestVersion) that is not synced with JCenter.\nTo consume a snapshot build add an additional maven repo as follows:\n```\nrepositories {\n    maven {\n        url 'https://dl.bintray.com/novoda-oss/snapshots/'\n    }\n}\n```\n\nYou can find the latest snapshot version following this [link](https://bintray.com/novoda-oss/snapshots/no-player/_latestVersion).\n\n## Contributing\n\nWe always welcome people to contribute new features or bug fixes, [here is how](https://github.com/novoda/novoda/blob/master/CONTRIBUTING.md).\n\nIf you have a problem, check the [Issues Page](https://github.com/novoda/no-player/issues) first to see if we are already working on it.\n\nLooking for community help? Browse the already asked [Stack Overflow Questions](http://stackoverflow.com/questions/tagged/support-no-player) or use the tag `support-no-player` when posting a new question.\n"
  },
  {
    "path": "build.gradle",
    "content": "allprojects {\n    version = '4.5.4'\n}\n\ndef teamPropsFile(propsFile) {\n    def teamPropsDir = rootProject.file('team-props')\n    return new File(teamPropsDir, propsFile)\n}\n\nbuildscript {\n    repositories {\n        google()\n        jcenter()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:3.3.1'\n        classpath 'com.novoda:bintray-release:0.9'\n        classpath 'com.novoda:gradle-static-analysis-plugin:0.8'\n        classpath 'com.novoda:gradle-build-properties-plugin:0.4.1'\n        classpath \"com.github.ben-manes:gradle-versions-plugin:0.20.0\"\n    }\n}\n\nsubprojects {\n    repositories {\n        google()\n        jcenter()\n    }\n\n    apply from: teamPropsFile('static-analysis.gradle')\n}\n\nallprojects {\n    apply plugin: 'com.github.ben-manes.versions'\n\n    dependencyUpdates.resolutionStrategy {\n        componentSelection { rules ->\n            rules.all { ComponentSelection selection ->\n                boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm'].any { qualifier ->\n                    selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\\d-]*/\n                }\n                if (rejected) {\n                    selection.reject('Release candidate')\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'bintray-release'\napply plugin: 'jacoco'\napply plugin: 'com.novoda.build-properties'\n\nbuildProperties {\n    cli {\n        using(project)\n    }\n    bintray {\n        def bintrayCredentials = {\n            boolean isDryRun = cli['dryRun'].or(true).boolean\n            return isDryRun ?\n                    ['bintrayRepo': 'n/a', 'bintrayUser': 'n/a', 'bintrayKey': 'n/a'] :\n                    new File(\"${System.getenv('BINTRAY_PROPERTIES')}\")\n        }\n        using(bintrayCredentials()).or(cli)\n        description = '''This should contain the following properties:\n                       | - bintrayRepo: name of the repo of the organisation to deploy the artifacts to\n                       | - bintrayUser: name of the account used to deploy the artifacts\n                       | - bintrayKey: API key of the account used to deploy the artifacts\n        '''.stripMargin()\n    }\n    publish {\n        def generateVersion = {\n            boolean isSnapshot = cli['bintraySnapshot'].or(false).boolean\n            if (isSnapshot) {\n                return \"SNAPSHOT-${System.getenv('BUILD_NUMBER') ?: 'LOCAL'}\";\n            }\n            boolean isExperimental = cli['bintrayExperimental'].or(false).boolean\n            if (isExperimental) {\n                return \"EXPERIMENTAL-${System.getenv('BUILD_NUMBER') ?: 'LOCAL'}\";\n            }\n            return version\n        }\n        using(['version': \"${generateVersion()}\"])\n                .or(buildProperties.bintray)\n    }\n}\n\nandroid {\n    compileSdkVersion 28\n    buildToolsVersion '28.0.3'\n\n    defaultConfig {\n        minSdkVersion 16\n        testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'\n    }\n\n    lintOptions {\n        lintConfig teamPropsFile('static-analysis/lint-config.xml')\n        abortOnError true\n        warningsAsErrors true\n    }\n\n    compileOptions {\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\n\ndependencies {\n    implementation 'com.google.android.exoplayer:exoplayer:2.9.6'\n\n    implementation 'com.android.support:support-annotations:28.0.0'\n\n    testImplementation 'junit:junit:4.12'\n    testImplementation 'org.mockito:mockito-core:2.27.0'\n    testImplementation 'org.easytesting:fest-assert-core:2.0M10'\n}\n\npublish {\n    userOrg = 'novoda'\n    repoName = buildProperties.publish['bintrayRepo'].string\n    groupId = 'com.novoda'\n    artifactId = 'no-player'\n    version = buildProperties.publish['version'].string\n    bintrayUser = buildProperties.publish['bintrayUser'].string\n    bintrayKey = buildProperties.publish['bintrayKey'].string\n    publishVersion = version\n    uploadName = 'no-player'\n    desc = 'player to wrap players'\n    website = 'https://github.com/novoda/no-player'\n}\n"
  },
  {
    "path": "core/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  package=\"com.novoda.noplayer\">\n\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n  <uses-permission android:name=\"android.permission.INTERNET\" />\n  <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n  <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\n  <application />\n\n</manifest>\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/AndroidMediaPlayerCapabilities.java",
    "content": "package com.novoda.noplayer;\n\nimport com.novoda.noplayer.drm.DrmType;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nclass AndroidMediaPlayerCapabilities implements PlayerCapabilities {\n\n    private static final List<DrmType> SUPPORTED_DRM_TYPES = Arrays.asList(DrmType.NONE, DrmType.WIDEVINE_CLASSIC);\n\n    @Override\n    public boolean supports(DrmType drmType) {\n        return SUPPORTED_DRM_TYPES.contains(drmType);\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/AspectRatioChangeCalculator.java",
    "content": "package com.novoda.noplayer;\n\nclass AspectRatioChangeCalculator {\n\n    private final Listener listener;\n\n    AspectRatioChangeCalculator(Listener listener) {\n        this.listener = listener;\n    }\n\n    void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {\n        float aspectRatio = determineAspectRatio(width, height, pixelWidthHeightRatio);\n        listener.onNewAspectRatio(aspectRatio);\n    }\n\n    private float determineAspectRatio(int videoWidth, int videoHeight, float pixelWidthHeightRatio) {\n        if (videoHeight == 0) {\n            return 1;\n        }\n        return (videoWidth * pixelWidthHeightRatio) / videoHeight;\n    }\n\n    interface Listener {\n\n        void onNewAspectRatio(float aspectRatio);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/ContentType.java",
    "content": "package com.novoda.noplayer;\n\npublic enum ContentType {\n    H264,\n    DASH,\n    HLS\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/DetailErrorType.java",
    "content": "package com.novoda.noplayer;\n\n/**\n * Assume all errors are thrown by Exo Player, MEDIA_PLAYER prefix will\n * indicate that the error is thrown by Media Player.\n */\npublic enum DetailErrorType {\n\n    // SOURCE,\n    LIVE_STALE_MANIFEST_AND_NEW_MANIFEST_COULD_NOT_LOAD_ERROR,\n    PARSING_MEDIA_DATA_OR_METADATA_ERROR,\n\n    AD_LOAD_ERROR_THEN_WILL_SKIP,\n    AD_GROUP_LOAD_ERROR_THEN_WILL_SKIP,\n    ALL_ADS_LOAD_ERROR_THEN_WILL_SKIP,\n    ADS_LOAD_UNEXPECTED_ERROR_THEN_WILL_SKIP,\n\n    CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT,\n    CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_NOT_SEEKABLE_TO_START,\n    CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_START_EXCEEDS_END,\n    CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_UNKNOWN_ERROR,\n    DATA_POSITION_OUT_OF_RANGE_ERROR,\n\n    SAMPLE_QUEUE_MAPPING_ERROR,\n    READING_LOCAL_FILE_ERROR,\n    UNEXPECTED_LOADING_ERROR,\n    DOWNLOAD_ERROR,\n    MERGING_MEDIA_SOURCE_CANNOT_MERGE_ITS_SOURCES,\n    TASK_CANNOT_PROCEED_PRIORITY_TOO_LOW,\n    CACHE_WRITING_DATA_ERROR,\n    READ_LOCAL_ASSET_ERROR,\n\n    MEDIA_PLAYER_IO,\n    MEDIA_PLAYER_MALFORMED,\n    MEDIA_PLAYER_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,\n    MEDIA_PLAYER_INFO_NOT_SEEKABLE,\n    MEDIA_PLAYER_SUBTITLE_TIMED_OUT,\n    MEDIA_PLAYER_UNSUPPORTED_SUBTITLE,\n\n    // CONNECTIVITY\n    HTTP_CANNOT_OPEN_ERROR,\n    HTTP_CANNOT_READ_ERROR,\n    HTTP_CANNOT_CLOSE_ERROR,\n    READ_CONTENT_URI_ERROR,\n    READ_FROM_UDP_ERROR,\n    HLS_PLAYLIST_STUCK_SERVER_SIDE_ERROR,\n    HLS_PLAYLIST_SERVER_HAS_RESET,\n    MEDIA_PLAYER_TIMED_OUT,\n\n    // DRM\n    UNSUPPORTED_DRM_SCHEME_ERROR,\n    DRM_INSTANTIATION_ERROR,\n    DRM_UNKNOWN_ERROR,\n    CANNOT_ACQUIRE_DRM_SESSION_MISSING_SCHEME_FOR_REQUIRED_UUID_ERROR,\n    DRM_SESSION_ERROR,\n    DRM_KEYS_EXPIRED_ERROR,\n    MEDIA_REQUIRES_DRM_SESSION_MANAGER_ERROR,\n    MEDIA_PLAYER_SERVER_DIED,\n    MEDIA_PLAYER_PREPARE_DRM_STATUS_PREPARATION_ERROR,\n    MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,\n    MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,\n\n    // CONTENT_DECRYPTION\n    FAIL_DECRYPT_DATA_DUE_NON_PLATFORM_COMPONENT_ERROR,\n    INSUFFICIENT_OUTPUT_PROTECTION_ERROR,\n    KEY_EXPIRED_ERROR,\n    KEY_NOT_FOUND_WHEN_DECRYPTION_ERROR,\n    RESOURCE_BUSY_ERROR_THEN_SHOULD_RETRY,\n    ATTEMPTED_ON_CLOSED_SEDDION_ERROR,\n    LICENSE_POLICY_REQUIRED_NOT_SUPPORTED_BY_DEVICE_ERROR,\n\n    // RENDERER_DECODER\n    AUDIO_SINK_CONFIGURATION_ERROR,\n    AUDIO_SINK_INITIALISATION_ERROR,\n    AUDIO_SINK_WRITE_ERROR,\n    AUDIO_UNHANDLED_FORMAT_ERROR,\n    AUDIO_DECODER_ERROR,\n    INITIALISATION_ERROR,\n    DECODING_SUBTITLE_ERROR,\n    MEDIA_PLAYER_INFO_AUDIO_NOT_PLAYING,\n    MEDIA_PLAYER_BAD_INTERLEAVING,\n    MEDIA_PLAYER_INFO_VIDEO_NOT_PLAYING,\n    MEDIA_PLAYER_INFO_VIDEO_TRACK_LAGGING,\n    UNEXPECTED_CODEC_ERROR,\n\n    // UNEXPECTED\n    EGL_OPERATION_ERROR,\n    SPURIOUS_AUDIO_TRACK_TIMESTAMP_ERROR,\n    MULTIPLE_RENDERER_MEDIA_CLOCK_ENABLED_ERROR,\n\n    UNKNOWN,\n    MEDIA_PLAYER_UNKNOWN\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/ExoPlayerCapabilities.java",
    "content": "package com.novoda.noplayer;\n\nimport com.novoda.noplayer.drm.DrmType;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nclass ExoPlayerCapabilities implements PlayerCapabilities {\n\n    private static final List<DrmType> SUPPORTED_DRM_TYPES = Arrays.asList(\n            DrmType.NONE,\n            DrmType.WIDEVINE_MODULAR_STREAM,\n            DrmType.WIDEVINE_MODULAR_DOWNLOAD\n    );\n\n    @Override\n    public boolean supports(DrmType drmType) {\n        return SUPPORTED_DRM_TYPES.contains(drmType);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/Listeners.java",
    "content": "package com.novoda.noplayer;\n\npublic interface Listeners {\n\n    /**\n     * Add an {@link NoPlayer.ErrorListener} to be notified of Player errors.\n     *\n     * @param errorListener to notify.\n     */\n    void addErrorListener(NoPlayer.ErrorListener errorListener);\n\n    /**\n     * Remove a given {@link NoPlayer.ErrorListener}.\n     *\n     * @param errorListener to remove.\n     */\n    void removeErrorListener(NoPlayer.ErrorListener errorListener);\n\n    /**\n     * Add a {@link NoPlayer.PreparedListener} to be notified when the {@link NoPlayer}\n     * is prepared and playback can begin.\n     *\n     * @param preparedListener to notify.\n     */\n    void addPreparedListener(NoPlayer.PreparedListener preparedListener);\n\n    /**\n     * Remove a given {@link NoPlayer.PreparedListener}.\n     *\n     * @param preparedListener to remove.\n     */\n    void removePreparedListener(NoPlayer.PreparedListener preparedListener);\n\n    /**\n     * Add a {@link NoPlayer.BufferStateListener} to be notified of buffer state events.\n     *\n     * @param bufferStateListener to notify.\n     */\n    void addBufferStateListener(NoPlayer.BufferStateListener bufferStateListener);\n\n    /**\n     * Remove a given {@link NoPlayer.BufferStateListener}.\n     *\n     * @param bufferStateListener to remove.\n     */\n    void removeBufferStateListener(NoPlayer.BufferStateListener bufferStateListener);\n\n    /**\n     * Add a {@link NoPlayer.CompletionListener} to be notified when a media asset has completed playback.\n     *\n     * @param completionListener to notify.\n     */\n    void addCompletionListener(NoPlayer.CompletionListener completionListener);\n\n    /**\n     * Remove a given {@link NoPlayer.CompletionListener}.\n     *\n     * @param completionListener to remove.\n     */\n    void removeCompletionListener(NoPlayer.CompletionListener completionListener);\n\n    /**\n     * Add a {@link NoPlayer.StateChangedListener} to be notified of Player state changes.\n     * e.g. Play/Pause/Stop\n     *\n     * @param stateChangedListener to notify.\n     */\n    void addStateChangedListener(NoPlayer.StateChangedListener stateChangedListener);\n\n    /**\n     * Remove a given {@link NoPlayer.StateChangedListener}.\n     *\n     * @param stateChangedListener to remove.\n     */\n    void removeStateChangedListener(NoPlayer.StateChangedListener stateChangedListener);\n\n    /**\n     * Add an {@link NoPlayer.InfoListener} to be notified of internal player callbacks\n     * with additional information.\n     *\n     * @param infoListener to notify.\n     */\n    void addInfoListener(NoPlayer.InfoListener infoListener);\n\n    /**\n     * Remove a given {@link NoPlayer.InfoListener}.\n     *\n     * @param infoListener to remove.\n     */\n    void removeInfoListener(NoPlayer.InfoListener infoListener);\n\n    /**\n     * Add a {@link NoPlayer.BitrateChangedListener} to be notified of video and audio bitrate changes.\n     *\n     * @param bitrateChangedListener to notify.\n     */\n    void addBitrateChangedListener(NoPlayer.BitrateChangedListener bitrateChangedListener);\n\n    /**\n     * Remove a given {@link NoPlayer.BitrateChangedListener}.\n     *\n     * @param bitrateChangedListener to remove.\n     */\n    void removeBitrateChangedListener(NoPlayer.BitrateChangedListener bitrateChangedListener);\n\n    /**\n     * Add a {@link NoPlayer.HeartbeatCallback} to be notified on every tick of playback with a {@link NoPlayer}.\n     *\n     * @param heartbeatCallback to notify.\n     */\n    void addHeartbeatCallback(NoPlayer.HeartbeatCallback heartbeatCallback);\n\n    /**\n     * Remove a given {@link NoPlayer.HeartbeatCallback}.\n     *\n     * @param heartbeatCallback to remove.\n     */\n    void removeHeartbeatCallback(NoPlayer.HeartbeatCallback heartbeatCallback);\n\n    /**\n     * Add a {@link NoPlayer.VideoSizeChangedListener} to be notified whenever the video size changes.\n     *\n     * @param videoSizeChangedListener to notify.\n     */\n    void addVideoSizeChangedListener(NoPlayer.VideoSizeChangedListener videoSizeChangedListener);\n\n    /**\n     * Remove a given {@link NoPlayer.VideoSizeChangedListener}.\n     *\n     * @param videoSizeChangedListener to remove.\n     */\n    void removeVideoSizeChangedListener(NoPlayer.VideoSizeChangedListener videoSizeChangedListener);\n\n    /**\n     * Add a given {@link NoPlayer.DroppedVideoFramesListener} to be notified when video playback drops frames\n     *\n     * @param droppedVideoFramesListener to notify\n     */\n    void addDroppedVideoFrames(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListener);\n\n    /**\n     * Remove a given {@link NoPlayer.DroppedVideoFramesListener}.\n     *\n     * @param droppedVideoFramesListener to remove.\n     */\n    void removeDroppedVideoFrames(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListener);\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/NoPlayer.java",
    "content": "package com.novoda.noplayer;\n\nimport android.net.Uri;\nimport android.support.annotation.FloatRange;\n\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.Bitrate;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\nimport com.novoda.noplayer.model.Timeout;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic interface NoPlayer extends PlayerState {\n\n    /**\n     * Retrieves a holder, which allows you to add and remove listeners on the Player.\n     *\n     * @return {@link Listeners} holder.\n     */\n    Listeners getListeners();\n\n    /**\n     * Plays content of a prepared Player.\n     *\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     * @see NoPlayer.PreparedListener\n     */\n    void play() throws IllegalStateException;\n\n    /**\n     * Deprecated: This does not perform the way it was originally intended. A seek can, and most likely will,\n     * occur after the content has already started playing. This can lead to some unexpected behaviour.\n     * Plays content of a prepared Player at a given position. Use {@link #loadVideo(Uri, Options)} passing\n     * a initial position to the {@link Options}.\n     *\n     * @param positionInMillis to start playing content from.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     * @see NoPlayer.PreparedListener\n     */\n    @Deprecated\n    void playAt(long positionInMillis) throws IllegalStateException;\n\n    /**\n     * Pauses content of a prepared Player.\n     *\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     * @see NoPlayer.PreparedListener\n     */\n    void pause() throws IllegalStateException;\n\n    /**\n     * Seeks content of a prepared Player to a given position.\n     * Will not cause content to play if not already playing.\n     *\n     * @param positionInMillis to seek content to.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     * @see NoPlayer.PreparedListener\n     */\n    void seekTo(long positionInMillis) throws IllegalStateException;\n\n    /**\n     * Stops playback of content and then requires call to {@link NoPlayer#loadVideo(Uri, Options)} to continue playback.\n     */\n    void stop();\n\n    /**\n     * Stops playback of content and drops all internal resources. The instance of Player should not be\n     * used after calling release.\n     */\n    void release();\n\n    /**\n     * Loads the video content and triggers the {@link NoPlayer.PreparedListener}.\n     *\n     * @param uri     link to the content.\n     * @param options to be passed to the underlying player.\n     * @throws IllegalStateException - if called before {@link NoPlayer#attach(PlayerView)}.\n     */\n    void loadVideo(Uri uri, Options options) throws IllegalStateException;\n\n    /**\n     * Loads the video content and triggers the {@link NoPlayer.PreparedListener}.\n     *\n     * @param uri                 link to the content.\n     * @param options             to be passed to the underlying player.\n     * @param timeout             amount of time to wait before triggering {@link LoadTimeoutCallback}.\n     * @param loadTimeoutCallback callback when loading has hit the timeout.\n     * @throws IllegalStateException - if called before {@link NoPlayer#attach(PlayerView)}.\n     */\n    void loadVideoWithTimeout(Uri uri, Options options, Timeout timeout, LoadTimeoutCallback loadTimeoutCallback);\n\n    /**\n     * Supplies information about the underlying player.\n     *\n     * @return {@link PlayerInformation}.\n     */\n    PlayerInformation getPlayerInformation();\n\n    /**\n     * Attaches a given {@link PlayerView} to the Player.\n     *\n     * @param playerView for displaying video content.\n     */\n    void attach(PlayerView playerView);\n\n    /**\n     * Detaches a given {@link PlayerView} from the Player.\n     *\n     * @param playerView for displaying video content.\n     */\n    void detach(PlayerView playerView);\n\n    /**\n     * Retrieves all of the available {@link PlayerVideoTrack} of a prepared Player.\n     *\n     * @return a list of available {@link PlayerVideoTrack} of a prepared Player.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     * @see NoPlayer.PreparedListener\n     */\n    List<PlayerVideoTrack> getVideoTracks() throws IllegalStateException;\n\n    /**\n     * Selects a given {@link PlayerVideoTrack}.\n     *\n     * @param videoTrack the video track to select.\n     * @return whether the selection was successful.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     */\n    boolean selectVideoTrack(PlayerVideoTrack videoTrack) throws IllegalStateException;\n\n    /**\n     * Retrieves the currently playing {@link PlayerVideoTrack} of a prepared Player wrapped\n     * as an {@link Optional} or {@link Optional#absent()} if unavailable.\n     *\n     * @return {@link PlayerVideoTrack}.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     * @see NoPlayer.PreparedListener\n     */\n    Optional<PlayerVideoTrack> getSelectedVideoTrack() throws IllegalStateException;\n\n    /**\n     * Clears the {@link PlayerVideoTrack} selection made in {@link NoPlayer#selectVideoTrack(PlayerVideoTrack)}.\n     *\n     * @return whether the clear was successful.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     */\n    boolean clearVideoTrackSelection() throws IllegalStateException;\n\n    /**\n     * Retrieves all of the available {@link PlayerAudioTrack} of a prepared Player.\n     *\n     * @return {@link AudioTracks} that contains a list of available {@link PlayerAudioTrack}.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     * @see NoPlayer.PreparedListener\n     */\n    AudioTracks getAudioTracks() throws IllegalStateException;\n\n    /**\n     * Selects a given {@link PlayerAudioTrack}.\n     *\n     * @param audioTrack the audio track to select.\n     * @return whether the selection was successful.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     */\n    boolean selectAudioTrack(PlayerAudioTrack audioTrack) throws IllegalStateException;\n\n    /**\n     * Clears the {@link PlayerAudioTrack} selection made in {@link NoPlayer#selectAudioTrack(PlayerAudioTrack)}.\n     *\n     * @return whether the clear was successful.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     */\n    boolean clearAudioTrackSelection() throws IllegalStateException;\n\n    /**\n     * Retrieves all of the available {@link PlayerSubtitleTrack} of a prepared Player.\n     *\n     * @return A list of available {@link PlayerSubtitleTrack}.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     * @see NoPlayer.PreparedListener\n     */\n    List<PlayerSubtitleTrack> getSubtitleTracks() throws IllegalStateException;\n\n    /**\n     * Selects and shows a given {@link PlayerSubtitleTrack} on an attached PlayerView.\n     *\n     * @param subtitleTrack the subtitle track to select.\n     * @return whether the selection was successful.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     */\n    boolean showSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException;\n\n    /**\n     * Clear and hide the subtitles on an attached PlayerView.\n     *\n     * @return whether the hide was successful.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     */\n    boolean hideSubtitleTrack() throws IllegalStateException;\n\n    /**\n     * Set the Player to repeat the content.\n     *\n     * @param repeating true to set repeating, false to disable.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     */\n    void setRepeating(boolean repeating) throws IllegalStateException;\n\n    /**\n     * Set the audio volume, with 0 being silence and 1 being unity gain.\n     *\n     * @param volume The audio volume.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     */\n    void setVolume(@FloatRange(from = 0.0f, to = 1.0f) float volume) throws IllegalStateException;\n\n    /**\n     * Return the audio volume, with 0 being silence and 1 being unity gain.\n     *\n     * @return audio volume.\n     * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}.\n     */\n    @FloatRange(from = 0.0f, to = 1.0f)\n    float getVolume() throws IllegalStateException;\n\n    /**\n     * Clears the maximum video bitrate, if set.\n     */\n    void clearMaxVideoBitrate();\n\n    /**\n     * Sets a maximum video bitrate. If the content is playing, the video will switch to a different quality.\n     *\n     * @param maxVideoBitrate The maximum video bitrate in bit per second.\n     */\n    void setMaxVideoBitrate(int maxVideoBitrate);\n\n    interface PlayerError {\n\n        PlayerErrorType type();\n\n        DetailErrorType detailType();\n\n        String message();\n    }\n\n    interface ErrorListener {\n\n        void onError(PlayerError error);\n    }\n\n    interface PreparedListener {\n\n        void onPrepared(PlayerState playerState);\n    }\n\n    interface BufferStateListener {\n\n        void onBufferStarted();\n\n        void onBufferCompleted();\n    }\n\n    interface CompletionListener {\n\n        void onCompletion();\n    }\n\n    interface StateChangedListener {\n\n        void onVideoPlaying();\n\n        void onVideoPaused();\n\n        void onVideoStopped();\n    }\n\n    interface BitrateChangedListener {\n\n        void onBitrateChanged(Bitrate audioBitrate, Bitrate videoBitrate);\n    }\n\n    interface VideoSizeChangedListener {\n\n        void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio);\n    }\n\n    /**\n     * A listener for debugging information.\n     */\n    interface InfoListener {\n\n        /**\n         * All event listeners attached to implementations of {@link NoPlayer} will\n         * forward information through this to provide debugging\n         * information to client applications.\n         *\n         * @param callingMethod       The method name from where this call originated.\n         * @param callingMethodParams Parameter name and value pairs from where this call originated.\n         *                            Pass only string representations not whole objects.\n         */\n        void onNewInfo(String callingMethod, Map<String, String> callingMethodParams);\n    }\n\n    interface LoadTimeoutCallback {\n\n        LoadTimeoutCallback NULL_IMPL = new LoadTimeoutCallback() {\n            @Override\n            public void onLoadTimeout() {\n                // do nothing\n            }\n        };\n\n        void onLoadTimeout();\n    }\n\n    interface HeartbeatCallback {\n\n        void onBeat(NoPlayer player);\n    }\n\n    interface DroppedVideoFramesListener {\n\n        void onDroppedVideoFrames(int droppedFrames, long elapsedMsSinceLastDroppedFrames);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/NoPlayerCreator.java",
    "content": "package com.novoda.noplayer;\n\nimport android.content.Context;\n\nimport com.novoda.noplayer.drm.DrmHandler;\nimport com.novoda.noplayer.drm.DrmType;\nimport com.novoda.noplayer.internal.exoplayer.NoPlayerExoPlayerCreator;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorException;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorFactory;\nimport com.novoda.noplayer.internal.mediaplayer.NoPlayerMediaPlayerCreator;\n\nimport java.util.List;\n\nclass NoPlayerCreator {\n\n    private final Context context;\n    private final List<PlayerType> prioritizedPlayerTypes;\n    private final NoPlayerExoPlayerCreator noPlayerExoPlayerCreator;\n    private final NoPlayerMediaPlayerCreator noPlayerMediaPlayerCreator;\n    private final DrmSessionCreatorFactory drmSessionCreatorFactory;\n\n    NoPlayerCreator(Context context,\n                    List<PlayerType> prioritizedPlayerTypes,\n                    NoPlayerExoPlayerCreator noPlayerExoPlayerCreator,\n                    NoPlayerMediaPlayerCreator noPlayerMediaPlayerCreator,\n                    DrmSessionCreatorFactory drmSessionCreatorFactory) {\n        this.context = context;\n        this.prioritizedPlayerTypes = prioritizedPlayerTypes;\n        this.noPlayerExoPlayerCreator = noPlayerExoPlayerCreator;\n        this.noPlayerMediaPlayerCreator = noPlayerMediaPlayerCreator;\n        this.drmSessionCreatorFactory = drmSessionCreatorFactory;\n    }\n\n    NoPlayer create(DrmType drmType, DrmHandler drmHandler, boolean downgradeSecureDecoder, boolean allowCrossProtocolRedirects) {\n        for (PlayerType player : prioritizedPlayerTypes) {\n            if (player.supports(drmType)) {\n                return createPlayerForType(player, drmType, drmHandler, downgradeSecureDecoder, allowCrossProtocolRedirects);\n            }\n        }\n        throw UnableToCreatePlayerException.unhandledDrmType(drmType);\n    }\n\n    private NoPlayer createPlayerForType(PlayerType playerType,\n                                         DrmType drmType,\n                                         DrmHandler drmHandler,\n                                         boolean downgradeSecureDecoder,\n                                         boolean allowCrossProtocolRedirects) {\n        switch (playerType) {\n            case MEDIA_PLAYER:\n                return noPlayerMediaPlayerCreator.createMediaPlayer(context);\n            case EXO_PLAYER:\n                try {\n                    DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(drmType, drmHandler);\n                    return noPlayerExoPlayerCreator.createExoPlayer(\n                            context,\n                            drmSessionCreator,\n                            downgradeSecureDecoder,\n                            allowCrossProtocolRedirects\n                    );\n                } catch (DrmSessionCreatorException exception) {\n                    throw new UnableToCreatePlayerException(exception);\n                }\n            default:\n                throw UnableToCreatePlayerException.unhandledPlayerType(playerType);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/NoPlayerError.java",
    "content": "package com.novoda.noplayer;\n\npublic class NoPlayerError implements NoPlayer.PlayerError {\n\n    private final PlayerErrorType playerErrorType;\n    private final DetailErrorType detailErrorType;\n    private final String message;\n\n    public NoPlayerError(PlayerErrorType playerErrorType, DetailErrorType detailErrorType, String message) {\n        this.playerErrorType = playerErrorType;\n        this.detailErrorType = detailErrorType;\n        this.message = message;\n    }\n\n    @Override\n    public PlayerErrorType type() {\n        return playerErrorType;\n    }\n\n    @Override\n    public DetailErrorType detailType() {\n        return detailErrorType;\n    }\n\n    @Override\n    public String message() {\n        return message;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/NoPlayerView.java",
    "content": "package com.novoda.noplayer;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.SurfaceView;\nimport android.view.View;\nimport android.widget.FrameLayout;\nimport com.google.android.exoplayer2.ui.AspectRatioFrameLayout;\nimport com.novoda.noplayer.model.TextCues;\n\npublic class NoPlayerView extends FrameLayout implements AspectRatioChangeCalculator.Listener, PlayerView {\n\n    private final AspectRatioChangeCalculator aspectRatioChangeCalculator;\n\n    private AspectRatioFrameLayout videoFrame;\n    private SurfaceView surfaceView;\n    private SubtitleView subtitleView;\n    private View shutterView;\n    private PlayerSurfaceHolder playerSurfaceHolder;\n\n    public NoPlayerView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public NoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        aspectRatioChangeCalculator = new AspectRatioChangeCalculator(this);\n    }\n\n    @Override\n    protected void onFinishInflate() {\n        super.onFinishInflate();\n        View.inflate(getContext(), R.layout.noplayer_view, this);\n        videoFrame = findViewById(R.id.video_frame);\n        shutterView = findViewById(R.id.shutter);\n        surfaceView = findViewById(R.id.surface_view);\n        subtitleView = findViewById(R.id.subtitles_layout);\n        playerSurfaceHolder = PlayerSurfaceHolder.create(surfaceView);\n    }\n\n    @Override\n    public void onNewAspectRatio(float aspectRatio) {\n        videoFrame.setAspectRatio(aspectRatio);\n    }\n\n    @Override\n    public View getContainerView() {\n        return surfaceView;\n    }\n\n    @Override\n    public PlayerSurfaceHolder getPlayerSurfaceHolder() {\n        return playerSurfaceHolder;\n    }\n\n    @Override\n    public NoPlayer.VideoSizeChangedListener getVideoSizeChangedListener() {\n        return videoSizeChangedListener;\n    }\n\n    @Override\n    public NoPlayer.StateChangedListener getStateChangedListener() {\n        return stateChangedListener;\n    }\n\n    @Override\n    public void showSubtitles() {\n        subtitleView.setVisibility(VISIBLE);\n    }\n\n    @Override\n    public void hideSubtitles() {\n        subtitleView.setVisibility(GONE);\n    }\n\n    @Override\n    public void setSubtitleCue(TextCues textCues) {\n        subtitleView.setCues(textCues);\n    }\n\n    private final NoPlayer.VideoSizeChangedListener videoSizeChangedListener = new NoPlayer.VideoSizeChangedListener() {\n        @Override\n        public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n            aspectRatioChangeCalculator.onVideoSizeChanged(width, height, pixelWidthHeightRatio);\n        }\n    };\n\n    private final NoPlayer.StateChangedListener stateChangedListener = new NoPlayer.StateChangedListener() {\n        @Override\n        public void onVideoPlaying() {\n            shutterView.setVisibility(INVISIBLE);\n        }\n\n        @Override\n        public void onVideoPaused() {\n            // We don't care\n        }\n\n        @Override\n        public void onVideoStopped() {\n            shutterView.setVisibility(VISIBLE);\n        }\n    };\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/Options.java",
    "content": "package com.novoda.noplayer;\n\nimport com.novoda.noplayer.internal.utils.Optional;\n\n/**\n * Options to customise the underlying player.\n */\npublic class Options {\n\n    private final ContentType contentType;\n    private final int minDurationBeforeQualityIncreaseInMillis;\n    private final int maxInitialBitrate;\n    private final int maxVideoBitrate;\n    private final Optional<Long> initialPositionInMillis;\n\n    /**\n     * Creates a {@link OptionsBuilder} from this Options.\n     *\n     * @return a new instance of {@link OptionsBuilder}.\n     */\n    public OptionsBuilder toOptionsBuilder() {\n        OptionsBuilder optionsBuilder = new OptionsBuilder()\n                .withContentType(contentType)\n                .withMinDurationBeforeQualityIncreaseInMillis(minDurationBeforeQualityIncreaseInMillis)\n                .withMaxInitialBitrate(maxInitialBitrate)\n                .withMaxVideoBitrate(maxVideoBitrate);\n\n        if (initialPositionInMillis.isPresent()) {\n            optionsBuilder = optionsBuilder.withInitialPositionInMillis(initialPositionInMillis.get());\n        }\n        return optionsBuilder;\n    }\n\n    Options(ContentType contentType,\n            int minDurationBeforeQualityIncreaseInMillis,\n            int maxInitialBitrate,\n            int maxVideoBitrate,\n            Optional<Long> initialPositionInMillis) {\n        this.contentType = contentType;\n        this.minDurationBeforeQualityIncreaseInMillis = minDurationBeforeQualityIncreaseInMillis;\n        this.maxInitialBitrate = maxInitialBitrate;\n        this.maxVideoBitrate = maxVideoBitrate;\n        this.initialPositionInMillis = initialPositionInMillis;\n    }\n\n    public ContentType contentType() {\n        return contentType;\n    }\n\n    public int minDurationBeforeQualityIncreaseInMillis() {\n        return minDurationBeforeQualityIncreaseInMillis;\n    }\n\n    public int maxInitialBitrate() {\n        return maxInitialBitrate;\n    }\n\n    public int maxVideoBitrate() {\n        return maxVideoBitrate;\n    }\n\n    public Optional<Long> getInitialPositionInMillis() {\n        return initialPositionInMillis;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        Options options = (Options) o;\n\n        if (minDurationBeforeQualityIncreaseInMillis != options.minDurationBeforeQualityIncreaseInMillis) {\n            return false;\n        }\n        if (maxInitialBitrate != options.maxInitialBitrate) {\n            return false;\n        }\n        if (maxVideoBitrate != options.maxVideoBitrate) {\n            return false;\n        }\n        if (contentType != options.contentType) {\n            return false;\n        }\n        return initialPositionInMillis != null\n                ? initialPositionInMillis.equals(options.initialPositionInMillis) : options.initialPositionInMillis == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = contentType != null ? contentType.hashCode() : 0;\n        result = 31 * result + minDurationBeforeQualityIncreaseInMillis;\n        result = 31 * result + maxInitialBitrate;\n        result = 31 * result + maxVideoBitrate;\n        result = 31 * result + (initialPositionInMillis != null ? initialPositionInMillis.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"Options{\"\n                + \"contentType=\" + contentType\n                + \", minDurationBeforeQualityIncreaseInMillis=\" + minDurationBeforeQualityIncreaseInMillis\n                + \", maxInitialBitrate=\" + maxInitialBitrate\n                + \", maxVideoBitrate=\" + maxVideoBitrate\n                + \", initialPositionInMillis=\" + initialPositionInMillis\n                + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/OptionsBuilder.java",
    "content": "package com.novoda.noplayer;\n\nimport android.net.Uri;\n\nimport com.novoda.noplayer.internal.utils.Optional;\n\n/**\n * Builds instances of {@link Options} for {@link NoPlayer#loadVideo(Uri, Options)}.\n */\npublic class OptionsBuilder {\n\n    private static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;\n    private static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;\n    private static final int DEFAULT_MAX_VIDEO_BITRATE = Integer.MAX_VALUE;\n\n    private ContentType contentType = ContentType.H264;\n    private int minDurationBeforeQualityIncreaseInMillis = DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS;\n    private int maxInitialBitrate = DEFAULT_MAX_INITIAL_BITRATE;\n    private int maxVideoBitrate = DEFAULT_MAX_VIDEO_BITRATE;\n    private Optional<Long> initialPositionInMillis = Optional.absent();\n\n    /**\n     * Sets {@link OptionsBuilder} to build {@link Options} with a given {@link ContentType}.\n     * This content type is passed to the underlying Player.\n     *\n     * @param contentType format of the content.\n     * @return {@link OptionsBuilder}.\n     */\n    public OptionsBuilder withContentType(ContentType contentType) {\n        this.contentType = contentType;\n        return this;\n    }\n\n    /**\n     * Sets {@link OptionsBuilder} to build {@link Options} so that the {@link NoPlayer}\n     * switches to a higher quality video track after given time.\n     *\n     * @param minDurationInMillis time elapsed before switching to a higher quality video track.\n     * @return {@link OptionsBuilder}.\n     */\n    public OptionsBuilder withMinDurationBeforeQualityIncreaseInMillis(int minDurationInMillis) {\n        this.minDurationBeforeQualityIncreaseInMillis = minDurationInMillis;\n        return this;\n    }\n\n    /**\n     * Sets {@link OptionsBuilder} to build {@link Options} with given maximum initial bitrate in order to\n     * control what is the quality with which {@link NoPlayer} starts the playback. Setting a higher value\n     * allows the player to choose a higher quality video track at the beginning.\n     *\n     * @param maxInitialBitrate maximum bitrate that limits the initial track selection.\n     * @return {@link OptionsBuilder}.\n     */\n    public OptionsBuilder withMaxInitialBitrate(int maxInitialBitrate) {\n        this.maxInitialBitrate = maxInitialBitrate;\n        return this;\n    }\n\n    /**\n     * Sets {@link OptionsBuilder} to build {@link Options} with given maximum video bitrate in order to\n     * control what is the maximum video quality with which {@link NoPlayer} starts the playback. Setting a higher value\n     * allows the player to choose a higher quality video track.\n     *\n     * @param maxVideoBitrate maximum bitrate that limits the initial track selection.\n     * @return {@link OptionsBuilder}\n     */\n    public OptionsBuilder withMaxVideoBitrate(int maxVideoBitrate) {\n        this.maxVideoBitrate = maxVideoBitrate;\n        return this;\n    }\n\n    /**\n     * Sets {@link OptionsBuilder} to build {@link Options} with given initial position in millis in order\n     * to specify the start position of the content that will be played. Omitting to set this will start\n     * playback at the beginning of the content.\n     *\n     * @param initialPositionInMillis position that the content should begin playback at.\n     * @return {@link OptionsBuilder}.\n     */\n    public OptionsBuilder withInitialPositionInMillis(long initialPositionInMillis) {\n        this.initialPositionInMillis = Optional.of(initialPositionInMillis);\n        return this;\n    }\n\n    /**\n     * Builds a new {@link Options} instance.\n     *\n     * @return a {@link Options} instance.\n     */\n    public Options build() {\n        return new Options(\n                contentType,\n                minDurationBeforeQualityIncreaseInMillis,\n                maxInitialBitrate,\n                maxVideoBitrate,\n                initialPositionInMillis\n        );\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/PlayerBuilder.java",
    "content": "package com.novoda.noplayer;\n\nimport android.content.Context;\nimport android.os.Handler;\nimport android.os.Looper;\n\nimport com.novoda.noplayer.drm.DownloadedModularDrm;\nimport com.novoda.noplayer.drm.DrmHandler;\nimport com.novoda.noplayer.drm.DrmType;\nimport com.novoda.noplayer.drm.StreamingModularDrm;\nimport com.novoda.noplayer.internal.drm.provision.ProvisionExecutorCreator;\nimport com.novoda.noplayer.internal.exoplayer.NoPlayerExoPlayerCreator;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorFactory;\nimport com.novoda.noplayer.internal.mediaplayer.NoPlayerMediaPlayerCreator;\nimport com.novoda.noplayer.internal.utils.AndroidDeviceVersion;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * Builds instances of {@link NoPlayer} for given configurations.\n */\npublic class PlayerBuilder {\n\n    private DrmType drmType = DrmType.NONE;\n    private DrmHandler drmHandler = DrmHandler.NO_DRM;\n    private List<PlayerType> prioritizedPlayerTypes = Arrays.asList(PlayerType.EXO_PLAYER, PlayerType.MEDIA_PLAYER);\n    private boolean downgradeSecureDecoder; /* initialised to false by default */\n    private boolean allowCrossProtocolRedirects; /* initialised to false by default */\n    private String userAgent = \"user-agent\";\n\n    /**\n     * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which supports Widevine classic DRM.\n     *\n     * @return {@link PlayerBuilder}\n     * @see NoPlayer\n     */\n    public PlayerBuilder withWidevineClassicDrm() {\n        return withDrm(DrmType.WIDEVINE_CLASSIC, DrmHandler.NO_DRM);\n    }\n\n    /**\n     * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which supports Widevine modular streaming DRM.\n     *\n     * @param streamingModularDrm Implementation of {@link StreamingModularDrm}.\n     * @return {@link PlayerBuilder}\n     * @see NoPlayer\n     */\n    public PlayerBuilder withWidevineModularStreamingDrm(StreamingModularDrm streamingModularDrm) {\n        return withDrm(DrmType.WIDEVINE_MODULAR_STREAM, streamingModularDrm);\n    }\n\n    /**\n     * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which supports Widevine modular download DRM.\n     *\n     * @param downloadedModularDrm Implementation of {@link DownloadedModularDrm}.\n     * @return {@link PlayerBuilder}\n     * @see NoPlayer\n     */\n    public PlayerBuilder withWidevineModularDownloadDrm(DownloadedModularDrm downloadedModularDrm) {\n        return withDrm(DrmType.WIDEVINE_MODULAR_DOWNLOAD, downloadedModularDrm);\n    }\n\n    /**\n     * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which supports the specified parameters.\n     *\n     * @param drmType    {@link DrmType}\n     * @param drmHandler {@link DrmHandler}\n     * @return {@link PlayerBuilder}\n     * @see NoPlayer\n     */\n    public PlayerBuilder withDrm(DrmType drmType, DrmHandler drmHandler) {\n        this.drmType = drmType;\n        this.drmHandler = drmHandler;\n        return this;\n    }\n\n    /**\n     * Sets {@link PlayerBuilder} to build a {@link NoPlayer} which will prioritise the underlying player when\n     * multiple underlying players share the same features.\n     *\n     * @param playerType First {@link PlayerType} with the highest priority.\n     * @param playerTypes Remaining {@link PlayerType} in order of priority.\n     * @return {@link PlayerBuilder}\n     * @see NoPlayer\n     */\n    public PlayerBuilder withPriority(PlayerType playerType, PlayerType... playerTypes) {\n        List<PlayerType> types = new ArrayList<>();\n        types.add(playerType);\n        types.addAll(Arrays.asList(playerTypes));\n        prioritizedPlayerTypes = types;\n        return this;\n    }\n\n    /**\n     * Forces secure decoder selection to be ignored in favour of using an insecure decoder.\n     * e.g. Forcing an L3 stream to play with an insecure decoder instead of a secure decoder by default.\n     *\n     * @return {@link PlayerBuilder}\n     */\n    public PlayerBuilder withDowngradedSecureDecoder() {\n        downgradeSecureDecoder = true;\n        return this;\n    }\n\n    /**\n     * @param userAgent The application's user-agent value\n     * @return {@link PlayerBuilder}\n     */\n    public PlayerBuilder withUserAgent(String userAgent) {\n        this.userAgent = userAgent;\n        return this;\n    }\n\n    /**\n     * Network connections will be allowed to perform redirects between HTTP and HTTPS protocols\n     * @return {@link PlayerBuilder}\n     */\n    public PlayerBuilder allowCrossProtocolRedirects() {\n        allowCrossProtocolRedirects = true;\n        return this;\n    }\n\n    /**\n     * Builds a new {@link NoPlayer} instance.\n     *\n     * @param context The {@link Context} associated with the player.\n     * @return a {@link NoPlayer} instance.\n     * @throws UnableToCreatePlayerException thrown when the configuration is not supported and there is no way to recover.\n     * @see NoPlayer\n     */\n    public NoPlayer build(Context context) throws UnableToCreatePlayerException {\n        Context applicationContext = context.getApplicationContext();\n        Handler handler = new Handler(Looper.getMainLooper());\n        ProvisionExecutorCreator provisionExecutorCreator = new ProvisionExecutorCreator();\n        DrmSessionCreatorFactory drmSessionCreatorFactory = new DrmSessionCreatorFactory(\n                AndroidDeviceVersion.newInstance(),\n                provisionExecutorCreator,\n                handler\n        );\n        NoPlayerCreator noPlayerCreator = new NoPlayerCreator(\n                applicationContext,\n                prioritizedPlayerTypes,\n                NoPlayerExoPlayerCreator.newInstance(userAgent, handler),\n                NoPlayerMediaPlayerCreator.newInstance(handler),\n                drmSessionCreatorFactory\n        );\n        return noPlayerCreator.create(drmType, drmHandler, downgradeSecureDecoder, allowCrossProtocolRedirects);\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/PlayerCapabilities.java",
    "content": "package com.novoda.noplayer;\n\nimport com.novoda.noplayer.drm.DrmType;\n\ninterface PlayerCapabilities {\n\n    boolean supports(DrmType drmType);\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/PlayerErrorType.java",
    "content": "package com.novoda.noplayer;\n\npublic enum PlayerErrorType {\n\n    SOURCE,\n    CONNECTIVITY,\n    DRM,\n    CONTENT_DECRYPTION,\n    DEVICE_MEDIA_CAPABILITIES,\n    RENDERER_DECODER,\n    UNEXPECTED,\n    UNKNOWN\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/PlayerInformation.java",
    "content": "package com.novoda.noplayer;\n\npublic interface PlayerInformation {\n\n    PlayerType getPlayerType();\n\n    String getVersion();\n\n    String getName();\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/PlayerState.java",
    "content": "package com.novoda.noplayer;\n\npublic interface PlayerState {\n\n    boolean isPlaying();\n\n    int videoWidth();\n\n    int videoHeight();\n\n    long playheadPositionInMillis();\n\n    long mediaDurationInMillis();\n\n    int bufferPercentage();\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/PlayerSurfaceHolder.java",
    "content": "package com.novoda.noplayer;\n\nimport android.support.annotation.Nullable;\nimport android.view.SurfaceView;\nimport android.view.TextureView;\nimport com.google.android.exoplayer2.Player;\n\npublic class PlayerSurfaceHolder {\n\n    @Nullable\n    private final SurfaceView surfaceView;\n    @Nullable\n    private final TextureView textureView;\n    private final PlayerViewSurfaceHolder surfaceHolder;\n\n    public static PlayerSurfaceHolder create(SurfaceView surfaceView) {\n        PlayerViewSurfaceHolder surfaceHolder = new PlayerViewSurfaceHolder();\n        surfaceView.getHolder().addCallback(surfaceHolder);\n        return new PlayerSurfaceHolder(surfaceView, null, surfaceHolder);\n    }\n\n    public static PlayerSurfaceHolder create(TextureView textureView) {\n        PlayerViewSurfaceHolder surfaceHolder = new PlayerViewSurfaceHolder();\n        textureView.setSurfaceTextureListener(surfaceHolder);\n        return new PlayerSurfaceHolder(null, textureView, surfaceHolder);\n    }\n\n    PlayerSurfaceHolder(@Nullable SurfaceView surfaceView, @Nullable TextureView textureView, PlayerViewSurfaceHolder surfaceHolder) {\n        this.surfaceView = surfaceView;\n        this.textureView = textureView;\n        this.surfaceHolder = surfaceHolder;\n    }\n\n    public SurfaceRequester getSurfaceRequester() {\n        return surfaceHolder;\n    }\n\n    public void attach(Player.VideoComponent videoPlayer) {\n        if (containsSurfaceView()) {\n            videoPlayer.setVideoSurfaceView(surfaceView);\n        } else if (containsTextureView()) {\n            videoPlayer.setVideoTextureView(textureView);\n        } else {\n            throw new IllegalArgumentException(\"Surface container does not contain any of the expected views\");\n        }\n    }\n\n    private boolean containsSurfaceView() {\n        return surfaceView != null;\n    }\n\n    private boolean containsTextureView() {\n        return textureView != null;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/PlayerType.java",
    "content": "package com.novoda.noplayer;\n\nimport com.novoda.noplayer.drm.DrmType;\n\npublic enum PlayerType {\n    MEDIA_PLAYER(new AndroidMediaPlayerCapabilities()),\n    EXO_PLAYER(new ExoPlayerCapabilities());\n\n    private final PlayerCapabilities playerCapabilities;\n\n    PlayerType(PlayerCapabilities playerCapabilities) {\n        this.playerCapabilities = playerCapabilities;\n    }\n\n    boolean supports(DrmType drmType) {\n        return playerCapabilities.supports(drmType);\n    }\n\n    public static PlayerType from(String rawPlayerType) {\n        for (PlayerType playerType : values()) {\n            if (playerType.name().equalsIgnoreCase(rawPlayerType)) {\n                return playerType;\n            }\n        }\n        throw new UnknownPlayerTypeException(rawPlayerType);\n    }\n\n    static class UnknownPlayerTypeException extends RuntimeException {\n\n        UnknownPlayerTypeException(String rawPlayerType) {\n            super(\"Can't create PlayerType from : \" + rawPlayerType);\n        }\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/PlayerView.java",
    "content": "package com.novoda.noplayer;\n\nimport android.view.View;\nimport com.novoda.noplayer.model.TextCues;\n\npublic interface PlayerView {\n\n    View getContainerView();\n\n    PlayerSurfaceHolder getPlayerSurfaceHolder();\n\n    NoPlayer.VideoSizeChangedListener getVideoSizeChangedListener();\n\n    NoPlayer.StateChangedListener getStateChangedListener();\n\n    void showSubtitles();\n\n    void hideSubtitles();\n\n    void setSubtitleCue(TextCues textCues);\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/PlayerViewSurfaceHolder.java",
    "content": "package com.novoda.noplayer;\n\nimport android.graphics.SurfaceTexture;\nimport android.support.annotation.Nullable;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport android.view.TextureView;\nimport com.novoda.noplayer.model.Either;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nclass PlayerViewSurfaceHolder implements SurfaceHolder.Callback, TextureView.SurfaceTextureListener, SurfaceRequester {\n\n    private final List<Callback> callbacks = new ArrayList<>();\n    @Nullable\n    private Either<Surface, SurfaceHolder> eitherSurface;\n\n    @Override\n    public void surfaceCreated(SurfaceHolder surfaceHolder) {\n        this.eitherSurface = Either.right(surfaceHolder);\n        notifyListeners(eitherSurface);\n        callbacks.clear();\n    }\n\n    @Override\n    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {\n        // do nothing\n    }\n\n    @Override\n    public void surfaceDestroyed(SurfaceHolder holder) {\n        setSurfaceNotReady();\n        callbacks.clear();\n    }\n\n    @Override\n    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {\n        this.eitherSurface = Either.left(new Surface(surfaceTexture));\n        notifyListeners(eitherSurface);\n        callbacks.clear();\n    }\n\n    private void notifyListeners(Either<Surface, SurfaceHolder> either) {\n        for (Callback callback : callbacks) {\n            callback.onSurfaceReady(either);\n        }\n    }\n\n    @Override\n    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {\n        // do nothing\n    }\n\n    @Override\n    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {\n        setSurfaceNotReady();\n        surface.release();\n        callbacks.clear();\n        return true;\n    }\n\n    @Override\n    public void onSurfaceTextureUpdated(SurfaceTexture surface) {\n        // do nothing\n    }\n\n    private void setSurfaceNotReady() {\n        eitherSurface = null;\n    }\n\n    @Override\n    public void requestSurface(Callback callback) {\n        if (isSurfaceReady()) {\n            callback.onSurfaceReady(eitherSurface);\n        } else {\n            callbacks.add(callback);\n        }\n    }\n\n    private boolean isSurfaceReady() {\n        return eitherSurface != null;\n    }\n\n    @Override\n    public void removeCallback(Callback callback) {\n        callbacks.remove(callback);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/SubtitlePainter.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.novoda.noplayer;\n\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Paint.Join;\nimport android.graphics.Paint.Style;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.text.Layout.Alignment;\nimport android.text.SpannableStringBuilder;\nimport android.text.StaticLayout;\nimport android.text.TextPaint;\nimport android.text.TextUtils;\nimport android.text.style.AbsoluteSizeSpan;\nimport android.text.style.RelativeSizeSpan;\nimport android.util.DisplayMetrics;\nimport android.util.Log;\n\nimport com.google.android.exoplayer2.text.CaptionStyleCompat;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.util.Util;\nimport com.novoda.noplayer.model.NoPlayerCue;\n\n// Adopted code, could use some refactoring but it's a complex job\n@SuppressWarnings({\"PMD.GodClass\", \"PMD.CyclomaticComplexity\", \"PMD.StdCyclomaticComplexity\", \"PMD.ModifiedCyclomaticComplexity\"})\nfinal class SubtitlePainter {\n\n    private static final String TAG = \"SubtitlePainter\";\n\n    private static final float INNER_PADDING_RATIO = 0.125f;\n    private static final float ROUNDING_HALF_PIXEL = 0.5f;\n    private static final float TWO_DP = 2f;\n    private static final double FLOAT_COMPARISON_EPSILON = .0000001;\n\n    private final RectF lineBounds = new RectF();\n\n    // Styled dimensions.\n    private final float cornerRadius;\n    private final float outlineWidth;\n    private final float shadowRadius;\n    private final float shadowOffset;\n    private final float spacingMult;\n    private final float spacingAdd;\n\n    private final TextPaint textPaint;\n    private final Paint paint;\n\n    // Previous input variables.\n    private CharSequence cueText;\n    private Alignment cueTextAlignment;\n    private Bitmap cueBitmap;\n    private float cueLine;\n    @Cue.LineType\n    private int cueLineType;\n    @Cue.AnchorType\n    private int cueLineAnchor;\n    private float cuePosition;\n    @Cue.AnchorType\n    private int cuePositionAnchor;\n    private float cueSize;\n    private float cueBitmapHeight;\n    private boolean applyEmbeddedStyles;\n    private boolean applyEmbeddedFontSizes;\n    private int foregroundColor;\n    private int backgroundColor;\n    private int windowColor;\n    private int edgeColor;\n    @CaptionStyleCompat.EdgeType\n    private int edgeType;\n    private float textSizePx;\n    private float bottomPaddingFraction;\n    private int parentLeft;\n    private int parentTop;\n    private int parentRight;\n    private int parentBottom;\n\n    // Derived drawing variables.\n    private StaticLayout textLayout;\n    private int textLeft;\n    private int textTop;\n    private int textPaddingX;\n    private Rect bitmapRect;\n\n    @SuppressWarnings(\"ResourceType\")        // We're hacking `spacingMult = styledAttributes.getFloat`\n    SubtitlePainter(Context context) {\n        int[] viewAttr = {android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier};\n        TypedArray styledAttributes = context.obtainStyledAttributes(null, viewAttr, 0, 0);\n        spacingAdd = styledAttributes.getDimensionPixelSize(0, 0);\n        spacingMult = styledAttributes.getFloat(1, 1);\n        styledAttributes.recycle();\n\n        Resources resources = context.getResources();\n        DisplayMetrics displayMetrics = resources.getDisplayMetrics();\n        int twoDpInPx = Math.round((TWO_DP * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT);\n        cornerRadius = twoDpInPx;\n        outlineWidth = twoDpInPx;\n        shadowRadius = twoDpInPx;\n        shadowOffset = twoDpInPx;\n\n        textPaint = new TextPaint();\n        textPaint.setAntiAlias(true);\n        textPaint.setSubpixelText(true);\n\n        paint = new Paint();\n        paint.setAntiAlias(true);\n        paint.setStyle(Style.FILL);\n    }\n\n    @SuppressWarnings({\"checkstyle:ParameterNumber\", \"PMD.ExcessiveParameterList\"}) // TODO group parameters into classes\n    void draw(NoPlayerCue cue,\n              boolean applyEmbeddedStyles,\n              boolean applyEmbeddedFontSizes,\n              float textSizePx,\n              float bottomPaddingFraction,\n              Canvas canvas,\n              int cueBoxLeft,\n              int cueBoxTop,\n              int cueBoxRight,\n              int cueBoxBottom) {\n        boolean isTextCue = cue.bitmap() == null;\n        int windowColor = Color.BLACK;\n        if (isTextCue) {\n            if (TextUtils.isEmpty(cue.text())) {\n                // Nothing to draw.\n                return;\n            }\n            windowColor = (cue.windowColorSet() && applyEmbeddedStyles)\n                    ? cue.windowColor() : Color.TRANSPARENT;\n        }\n        if (nothingHasChanged(\n                cue,\n                applyEmbeddedStyles,\n                applyEmbeddedFontSizes,\n                textSizePx,\n                bottomPaddingFraction,\n                cueBoxLeft,\n                cueBoxTop,\n                cueBoxRight,\n                cueBoxBottom,\n                windowColor)) {\n            // We can use the cached layout.\n            drawLayout(canvas, isTextCue);\n            return;\n        }\n\n        this.cueText = cue.text();\n        this.cueTextAlignment = cue.textAlignment();\n        this.cueBitmap = cue.bitmap();\n        this.cueLine = cue.line();\n        this.cueLineType = cue.lineType();\n        this.cueLineAnchor = cue.lineAnchor();\n        this.cuePosition = cue.position();\n        this.cuePositionAnchor = cue.positionAnchor();\n        this.cueSize = cue.size();\n        this.cueBitmapHeight = cue.bitmapHeight();\n        this.applyEmbeddedStyles = applyEmbeddedStyles;\n        this.applyEmbeddedFontSizes = applyEmbeddedFontSizes;\n        this.foregroundColor = Color.WHITE;\n        this.backgroundColor = Color.BLACK;\n        this.windowColor = windowColor;\n        this.edgeType = 0;\n        this.edgeColor = Color.WHITE;\n        textPaint.setTypeface(null);\n        this.textSizePx = textSizePx;\n        this.bottomPaddingFraction = bottomPaddingFraction;\n        this.parentLeft = cueBoxLeft;\n        this.parentTop = cueBoxTop;\n        this.parentRight = cueBoxRight;\n        this.parentBottom = cueBoxBottom;\n\n        if (isTextCue) {\n            setupTextLayout();\n        } else {\n            setupBitmapLayout();\n        }\n        drawLayout(canvas, isTextCue);\n    }\n\n    @SuppressWarnings({\"checkstyle:ParameterNumber\", \"PMD.ExcessiveParameterList\"})     // TODO group parameters into classes\n    private boolean nothingHasChanged(NoPlayerCue cue,\n                                      boolean applyEmbeddedStyles,\n                                      boolean applyEmbeddedFontSizes,\n                                      float textSizePx,\n                                      float bottomPaddingFraction,\n                                      int cueBoxLeft,\n                                      int cueBoxTop,\n                                      int cueBoxRight,\n                                      int cueBoxBottom,\n                                      int windowColor) {\n        return areCharSequencesEqual(cueText, cue.text())\n                && Util.areEqual(cueTextAlignment, cue.textAlignment())\n                && cueBitmap == cue.bitmap()\n                && cueLine == cue.line()\n                && cueLineType == cue.lineType()\n                && Util.areEqual(cueLineAnchor, cue.lineAnchor())\n                && cuePosition == cue.position()\n                && Util.areEqual(cuePositionAnchor, cue.positionAnchor())\n                && cueSize == cue.size()\n                && cueBitmapHeight == cue.bitmapHeight()\n                && this.applyEmbeddedStyles == applyEmbeddedStyles\n                && this.applyEmbeddedFontSizes == applyEmbeddedFontSizes\n                && foregroundColor == Color.WHITE\n                && backgroundColor == Color.BLACK\n                && this.windowColor == windowColor\n                && edgeType == 0\n                && edgeColor == Color.WHITE\n                && Util.areEqual(textPaint.getTypeface(), null)\n                && this.textSizePx == textSizePx\n                && this.bottomPaddingFraction == bottomPaddingFraction\n                && parentLeft == cueBoxLeft\n                && parentTop == cueBoxTop\n                && parentRight == cueBoxRight\n                && parentBottom == cueBoxBottom;\n    }\n\n    @SuppressWarnings({\"PMD.ExcessiveMethodLength\", \"PMD.NPathComplexity\" })  // TODO break this method up\n    private void setupTextLayout() {\n        int parentWidth = parentRight - parentLeft;\n\n        textPaint.setTextSize(textSizePx);\n        int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + ROUNDING_HALF_PIXEL);\n\n        int availableWidth = parentWidth - textPaddingX * 2;\n        if (isCueDimensionSet(cueSize)) {\n            availableWidth *= cueSize;\n        }\n        if (availableWidth <= 0) {\n            Log.w(TAG, \"Skipped drawing subtitle cue (insufficient space)\");\n            return;\n        }\n\n        // Remove embedded styling or font size if requested.\n        CharSequence cueText;\n        if (applyEmbeddedFontSizes && applyEmbeddedStyles) {\n            cueText = this.cueText;\n        } else if (!applyEmbeddedStyles) {\n            cueText = this.cueText.toString(); // Equivalent to erasing all spans.\n        } else {\n            SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText);\n            int cueLength = newCueText.length();\n            AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class);\n            RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class);\n            for (AbsoluteSizeSpan absSpan : absSpans) {\n                newCueText.removeSpan(absSpan);\n            }\n            for (RelativeSizeSpan relSpan : relSpans) {\n                newCueText.removeSpan(relSpan);\n            }\n            cueText = newCueText;\n        }\n\n        Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment;\n        textLayout = new StaticLayout(cueText, textPaint, availableWidth, textAlignment, spacingMult,\n                spacingAdd, true);\n        int textWidth = 0;\n        int lineCount = textLayout.getLineCount();\n        for (int i = 0; i < lineCount; i++) {\n            textWidth = Math.max((int) Math.ceil(textLayout.getLineWidth(i)), textWidth);\n        }\n        if (isCueDimensionSet(cueSize) && textWidth < availableWidth) {\n            textWidth = availableWidth;\n        }\n        textWidth += textPaddingX * 2;\n\n        int textLeft;\n        int textRight;\n        if (isCueDimensionSet(cuePosition)) {\n            int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft;\n            textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth\n                    : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2\n                    : anchorPosition;\n            textLeft = Math.max(textLeft, parentLeft);\n            textRight = Math.min(textLeft + textWidth, parentRight);\n        } else {\n            textLeft = (parentWidth - textWidth) / 2;\n            textRight = textLeft + textWidth;\n        }\n\n        textWidth = textRight - textLeft;\n        if (textWidth <= 0) {\n            Log.w(TAG, \"Skipped drawing subtitle cue (invalid horizontal positioning)\");\n            return;\n        }\n\n        int parentHeight = parentBottom - parentTop;\n        int textHeight = textLayout.getHeight();\n\n        int textTop;\n        if (isCueDimensionSet(cueLine)) {\n            int anchorPosition;\n            if (cueLineType == Cue.LINE_TYPE_FRACTION) {\n                anchorPosition = Math.round(parentHeight * cueLine) + parentTop;\n            } else {\n                // cueLineType == Cue.LINE_TYPE_NUMBER\n                int firstLineHeight = textLayout.getLineBottom(0) - textLayout.getLineTop(0);\n                if (cueLine >= 0) {\n                    anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop;\n                } else {\n                    anchorPosition = Math.round((cueLine + 1) * firstLineHeight) + parentBottom;\n                }\n            }\n            textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight\n                    : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2\n                    : anchorPosition;\n            if (textTop + textHeight > parentBottom) {\n                textTop = parentBottom - textHeight;\n            } else if (textTop < parentTop) {\n                textTop = parentTop;\n            }\n        } else {\n            textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction);\n        }\n\n        // Update the derived drawing variables.\n        this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult,\n                spacingAdd, true);\n        this.textLeft = textLeft;\n        this.textTop = textTop;\n        this.textPaddingX = textPaddingX;\n    }\n\n    @SuppressWarnings(\"PMD.NPathComplexity\")  // TODO break this method up\n    private void setupBitmapLayout() {\n        int parentWidth = parentRight - parentLeft;\n        int parentHeight = parentBottom - parentTop;\n        float anchorX = parentLeft + (parentWidth * cuePosition);\n        float anchorY = parentTop + (parentHeight * cueLine);\n        int width = Math.round(parentWidth * cueSize);\n\n        int height = isCueDimensionSet(cueBitmapHeight)\n                ? Math.round(parentHeight * cueBitmapHeight)\n                : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()));\n\n        int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END\n                ? (anchorX - width)\n                : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2f)) : anchorX);\n\n        int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END\n                ? (anchorY - height)\n                : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2f)) : anchorY);\n\n        bitmapRect = new Rect(x, y, x + width, y + height);\n    }\n\n    private boolean isCueDimensionSet(float cueDimension) {\n        return Math.abs(cueDimension - Cue.DIMEN_UNSET) > FLOAT_COMPARISON_EPSILON;\n    }\n\n    private void drawLayout(Canvas canvas, boolean isTextCue) {\n        if (isTextCue) {\n            drawTextLayout(canvas);\n        } else {\n            drawBitmapLayout(canvas);\n        }\n    }\n\n    @SuppressWarnings(\"PMD.NPathComplexity\")  // TODO break this method up\n    private void drawTextLayout(Canvas canvas) {\n        StaticLayout layout = textLayout;\n        if (layout == null) {\n            // Nothing to draw.\n            return;\n        }\n\n        int saveCount = canvas.save();\n        canvas.translate(textLeft, textTop);\n\n        if (Color.alpha(windowColor) > 0) {\n            paint.setColor(windowColor);\n            canvas.drawRect(-textPaddingX, 0, layout.getWidth() + textPaddingX, layout.getHeight(),\n                    paint);\n        }\n\n        if (Color.alpha(backgroundColor) > 0) {\n            paint.setColor(backgroundColor);\n            float previousBottom = layout.getLineTop(0);\n            int lineCount = layout.getLineCount();\n            for (int i = 0; i < lineCount; i++) {\n                lineBounds.left = layout.getLineLeft(i) - textPaddingX;\n                lineBounds.right = layout.getLineRight(i) + textPaddingX;\n                lineBounds.top = previousBottom;\n                lineBounds.bottom = layout.getLineBottom(i);\n                previousBottom = lineBounds.bottom;\n                canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint);\n            }\n        }\n\n        if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) {\n            textPaint.setStrokeJoin(Join.ROUND);\n            textPaint.setStrokeWidth(outlineWidth);\n            textPaint.setColor(edgeColor);\n            textPaint.setStyle(Style.FILL_AND_STROKE);\n            layout.draw(canvas);\n        } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) {\n            textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor);\n        } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED\n                || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) {\n            boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED;\n            int colorUp = raised ? Color.WHITE : edgeColor;\n            int colorDown = raised ? edgeColor : Color.WHITE;\n            float offset = shadowRadius / 2;\n            textPaint.setColor(foregroundColor);\n            textPaint.setStyle(Style.FILL);\n            textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp);\n            layout.draw(canvas);\n            textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown);\n        }\n\n        textPaint.setColor(foregroundColor);\n        textPaint.setStyle(Style.FILL);\n        layout.draw(canvas);\n        textPaint.setShadowLayer(0, 0, 0, 0);\n\n        canvas.restoreToCount(saveCount);\n    }\n\n    private void drawBitmapLayout(Canvas canvas) {\n        canvas.drawBitmap(cueBitmap, null, bitmapRect, null);\n    }\n\n    /**\n     * This method is used instead of {@link TextUtils#equals(CharSequence, CharSequence)} because the\n     * latter only checks the text of each sequence, and does not check for equality of styling that\n     * may be embedded within the {@link CharSequence}s.\n     */\n    @SuppressWarnings(\"PMD.CompareObjectsWithEquals\")   // We do, but we first try to shortcut by comparing references\n    private static boolean areCharSequencesEqual(CharSequence first, CharSequence second) {\n        // Some CharSequence implementations don't perform a cheap referential equality check in their\n        // equals methods, so we perform one explicitly here.\n        return first == second || (first != null && first.equals(second));\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/SubtitleView.java",
    "content": "package com.novoda.noplayer;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport com.novoda.noplayer.model.TextCues;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic final class SubtitleView extends View {\n\n    private static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f;\n    private static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;\n\n    private static final boolean APPLY_EMBEDDED_STYLES = true;\n    private static final boolean APPLY_EMBEDDED_FONT_STYLES = true;\n\n    private static final int ZERO_PIXELS = 0;\n\n    private final List<SubtitlePainter> painters;\n\n    private TextCues textCues;\n\n    public SubtitleView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        painters = new ArrayList<>();\n    }\n\n    public void setCues(TextCues textCues) {\n        if (textCues.equals(this.textCues)) {\n            return;\n        }\n\n        this.textCues = textCues;\n        int cueCount = textCues.size();\n\n        while (painters.size() < cueCount) {\n            painters.add(new SubtitlePainter(getContext()));\n        }\n\n        invalidate();\n    }\n\n    @Override\n    public void dispatchDraw(Canvas canvas) {\n        if (textCues == null || textCues.isEmpty()) {\n            return;\n        }\n\n        int rawTop = getTop();\n        int rawBottom = getBottom();\n\n        int left = getLeft() + getPaddingLeft();\n        int top = rawTop + getPaddingTop();\n        int right = getRight() + getPaddingRight();\n        int bottom = rawBottom - getPaddingBottom();\n\n        if (bottom <= top || right <= left) {\n            return;\n        }\n\n        float textSizeInPixels = DEFAULT_TEXT_SIZE_FRACTION * (bottom - top);\n\n        if (textSizeInPixels <= ZERO_PIXELS) {\n            return;\n        }\n\n        int cueCount = textCues.size();\n        for (int i = 0; i < cueCount; i++) {\n            painters.get(i).draw(\n                    textCues.get(i),\n                    APPLY_EMBEDDED_STYLES,\n                    APPLY_EMBEDDED_FONT_STYLES,\n                    textSizeInPixels,\n                    DEFAULT_BOTTOM_PADDING_FRACTION,\n                    canvas,\n                    left,\n                    top,\n                    right,\n                    bottom\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/SurfaceRequester.java",
    "content": "package com.novoda.noplayer;\n\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport com.novoda.noplayer.model.Either;\n\npublic interface SurfaceRequester {\n\n    void requestSurface(Callback callback);\n\n    void removeCallback(Callback callback);\n\n    interface Callback {\n\n        void onSurfaceReady(Either<Surface, SurfaceHolder> surface);\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/UnableToCreatePlayerException.java",
    "content": "package com.novoda.noplayer;\n\nimport com.novoda.noplayer.drm.DrmType;\nimport com.novoda.noplayer.internal.utils.AndroidDeviceVersion;\n\npublic class UnableToCreatePlayerException extends RuntimeException {\n\n    static UnableToCreatePlayerException unhandledDrmType(DrmType drmType) {\n        return new UnableToCreatePlayerException(\"Unhandled DrmType: \" + drmType);\n    }\n\n    static UnableToCreatePlayerException unhandledPlayerType(PlayerType playerType) {\n        return new UnableToCreatePlayerException(\"Unhandled player type: \" + playerType.name());\n    }\n\n    public static UnableToCreatePlayerException deviceDoesNotMeetTargetApiException(DrmType drmType,\n                                                                                    int targetApiLevel,\n                                                                                    AndroidDeviceVersion actualApiLevel) {\n        return new UnableToCreatePlayerException(\n                \"Device must be target: \"\n                        + targetApiLevel\n                        + \" but was: \"\n                        + actualApiLevel.sdkInt()\n                        + \" for DRM type: \"\n                        + drmType.name()\n        );\n    }\n\n    UnableToCreatePlayerException(Throwable cause) {\n        super(cause);\n    }\n\n    private UnableToCreatePlayerException(String reason) {\n        super(reason);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/drm/DownloadedModularDrm.java",
    "content": "package com.novoda.noplayer.drm;\n\nimport com.novoda.noplayer.model.KeySetId;\n\npublic interface DownloadedModularDrm extends DrmHandler {\n\n    KeySetId getKeySetId();\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/drm/DrmHandler.java",
    "content": "package com.novoda.noplayer.drm;\n\n@SuppressWarnings({\n        \"checkstyle:interfaceistype\",\n        \"PMD.AvoidConstantsInterface\",\n        \"PMD.ConstantsInInterface\"\n}) // This is to allow for multiple different types of DRM\npublic interface DrmHandler {\n\n    DrmHandler NO_DRM = new DrmHandler() {\n    };\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/drm/DrmType.java",
    "content": "package com.novoda.noplayer.drm;\n\npublic enum DrmType {\n    NONE,\n    WIDEVINE_CLASSIC,\n    WIDEVINE_MODULAR_STREAM,\n    WIDEVINE_MODULAR_DOWNLOAD\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/drm/ModularDrmKeyRequest.java",
    "content": "package com.novoda.noplayer.drm;\n\nimport java.util.Arrays;\n\npublic class ModularDrmKeyRequest {\n\n    private final String url;\n    private final byte[] data;\n\n    public ModularDrmKeyRequest(String url, byte[] data) {\n        this.url = url;\n        this.data = Arrays.copyOf(data, data.length);\n    }\n\n    public String url() {\n        return url;\n    }\n\n    public byte[] data() {\n        return Arrays.copyOf(data, data.length);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        ModularDrmKeyRequest that = (ModularDrmKeyRequest) o;\n\n        if (url != null ? !url.equals(that.url) : that.url != null) {\n            return false;\n        }\n        return Arrays.equals(data, that.data);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = url != null ? url.hashCode() : 0;\n        result = 31 * result + Arrays.hashCode(data);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"ModularDrmKeyRequest{\"\n                + \"url='\" + url + '\\''\n                + \", data=\" + Arrays.toString(data)\n                + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/drm/ModularDrmProvisionRequest.java",
    "content": "package com.novoda.noplayer.drm;\n\nimport java.util.Arrays;\n\npublic class ModularDrmProvisionRequest {\n\n    private final String url;\n    private final byte[] data;\n\n    public ModularDrmProvisionRequest(String url, byte[] data) {\n        this.url = url;\n        this.data = Arrays.copyOf(data, data.length);\n    }\n\n    public String url() {\n        return url;\n    }\n\n    public byte[] data() {\n        return Arrays.copyOf(data, data.length);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        ModularDrmProvisionRequest that = (ModularDrmProvisionRequest) o;\n\n        if (url != null ? !url.equals(that.url) : that.url != null) {\n            return false;\n        }\n        return Arrays.equals(data, that.data);\n    }\n\n    @Override\n    public int hashCode() {\n        int result = url != null ? url.hashCode() : 0;\n        result = 31 * result + Arrays.hashCode(data);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"ModularDrmProvisionRequest{\"\n                + \"url='\" + url + '\\''\n                + \", data=\" + Arrays.toString(data)\n                + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/drm/StreamingModularDrm.java",
    "content": "package com.novoda.noplayer.drm;\n\npublic interface StreamingModularDrm extends DrmHandler {\n\n    byte[] executeKeyRequest(ModularDrmKeyRequest request) throws DrmRequestException;\n\n    final class DrmRequestException extends Exception {\n\n        public static DrmRequestException from(Exception e) {\n            return new DrmRequestException(\"Drm http request failed : \" + e.getMessage(), e);\n        }\n\n        public static DrmRequestException invalidHttpCode(int code, String body) {\n            return new DrmRequestException(\"Unexpected response HTTP code: \" + code + \" | \" + body);\n        }\n\n        private DrmRequestException(String detailMessage) {\n            super(detailMessage);\n        }\n\n        private DrmRequestException(String message, Throwable cause) {\n            super(message, cause);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/external/exoplayer/text/webvtt/CssParser.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.novoda.noplayer.external.exoplayer.text.webvtt;\n\nimport android.text.TextUtils;\n\nimport com.google.android.exoplayer2.text.webvtt.WebvttCssStyle;\nimport com.google.android.exoplayer2.util.ColorParser;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\nimport java.util.Arrays;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS\n * features.\n */\n/* package */ final class CssParser {\n\n  private static final String PROPERTY_BGCOLOR = \"background-color\";\n  private static final String PROPERTY_FONT_FAMILY = \"font-family\";\n  private static final String PROPERTY_FONT_WEIGHT = \"font-weight\";\n  private static final String PROPERTY_TEXT_DECORATION = \"text-decoration\";\n  private static final String VALUE_BOLD = \"bold\";\n  private static final String VALUE_UNDERLINE = \"underline\";\n  private static final String BLOCK_START = \"{\";\n  private static final String BLOCK_END = \"}\";\n  private static final String PROPERTY_FONT_STYLE = \"font-style\";\n  private static final String VALUE_ITALIC = \"italic\";\n\n  private static final Pattern VOICE_NAME_PATTERN = Pattern.compile(\"\\\\[voice=\\\"([^\\\"]*)\\\"\\\\]\");\n\n  // Temporary utility data structures.\n  private final ParsableByteArray styleInput;\n  private final StringBuilder stringBuilder;\n\n  public CssParser() {\n    styleInput = new ParsableByteArray();\n    stringBuilder = new StringBuilder();\n  }\n\n  /**\n   * Takes a CSS style block and consumes up to the first empty line found. Attempts to parse the\n   * contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or\n   * {@code null} otherwise.\n   *\n   * @param input The input from which the style block should be read.\n   * @return A {@link WebvttCssStyle} that represents the parsed block.\n   */\n  public WebvttCssStyle parseBlock(ParsableByteArray input) {\n    stringBuilder.setLength(0);\n    int initialInputPosition = input.getPosition();\n    skipStyleBlock(input);\n    styleInput.reset(input.data, input.getPosition());\n    styleInput.setPosition(initialInputPosition);\n    String selector = parseSelector(styleInput, stringBuilder);\n    if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) {\n      return null;\n    }\n    WebvttCssStyle style = new WebvttCssStyle();\n    applySelectorToStyle(style, selector);\n    String token = null;\n    boolean blockEndFound = false;\n    while (!blockEndFound) {\n      int position = styleInput.getPosition();\n      token = parseNextToken(styleInput, stringBuilder);\n      blockEndFound = token == null || BLOCK_END.equals(token);\n      if (!blockEndFound) {\n        styleInput.setPosition(position);\n        parseStyleDeclaration(styleInput, style, stringBuilder);\n      }\n    }\n    return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly.\n  }\n\n  /**\n   * Returns a string containing the selector. The input is expected to have the form\n   * {@code ::cue(tag#id.class1.class2[voice=\"someone\"]}, where every element is optional.\n   *\n   * @param input From which the selector is obtained.\n   * @return A string containing the target, empty string if the selector is universal\n   *     (targets all cues) or null if an error was encountered.\n   */\n  private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {\n    skipWhitespaceAndComments(input);\n    if (input.bytesLeft() < 5) {\n      return null;\n    }\n    String cueSelector = input.readString(5);\n    if (!\"::cue\".equals(cueSelector)) {\n      return null;\n    }\n    int position = input.getPosition();\n    String token = parseNextToken(input, stringBuilder);\n    if (token == null) {\n      return null;\n    }\n    if (BLOCK_START.equals(token)) {\n      input.setPosition(position);\n      return \"\";\n    }\n    String target = null;\n    if (\"(\".equals(token)) {\n      target = readCueTarget(input);\n    }\n    token = parseNextToken(input, stringBuilder);\n    if (!\")\".equals(token) || token == null) {\n      return null;\n    }\n    return target;\n  }\n\n  /**\n   * Reads the contents of ::cue() and returns it as a string.\n   */\n  private static String readCueTarget(ParsableByteArray input) {\n    int position = input.getPosition();\n    int limit = input.limit();\n    boolean cueTargetEndFound = false;\n    while (position < limit && !cueTargetEndFound) {\n      char c = (char) input.data[position++];\n      cueTargetEndFound = c == ')';\n    }\n    return input.readString(--position - input.getPosition()).trim();\n    // --offset to return ')' to the input.\n  }\n\n  private static void parseStyleDeclaration(ParsableByteArray input, WebvttCssStyle style,\n                                            StringBuilder stringBuilder) {\n    skipWhitespaceAndComments(input);\n    String property = parseIdentifier(input, stringBuilder);\n    if (\"\".equals(property)) {\n      return;\n    }\n    if (!\":\".equals(parseNextToken(input, stringBuilder))) {\n      return;\n    }\n    skipWhitespaceAndComments(input);\n    String value = parsePropertyValue(input, stringBuilder);\n    if (value == null || \"\".equals(value)) {\n      return;\n    }\n    int position = input.getPosition();\n    String token = parseNextToken(input, stringBuilder);\n    if (\";\".equals(token)) {\n      // The style declaration is well formed.\n    } else if (BLOCK_END.equals(token)) {\n      // The style declaration is well formed and we can go on, but the closing bracket had to be\n      // fed back.\n      input.setPosition(position);\n    } else {\n      // The style declaration is not well formed.\n      return;\n    }\n    // At this point we have a presumably valid declaration, we need to parse it and fill the style.\n    if (\"color\".equals(property)) {\n      style.setFontColor(ColorParser.parseCssColor(value));\n    } else if (PROPERTY_BGCOLOR.equals(property)) {\n      style.setBackgroundColor(ColorParser.parseCssColor(value));\n    } else if (PROPERTY_TEXT_DECORATION.equals(property)) {\n      if (VALUE_UNDERLINE.equals(value)) {\n        style.setUnderline(true);\n      }\n    } else if (PROPERTY_FONT_FAMILY.equals(property)) {\n      style.setFontFamily(value);\n    } else if (PROPERTY_FONT_WEIGHT.equals(property)) {\n      if (VALUE_BOLD.equals(value)) {\n        style.setBold(true);\n      }\n    } else if (PROPERTY_FONT_STYLE.equals(property)) {\n      if (VALUE_ITALIC.equals(value)) {\n        style.setItalic(true);\n      }\n    }\n    // TODO: Fill remaining supported styles.\n  }\n\n  // Visible for testing.\n  /* package */ static void skipWhitespaceAndComments(ParsableByteArray input) {\n    boolean skipping = true;\n    while (input.bytesLeft() > 0 && skipping) {\n      skipping = maybeSkipWhitespace(input) || maybeSkipComment(input);\n    }\n  }\n\n  // Visible for testing.\n  /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) {\n    skipWhitespaceAndComments(input);\n    if (input.bytesLeft() == 0) {\n      return null;\n    }\n    String identifier = parseIdentifier(input, stringBuilder);\n    if (!\"\".equals(identifier)) {\n      return identifier;\n    }\n    // We found a delimiter.\n    return \"\" + (char) input.readUnsignedByte();\n  }\n\n  private static boolean maybeSkipWhitespace(ParsableByteArray input) {\n    switch(peekCharAtPosition(input, input.getPosition())) {\n      case '\\t':\n      case '\\r':\n      case '\\n':\n      case '\\f':\n      case ' ':\n        input.skipBytes(1);\n        return true;\n      default:\n        return false;\n    }\n  }\n\n  // Visible for testing.\n  /* package */ static void skipStyleBlock(ParsableByteArray input) {\n    // The style block cannot contain empty lines, so we assume the input ends when a empty line\n    // is found.\n    String line;\n    do {\n      line = input.readLine();\n    } while (!TextUtils.isEmpty(line));\n  }\n\n  private static char peekCharAtPosition(ParsableByteArray input, int position) {\n    return (char) input.data[position];\n  }\n\n  private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) {\n    StringBuilder expressionBuilder = new StringBuilder();\n    String token;\n    int position;\n    boolean expressionEndFound = false;\n    // TODO: Add support for \"Strings in quotes with spaces\".\n    while (!expressionEndFound) {\n      position = input.getPosition();\n      token = parseNextToken(input, stringBuilder);\n      if (token == null) {\n        // Syntax error.\n        return null;\n      }\n      if (BLOCK_END.equals(token) || \";\".equals(token)) {\n        input.setPosition(position);\n        expressionEndFound = true;\n      } else {\n        expressionBuilder.append(token);\n      }\n    }\n    return expressionBuilder.toString();\n  }\n\n  private static boolean maybeSkipComment(ParsableByteArray input) {\n    int position = input.getPosition();\n    int limit = input.limit();\n    byte[] data = input.data;\n    if (position + 2 <= limit && data[position++] == '/' && data[position++] == '*') {\n      while (position + 1 < limit) {\n        char skippedChar = (char) data[position++];\n        if (skippedChar == '*') {\n          if (((char) data[position]) == '/') {\n            position++;\n            limit = position;\n          }\n        }\n      }\n      input.skipBytes(limit - input.getPosition());\n      return true;\n    }\n    return false;\n  }\n\n  private static String parseIdentifier(ParsableByteArray input, StringBuilder stringBuilder) {\n    stringBuilder.setLength(0);\n    int position = input.getPosition();\n    int limit = input.limit();\n    boolean identifierEndFound = false;\n    while (position  < limit && !identifierEndFound) {\n      char c = (char) input.data[position];\n      if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '#'\n          || c == '-' || c == '.' || c == '_') {\n        position++;\n        stringBuilder.append(c);\n      } else {\n        identifierEndFound = true;\n      }\n    }\n    input.skipBytes(position - input.getPosition());\n    return stringBuilder.toString();\n  }\n\n  /**\n   * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form\n   * {@code ::cue(tag#id.class1.class2[voice=\"someone\"]}, where every element is optional.\n   */\n  private void applySelectorToStyle(WebvttCssStyle style, String selector) {\n    if (\"\".equals(selector)) {\n      return; // Universal selector.\n    }\n    int voiceStartIndex = selector.indexOf('[');\n    if (voiceStartIndex != -1) {\n      Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartIndex));\n      if (matcher.matches()) {\n        style.setTargetVoice(matcher.group(1));\n      }\n      selector = selector.substring(0, voiceStartIndex);\n    }\n    String[] classDivision = selector.split(\"\\\\.\");\n    String tagAndIdDivision = classDivision[0];\n    int idPrefixIndex = tagAndIdDivision.indexOf('#');\n    if (idPrefixIndex != -1) {\n      style.setTargetTagName(tagAndIdDivision.substring(0, idPrefixIndex));\n      style.setTargetId(tagAndIdDivision.substring(idPrefixIndex + 1)); // We discard the '#'.\n    } else {\n      style.setTargetTagName(tagAndIdDivision);\n    }\n    if (classDivision.length > 1) {\n      style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length));\n    }\n  }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/external/exoplayer/text/webvtt/WebvttCueParser.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.novoda.noplayer.external.exoplayer.text.webvtt;\n\nimport android.graphics.Typeface;\nimport android.support.annotation.NonNull;\nimport android.text.Layout.Alignment;\nimport android.text.Spannable;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.TextUtils;\nimport android.text.style.AbsoluteSizeSpan;\nimport android.text.style.AlignmentSpan;\nimport android.text.style.BackgroundColorSpan;\nimport android.text.style.ForegroundColorSpan;\nimport android.text.style.RelativeSizeSpan;\nimport android.text.style.StrikethroughSpan;\nimport android.text.style.StyleSpan;\nimport android.text.style.TypefaceSpan;\nimport android.text.style.UnderlineSpan;\nimport android.util.Log;\n\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.webvtt.WebvttCssStyle;\nimport com.google.android.exoplayer2.text.webvtt.WebvttCue;\nimport com.google.android.exoplayer2.text.webvtt.WebvttParserUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\nimport com.novoda.noplayer.external.exoplayer.util.ColorParser;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Stack;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues)\n */\npublic final class WebvttCueParser {\n\n    public static final Pattern CUE_HEADER_PATTERN = Pattern\n        .compile(\"^(\\\\S+)\\\\s+-->\\\\s+(\\\\S+)(.*)?$\");\n\n    private static final Pattern CUE_SETTING_PATTERN = Pattern.compile(\"(\\\\S+?):(\\\\S+)\");\n\n    private static final char CHAR_LESS_THAN = '<';\n    private static final char CHAR_GREATER_THAN = '>';\n    private static final char CHAR_SLASH = '/';\n    private static final char CHAR_AMPERSAND = '&';\n    private static final char CHAR_SEMI_COLON = ';';\n    private static final char CHAR_SPACE = ' ';\n\n    private static final String ENTITY_LESS_THAN = \"lt\";\n    private static final String ENTITY_GREATER_THAN = \"gt\";\n    private static final String ENTITY_AMPERSAND = \"amp\";\n    private static final String ENTITY_NON_BREAK_SPACE = \"nbsp\";\n\n    private static final String TAG_BOLD = \"b\";\n    private static final String TAG_ITALIC = \"i\";\n    private static final String TAG_UNDERLINE = \"u\";\n    private static final String TAG_CLASS = \"c\";\n    private static final String TAG_VOICE = \"v\";\n    private static final String TAG_LANG = \"lang\";\n\n    private static final int STYLE_BOLD = Typeface.BOLD;\n    private static final int STYLE_ITALIC = Typeface.ITALIC;\n\n    private static final String TAG = \"WebvttCueParser\";\n\n    private final StringBuilder textBuilder;\n\n    public WebvttCueParser() {\n        textBuilder = new StringBuilder();\n    }\n\n    /**\n     * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text.\n     *\n     * @param webvttData Parsable WebVTT file data.\n     * @param builder    Builder for WebVTT Cues.\n     * @param styles     List of styles defined by the CSS style blocks preceeding the cues.\n     * @return Whether a valid Cue was found.\n     */\n    public boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder,\n                            List<WebvttCssStyle> styles) {\n        String firstLine = webvttData.readLine();\n        if (firstLine == null) {\n            return false;\n        }\n        Matcher cueHeaderMatcher = com.google.android.exoplayer2.text.webvtt.WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);\n        if (cueHeaderMatcher.matches()) {\n            // We have found the timestamps in the first line. No id present.\n            return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles);\n        }\n        // The first line is not the timestamps, but could be the cue id.\n        String secondLine = webvttData.readLine();\n        if (secondLine == null) {\n            return false;\n        }\n        cueHeaderMatcher = com.google.android.exoplayer2.text.webvtt.WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine);\n        if (cueHeaderMatcher.matches()) {\n            // We can do the rest of the parsing, including the id.\n            return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder,\n                            styles\n            );\n        }\n        return false;\n    }\n\n    /**\n     * Parses a string containing a list of cue settings.\n     *\n     * @param cueSettingsList String containing the settings for a given cue.\n     * @param builder         The {@link WebvttCue.Builder} where incremental construction takes place.\n     */\n  /* package */\n    static void parseCueSettingsList(String cueSettingsList,\n                                     WebvttCue.Builder builder) {\n        // Parse the cue settings list.\n        Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList);\n        while (cueSettingMatcher.find()) {\n            String name = cueSettingMatcher.group(1);\n            String value = cueSettingMatcher.group(2);\n            try {\n                if (\"line\".equals(name)) {\n                    parseLineAttribute(value, builder);\n                } else if (\"align\".equals(name)) {\n                    builder.setTextAlignment(parseTextAlignment(value));\n                } else if (\"position\".equals(name)) {\n                    parsePositionAttribute(value, builder);\n                } else if (\"size\".equals(name)) {\n                    builder.setWidth(WebvttParserUtil.parsePercentage(value));\n                } else {\n                    Log.w(TAG, \"Unknown cue setting \" + name + \":\" + value);\n                }\n            } catch (NumberFormatException e) {\n                Log.w(TAG, \"Skipping bad cue setting: \" + cueSettingMatcher.group());\n            }\n        }\n    }\n\n    /**\n     * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}.\n     *\n     * @param id      Id of the cue, {@code null} if it is not present.\n     * @param markup  The markup text to be parsed.\n     * @param styles  List of styles defined by the CSS style blocks preceeding the cues.\n     * @param builder Output builder.\n     */\n  /* package */\n    static void parseCueText(String id, String markup, WebvttCue.Builder builder,\n                             List<WebvttCssStyle> styles) {\n        SpannableStringBuilder spannedText = new SpannableStringBuilder();\n        Stack<StartTag> startTagStack = new Stack<>();\n        List<StyleMatch> scratchStyleMatches = new ArrayList<>();\n        int pos = 0;\n        while (pos < markup.length()) {\n            char curr = markup.charAt(pos);\n            switch (curr) {\n                case CHAR_LESS_THAN:\n                    if (pos + 1 >= markup.length()) {\n                        pos++;\n                        break; // avoid ArrayOutOfBoundsException\n                    }\n                    int ltPos = pos;\n                    boolean isClosingTag = markup.charAt(ltPos + 1) == CHAR_SLASH;\n                    pos = findEndOfTag(markup, ltPos + 1);\n                    boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH;\n                    String fullTagExpression = markup.substring(\n                        ltPos + (isClosingTag ? 2 : 1),\n                        isVoidTag ? pos - 2 : pos - 1\n                    );\n                    String tagName = getTagName(fullTagExpression);\n                    if (tagName == null || !isSupportedTag(tagName)) {\n                        continue;\n                    }\n                    if (isClosingTag) {\n                        StartTag startTag;\n                        do {\n                            if (startTagStack.isEmpty()) {\n                                break;\n                            }\n                            startTag = startTagStack.pop();\n                            applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches);\n                        } while (!startTag.name.equals(tagName));\n                    } else if (!isVoidTag) {\n                        startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length()));\n                    }\n                    break;\n                case CHAR_AMPERSAND:\n                    int semiColonEndIndex = markup.indexOf(CHAR_SEMI_COLON, pos + 1);\n                    int spaceEndIndex = markup.indexOf(CHAR_SPACE, pos + 1);\n                    int entityEndIndex = semiColonEndIndex == -1 ? spaceEndIndex\n                        : (spaceEndIndex == -1 ? semiColonEndIndex\n                        : Math.min(semiColonEndIndex, spaceEndIndex));\n                    if (entityEndIndex != -1) {\n                        applyEntity(markup.substring(pos + 1, entityEndIndex), spannedText);\n                        if (entityEndIndex == spaceEndIndex) {\n                            spannedText.append(\" \");\n                        }\n                        pos = entityEndIndex + 1;\n                    } else {\n                        spannedText.append(curr);\n                        pos++;\n                    }\n                    break;\n                default:\n                    spannedText.append(curr);\n                    pos++;\n                    break;\n            }\n        }\n        // apply unclosed tags\n        while (!startTagStack.isEmpty()) {\n            applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches);\n        }\n        applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles,\n                         scratchStyleMatches\n        );\n        builder.setText(spannedText);\n    }\n\n    private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData,\n                                    WebvttCue.Builder builder, StringBuilder textBuilder, List<WebvttCssStyle> styles) {\n        try {\n            // Parse the cue start and end times.\n            builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))\n                .setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)));\n        } catch (NumberFormatException e) {\n            Log.w(TAG, \"Skipping cue with bad header: \" + cueHeaderMatcher.group());\n            return false;\n        }\n\n        parseCueSettingsList(cueHeaderMatcher.group(3), builder);\n\n        // Parse the cue text.\n        textBuilder.setLength(0);\n        String line;\n        while (!TextUtils.isEmpty(line = webvttData.readLine())) {\n            if (textBuilder.length() > 0) {\n                textBuilder.append(\"\\n\");\n            }\n            textBuilder.append(line.trim());\n        }\n        parseCueText(id, textBuilder.toString(), builder, styles);\n        return true;\n    }\n\n    // Internal methods\n\n    private static void parseLineAttribute(String s, WebvttCue.Builder builder)\n        throws NumberFormatException {\n        int commaIndex = s.indexOf(',');\n        if (commaIndex != -1) {\n            builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));\n            s = s.substring(0, commaIndex);\n        } else {\n            builder.setLineAnchor(Cue.TYPE_UNSET);\n        }\n        if (s.endsWith(\"%\")) {\n            builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION);\n        } else {\n            int lineNumber = Integer.parseInt(s);\n            if (lineNumber < 0) {\n                // WebVTT defines line -1 as last visible row when lineAnchor is ANCHOR_TYPE_START, where-as\n                // Cue defines it to be the first row that's not visible.\n                lineNumber--;\n            }\n            builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER);\n        }\n    }\n\n    private static void parsePositionAttribute(String s, WebvttCue.Builder builder)\n        throws NumberFormatException {\n        int commaIndex = s.indexOf(',');\n        if (commaIndex != -1) {\n            builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));\n            s = s.substring(0, commaIndex);\n        } else {\n            builder.setPositionAnchor(Cue.TYPE_UNSET);\n        }\n        builder.setPosition(WebvttParserUtil.parsePercentage(s));\n    }\n\n    private static int parsePositionAnchor(String s) {\n        switch (s) {\n            case \"start\":\n                return Cue.ANCHOR_TYPE_START;\n            case \"center\":\n            case \"middle\":\n                return Cue.ANCHOR_TYPE_MIDDLE;\n            case \"end\":\n                return Cue.ANCHOR_TYPE_END;\n            default:\n                Log.w(TAG, \"Invalid anchor value: \" + s);\n                return Cue.TYPE_UNSET;\n        }\n    }\n\n    private static Alignment parseTextAlignment(String s) {\n        switch (s) {\n            case \"start\":\n            case \"left\":\n                return Alignment.ALIGN_NORMAL;\n            case \"center\":\n            case \"middle\":\n                return Alignment.ALIGN_CENTER;\n            case \"end\":\n            case \"right\":\n                return Alignment.ALIGN_OPPOSITE;\n            default:\n                Log.w(TAG, \"Invalid alignment value: \" + s);\n                return null;\n        }\n    }\n\n    /**\n     * Find end of tag (&gt;). The position returned is the position of the &gt; plus one (exclusive).\n     *\n     * @param markup   The WebVTT cue markup to be parsed.\n     * @param startPos The position from where to start searching for the end of tag.\n     * @return The position of the end of tag plus 1 (one).\n     */\n    private static int findEndOfTag(String markup, int startPos) {\n        int index = markup.indexOf(CHAR_GREATER_THAN, startPos);\n        return index == -1 ? markup.length() : index + 1;\n    }\n\n    private static void applyEntity(String entity, SpannableStringBuilder spannedText) {\n        switch (entity) {\n            case ENTITY_LESS_THAN:\n                spannedText.append('<');\n                break;\n            case ENTITY_GREATER_THAN:\n                spannedText.append('>');\n                break;\n            case ENTITY_NON_BREAK_SPACE:\n                spannedText.append(' ');\n                break;\n            case ENTITY_AMPERSAND:\n                spannedText.append('&');\n                break;\n            default:\n                Log.w(TAG, \"ignoring unsupported entity: '&\" + entity + \";'\");\n                break;\n        }\n    }\n\n    private static boolean isSupportedTag(String tagName) {\n        switch (tagName) {\n            case TAG_BOLD:\n            case TAG_CLASS:\n            case TAG_ITALIC:\n            case TAG_LANG:\n            case TAG_UNDERLINE:\n            case TAG_VOICE:\n                return true;\n            default:\n                return false;\n        }\n    }\n\n    private static void applySpansForTag(String cueId, StartTag startTag, SpannableStringBuilder text,\n                                         List<WebvttCssStyle> styles, List<StyleMatch> scratchStyleMatches) {\n        int start = startTag.position;\n        int end = text.length();\n        switch (startTag.name) {\n            case TAG_BOLD:\n                text.setSpan(new StyleSpan(STYLE_BOLD), start, end,\n                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE\n                );\n                break;\n            case TAG_ITALIC:\n                text.setSpan(new StyleSpan(STYLE_ITALIC), start, end,\n                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE\n                );\n                break;\n            case TAG_UNDERLINE:\n                text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n                break;\n            case TAG_CLASS:\n                applySupportedClasses(text, startTag.classes, start, end);\n                break;\n            case TAG_LANG:\n            case TAG_VOICE:\n            case \"\": // Case of the \"whole cue\" virtual tag.\n                break;\n            default:\n                return;\n        }\n        scratchStyleMatches.clear();\n        getApplicableStyles(styles, cueId, startTag, scratchStyleMatches);\n        int styleMatchesCount = scratchStyleMatches.size();\n        for (int i = 0; i < styleMatchesCount; i++) {\n            applyStyleToText(text, scratchStyleMatches.get(i).style, start, end);\n        }\n    }\n\n    private static void applySupportedClasses(SpannableStringBuilder text, String[] classes,\n                                              int start, int end) {\n        for (String className : classes) {\n            if (ColorParser.isNamedColor(className)) {\n                int color = ColorParser.parseCssColor(className);\n                text.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\n            }\n        }\n    }\n\n    private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttCssStyle style,\n                                         int start, int end) {\n        if (style == null) {\n            return;\n        }\n        if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {\n            spannedText.setSpan(new StyleSpan(style.getStyle()), start, end,\n                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE\n            );\n        }\n        if (style.isLinethrough()) {\n            spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        }\n        if (style.isUnderline()) {\n            spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);\n        }\n        if (style.hasFontColor()) {\n            spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,\n                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE\n            );\n        }\n        if (style.hasBackgroundColor()) {\n            spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,\n                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE\n            );\n        }\n        if (style.getFontFamily() != null) {\n            spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,\n                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE\n            );\n        }\n        if (style.getTextAlign() != null) {\n            spannedText.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,\n                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE\n            );\n        }\n        switch (style.getFontSizeUnit()) {\n            case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL:\n                spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,\n                                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE\n                );\n                break;\n            case WebvttCssStyle.FONT_SIZE_UNIT_EM:\n                spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,\n                                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE\n                );\n                break;\n            case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:\n                spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,\n                                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE\n                );\n                break;\n            case WebvttCssStyle.UNSPECIFIED:\n                // Do nothing.\n                break;\n        }\n    }\n\n    /**\n     * Returns the tag name for the given tag contents.\n     *\n     * @param tagExpression Characters between &amp;lt: and &amp;gt; of a start or end tag.\n     * @return The name of tag.\n     */\n    private static String getTagName(String tagExpression) {\n        tagExpression = tagExpression.trim();\n        if (tagExpression.isEmpty()) {\n            return null;\n        }\n        return tagExpression.split(\"[ \\\\.]\")[0];\n    }\n\n    private static void getApplicableStyles(List<WebvttCssStyle> declaredStyles, String id,\n                                            StartTag tag, List<StyleMatch> output) {\n        int styleCount = declaredStyles.size();\n        for (int i = 0; i < styleCount; i++) {\n            WebvttCssStyle style = declaredStyles.get(i);\n            int score = style.getSpecificityScore(id, tag.name, tag.classes, tag.voice);\n            if (score > 0) {\n                output.add(new StyleMatch(score, style));\n            }\n        }\n        Collections.sort(output);\n    }\n\n    private static final class StyleMatch implements Comparable<StyleMatch> {\n\n        public final int score;\n        public final WebvttCssStyle style;\n\n        public StyleMatch(int score, WebvttCssStyle style) {\n            this.score = score;\n            this.style = style;\n        }\n\n        @Override\n        public int compareTo(@NonNull StyleMatch another) {\n            return this.score - another.score;\n        }\n\n    }\n\n    private static final class StartTag {\n\n        private static final String[] NO_CLASSES = new String[0];\n\n        public final String name;\n        public final int position;\n        public final String voice;\n        public final String[] classes;\n\n        private StartTag(String name, int position, String voice, String[] classes) {\n            this.position = position;\n            this.name = name;\n            this.voice = voice;\n            this.classes = classes;\n        }\n\n        public static StartTag buildStartTag(String fullTagExpression, int position) {\n            fullTagExpression = fullTagExpression.trim();\n            if (fullTagExpression.isEmpty()) {\n                return null;\n            }\n            int voiceStartIndex = fullTagExpression.indexOf(\" \");\n            String voice;\n            if (voiceStartIndex == -1) {\n                voice = \"\";\n            } else {\n                voice = fullTagExpression.substring(voiceStartIndex).trim();\n                fullTagExpression = fullTagExpression.substring(0, voiceStartIndex);\n            }\n            String[] nameAndClasses = fullTagExpression.split(\"\\\\.\");\n            String name = nameAndClasses[0];\n            String[] classes;\n            if (nameAndClasses.length > 1) {\n                classes = Arrays.copyOfRange(nameAndClasses, 1, nameAndClasses.length);\n            } else {\n                classes = NO_CLASSES;\n            }\n            return new StartTag(name, position, voice, classes);\n        }\n\n        public static StartTag buildWholeCueVirtualTag() {\n            return new StartTag(\"\", 0, \"\", new String[0]);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/external/exoplayer/text/webvtt/WebvttDecoder.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.novoda.noplayer.external.exoplayer.text.webvtt;\n\nimport android.text.TextUtils;\n\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.text.SimpleSubtitleDecoder;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.google.android.exoplayer2.text.webvtt.WebvttCssStyle;\nimport com.google.android.exoplayer2.text.webvtt.WebvttCue;\nimport com.google.android.exoplayer2.text.webvtt.WebvttParserUtil;\nimport com.google.android.exoplayer2.util.ParsableByteArray;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * A {@link SimpleSubtitleDecoder} for WebVTT.\n * <p>\n * @see <a href=\"http://dev.w3.org/html5/webvtt\">WebVTT specification</a>\n */\npublic final class WebvttDecoder extends SimpleSubtitleDecoder {\n\n  private static final int EVENT_NONE = -1;\n  private static final int EVENT_END_OF_FILE = 0;\n  private static final int EVENT_COMMENT = 1;\n  private static final int EVENT_STYLE_BLOCK = 2;\n  private static final int EVENT_CUE = 3;\n\n  private static final String COMMENT_START = \"NOTE\";\n  private static final String STYLE_START = \"STYLE\";\n\n  private final WebvttCueParser cueParser;\n  private final ParsableByteArray parsableWebvttData;\n  private final WebvttCue.Builder webvttCueBuilder;\n  private final CssParser cssParser;\n  private final List<WebvttCssStyle> definedStyles;\n\n  public WebvttDecoder() {\n    super(\"WebvttDecoder\");\n    cueParser = new WebvttCueParser();\n    parsableWebvttData = new ParsableByteArray();\n    webvttCueBuilder = new WebvttCue.Builder();\n    cssParser = new CssParser();\n    definedStyles = new ArrayList<>();\n  }\n\n  @Override\n  protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset)\n      throws SubtitleDecoderException {\n    parsableWebvttData.reset(bytes, length);\n    // Initialization for consistent starting state.\n    webvttCueBuilder.reset();\n    definedStyles.clear();\n\n    // Validate the first line of the header, and skip the remainder.\n    try {\n      WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);\n    } catch (ParserException e) {\n      throw new SubtitleDecoderException(e);\n    }\n    while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {\n    }\n\n    int event;\n    ArrayList<WebvttCue> subtitles = new ArrayList<>();\n    while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) {\n      if (event == EVENT_COMMENT) {\n        skipComment(parsableWebvttData);\n      } else if (event == EVENT_STYLE_BLOCK) {\n        if (!subtitles.isEmpty()) {\n          throw new SubtitleDecoderException(\"A style block was found after the first cue.\");\n        }\n        parsableWebvttData.readLine(); // Consume the \"STYLE\" header.\n        WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData);\n        if (styleBlock != null) {\n          definedStyles.add(styleBlock);\n        }\n      } else if (event == EVENT_CUE) {\n        if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {\n          subtitles.add(webvttCueBuilder.build());\n          webvttCueBuilder.reset();\n        }\n      }\n    }\n    return new WebvttSubtitle(subtitles);\n  }\n\n  /**\n   * Positions the input right before the next event, and returns the kind of event found. Does not\n   * consume any data from such event, if any.\n   *\n   * @return The kind of event found.\n   */\n  private static int getNextEvent(ParsableByteArray parsableWebvttData) {\n    int foundEvent = EVENT_NONE;\n    int currentInputPosition = 0;\n    while (foundEvent == EVENT_NONE) {\n      currentInputPosition = parsableWebvttData.getPosition();\n      String line = parsableWebvttData.readLine();\n      if (line == null) {\n        foundEvent = EVENT_END_OF_FILE;\n      } else if (STYLE_START.equals(line)) {\n        foundEvent = EVENT_STYLE_BLOCK;\n      } else if (COMMENT_START.startsWith(line)) {\n        foundEvent = EVENT_COMMENT;\n      } else {\n        foundEvent = EVENT_CUE;\n      }\n    }\n    parsableWebvttData.setPosition(currentInputPosition);\n    return foundEvent;\n  }\n\n  private static void skipComment(ParsableByteArray parsableWebvttData) {\n    while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}\n  }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/external/exoplayer/text/webvtt/WebvttSubtitle.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.novoda.noplayer.external.exoplayer.text.webvtt;\n\nimport android.text.SpannableStringBuilder;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.Subtitle;\nimport com.google.android.exoplayer2.text.webvtt.WebvttCue;\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A representation of a WebVTT subtitle.\n */\n/* package */ final class WebvttSubtitle implements Subtitle {\n\n  private final List<WebvttCue> cues;\n  private final int numCues;\n  private final long[] cueTimesUs;\n  private final long[] sortedCueTimesUs;\n\n  /**\n   * @param cues A list of the cues in this subtitle.\n   */\n  public WebvttSubtitle(List<WebvttCue> cues) {\n    this.cues = cues;\n    numCues = cues.size();\n    cueTimesUs = new long[2 * numCues];\n    for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {\n      WebvttCue cue = cues.get(cueIndex);\n      int arrayIndex = cueIndex * 2;\n      cueTimesUs[arrayIndex] = cue.startTime;\n      cueTimesUs[arrayIndex + 1] = cue.endTime;\n    }\n    sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);\n    Arrays.sort(sortedCueTimesUs);\n  }\n\n  @Override\n  public int getNextEventTimeIndex(long timeUs) {\n    int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false);\n    return index < sortedCueTimesUs.length ? index : C.INDEX_UNSET;\n  }\n\n  @Override\n  public int getEventTimeCount() {\n    return sortedCueTimesUs.length;\n  }\n\n  @Override\n  public long getEventTime(int index) {\n    Assertions.checkArgument(index >= 0);\n    Assertions.checkArgument(index < sortedCueTimesUs.length);\n    return sortedCueTimesUs[index];\n  }\n\n  @Override\n  public List<Cue> getCues(long timeUs) {\n    ArrayList<Cue> list = null;\n    WebvttCue firstNormalCue = null;\n    SpannableStringBuilder normalCueTextBuilder = null;\n\n    for (int i = 0; i < numCues; i++) {\n      if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) {\n        if (list == null) {\n          list = new ArrayList<>();\n        }\n        WebvttCue cue = cues.get(i);\n        if (cue.isNormalCue()) {\n          // we want to merge all of the normal cues into a single cue to ensure they are drawn\n          // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple\n          // normal cues, otherwise we can just append the single normal cue\n          if (firstNormalCue == null) {\n            firstNormalCue = cue;\n          } else if (normalCueTextBuilder == null) {\n            normalCueTextBuilder = new SpannableStringBuilder();\n            normalCueTextBuilder.append(firstNormalCue.text).append(\"\\n\").append(cue.text);\n          } else {\n            normalCueTextBuilder.append(\"\\n\").append(cue.text);\n          }\n        } else {\n          list.add(cue);\n        }\n      }\n    }\n    if (normalCueTextBuilder != null) {\n      // there were multiple normal cues, so create a new cue with all of the text\n      list.add(new WebvttCue(normalCueTextBuilder));\n    } else if (firstNormalCue != null) {\n      // there was only a single normal cue, so just add it to the list\n      list.add(firstNormalCue);\n    }\n\n    if (list != null) {\n      return list;\n    } else {\n      return Collections.emptyList();\n    }\n  }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/external/exoplayer/util/ColorParser.java",
    "content": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.novoda.noplayer.external.exoplayer.util;\n\nimport android.text.TextUtils;\n\nimport com.google.android.exoplayer2.util.Assertions;\nimport com.google.android.exoplayer2.util.Util;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Parser for color expressions found in styling formats, e.g. TTML and CSS.\n *\n * @see <a href=\"https://w3c.github.io/webvtt/#styling\">WebVTT CSS Styling</a>\n * @see <a href=\"https://www.w3.org/TR/ttml2/\">Timed Text Markup Language 2 (TTML2) - 10.3.5</a>\n **/\npublic final class ColorParser {\n\n  private static final String RGB = \"rgb\";\n  private static final String RGBA = \"rgba\";\n\n  private static final Pattern RGB_PATTERN = Pattern.compile(\n      \"^rgb\\\\((\\\\d{1,3}),(\\\\d{1,3}),(\\\\d{1,3})\\\\)$\");\n\n  private static final Pattern RGBA_PATTERN_INT_ALPHA = Pattern.compile(\n      \"^rgba\\\\((\\\\d{1,3}),(\\\\d{1,3}),(\\\\d{1,3}),(\\\\d{1,3})\\\\)$\");\n\n  private static final Pattern RGBA_PATTERN_FLOAT_ALPHA = Pattern.compile(\n      \"^rgba\\\\((\\\\d{1,3}),(\\\\d{1,3}),(\\\\d{1,3}),(\\\\d*\\\\.?\\\\d*?)\\\\)$\");\n\n  private static final Map<String, Integer> COLOR_MAP;\n\n  public static boolean isNamedColor(String expression) {\n    return COLOR_MAP.containsKey(expression);\n  }\n\n  /**\n   * Parses a TTML color expression.\n   *\n   * @param colorExpression The color expression.\n   * @return The parsed ARGB color.\n   */\n  public static int parseTtmlColor(String colorExpression) {\n    return parseColorInternal(colorExpression, false);\n  }\n\n  /**\n   * Parses a CSS color expression.\n   *\n   * @param colorExpression The color expression.\n   * @return The parsed ARGB color.\n   */\n  public static int parseCssColor(String colorExpression) {\n    return parseColorInternal(colorExpression, true);\n  }\n\n  private static int parseColorInternal(String colorExpression, boolean alphaHasFloatFormat) {\n    Assertions.checkArgument(!TextUtils.isEmpty(colorExpression));\n    colorExpression = colorExpression.replace(\" \", \"\");\n    if (colorExpression.charAt(0) == '#') {\n      // Parse using Long to avoid failure when colorExpression is greater than #7FFFFFFF.\n      int color = (int) Long.parseLong(colorExpression.substring(1), 16);\n      if (colorExpression.length() == 7) {\n        // Set the alpha value\n        color |= 0xFF000000;\n      } else if (colorExpression.length() == 9) {\n        // We have #RRGGBBAA, but we need #AARRGGBB\n        color = ((color & 0xFF) << 24) | (color >>> 8);\n      } else {\n        throw new IllegalArgumentException();\n      }\n      return color;\n    } else if (colorExpression.startsWith(RGBA)) {\n      Matcher matcher = (alphaHasFloatFormat ? RGBA_PATTERN_FLOAT_ALPHA : RGBA_PATTERN_INT_ALPHA)\n          .matcher(colorExpression);\n      if (matcher.matches()) {\n        return argb(\n          alphaHasFloatFormat ? (int) (255 * Float.parseFloat(matcher.group(4)))\n              : Integer.parseInt(matcher.group(4), 10),\n          Integer.parseInt(matcher.group(1), 10),\n          Integer.parseInt(matcher.group(2), 10),\n          Integer.parseInt(matcher.group(3), 10)\n        );\n      }\n    } else if (colorExpression.startsWith(RGB)) {\n      Matcher matcher = RGB_PATTERN.matcher(colorExpression);\n      if (matcher.matches()) {\n        return rgb(\n          Integer.parseInt(matcher.group(1), 10),\n          Integer.parseInt(matcher.group(2), 10),\n          Integer.parseInt(matcher.group(3), 10)\n        );\n      }\n    } else {\n      // we use our own color map\n      Integer color = COLOR_MAP.get(Util.toLowerInvariant(colorExpression));\n      if (color != null) {\n        return color;\n      }\n    }\n    throw new IllegalArgumentException();\n  }\n\n  private static int argb(int alpha, int red, int green, int blue) {\n    return (alpha << 24) | (red << 16) | (green << 8) | blue;\n  }\n\n  private static int rgb(int red, int green, int blue) {\n    return argb(0xFF, red, green, blue);\n  }\n\n  static {\n    COLOR_MAP = new HashMap<>();\n    COLOR_MAP.put(\"aliceblue\", 0xFFF0F8FF);\n    COLOR_MAP.put(\"antiquewhite\", 0xFFFAEBD7);\n    COLOR_MAP.put(\"aqua\", 0xFF00FFFF);\n    COLOR_MAP.put(\"aquamarine\", 0xFF7FFFD4);\n    COLOR_MAP.put(\"azure\", 0xFFF0FFFF);\n    COLOR_MAP.put(\"beige\", 0xFFF5F5DC);\n    COLOR_MAP.put(\"bisque\", 0xFFFFE4C4);\n    COLOR_MAP.put(\"black\", 0xFF000000);\n    COLOR_MAP.put(\"blanchedalmond\", 0xFFFFEBCD);\n    COLOR_MAP.put(\"blue\", 0xFF0000FF);\n    COLOR_MAP.put(\"blueviolet\", 0xFF8A2BE2);\n    COLOR_MAP.put(\"brown\", 0xFFA52A2A);\n    COLOR_MAP.put(\"burlywood\", 0xFFDEB887);\n    COLOR_MAP.put(\"cadetblue\", 0xFF5F9EA0);\n    COLOR_MAP.put(\"chartreuse\", 0xFF7FFF00);\n    COLOR_MAP.put(\"chocolate\", 0xFFD2691E);\n    COLOR_MAP.put(\"coral\", 0xFFFF7F50);\n    COLOR_MAP.put(\"cornflowerblue\", 0xFF6495ED);\n    COLOR_MAP.put(\"cornsilk\", 0xFFFFF8DC);\n    COLOR_MAP.put(\"crimson\", 0xFFDC143C);\n    COLOR_MAP.put(\"cyan\", 0xFF00FFFF);\n    COLOR_MAP.put(\"darkblue\", 0xFF00008B);\n    COLOR_MAP.put(\"darkcyan\", 0xFF008B8B);\n    COLOR_MAP.put(\"darkgoldenrod\", 0xFFB8860B);\n    COLOR_MAP.put(\"darkgray\", 0xFFA9A9A9);\n    COLOR_MAP.put(\"darkgreen\", 0xFF006400);\n    COLOR_MAP.put(\"darkgrey\", 0xFFA9A9A9);\n    COLOR_MAP.put(\"darkkhaki\", 0xFFBDB76B);\n    COLOR_MAP.put(\"darkmagenta\", 0xFF8B008B);\n    COLOR_MAP.put(\"darkolivegreen\", 0xFF556B2F);\n    COLOR_MAP.put(\"darkorange\", 0xFFFF8C00);\n    COLOR_MAP.put(\"darkorchid\", 0xFF9932CC);\n    COLOR_MAP.put(\"darkred\", 0xFF8B0000);\n    COLOR_MAP.put(\"darksalmon\", 0xFFE9967A);\n    COLOR_MAP.put(\"darkseagreen\", 0xFF8FBC8F);\n    COLOR_MAP.put(\"darkslateblue\", 0xFF483D8B);\n    COLOR_MAP.put(\"darkslategray\", 0xFF2F4F4F);\n    COLOR_MAP.put(\"darkslategrey\", 0xFF2F4F4F);\n    COLOR_MAP.put(\"darkturquoise\", 0xFF00CED1);\n    COLOR_MAP.put(\"darkviolet\", 0xFF9400D3);\n    COLOR_MAP.put(\"deeppink\", 0xFFFF1493);\n    COLOR_MAP.put(\"deepskyblue\", 0xFF00BFFF);\n    COLOR_MAP.put(\"dimgray\", 0xFF696969);\n    COLOR_MAP.put(\"dimgrey\", 0xFF696969);\n    COLOR_MAP.put(\"dodgerblue\", 0xFF1E90FF);\n    COLOR_MAP.put(\"firebrick\", 0xFFB22222);\n    COLOR_MAP.put(\"floralwhite\", 0xFFFFFAF0);\n    COLOR_MAP.put(\"forestgreen\", 0xFF228B22);\n    COLOR_MAP.put(\"fuchsia\", 0xFFFF00FF);\n    COLOR_MAP.put(\"gainsboro\", 0xFFDCDCDC);\n    COLOR_MAP.put(\"ghostwhite\", 0xFFF8F8FF);\n    COLOR_MAP.put(\"gold\", 0xFFFFD700);\n    COLOR_MAP.put(\"goldenrod\", 0xFFDAA520);\n    COLOR_MAP.put(\"gray\", 0xFF808080);\n    COLOR_MAP.put(\"green\", 0xFF008000);\n    COLOR_MAP.put(\"greenyellow\", 0xFFADFF2F);\n    COLOR_MAP.put(\"grey\", 0xFF808080);\n    COLOR_MAP.put(\"honeydew\", 0xFFF0FFF0);\n    COLOR_MAP.put(\"hotpink\", 0xFFFF69B4);\n    COLOR_MAP.put(\"indianred\", 0xFFCD5C5C);\n    COLOR_MAP.put(\"indigo\", 0xFF4B0082);\n    COLOR_MAP.put(\"ivory\", 0xFFFFFFF0);\n    COLOR_MAP.put(\"khaki\", 0xFFF0E68C);\n    COLOR_MAP.put(\"lavender\", 0xFFE6E6FA);\n    COLOR_MAP.put(\"lavenderblush\", 0xFFFFF0F5);\n    COLOR_MAP.put(\"lawngreen\", 0xFF7CFC00);\n    COLOR_MAP.put(\"lemonchiffon\", 0xFFFFFACD);\n    COLOR_MAP.put(\"lightblue\", 0xFFADD8E6);\n    COLOR_MAP.put(\"lightcoral\", 0xFFF08080);\n    COLOR_MAP.put(\"lightcyan\", 0xFFE0FFFF);\n    COLOR_MAP.put(\"lightgoldenrodyellow\", 0xFFFAFAD2);\n    COLOR_MAP.put(\"lightgray\", 0xFFD3D3D3);\n    COLOR_MAP.put(\"lightgreen\", 0xFF90EE90);\n    COLOR_MAP.put(\"lightgrey\", 0xFFD3D3D3);\n    COLOR_MAP.put(\"lightpink\", 0xFFFFB6C1);\n    COLOR_MAP.put(\"lightsalmon\", 0xFFFFA07A);\n    COLOR_MAP.put(\"lightseagreen\", 0xFF20B2AA);\n    COLOR_MAP.put(\"lightskyblue\", 0xFF87CEFA);\n    COLOR_MAP.put(\"lightslategray\", 0xFF778899);\n    COLOR_MAP.put(\"lightslategrey\", 0xFF778899);\n    COLOR_MAP.put(\"lightsteelblue\", 0xFFB0C4DE);\n    COLOR_MAP.put(\"lightyellow\", 0xFFFFFFE0);\n    COLOR_MAP.put(\"lime\", 0xFF00FF00);\n    COLOR_MAP.put(\"limegreen\", 0xFF32CD32);\n    COLOR_MAP.put(\"linen\", 0xFFFAF0E6);\n    COLOR_MAP.put(\"magenta\", 0xFFFF00FF);\n    COLOR_MAP.put(\"maroon\", 0xFF800000);\n    COLOR_MAP.put(\"mediumaquamarine\", 0xFF66CDAA);\n    COLOR_MAP.put(\"mediumblue\", 0xFF0000CD);\n    COLOR_MAP.put(\"mediumorchid\", 0xFFBA55D3);\n    COLOR_MAP.put(\"mediumpurple\", 0xFF9370DB);\n    COLOR_MAP.put(\"mediumseagreen\", 0xFF3CB371);\n    COLOR_MAP.put(\"mediumslateblue\", 0xFF7B68EE);\n    COLOR_MAP.put(\"mediumspringgreen\", 0xFF00FA9A);\n    COLOR_MAP.put(\"mediumturquoise\", 0xFF48D1CC);\n    COLOR_MAP.put(\"mediumvioletred\", 0xFFC71585);\n    COLOR_MAP.put(\"midnightblue\", 0xFF191970);\n    COLOR_MAP.put(\"mintcream\", 0xFFF5FFFA);\n    COLOR_MAP.put(\"mistyrose\", 0xFFFFE4E1);\n    COLOR_MAP.put(\"moccasin\", 0xFFFFE4B5);\n    COLOR_MAP.put(\"navajowhite\", 0xFFFFDEAD);\n    COLOR_MAP.put(\"navy\", 0xFF000080);\n    COLOR_MAP.put(\"oldlace\", 0xFFFDF5E6);\n    COLOR_MAP.put(\"olive\", 0xFF808000);\n    COLOR_MAP.put(\"olivedrab\", 0xFF6B8E23);\n    COLOR_MAP.put(\"orange\", 0xFFFFA500);\n    COLOR_MAP.put(\"orangered\", 0xFFFF4500);\n    COLOR_MAP.put(\"orchid\", 0xFFDA70D6);\n    COLOR_MAP.put(\"palegoldenrod\", 0xFFEEE8AA);\n    COLOR_MAP.put(\"palegreen\", 0xFF98FB98);\n    COLOR_MAP.put(\"paleturquoise\", 0xFFAFEEEE);\n    COLOR_MAP.put(\"palevioletred\", 0xFFDB7093);\n    COLOR_MAP.put(\"papayawhip\", 0xFFFFEFD5);\n    COLOR_MAP.put(\"peachpuff\", 0xFFFFDAB9);\n    COLOR_MAP.put(\"peru\", 0xFFCD853F);\n    COLOR_MAP.put(\"pink\", 0xFFFFC0CB);\n    COLOR_MAP.put(\"plum\", 0xFFDDA0DD);\n    COLOR_MAP.put(\"powderblue\", 0xFFB0E0E6);\n    COLOR_MAP.put(\"purple\", 0xFF800080);\n    COLOR_MAP.put(\"rebeccapurple\", 0xFF663399);\n    COLOR_MAP.put(\"red\", 0xFFFF0000);\n    COLOR_MAP.put(\"rosybrown\", 0xFFBC8F8F);\n    COLOR_MAP.put(\"royalblue\", 0xFF4169E1);\n    COLOR_MAP.put(\"saddlebrown\", 0xFF8B4513);\n    COLOR_MAP.put(\"salmon\", 0xFFFA8072);\n    COLOR_MAP.put(\"sandybrown\", 0xFFF4A460);\n    COLOR_MAP.put(\"seagreen\", 0xFF2E8B57);\n    COLOR_MAP.put(\"seashell\", 0xFFFFF5EE);\n    COLOR_MAP.put(\"sienna\", 0xFFA0522D);\n    COLOR_MAP.put(\"silver\", 0xFFC0C0C0);\n    COLOR_MAP.put(\"skyblue\", 0xFF87CEEB);\n    COLOR_MAP.put(\"slateblue\", 0xFF6A5ACD);\n    COLOR_MAP.put(\"slategray\", 0xFF708090);\n    COLOR_MAP.put(\"slategrey\", 0xFF708090);\n    COLOR_MAP.put(\"snow\", 0xFFFFFAFA);\n    COLOR_MAP.put(\"springgreen\", 0xFF00FF7F);\n    COLOR_MAP.put(\"steelblue\", 0xFF4682B4);\n    COLOR_MAP.put(\"tan\", 0xFFD2B48C);\n    COLOR_MAP.put(\"teal\", 0xFF008080);\n    COLOR_MAP.put(\"thistle\", 0xFFD8BFD8);\n    COLOR_MAP.put(\"tomato\", 0xFFFF6347);\n    COLOR_MAP.put(\"transparent\", 0x00000000);\n    COLOR_MAP.put(\"turquoise\", 0xFF40E0D0);\n    COLOR_MAP.put(\"violet\", 0xFFEE82EE);\n    COLOR_MAP.put(\"wheat\", 0xFFF5DEB3);\n    COLOR_MAP.put(\"white\", 0xFFFFFFFF);\n    COLOR_MAP.put(\"whitesmoke\", 0xFFF5F5F5);\n    COLOR_MAP.put(\"yellow\", 0xFFFFFF00);\n    COLOR_MAP.put(\"yellowgreen\", 0xFF9ACD32);\n  }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/Clock.java",
    "content": "package com.novoda.noplayer.internal;\n\nimport java.io.Serializable;\n\npublic interface Clock extends Serializable {\n\n    long getCurrentTime();\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/Heart.java",
    "content": "package com.novoda.noplayer.internal;\n\nimport android.os.Handler;\n\nimport com.novoda.noplayer.NoPlayer;\n\n@SuppressWarnings(\"checkstyle:FinalClass\")  // We cannot make it final as we need to mock it in tests\npublic class Heart {\n\n    private static final long HEART_BEAT_FREQUENCY_IN_MILLIS = 500;\n\n    private final Handler handler;\n    private final long heartbeatFrequency;\n\n    private Heartbeat heartbeatAction;\n\n    private boolean beating;\n\n    public static Heart newInstance(Handler handler) {\n        return new Heart(handler, HEART_BEAT_FREQUENCY_IN_MILLIS);\n    }\n\n    private Heart(Handler handler, long heartbeatFrequencyInMillis) {\n        this.handler = handler;\n        this.heartbeatFrequency = heartbeatFrequencyInMillis;\n    }\n\n    public void bind(Heartbeat onHeartbeat) {\n        this.heartbeatAction = onHeartbeat;\n    }\n\n    public void startBeatingHeart() {\n        if (heartbeatAction == null) {\n            throw new IllegalStateException(\"You must call bind() with a valid non-null \" + Heartbeat.class.getSimpleName());\n        }\n        stopBeatingHeart();\n        beating = true;\n        handler.post(heartbeat);\n    }\n\n    private final Runnable heartbeat = new Runnable() {\n        @Override\n        public void run() {\n            handler.post(heartbeatAction);\n            scheduleNextBeat();\n        }\n    };\n\n    private void scheduleNextBeat() {\n        handler.postDelayed(heartbeat, heartbeatFrequency);\n    }\n\n    public void stopBeatingHeart() {\n        beating = false;\n        handler.removeCallbacks(heartbeat);\n    }\n\n    public void forceBeat() {\n        if (heartbeatAction == null) {\n            throw new IllegalStateException(\"You must call bind() with a valid non-null \" + Heartbeat.class.getSimpleName());\n        }\n        handler.post(heartbeatAction);\n    }\n\n    public boolean isBeating() {\n        return beating;\n    }\n\n    public static class Heartbeat implements Runnable {\n\n        private final NoPlayer.HeartbeatCallback callback;\n        private final NoPlayer player;\n\n        public Heartbeat(NoPlayer.HeartbeatCallback callback, NoPlayer player) {\n            this.callback = callback;\n            this.player = player;\n        }\n\n        @Override\n        public void run() {\n            if (player.isPlaying()) {\n                callback.onBeat(player);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/SystemClock.java",
    "content": "package com.novoda.noplayer.internal;\n\npublic class SystemClock implements Clock {\n\n    @Override\n    public long getCurrentTime() {\n        return System.currentTimeMillis();\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/drm/provision/HttpPostingProvisionExecutor.java",
    "content": "package com.novoda.noplayer.internal.drm.provision;\n\nimport com.novoda.noplayer.drm.ModularDrmProvisionRequest;\n\nimport java.io.IOException;\nimport java.nio.charset.Charset;\n\nclass HttpPostingProvisionExecutor implements ProvisionExecutor {\n\n    private static final String PARAMETER_SIGNED_REQUEST = \"&signedRequest=\";\n\n    private final HttpUrlConnectionPoster httpPoster;\n    private final ProvisioningCapabilities capabilities;\n\n    HttpPostingProvisionExecutor(HttpUrlConnectionPoster httpPoster, ProvisioningCapabilities capabilities) {\n        this.httpPoster = httpPoster;\n        this.capabilities = capabilities;\n    }\n\n    @Override\n    public byte[] execute(ModularDrmProvisionRequest request) throws IOException, UnableToProvisionException {\n        if (isIncapableOfProvisioning()) {\n            throw new UnableToProvisionException();\n        }\n        String provisioningUrl = buildProvisioningUrl(request);\n        return httpPoster.post(provisioningUrl);\n    }\n\n    private boolean isIncapableOfProvisioning() {\n        return !capabilities.canProvision();\n    }\n\n    private String buildProvisioningUrl(ModularDrmProvisionRequest request) {\n        return request.url() + PARAMETER_SIGNED_REQUEST + new String(request.data(), Charset.forName(\"UTF-8\"));\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/drm/provision/HttpUrlConnectionPoster.java",
    "content": "package com.novoda.noplayer.internal.drm.provision;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\n\nclass HttpUrlConnectionPoster {\n\n    private static final String POST_REQUEST_METHOD = \"POST\";\n    private static final int RESPONSE_BUFFER_SIZE = 1024 * 4;\n\n    byte[] post(String url) throws IOException {\n        HttpURLConnection urlConnection = null;\n        try {\n            urlConnection = (HttpURLConnection) new URL(url).openConnection();\n            urlConnection.setRequestMethod(POST_REQUEST_METHOD);\n            urlConnection.setDoInput(true);\n            return byteArrayFrom(urlConnection);\n        } finally {\n            if (urlConnection != null) {\n                urlConnection.disconnect();\n            }\n        }\n    }\n\n    private byte[] byteArrayFrom(HttpURLConnection urlConnection) throws IOException {\n        InputStream inputStream = urlConnection.getInputStream();\n        try {\n            return byteArrayFrom(inputStream);\n        } finally {\n            inputStream.close();\n        }\n    }\n\n    private byte[] byteArrayFrom(InputStream inputStream) throws IOException {\n        byte[] buffer = new byte[RESPONSE_BUFFER_SIZE];\n        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();\n        int bytesRead;\n        while ((bytesRead = inputStream.read(buffer)) != -1) {\n            outputStream.write(buffer, 0, bytesRead);\n        }\n        try {\n            return outputStream.toByteArray();\n        } finally {\n            outputStream.close();\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/drm/provision/ProvisionExecutor.java",
    "content": "package com.novoda.noplayer.internal.drm.provision;\n\nimport com.novoda.noplayer.drm.ModularDrmProvisionRequest;\n\nimport java.io.IOException;\n\npublic interface ProvisionExecutor {\n\n    byte[] execute(ModularDrmProvisionRequest request) throws IOException, UnableToProvisionException;\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/drm/provision/ProvisionExecutorCreator.java",
    "content": "package com.novoda.noplayer.internal.drm.provision;\n\npublic class ProvisionExecutorCreator {\n\n    public ProvisionExecutor create() {\n        HttpUrlConnectionPoster httpPoster = new HttpUrlConnectionPoster();\n        ProvisioningCapabilities capabilities = ProvisioningCapabilities.newInstance();\n        return new HttpPostingProvisionExecutor(httpPoster, capabilities);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/drm/provision/ProvisioningCapabilities.java",
    "content": "package com.novoda.noplayer.internal.drm.provision;\n\nimport android.os.Build;\nimport android.support.annotation.VisibleForTesting;\n\nclass ProvisioningCapabilities {\n\n    private final int deviceOsVersion;\n\n    static ProvisioningCapabilities newInstance() {\n        return new ProvisioningCapabilities(Build.VERSION.SDK_INT);\n    }\n\n    @VisibleForTesting\n    ProvisioningCapabilities(int deviceOsVersion) {\n        this.deviceOsVersion = deviceOsVersion;\n    }\n\n    boolean canProvision() {\n        return deviceOsVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/drm/provision/UnableToProvisionException.java",
    "content": "package com.novoda.noplayer.internal.drm.provision;\n\nimport android.os.Build;\n\npublic class UnableToProvisionException extends Exception {\n\n    UnableToProvisionException() {\n        super(\"Device is : \" + Build.VERSION.SDK_INT\n                + \", which is does not support provisioning, \"\n                + Build.VERSION_CODES.JELLY_BEAN_MR2\n                + \" and higher is required\");\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/BandwidthMeterCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport android.content.Context;\n\nimport com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;\n\nclass BandwidthMeterCreator {\n    private final Context context;\n\n    BandwidthMeterCreator(Context context) {\n        this.context = context;\n    }\n\n    DefaultBandwidthMeter create(long maxInitialBitrate) {\n        return new DefaultBandwidthMeter.Builder(context)\n                .setInitialBitrateEstimate(maxInitialBitrate)\n                .build();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelector.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.novoda.noplayer.ContentType;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerAudioTrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerSubtitleTrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerVideoTrackSelector;\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\n\nimport java.util.List;\n\nclass CompositeTrackSelector {\n\n    private final DefaultTrackSelector defaultTrackSelector;\n    private final ExoPlayerAudioTrackSelector audioTrackSelector;\n    private final ExoPlayerVideoTrackSelector videoTrackSelector;\n    private final ExoPlayerSubtitleTrackSelector subtitleTrackSelector;\n\n    CompositeTrackSelector(DefaultTrackSelector defaultTrackSelector,\n                           ExoPlayerAudioTrackSelector audioTrackSelector,\n                           ExoPlayerVideoTrackSelector videoTrackSelector,\n                           ExoPlayerSubtitleTrackSelector subtitleTrackSelector) {\n        this.defaultTrackSelector = defaultTrackSelector;\n        this.audioTrackSelector = audioTrackSelector;\n        this.videoTrackSelector = videoTrackSelector;\n        this.subtitleTrackSelector = subtitleTrackSelector;\n    }\n\n    TrackSelector trackSelector() {\n        return defaultTrackSelector;\n    }\n\n    boolean selectAudioTrack(PlayerAudioTrack audioTrack, RendererTypeRequester rendererTypeRequester) {\n        return audioTrackSelector.selectAudioTrack(audioTrack, rendererTypeRequester);\n    }\n\n    AudioTracks getAudioTracks(RendererTypeRequester rendererTypeRequester) {\n        return audioTrackSelector.getAudioTracks(rendererTypeRequester);\n    }\n\n    boolean clearAudioTrack(RendererTypeRequester rendererTypeRequester) {\n        return audioTrackSelector.clearAudioTrack(rendererTypeRequester);\n    }\n\n    boolean selectVideoTrack(PlayerVideoTrack videoTrack, RendererTypeRequester rendererTypeRequester) {\n        return videoTrackSelector.selectVideoTrack(videoTrack, rendererTypeRequester);\n    }\n\n    List<PlayerVideoTrack> getVideoTracks(RendererTypeRequester rendererTypeRequester, ContentType contentType) {\n        return videoTrackSelector.getVideoTracks(rendererTypeRequester, contentType);\n    }\n\n    Optional<PlayerVideoTrack> getSelectedVideoTrack(SimpleExoPlayer exoPlayer,\n                                                     RendererTypeRequester rendererTypeRequester,\n                                                     ContentType contentType) {\n        return videoTrackSelector.getSelectedVideoTrack(exoPlayer, rendererTypeRequester, contentType);\n    }\n\n    boolean clearVideoTrack(RendererTypeRequester rendererTypeRequester) {\n        return videoTrackSelector.clearVideoTrack(rendererTypeRequester);\n    }\n\n    boolean selectTextTrack(PlayerSubtitleTrack subtitleTrack, RendererTypeRequester rendererTypeRequester) {\n        return subtitleTrackSelector.selectTextTrack(subtitleTrack, rendererTypeRequester);\n    }\n\n    List<PlayerSubtitleTrack> getSubtitleTracks(RendererTypeRequester rendererTypeRequester) {\n        return subtitleTrackSelector.getSubtitleTracks(rendererTypeRequester);\n    }\n\n    boolean clearSubtitleTrack(RendererTypeRequester rendererTypeRequester) {\n        return subtitleTrackSelector.clearSubtitleTrack(rendererTypeRequester);\n    }\n\n    void clearMaxVideoBitrate() {\n        videoTrackSelector.clearMaxVideoBitrate();\n    }\n\n    void setMaxVideoBitrate(int maxVideoBitrate) {\n        videoTrackSelector.setMaxVideoBitrate(maxVideoBitrate);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelectorCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;\nimport com.google.android.exoplayer2.util.Clock;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerAudioTrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerSubtitleTrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerTrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.ExoPlayerVideoTrackSelector;\n\nclass CompositeTrackSelectorCreator {\n\n    CompositeTrackSelector create(Options options, DefaultBandwidthMeter bandwidthMeter) {\n        TrackSelection.Factory adaptiveTrackSelectionFactory = new AdaptiveTrackSelection.Factory(\n                bandwidthMeter,\n                options.minDurationBeforeQualityIncreaseInMillis(),\n                AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,\n                AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,\n                AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION,\n                AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,\n                AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,\n                Clock.DEFAULT\n        );\n        DefaultTrackSelector trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);\n        DefaultTrackSelector.Parameters trackSelectorParameters = trackSelector.buildUponParameters()\n                .setMaxVideoBitrate(options.maxVideoBitrate())\n                .build();\n        trackSelector.setParameters(trackSelectorParameters);\n\n        ExoPlayerTrackSelector exoPlayerTrackSelector = ExoPlayerTrackSelector.newInstance(trackSelector);\n        ExoPlayerAudioTrackSelector audioTrackSelector = new ExoPlayerAudioTrackSelector(exoPlayerTrackSelector);\n        ExoPlayerVideoTrackSelector videoTrackSelector = new ExoPlayerVideoTrackSelector(exoPlayerTrackSelector);\n        ExoPlayerSubtitleTrackSelector subtitleTrackSelector = new ExoPlayerSubtitleTrackSelector(exoPlayerTrackSelector);\n        return new CompositeTrackSelector(trackSelector, audioTrackSelector, videoTrackSelector, subtitleTrackSelector);\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport android.content.Context;\nimport android.support.annotation.NonNull;\n\nimport com.google.android.exoplayer2.DefaultLoadControl;\nimport com.google.android.exoplayer2.ExoPlayerFactory;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.text.SubtitleDecoderFactory;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator;\nimport com.novoda.noplayer.text.NoPlayerSubtitleDecoderFactory;\n\nimport static com.novoda.noplayer.internal.exoplayer.SimpleRenderersFactory.EXTENSION_RENDERER_MODE_OFF;\n\nclass ExoPlayerCreator {\n\n    private static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000;\n\n    private final Context context;\n\n    ExoPlayerCreator(Context context) {\n        this.context = context;\n    }\n\n    @NonNull\n    public SimpleExoPlayer create(DrmSessionCreator drmSessionCreator,\n                                  DefaultDrmSessionEventListener drmSessionEventListener,\n                                  MediaCodecSelector mediaCodecSelector,\n                                  TrackSelector trackSelector) {\n        DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = drmSessionCreator.create(drmSessionEventListener);\n        SubtitleDecoderFactory subtitleDecoderFactory = new NoPlayerSubtitleDecoderFactory();\n        RenderersFactory renderersFactory = new SimpleRenderersFactory(\n                context,\n                EXTENSION_RENDERER_MODE_OFF,\n                DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS,\n                mediaCodecSelector,\n                subtitleDecoderFactory\n        );\n\n        DefaultLoadControl loadControl = new DefaultLoadControl();\n        return ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl, drmSessionManager);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerCueMapper.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.google.android.exoplayer2.text.Cue;\nimport com.novoda.noplayer.model.NoPlayerCue;\nimport com.novoda.noplayer.model.TextCues;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nfinal class ExoPlayerCueMapper {\n\n    private ExoPlayerCueMapper() {\n        // static class.\n    }\n\n    static TextCues map(List<Cue> cues) {\n        if (cues == null) {\n            return TextCues.of(Collections.<NoPlayerCue>emptyList());\n        }\n\n        List<NoPlayerCue> noPlayerCues = new ArrayList<>(cues.size());\n\n        for (Cue cue : cues) {\n            NoPlayerCue noPlayerCue = new NoPlayerCue(\n                    cue.text,\n                    cue.textAlignment,\n                    cue.bitmap,\n                    cue.line,\n                    cue.lineType,\n                    cue.lineAnchor,\n                    cue.position,\n                    cue.positionAnchor,\n                    cue.size,\n                    cue.bitmapHeight,\n                    cue.windowColorSet,\n                    cue.windowColor\n            );\n            noPlayerCues.add(noPlayerCue);\n        }\n        return TextCues.of(noPlayerCues);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport android.net.Uri;\nimport android.support.annotation.Nullable;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.audio.AudioAttributes;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.PlayerSurfaceHolder;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator;\nimport com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.MediaSourceFactory;\nimport com.novoda.noplayer.internal.utils.AndroidDeviceVersion;\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\n\nimport java.util.List;\n\nclass ExoPlayerFacade {\n\n    private static final boolean DO_NOT_RESET_STATE = false;\n\n    private final BandwidthMeterCreator bandwidthMeterCreator;\n    private final AndroidDeviceVersion androidDeviceVersion;\n    private final MediaSourceFactory mediaSourceFactory;\n    private final CompositeTrackSelectorCreator trackSelectorCreator;\n    private final ExoPlayerCreator exoPlayerCreator;\n    private final RendererTypeRequesterCreator rendererTypeRequesterCreator;\n\n    @Nullable\n    private SimpleExoPlayer exoPlayer;\n    @Nullable\n    private CompositeTrackSelector compositeTrackSelector;\n    @Nullable\n    private RendererTypeRequester rendererTypeRequester;\n    @Nullable\n    private Options options;\n\n    ExoPlayerFacade(BandwidthMeterCreator bandwidthMeterCreator,\n                    AndroidDeviceVersion androidDeviceVersion,\n                    MediaSourceFactory mediaSourceFactory,\n                    CompositeTrackSelectorCreator trackSelectorCreator,\n                    ExoPlayerCreator exoPlayerCreator,\n                    RendererTypeRequesterCreator rendererTypeRequesterCreator) {\n        this.bandwidthMeterCreator = bandwidthMeterCreator;\n        this.androidDeviceVersion = androidDeviceVersion;\n        this.mediaSourceFactory = mediaSourceFactory;\n        this.trackSelectorCreator = trackSelectorCreator;\n        this.exoPlayerCreator = exoPlayerCreator;\n        this.rendererTypeRequesterCreator = rendererTypeRequesterCreator;\n    }\n\n    boolean isPlaying() {\n        return exoPlayer != null && exoPlayer.getPlayWhenReady();\n    }\n\n    long playheadPositionInMillis() throws IllegalStateException {\n        assertVideoLoaded();\n        return exoPlayer.getCurrentPosition();\n    }\n\n    long mediaDurationInMillis() throws IllegalStateException {\n        assertVideoLoaded();\n        return exoPlayer.getDuration();\n    }\n\n    int bufferPercentage() throws IllegalStateException {\n        assertVideoLoaded();\n        return exoPlayer.getBufferedPercentage();\n    }\n\n    void play(long positionInMillis) throws IllegalStateException {\n        seekTo(positionInMillis);\n        play();\n    }\n\n    void play() throws IllegalStateException {\n        assertVideoLoaded();\n        exoPlayer.setPlayWhenReady(true);\n    }\n\n    void pause() throws IllegalStateException {\n        assertVideoLoaded();\n        exoPlayer.setPlayWhenReady(false);\n    }\n\n    void seekTo(long positionInMillis) throws IllegalStateException {\n        assertVideoLoaded();\n        exoPlayer.seekTo(positionInMillis);\n    }\n\n    void release() {\n        if (exoPlayer != null) {\n            exoPlayer.release();\n            exoPlayer = null;\n        }\n    }\n\n    void loadVideo(PlayerSurfaceHolder playerSurfaceHolder,\n                   DrmSessionCreator drmSessionCreator,\n                   Uri uri,\n                   Options options,\n                   ExoPlayerForwarder forwarder,\n                   MediaCodecSelector mediaCodecSelector) {\n        this.options = options;\n\n        DefaultBandwidthMeter bandwidthMeter = bandwidthMeterCreator.create(options.maxInitialBitrate());\n\n        compositeTrackSelector = trackSelectorCreator.create(options, bandwidthMeter);\n        exoPlayer = exoPlayerCreator.create(\n                drmSessionCreator,\n                forwarder.drmSessionEventListener(),\n                mediaCodecSelector,\n                compositeTrackSelector.trackSelector()\n        );\n        rendererTypeRequester = rendererTypeRequesterCreator.createfrom(exoPlayer);\n        exoPlayer.addListener(forwarder.exoPlayerEventListener());\n        exoPlayer.addAnalyticsListener(forwarder.analyticsListener());\n        exoPlayer.addVideoListener(forwarder.videoListener());\n\n        setMovieAudioAttributes(exoPlayer);\n\n        MediaSource mediaSource = mediaSourceFactory.create(\n                options,\n                uri,\n                forwarder.mediaSourceEventListener(),\n                bandwidthMeter\n        );\n        attachToSurface(playerSurfaceHolder);\n\n        boolean hasInitialPosition = options.getInitialPositionInMillis().isPresent();\n        if (hasInitialPosition) {\n            Long initialPositionInMillis = options.getInitialPositionInMillis().get();\n            exoPlayer.seekTo(initialPositionInMillis);\n        }\n\n        exoPlayer.prepare(mediaSource, !hasInitialPosition, DO_NOT_RESET_STATE);\n    }\n\n    private void setMovieAudioAttributes(SimpleExoPlayer exoPlayer) {\n        if (androidDeviceVersion.isLollipopTwentyOneOrAbove()) {\n            AudioAttributes audioAttributes = new AudioAttributes.Builder()\n                    .setContentType(C.CONTENT_TYPE_MOVIE)\n                    .build();\n            exoPlayer.setAudioAttributes(audioAttributes);\n        }\n    }\n\n    private void attachToSurface(PlayerSurfaceHolder playerSurfaceHolder) {\n        playerSurfaceHolder.attach(exoPlayer);\n    }\n\n    AudioTracks getAudioTracks() throws IllegalStateException {\n        assertVideoLoaded();\n        return compositeTrackSelector.getAudioTracks(rendererTypeRequester);\n    }\n\n    boolean selectAudioTrack(PlayerAudioTrack audioTrack) throws IllegalStateException {\n        assertVideoLoaded();\n        return compositeTrackSelector.selectAudioTrack(audioTrack, rendererTypeRequester);\n    }\n\n    boolean clearAudioTrackSelection() {\n        assertVideoLoaded();\n        return compositeTrackSelector.clearAudioTrack(rendererTypeRequester);\n    }\n\n    boolean selectVideoTrack(PlayerVideoTrack playerVideoTrack) {\n        assertVideoLoaded();\n        return compositeTrackSelector.selectVideoTrack(playerVideoTrack, rendererTypeRequester);\n    }\n\n    Optional<PlayerVideoTrack> getSelectedVideoTrack() {\n        assertVideoLoaded();\n        return compositeTrackSelector.getSelectedVideoTrack(exoPlayer, rendererTypeRequester, options.contentType());\n    }\n\n    List<PlayerVideoTrack> getVideoTracks() {\n        assertVideoLoaded();\n        return compositeTrackSelector.getVideoTracks(rendererTypeRequester, options.contentType());\n    }\n\n    boolean clearVideoTrackSelection() {\n        assertVideoLoaded();\n        return compositeTrackSelector.clearVideoTrack(rendererTypeRequester);\n    }\n\n    void setSubtitleRendererOutput(TextRendererOutput textRendererOutput) throws IllegalStateException {\n        assertVideoLoaded();\n        exoPlayer.addTextOutput(textRendererOutput.output());\n    }\n\n    void removeSubtitleRendererOutput(TextRendererOutput textRendererOutput) throws IllegalStateException {\n        assertVideoLoaded();\n        exoPlayer.removeTextOutput(textRendererOutput.output());\n    }\n\n    boolean selectSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException {\n        assertVideoLoaded();\n        return compositeTrackSelector.selectTextTrack(subtitleTrack, rendererTypeRequester);\n    }\n\n    List<PlayerSubtitleTrack> getSubtitleTracks() throws IllegalStateException {\n        assertVideoLoaded();\n        return compositeTrackSelector.getSubtitleTracks(rendererTypeRequester);\n    }\n\n    boolean hasPlayedContent() {\n        return exoPlayer != null;\n    }\n\n    boolean clearSubtitleTrackSelection() throws IllegalStateException {\n        assertVideoLoaded();\n        return compositeTrackSelector.clearSubtitleTrack(rendererTypeRequester);\n    }\n\n    void setRepeating(boolean repeating) {\n        assertVideoLoaded();\n        exoPlayer.setRepeatMode(repeating ? Player.REPEAT_MODE_ALL : Player.REPEAT_MODE_OFF);\n    }\n\n    void setVolume(float volume) {\n        assertVideoLoaded();\n        exoPlayer.setVolume(volume);\n    }\n\n    float getVolume() {\n        assertVideoLoaded();\n        return exoPlayer.getVolume();\n    }\n\n    void clearMaxVideoBitrate() {\n        assertVideoLoaded();\n        compositeTrackSelector.clearMaxVideoBitrate();\n    }\n\n    void setMaxVideoBitrate(int maxVideoBitrate) {\n        assertVideoLoaded();\n        compositeTrackSelector.setMaxVideoBitrate(maxVideoBitrate);\n    }\n\n    private void assertVideoLoaded() {\n        if (exoPlayer == null) {\n            throw new IllegalStateException(\"Video must be loaded before trying to interact with the player\");\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerInformation.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.novoda.noplayer.PlayerInformation;\nimport com.novoda.noplayer.PlayerType;\n\nclass ExoPlayerInformation implements PlayerInformation {\n    @Override\n    public PlayerType getPlayerType() {\n        return PlayerType.EXO_PLAYER;\n    }\n\n    @Override\n    public String getVersion() {\n        return ExoPlayerLibraryInfo.VERSION;\n    }\n\n    @Override\n    public String getName() {\n        return \"ExoPlayer\";\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerTwoImpl.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport android.net.Uri;\nimport android.support.annotation.Nullable;\nimport android.view.View;\n\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.novoda.noplayer.Listeners;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.PlayerInformation;\nimport com.novoda.noplayer.PlayerState;\nimport com.novoda.noplayer.PlayerView;\nimport com.novoda.noplayer.internal.Heart;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator;\nimport com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder;\nimport com.novoda.noplayer.internal.listeners.PlayerListenersHolder;\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.LoadTimeout;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\nimport com.novoda.noplayer.model.Timeout;\n\nimport java.util.List;\n\n// Not much we can do, wrapping ExoPlayer is a lot of work\n@SuppressWarnings(\"PMD.GodClass\")\nclass ExoPlayerTwoImpl implements NoPlayer {\n\n    private final ExoPlayerFacade exoPlayer;\n    private final PlayerListenersHolder listenersHolder;\n    private final ExoPlayerForwarder forwarder;\n    private final Heart heart;\n    private final DrmSessionCreator drmSessionCreator;\n    private final MediaCodecSelector mediaCodecSelector;\n    private final LoadTimeout loadTimeout;\n\n    @Nullable\n    private PlayerView playerView;\n\n    private int videoWidth;\n    private int videoHeight;\n    private TextRendererOutput textRendererOutput;\n\n    ExoPlayerTwoImpl(ExoPlayerFacade exoPlayer,\n                     PlayerListenersHolder listenersHolder,\n                     ExoPlayerForwarder exoPlayerForwarder,\n                     LoadTimeout loadTimeoutParam,\n                     Heart heart,\n                     DrmSessionCreator drmSessionCreator,\n                     MediaCodecSelector mediaCodecSelector) {\n        this.exoPlayer = exoPlayer;\n        this.listenersHolder = listenersHolder;\n        this.loadTimeout = loadTimeoutParam;\n        this.forwarder = exoPlayerForwarder;\n        this.heart = heart;\n        this.drmSessionCreator = drmSessionCreator;\n        this.mediaCodecSelector = mediaCodecSelector;\n    }\n\n    void initialise() {\n        heart.bind(new Heart.Heartbeat(listenersHolder.getHeartbeatCallbacks(), this));\n        forwarder.bind(listenersHolder.getPreparedListeners(), this);\n        forwarder.bind(listenersHolder.getCompletionListeners(), listenersHolder.getStateChangedListeners());\n        forwarder.bind(listenersHolder.getErrorListeners());\n        forwarder.bind(listenersHolder.getBufferStateListeners());\n        forwarder.bind(listenersHolder.getVideoSizeChangedListeners());\n        forwarder.bind(listenersHolder.getBitrateChangedListeners());\n        forwarder.bind(listenersHolder.getInfoListeners());\n        forwarder.bind(listenersHolder.getDroppedVideoFramesListeners());\n        listenersHolder.addPreparedListener(new PreparedListener() {\n            @Override\n            public void onPrepared(PlayerState playerState) {\n                loadTimeout.cancel();\n            }\n        });\n        listenersHolder.addErrorListener(new ErrorListener() {\n            @Override\n            public void onError(PlayerError error) {\n                reset();\n            }\n        });\n        listenersHolder.addVideoSizeChangedListener(new VideoSizeChangedListener() {\n            @Override\n            public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n                videoWidth = width;\n                videoHeight = height;\n            }\n        });\n    }\n\n    @Override\n    public boolean isPlaying() {\n        return exoPlayer.isPlaying();\n    }\n\n    @Override\n    public int videoWidth() {\n        return videoWidth;\n    }\n\n    @Override\n    public int videoHeight() {\n        return videoHeight;\n    }\n\n    @Override\n    public long playheadPositionInMillis() throws IllegalStateException {\n        return exoPlayer.playheadPositionInMillis();\n    }\n\n    @Override\n    public long mediaDurationInMillis() throws IllegalStateException {\n        return exoPlayer.mediaDurationInMillis();\n    }\n\n    @Override\n    public int bufferPercentage() throws IllegalStateException {\n        return exoPlayer.bufferPercentage();\n    }\n\n    @Override\n    public void setRepeating(boolean repeating) {\n        exoPlayer.setRepeating(repeating);\n    }\n\n    @Override\n    public void setVolume(float volume) {\n        exoPlayer.setVolume(volume);\n    }\n\n    @Override\n    public float getVolume() {\n        return exoPlayer.getVolume();\n    }\n\n    @Override\n    public void clearMaxVideoBitrate() {\n        exoPlayer.clearMaxVideoBitrate();\n    }\n\n    @Override\n    public void setMaxVideoBitrate(int maxVideoBitrate) {\n        exoPlayer.setMaxVideoBitrate(maxVideoBitrate);\n    }\n\n    @Override\n    public Listeners getListeners() {\n        return listenersHolder;\n    }\n\n    @Override\n    public void play() throws IllegalStateException {\n        heart.startBeatingHeart();\n        exoPlayer.play();\n        listenersHolder.getStateChangedListeners().onVideoPlaying();\n    }\n\n    @Override\n    public void playAt(long positionInMillis) throws IllegalStateException {\n        seekTo(positionInMillis);\n        play();\n    }\n\n    @Override\n    public void pause() throws IllegalStateException {\n        exoPlayer.pause();\n        listenersHolder.getStateChangedListeners().onVideoPaused();\n        if (heart.isBeating()) {\n            heart.stopBeatingHeart();\n            heart.forceBeat();\n        }\n    }\n\n    @Override\n    public void seekTo(long positionInMillis) throws IllegalStateException {\n        exoPlayer.seekTo(positionInMillis);\n    }\n\n    @Override\n    public void stop() {\n        reset();\n        listenersHolder.getStateChangedListeners().onVideoStopped();\n    }\n\n    @Override\n    public void release() {\n        stop();\n        listenersHolder.clear();\n    }\n\n    private void reset() {\n        listenersHolder.resetState();\n        loadTimeout.cancel();\n        heart.stopBeatingHeart();\n        exoPlayer.release();\n        destroySurfaceByHidingVideoContainer();\n    }\n\n    private void destroySurfaceByHidingVideoContainer() {\n        if (playerView != null) {\n            playerView.getContainerView().setVisibility(View.GONE);\n        }\n    }\n\n    @Override\n    public void loadVideo(final Uri uri, final Options options) {\n        if (exoPlayer.hasPlayedContent()) {\n            stop();\n        }\n        assertPlayerViewIsAttached();\n        exoPlayer.loadVideo(playerView.getPlayerSurfaceHolder(), drmSessionCreator, uri, options, forwarder, mediaCodecSelector);\n        createSurfaceByShowingVideoContainer();\n    }\n\n    private void assertPlayerViewIsAttached() {\n        if (playerView == null) {\n            throw new IllegalStateException(\"A PlayerView must be attached in order to loadVideo\");\n        }\n    }\n\n    private void createSurfaceByShowingVideoContainer() {\n        playerView.getContainerView().setVisibility(View.VISIBLE);\n    }\n\n    @Override\n    public void loadVideoWithTimeout(Uri uri, Options options, Timeout timeout, LoadTimeoutCallback loadTimeoutCallback) {\n        loadTimeout.start(timeout, loadTimeoutCallback);\n        loadVideo(uri, options);\n    }\n\n    @Override\n    public PlayerInformation getPlayerInformation() {\n        return new ExoPlayerInformation();\n    }\n\n    @Override\n    public void attach(PlayerView playerView) {\n        this.playerView = playerView;\n        listenersHolder.addStateChangedListener(playerView.getStateChangedListener());\n        listenersHolder.addVideoSizeChangedListener(playerView.getVideoSizeChangedListener());\n    }\n\n    @Override\n    public void detach(PlayerView playerView) {\n        listenersHolder.removeStateChangedListener(playerView.getStateChangedListener());\n        listenersHolder.removeVideoSizeChangedListener(playerView.getVideoSizeChangedListener());\n        removeSubtitleRenderer();\n        this.playerView = null;\n    }\n\n    @Override\n    public boolean selectAudioTrack(PlayerAudioTrack audioTrack) throws IllegalStateException {\n        return exoPlayer.selectAudioTrack(audioTrack);\n    }\n\n    @Override\n    public boolean clearAudioTrackSelection() throws IllegalStateException {\n        return exoPlayer.clearAudioTrackSelection();\n    }\n\n    @Override\n    public boolean showSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException {\n        setSubtitleRendererOutput();\n        playerView.showSubtitles();\n        return exoPlayer.selectSubtitleTrack(subtitleTrack);\n    }\n\n    private void setSubtitleRendererOutput() throws IllegalStateException {\n        removeSubtitleRenderer();\n        textRendererOutput = new TextRendererOutput(playerView);\n        exoPlayer.setSubtitleRendererOutput(textRendererOutput);\n    }\n\n    @Override\n    public boolean hideSubtitleTrack() throws IllegalStateException {\n        playerView.hideSubtitles();\n        removeSubtitleRenderer();\n        return exoPlayer.clearSubtitleTrackSelection();\n    }\n\n    private void removeSubtitleRenderer() {\n        if (textRendererOutput != null) {\n            exoPlayer.removeSubtitleRendererOutput(textRendererOutput);\n        }\n    }\n\n    @Override\n    public AudioTracks getAudioTracks() throws IllegalStateException {\n        return exoPlayer.getAudioTracks();\n    }\n\n    @Override\n    public boolean selectVideoTrack(PlayerVideoTrack videoTrack) throws IllegalStateException {\n        return exoPlayer.selectVideoTrack(videoTrack);\n    }\n\n    @Override\n    public Optional<PlayerVideoTrack> getSelectedVideoTrack() throws IllegalStateException {\n        return exoPlayer.getSelectedVideoTrack();\n    }\n\n    @Override\n    public boolean clearVideoTrackSelection() throws IllegalStateException {\n        return exoPlayer.clearVideoTrackSelection();\n    }\n\n    @Override\n    public List<PlayerVideoTrack> getVideoTracks() throws IllegalStateException {\n        return exoPlayer.getVideoTracks();\n    }\n\n    @Override\n    public List<PlayerSubtitleTrack> getSubtitleTracks() throws IllegalStateException {\n        return exoPlayer.getSubtitleTracks();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/NoPlayerExoPlayerCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport android.content.Context;\nimport android.os.Handler;\n\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.Heart;\nimport com.novoda.noplayer.internal.SystemClock;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator;\nimport com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.MediaSourceFactory;\nimport com.novoda.noplayer.internal.listeners.PlayerListenersHolder;\nimport com.novoda.noplayer.internal.utils.AndroidDeviceVersion;\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.LoadTimeout;\n\npublic class NoPlayerExoPlayerCreator {\n\n    private final InternalCreator internalCreator;\n\n    public static NoPlayerExoPlayerCreator newInstance(String userAgent, Handler handler) {\n        InternalCreator internalCreator = new InternalCreator(userAgent, handler, Optional.<DataSource.Factory>absent());\n        return new NoPlayerExoPlayerCreator(internalCreator);\n    }\n\n    public static NoPlayerExoPlayerCreator newInstance(String userAgent, Handler handler, DataSource.Factory dataSourceFactory) {\n        InternalCreator internalCreator = new InternalCreator(userAgent, handler, Optional.of(dataSourceFactory));\n        return new NoPlayerExoPlayerCreator(internalCreator);\n    }\n\n    NoPlayerExoPlayerCreator(InternalCreator internalCreator) {\n        this.internalCreator = internalCreator;\n    }\n\n    public NoPlayer createExoPlayer(Context context,\n                                    DrmSessionCreator drmSessionCreator,\n                                    boolean downgradeSecureDecoder,\n                                    boolean allowCrossProtocolRedirects) {\n        ExoPlayerTwoImpl player = internalCreator.create(context, drmSessionCreator, downgradeSecureDecoder, allowCrossProtocolRedirects);\n        player.initialise();\n        return player;\n    }\n\n    static class InternalCreator {\n\n        private final Handler handler;\n        private final Optional<DataSource.Factory> dataSourceFactory;\n        private final String userAgent;\n\n        InternalCreator(String userAgent, Handler handler, Optional<DataSource.Factory> dataSourceFactory) {\n            this.userAgent = userAgent;\n            this.handler = handler;\n            this.dataSourceFactory = dataSourceFactory;\n        }\n\n        ExoPlayerTwoImpl create(Context context,\n                                DrmSessionCreator drmSessionCreator,\n                                boolean downgradeSecureDecoder,\n                                boolean allowCrossProtocolRedirects) {\n            MediaSourceFactory mediaSourceFactory = new MediaSourceFactory(\n                    context,\n                    userAgent,\n                    handler,\n                    dataSourceFactory,\n                    allowCrossProtocolRedirects\n            );\n\n            MediaCodecSelector mediaCodecSelector = downgradeSecureDecoder\n                    ? SecurityDowngradingCodecSelector.newInstance()\n                    : MediaCodecSelector.DEFAULT_WITH_FALLBACK;\n\n            CompositeTrackSelectorCreator trackSelectorCreator = new CompositeTrackSelectorCreator();\n\n            ExoPlayerCreator exoPlayerCreator = new ExoPlayerCreator(context);\n            RendererTypeRequesterCreator rendererTypeRequesterCreator = new RendererTypeRequesterCreator();\n            AndroidDeviceVersion androidDeviceVersion = AndroidDeviceVersion.newInstance();\n            BandwidthMeterCreator bandwidthMeterCreator = new BandwidthMeterCreator(context);\n            ExoPlayerFacade exoPlayerFacade = new ExoPlayerFacade(\n                    bandwidthMeterCreator,\n                    androidDeviceVersion,\n                    mediaSourceFactory,\n                    trackSelectorCreator,\n                    exoPlayerCreator,\n                    rendererTypeRequesterCreator\n            );\n\n            PlayerListenersHolder listenersHolder = new PlayerListenersHolder();\n            ExoPlayerForwarder exoPlayerForwarder = new ExoPlayerForwarder();\n            LoadTimeout loadTimeout = new LoadTimeout(new SystemClock(), handler);\n            Heart heart = Heart.newInstance(handler);\n\n            return new ExoPlayerTwoImpl(\n                    exoPlayerFacade,\n                    listenersHolder,\n                    exoPlayerForwarder,\n                    loadTimeout,\n                    heart,\n                    drmSessionCreator,\n                    mediaCodecSelector\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/RendererTypeRequester.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\npublic interface RendererTypeRequester {\n\n    int getRendererTypeFor(int index);\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/RendererTypeRequesterCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.google.android.exoplayer2.SimpleExoPlayer;\n\nclass RendererTypeRequesterCreator {\n\n    RendererTypeRequester createfrom(final SimpleExoPlayer exoPlayer) {\n        return new RendererTypeRequester() {\n            @Override\n            public int getRendererTypeFor(int index) {\n                return exoPlayer.getRendererType(index);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/SecurityDowngradingCodecSelector.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.google.android.exoplayer2.mediacodec.MediaCodecInfo;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\n\nimport java.util.List;\n\nclass SecurityDowngradingCodecSelector implements MediaCodecSelector {\n\n    private static final boolean USE_INSECURE_DECODER = false;\n\n    private final InternalMediaCodecUtil internalMediaCodecUtil;\n\n    public static SecurityDowngradingCodecSelector newInstance() {\n        InternalMediaCodecUtil internalMediaCodecUtil = new InternalMediaCodecUtil();\n        return new SecurityDowngradingCodecSelector(internalMediaCodecUtil);\n    }\n\n    SecurityDowngradingCodecSelector(InternalMediaCodecUtil internalMediaCodecUtil) {\n        this.internalMediaCodecUtil = internalMediaCodecUtil;\n    }\n\n    @Override\n    public List<MediaCodecInfo> getDecoderInfos(String mimeType, boolean requiresSecureDecoder) throws MediaCodecUtil.DecoderQueryException {\n        return internalMediaCodecUtil.getDecoderInfos(mimeType, USE_INSECURE_DECODER);\n    }\n\n    @Override\n    public MediaCodecInfo getPassthroughDecoderInfo() throws MediaCodecUtil.DecoderQueryException {\n        return internalMediaCodecUtil.getPassthroughDecoderInfo();\n    }\n\n    static class InternalMediaCodecUtil {\n\n        List<MediaCodecInfo> getDecoderInfos(String mimeType, boolean requiresSecureDecoder) throws MediaCodecUtil.DecoderQueryException {\n            return MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder);\n        }\n\n        MediaCodecInfo getPassthroughDecoderInfo() throws MediaCodecUtil.DecoderQueryException {\n            return MediaCodecUtil.getPassthroughDecoderInfo();\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/SimpleRenderersFactory.java",
    "content": "/*\n * Copyright (C) 2017 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.novoda.noplayer.internal.exoplayer;\n\nimport android.content.Context;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.support.annotation.IntDef;\nimport android.support.annotation.Nullable;\nimport android.util.Log;\n\nimport com.google.android.exoplayer2.Renderer;\nimport com.google.android.exoplayer2.RenderersFactory;\nimport com.google.android.exoplayer2.audio.AudioCapabilities;\nimport com.google.android.exoplayer2.audio.AudioProcessor;\nimport com.google.android.exoplayer2.audio.AudioRendererEventListener;\nimport com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.metadata.MetadataOutput;\nimport com.google.android.exoplayer2.metadata.MetadataRenderer;\nimport com.google.android.exoplayer2.text.SubtitleDecoder;\nimport com.google.android.exoplayer2.text.SubtitleDecoderFactory;\nimport com.google.android.exoplayer2.text.TextOutput;\nimport com.google.android.exoplayer2.text.TextRenderer;\nimport com.google.android.exoplayer2.trackselection.TrackSelector;\nimport com.google.android.exoplayer2.video.MediaCodecVideoRenderer;\nimport com.google.android.exoplayer2.video.VideoRendererEventListener;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.reflect.Constructor;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Default {@link RenderersFactory} implementation.\n */\nclass SimpleRenderersFactory implements RenderersFactory {\n\n    private static final boolean DO_NOT_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS = false;\n    private static final boolean INIT_ARGS = true;\n    private static final boolean PLAY_CLEAR_SAMPLES_WITHOUT_KEYS = true;\n\n    /**\n     * Modes for using extension renderers.\n     */\n    @Retention(RetentionPolicy.SOURCE)\n    @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON,\n            EXTENSION_RENDERER_MODE_PREFER})\n    @interface ExtensionRendererMode {\n\n    }\n\n    /**\n     * Do not allow use of extension renderers.\n     */\n    static final int EXTENSION_RENDERER_MODE_OFF = 0;\n\n    /**\n     * Allow use of extension renderers. Extension renderers are indexed after core renderers of the\n     * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore\n     * prefer to use a core renderer to an extension renderer in the case that both are able to play\n     * a given track.\n     */\n    static final int EXTENSION_RENDERER_MODE_ON = 1;\n    /**\n     * Allow use of extension renderers. Extension renderers are indexed before core renderers of the\n     * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore\n     * prefer to use an extension renderer to a core renderer in the case that both are able to play\n     * a given track.\n     */\n    static final int EXTENSION_RENDERER_MODE_PREFER = 2;\n    private static final String TAG = \"DefaultRenderersFactory\";\n\n    private static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;\n\n    private final Context context;\n\n    @ExtensionRendererMode\n    private final int extensionRendererMode;\n\n    private final long allowedVideoJoiningTimeMs;\n    private final MediaCodecSelector mediaCodecSelector;\n    private final SubtitleDecoderFactory subtitleDecoderFactory;\n\n    /**\n     * @param context                   A {@link Context}.\n     * @param extensionRendererMode     The extension renderer mode, which determines if and how\n     *                                  available extension renderers are used. Note that extensions must be included in the\n     *                                  application build for them to be considered available.\n     * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt\n     *                                  to seamlessly join an ongoing playback.\n     * @param mediaCodecSelector        Used for selecting the codec for the video renderer.\n     * @param subtitleDecoderFactory    A factory from which to obtain {@link SubtitleDecoder} instances.\n     */\n    SimpleRenderersFactory(Context context,\n                           @ExtensionRendererMode int extensionRendererMode,\n                           long allowedVideoJoiningTimeMs,\n                           MediaCodecSelector mediaCodecSelector,\n                           SubtitleDecoderFactory subtitleDecoderFactory) {\n        this.context = context;\n        this.extensionRendererMode = extensionRendererMode;\n        this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;\n        this.mediaCodecSelector = mediaCodecSelector;\n        this.subtitleDecoderFactory = subtitleDecoderFactory;\n    }\n\n    @Override\n    public Renderer[] createRenderers(Handler eventHandler,\n                                      VideoRendererEventListener videoRendererEventListener,\n                                      AudioRendererEventListener audioRendererEventListener,\n                                      TextOutput textRendererOutput,\n                                      MetadataOutput metadataRendererOutput,\n                                      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {\n        ArrayList<Renderer> renderersList = new ArrayList<>();\n        buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs,\n                eventHandler, videoRendererEventListener, extensionRendererMode, renderersList);\n        buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(),\n                eventHandler, audioRendererEventListener, extensionRendererMode, renderersList);\n        buildTextRenderers(textRendererOutput, eventHandler.getLooper(), renderersList, subtitleDecoderFactory);\n        buildMetadataRenderers(metadataRendererOutput, eventHandler.getLooper(),\n                renderersList);\n        buildMiscellaneousRenderers();\n        return renderersList.toArray(new Renderer[renderersList.size()]);\n    }\n\n    /**\n     * Builds video renderers for use by the player.\n     *\n     * @param context                   The {@link Context} associated with the player.\n     * @param drmSessionManager         An optional {@link DrmSessionManager}. May be null if the player\n     *                                  will not be used for DRM protected playbacks.\n     * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video\n     *                                  renderers can attempt to seamlessly join an ongoing playback.\n     * @param eventHandler              A handler associated with the main thread's looper.\n     * @param eventListener             An event listener.\n     * @param extensionRendererMode     The extension renderer mode.\n     * @param outRenderers              An array to which the built renderers should be appended.\n     */\n    @SuppressWarnings({\"PMD.AvoidCatchingGenericException\"})   // Using reflection and these APIs mean we need to do it\n    private void buildVideoRenderers(Context context,\n                                     DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n                                     long allowedVideoJoiningTimeMs,\n                                     Handler eventHandler,\n                                     VideoRendererEventListener eventListener,\n                                     @ExtensionRendererMode int extensionRendererMode,\n                                     List<Renderer> outRenderers) {\n        outRenderers.add(new MediaCodecVideoRenderer(context,\n                mediaCodecSelector,\n                allowedVideoJoiningTimeMs,\n                drmSessionManager,\n                DO_NOT_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS,\n                eventHandler,\n                eventListener,\n                MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));\n\n        if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {\n            return;\n        }\n        int extensionRendererIndex = outRenderers.size();\n        if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {\n            extensionRendererIndex--;\n        }\n\n        try {\n            Class<?> clazz = Class.forName(\"com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer\");\n            Constructor<?> constructor = clazz.getConstructor(boolean.class, long.class, Handler.class, VideoRendererEventListener.class, int.class);\n            Renderer renderer = (Renderer) constructor.newInstance(INIT_ARGS,\n                    allowedVideoJoiningTimeMs,\n                    eventHandler,\n                    eventListener,\n                    MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);\n            outRenderers.add(extensionRendererIndex, renderer);\n            Log.i(TAG, \"Loaded LibvpxVideoRenderer.\");\n        } catch (ClassNotFoundException e) {\n            // Expected if the app was built without the extension.\n        } catch (Exception e) {\n            throw new RendererInstantiationException(\"LibvpxVideoRenderer\", e);\n        }\n    }\n\n    /**\n     * Builds audio renderers for use by the player.\n     *\n     * @param context               The {@link Context} associated with the player.\n     * @param drmSessionManager     An optional {@link DrmSessionManager}. May be null if the player\n     *                              will not be used for DRM protected playbacks.\n     * @param audioProcessors       An array of {@link AudioProcessor}s that will process PCM audio\n     *                              buffers before output. May be empty.\n     * @param eventHandler          A handler to use when invoking event listeners and outputs.\n     * @param eventListener         An event listener.\n     * @param extensionRendererMode The extension renderer mode.\n     * @param outRenderers          An array to which the built renderers should be appended.\n     */\n    @SuppressWarnings({\"PMD.AvoidCatchingGenericException\"})   // Using reflection and these APIs mean we need to do it\n    private void buildAudioRenderers(Context context,\n                                     DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,\n                                     AudioProcessor[] audioProcessors,\n                                     Handler eventHandler,\n                                     AudioRendererEventListener eventListener,\n                                     @ExtensionRendererMode int extensionRendererMode,\n                                     List<Renderer> outRenderers) {\n        MediaCodecAudioRenderer mediaCodecAudioRenderer = new MediaCodecAudioRenderer(\n                context,\n                mediaCodecSelector,\n                drmSessionManager,\n                PLAY_CLEAR_SAMPLES_WITHOUT_KEYS,\n                eventHandler,\n                eventListener,\n                AudioCapabilities.getCapabilities(context),\n                audioProcessors\n        );\n\n        outRenderers.add(mediaCodecAudioRenderer);\n\n        if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {\n            return;\n        }\n        int extensionRendererIndex = outRenderers.size();\n        if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {\n            extensionRendererIndex--;\n        }\n\n        try {\n            Class<?> clazz = Class.forName(\"com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer\");\n            Constructor<?> constructor = clazz.getConstructor(Handler.class, AudioRendererEventListener.class, AudioProcessor[].class);\n            Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);\n            outRenderers.add(extensionRendererIndex++, renderer);\n            Log.i(TAG, \"Loaded LibopusAudioRenderer.\");\n        } catch (ClassNotFoundException e) {\n            // Expected if the app was built without the extension.\n        } catch (Exception e) {\n            throw new RendererInstantiationException(\"LibopusAudioRenderer\", e);\n        }\n\n        try {\n            Class<?> clazz = Class.forName(\"com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer\");\n            Constructor<?> constructor = clazz.getConstructor(Handler.class, AudioRendererEventListener.class, AudioProcessor[].class);\n            Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);\n            outRenderers.add(extensionRendererIndex++, renderer);\n            Log.i(TAG, \"Loaded LibflacAudioRenderer.\");\n        } catch (ClassNotFoundException e) {\n            // Expected if the app was built without the extension.\n        } catch (Exception e) {\n            throw new RendererInstantiationException(\"LibflacAudioRenderer\", e);\n        }\n\n        try {\n            Class<?> clazz = Class.forName(\"com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer\");\n            Constructor<?> constructor = clazz.getConstructor(Handler.class, AudioRendererEventListener.class, AudioProcessor[].class);\n            Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);\n            outRenderers.add(extensionRendererIndex, renderer);\n            Log.i(TAG, \"Loaded FfmpegAudioRenderer.\");\n        } catch (ClassNotFoundException e) {\n            // Expected if the app was built without the extension.\n        } catch (Exception e) {\n            throw new RendererInstantiationException(\"FfmpegAudioRenderer\", e);\n        }\n    }\n\n    /**\n     * Builds text renderers for use by the player.\n     *  @param output       An output for the renderers.\n     * @param outputLooper The looper associated with the thread on which the output should be\n     *                     called.\n     * @param outRenderers An array to which the built renderers should be appended.\n     * @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances.\n     */\n    private void buildTextRenderers(TextOutput output, Looper outputLooper, List<Renderer> outRenderers, SubtitleDecoderFactory decoderFactory) {\n        outRenderers.add(new TextRenderer(output, outputLooper, decoderFactory));\n    }\n\n    /**\n     * Builds metadata renderers for use by the player.\n     *\n     * @param output       An output for the renderers.\n     * @param outputLooper The looper associated with the thread on which the output should be\n     *                     called.\n     * @param outRenderers An array to which the built renderers should be appended.\n     */\n    private void buildMetadataRenderers(MetadataOutput output, Looper outputLooper, List<Renderer> outRenderers) {\n        outRenderers.add(new MetadataRenderer(output, outputLooper));\n    }\n\n    /**\n     * Builds any miscellaneous renderers used by the player.\n     */\n    private void buildMiscellaneousRenderers() {\n        // Do nothing.\n    }\n\n    /**\n     * Builds an array of {@link AudioProcessor}s that will process PCM audio before output.\n     */\n    private AudioProcessor[] buildAudioProcessors() {\n        return new AudioProcessor[0];\n    }\n\n    public static class RendererInstantiationException extends RuntimeException {\n\n        RendererInstantiationException(String rendererName, Throwable cause) {\n            super(\"Unable to instantiate renderer \" + rendererName, cause);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/TextRendererOutput.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.google.android.exoplayer2.text.Cue;\nimport com.google.android.exoplayer2.text.TextRenderer;\nimport com.novoda.noplayer.PlayerView;\nimport com.novoda.noplayer.model.TextCues;\n\nimport java.util.List;\n\nclass TextRendererOutput {\n\n    private final PlayerView playerView;\n\n    TextRendererOutput(PlayerView playerView) {\n        this.playerView = playerView;\n    }\n\n    TextRenderer.Output output() {\n        return new TextRenderer.Output() {\n            @Override\n            public void onCues(List<Cue> cues) {\n                TextCues textCues = ExoPlayerCueMapper.map(cues);\n                playerView.setSubtitleCue(textCues);\n            }\n        };\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof TextRendererOutput)) {\n            return false;\n        }\n\n        TextRendererOutput that = (TextRendererOutput) o;\n\n        return playerView.equals(that.playerView);\n    }\n\n    @Override\n    public int hashCode() {\n        return playerView.hashCode();\n    }\n\n    @Override\n    public String toString() {\n        return \"TextRendererOutput{\"\n                + \"playerView=\" + playerView\n                + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/DownloadDrmSessionCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.os.Handler;\n\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.novoda.noplayer.drm.DownloadedModularDrm;\n\nclass DownloadDrmSessionCreator implements DrmSessionCreator {\n\n    private final DownloadedModularDrm downloadedModularDrm;\n    private final FrameworkMediaDrmCreator mediaDrmCreator;\n    private final Handler handler;\n\n    DownloadDrmSessionCreator(DownloadedModularDrm downloadedModularDrm, FrameworkMediaDrmCreator mediaDrmCreator, Handler handler) {\n        this.downloadedModularDrm = downloadedModularDrm;\n        this.mediaDrmCreator = mediaDrmCreator;\n        this.handler = handler;\n    }\n\n    @Override\n    public DrmSessionManager<FrameworkMediaCrypto> create(DefaultDrmSessionEventListener eventListener) {\n        return new LocalDrmSessionManager(\n                downloadedModularDrm.getKeySetId(),\n                mediaDrmCreator.create(WIDEVINE_MODULAR_UUID),\n                WIDEVINE_MODULAR_UUID,\n                handler,\n                eventListener\n        );\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/DrmSessionCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.support.annotation.Nullable;\n\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\n\nimport java.util.UUID;\n\npublic interface DrmSessionCreator {\n\n    UUID WIDEVINE_MODULAR_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);\n\n    @Nullable\n    DrmSessionManager<FrameworkMediaCrypto> create(DefaultDrmSessionEventListener eventListener);\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/DrmSessionCreatorException.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport com.novoda.noplayer.drm.DrmType;\n\npublic final class DrmSessionCreatorException extends Exception {\n\n    static DrmSessionCreatorException noDrmHandlerFor(DrmType drmType) {\n        return new DrmSessionCreatorException(\"No DrmHandler for DrmType: \" + drmType);\n    }\n\n    private DrmSessionCreatorException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/DrmSessionCreatorFactory.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.os.Build;\nimport android.os.Handler;\n\nimport com.novoda.noplayer.UnableToCreatePlayerException;\nimport com.novoda.noplayer.drm.DownloadedModularDrm;\nimport com.novoda.noplayer.drm.DrmHandler;\nimport com.novoda.noplayer.drm.DrmType;\nimport com.novoda.noplayer.drm.StreamingModularDrm;\nimport com.novoda.noplayer.internal.drm.provision.ProvisionExecutor;\nimport com.novoda.noplayer.internal.drm.provision.ProvisionExecutorCreator;\nimport com.novoda.noplayer.internal.utils.AndroidDeviceVersion;\n\npublic class DrmSessionCreatorFactory {\n\n    private final AndroidDeviceVersion androidDeviceVersion;\n    private final ProvisionExecutorCreator provisionExecutorCreator;\n    private final Handler handler;\n\n    public DrmSessionCreatorFactory(AndroidDeviceVersion androidDeviceVersion, ProvisionExecutorCreator provisionExecutorCreator, Handler handler) {\n        this.androidDeviceVersion = androidDeviceVersion;\n        this.provisionExecutorCreator = provisionExecutorCreator;\n        this.handler = handler;\n    }\n\n    public DrmSessionCreator createFor(DrmType drmType, DrmHandler drmHandler) throws DrmSessionCreatorException {\n        switch (drmType) {\n            case NONE:\n                // Fall-through.\n            case WIDEVINE_CLASSIC:\n                return new NoDrmSessionCreator();\n            case WIDEVINE_MODULAR_STREAM:\n                assertThatApiLevelIsJellyBeanEighteenOrAbove(drmType);\n                return createModularStream((StreamingModularDrm) drmHandler);\n            case WIDEVINE_MODULAR_DOWNLOAD:\n                assertThatApiLevelIsJellyBeanEighteenOrAbove(drmType);\n                return createModularDownload((DownloadedModularDrm) drmHandler);\n            default:\n                throw DrmSessionCreatorException.noDrmHandlerFor(drmType);\n        }\n    }\n\n    private void assertThatApiLevelIsJellyBeanEighteenOrAbove(DrmType drmType) {\n        if (androidDeviceVersion.isJellyBeanEighteenOrAbove()) {\n            return;\n        }\n        throw UnableToCreatePlayerException.deviceDoesNotMeetTargetApiException(\n                drmType,\n                Build.VERSION_CODES.JELLY_BEAN_MR2,\n                androidDeviceVersion\n        );\n    }\n\n    private DrmSessionCreator createModularStream(StreamingModularDrm drmHandler) {\n        ProvisionExecutor provisionExecutor = provisionExecutorCreator.create();\n        ProvisioningModularDrmCallback mediaDrmCallback = new ProvisioningModularDrmCallback(\n                drmHandler,\n                provisionExecutor\n        );\n        FrameworkMediaDrmCreator mediaDrmCreator = new FrameworkMediaDrmCreator();\n        return new StreamingDrmSessionCreator(mediaDrmCallback, mediaDrmCreator, handler);\n    }\n\n    private DownloadDrmSessionCreator createModularDownload(DownloadedModularDrm drmHandler) {\n        FrameworkMediaDrmCreator mediaDrmCreator = new FrameworkMediaDrmCreator();\n        return new DownloadDrmSessionCreator(drmHandler, mediaDrmCreator, handler);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/FrameworkDrmSession.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\n\ninterface FrameworkDrmSession extends DrmSession<FrameworkMediaCrypto> {\n\n    SessionId getSessionId();\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/FrameworkMediaDrmCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport com.google.android.exoplayer2.drm.FrameworkMediaDrm;\nimport com.google.android.exoplayer2.drm.UnsupportedDrmException;\n\nimport java.util.UUID;\n\nclass FrameworkMediaDrmCreator {\n\n    @SuppressWarnings(\"PMD.PreserveStackTrace\")  // We just unwrap the exception because we don't care about the UnsupportedDrmException itself\n    FrameworkMediaDrm create(UUID uuid) {\n        try {\n            return FrameworkMediaDrm.newInstance(uuid);\n        } catch (UnsupportedDrmException e) {\n            throw new FrameworkMediaDrmException(e.getMessage(), e.getCause());\n        }\n    }\n\n    private static class FrameworkMediaDrmException extends RuntimeException {\n\n        FrameworkMediaDrmException(String message, Throwable cause) {\n            super(message, cause);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/InvalidDrmSession.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.support.annotation.Nullable;\n\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\n\nimport java.util.Map;\n\nclass InvalidDrmSession implements FrameworkDrmSession {\n\n    private static final byte[] ABSENT_OFFLINE_LICENSE_KEY_SET_ID = null;\n\n    private final DrmSessionException drmSessionException;\n\n    InvalidDrmSession(DrmSessionException drmSessionException) {\n        this.drmSessionException = drmSessionException;\n    }\n\n    @Override\n    public int getState() {\n        return DrmSession.STATE_ERROR;\n    }\n\n    @Override\n    public FrameworkMediaCrypto getMediaCrypto() {\n        throw new IllegalStateException();\n    }\n\n    @Override\n    public DrmSessionException getError() {\n        return drmSessionException;\n    }\n\n    @Override\n    public Map<String, String> queryKeyStatus() {\n        throw new IllegalStateException();\n    }\n\n    @SuppressWarnings(\"PMD.MethodReturnsInternalArray\") // We return a constant null array\n    @Override\n    @Nullable\n    public byte[] getOfflineLicenseKeySetId() {\n        return ABSENT_OFFLINE_LICENSE_KEY_SET_ID;\n    }\n\n    @Override\n    public SessionId getSessionId() {\n        return SessionId.absent();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        InvalidDrmSession that = (InvalidDrmSession) o;\n\n        return drmSessionException != null ? drmSessionException.equals(that.drmSessionException) : that.drmSessionException == null;\n    }\n\n    @Override\n    public int hashCode() {\n        return drmSessionException != null ? drmSessionException.hashCode() : 0;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/LocalDrmSession.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.support.annotation.Nullable;\n\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.novoda.noplayer.model.KeySetId;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nclass LocalDrmSession implements FrameworkDrmSession {\n\n    private static final DrmSessionException NO_EXCEPTION = null;\n\n    private final FrameworkMediaCrypto mediaCrypto;\n    private final KeySetId keySetIdToRestore;\n    private final SessionId sessionId;\n\n    LocalDrmSession(FrameworkMediaCrypto mediaCrypto, KeySetId keySetIdToRestore, SessionId sessionId) {\n        this.mediaCrypto = mediaCrypto;\n        this.keySetIdToRestore = keySetIdToRestore;\n        this.sessionId = sessionId;\n    }\n\n    @Override\n    public int getState() {\n        return STATE_OPENED_WITH_KEYS;\n    }\n\n    @Override\n    public FrameworkMediaCrypto getMediaCrypto() {\n        return mediaCrypto;\n    }\n\n    @Nullable\n    @Override\n    public DrmSessionException getError() {\n        return NO_EXCEPTION;\n    }\n\n    @Override\n    public Map<String, String> queryKeyStatus() {\n        return Collections.unmodifiableMap(new HashMap<String, String>());\n    }\n\n    @Override\n    public byte[] getOfflineLicenseKeySetId() {\n        return keySetIdToRestore.asBytes();\n    }\n\n    @Override\n    public SessionId getSessionId() {\n        return sessionId;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        LocalDrmSession that = (LocalDrmSession) o;\n\n        if (mediaCrypto != null ? !mediaCrypto.equals(that.mediaCrypto) : that.mediaCrypto != null) {\n            return false;\n        }\n        if (keySetIdToRestore != null ? !keySetIdToRestore.equals(that.keySetIdToRestore) : that.keySetIdToRestore != null) {\n            return false;\n        }\n        return sessionId != null ? sessionId.equals(that.sessionId) : that.sessionId == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = mediaCrypto != null ? mediaCrypto.hashCode() : 0;\n        result = 31 * result + (keySetIdToRestore != null ? keySetIdToRestore.hashCode() : 0);\n        result = 31 * result + (sessionId != null ? sessionId.hashCode() : 0);\n        return result;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/LocalDrmSessionManager.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Looper;\n\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.ExoMediaDrm;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.novoda.noplayer.model.KeySetId;\n\nimport java.util.UUID;\n\nclass LocalDrmSessionManager implements DrmSessionManager<FrameworkMediaCrypto> {\n\n    private final KeySetId keySetIdToRestore;\n    private final ExoMediaDrm<FrameworkMediaCrypto> mediaDrm;\n    private final DefaultDrmSessionEventListener eventListener;\n    private final UUID drmScheme;\n    private final Handler handler;\n\n    LocalDrmSessionManager(KeySetId keySetIdToRestore,\n                           ExoMediaDrm<FrameworkMediaCrypto> mediaDrm,\n                           UUID drmScheme,\n                           Handler handler,\n                           DefaultDrmSessionEventListener eventListener) {\n        this.keySetIdToRestore = keySetIdToRestore;\n        this.mediaDrm = mediaDrm;\n        this.eventListener = eventListener;\n        this.drmScheme = drmScheme;\n        this.handler = handler;\n    }\n\n    @Override\n    public boolean canAcquireSession(DrmInitData drmInitData) {\n        DrmInitData.SchemeData schemeData = drmInitData.get(drmScheme);\n        return schemeData != null;\n    }\n\n    @SuppressWarnings(\"PMD.AvoidCatchingGenericException\") // We are forced to catch Exception as ResourceBusyException is minSdk 19\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)\n    @Override\n    public DrmSession<FrameworkMediaCrypto> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {\n        DrmSession<FrameworkMediaCrypto> drmSession;\n\n        try {\n            SessionId sessionId = SessionId.of(mediaDrm.openSession());\n            FrameworkMediaCrypto mediaCrypto = mediaDrm.createMediaCrypto(sessionId.asBytes());\n\n            mediaDrm.restoreKeys(sessionId.asBytes(), keySetIdToRestore.asBytes());\n\n            drmSession = new LocalDrmSession(mediaCrypto, keySetIdToRestore, sessionId);\n        } catch (Exception exception) {\n            drmSession = new InvalidDrmSession(new DrmSession.DrmSessionException(exception));\n            notifyErrorListener(drmSession);\n        }\n        return drmSession;\n    }\n\n    private void notifyErrorListener(DrmSession<FrameworkMediaCrypto> drmSession) {\n        final DrmSession.DrmSessionException error = drmSession.getError();\n        handler.post(new Runnable() {\n            @Override\n            public void run() {\n                eventListener.onDrmSessionManagerError(error);\n            }\n        });\n    }\n\n    @Override\n    public void releaseSession(DrmSession<FrameworkMediaCrypto> drmSession) {\n        FrameworkDrmSession frameworkDrmSession = (FrameworkDrmSession) drmSession;\n        SessionId sessionId = frameworkDrmSession.getSessionId();\n        mediaDrm.closeSession(sessionId.asBytes());\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/NoDrmSessionCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.support.annotation.Nullable;\n\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\n\nclass NoDrmSessionCreator implements DrmSessionCreator {\n\n    private static final DrmSessionManager<FrameworkMediaCrypto> NO_DRM_SESSION = null;\n\n    @Nullable\n    @Override\n    public DrmSessionManager<FrameworkMediaCrypto> create(DefaultDrmSessionEventListener eventListener) {\n        return NO_DRM_SESSION;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/ProvisioningModularDrmCallback.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport com.google.android.exoplayer2.drm.ExoMediaDrm;\nimport com.google.android.exoplayer2.drm.MediaDrmCallback;\nimport com.novoda.noplayer.drm.ModularDrmKeyRequest;\nimport com.novoda.noplayer.drm.ModularDrmProvisionRequest;\nimport com.novoda.noplayer.drm.StreamingModularDrm;\nimport com.novoda.noplayer.internal.drm.provision.ProvisionExecutor;\n\nimport java.util.UUID;\n\nclass ProvisioningModularDrmCallback implements MediaDrmCallback {\n\n    private final StreamingModularDrm streamingModularDrm;\n    private final ProvisionExecutor provisionExecutor;\n\n    ProvisioningModularDrmCallback(StreamingModularDrm streamingModularDrm, ProvisionExecutor provisionExecutor) {\n        this.streamingModularDrm = streamingModularDrm;\n        this.provisionExecutor = provisionExecutor;\n    }\n\n    @Override\n    public byte[] executeProvisionRequest(UUID uuid, ExoMediaDrm.ProvisionRequest request) throws Exception {\n        return provisionExecutor.execute(new ModularDrmProvisionRequest(request.getDefaultUrl(), request.getData()));\n    }\n\n    @Override\n    public byte[] executeKeyRequest(UUID uuid, ExoMediaDrm.KeyRequest request) throws Exception {\n        return streamingModularDrm.executeKeyRequest(new ModularDrmKeyRequest(request.getLicenseServerUrl(), request.getData()));\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/SessionId.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport java.util.Arrays;\n\nfinal class SessionId {\n\n    private final byte[] sessionIdBytes;\n\n    static SessionId absent() {\n        return new SessionId(new byte[0]);\n    }\n\n    static SessionId of(byte[] sessionId) {\n        return new SessionId(Arrays.copyOf(sessionId, sessionId.length));\n    }\n\n    @SuppressWarnings(\"PMD.ArrayIsStoredDirectly\")  // This can only come from the factory methods, which do defensive copying\n    private SessionId(byte[] sessionIdBytes) {\n        this.sessionIdBytes = sessionIdBytes;\n    }\n\n    byte[] asBytes() {\n        return Arrays.copyOf(sessionIdBytes, sessionIdBytes.length);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        SessionId sessionId = (SessionId) o;\n\n        return Arrays.equals(sessionIdBytes, sessionId.sessionIdBytes);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(sessionIdBytes);\n    }\n\n    @Override\n    public String toString() {\n        return \"SessionId{\"\n                + \"asBytes=\" + Arrays.toString(sessionIdBytes)\n                + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/drm/StreamingDrmSessionCreator.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.os.Handler;\n\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionManager;\nimport com.google.android.exoplayer2.drm.DrmSessionManager;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.drm.FrameworkMediaDrm;\nimport com.google.android.exoplayer2.drm.MediaDrmCallback;\n\nimport java.util.HashMap;\n\nclass StreamingDrmSessionCreator implements DrmSessionCreator {\n\n    @SuppressWarnings(\"PMD.LooseCoupling\")  // Unfortunately the DefaultDrmSessionManager takes a HashMap, not a Map\n    private static final HashMap<String, String> NO_OPTIONAL_PARAMETERS = null;\n\n    private final MediaDrmCallback mediaDrmCallback;\n    private final FrameworkMediaDrmCreator frameworkMediaDrmCreator;\n    private final Handler handler;\n\n    StreamingDrmSessionCreator(MediaDrmCallback mediaDrmCallback, FrameworkMediaDrmCreator frameworkMediaDrmCreator, Handler handler) {\n        this.mediaDrmCallback = mediaDrmCallback;\n        this.frameworkMediaDrmCreator = frameworkMediaDrmCreator;\n        this.handler = handler;\n    }\n\n    @Override\n    public DrmSessionManager<FrameworkMediaCrypto> create(DefaultDrmSessionEventListener eventListener) {\n        FrameworkMediaDrm frameworkMediaDrm = frameworkMediaDrmCreator.create(WIDEVINE_MODULAR_UUID);\n\n        DefaultDrmSessionManager<FrameworkMediaCrypto> defaultDrmSessionManager = new DefaultDrmSessionManager<>(\n                WIDEVINE_MODULAR_UUID,\n                frameworkMediaDrm,\n                mediaDrmCallback,\n                NO_OPTIONAL_PARAMETERS\n        );\n        defaultDrmSessionManager.removeListener(eventListener);\n        defaultDrmSessionManager.addListener(handler, eventListener);\n\n        return defaultDrmSessionManager;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/ErrorFormatter.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.error;\n\nimport android.media.MediaCodec;\nimport android.os.Build;\nimport android.support.annotation.RequiresApi;\n\nfinal class ErrorFormatter {\n\n    private ErrorFormatter() {\n        // Static class.\n    }\n\n    static String formatMessage(Throwable throwable) {\n        return throwable.getClass().getName() + \": \" + throwable.getMessage();\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    static String formatCodecException(MediaCodec.CodecException exception) {\n        String diagnosticInformation = \"diagnosticInformation=\" + exception.getDiagnosticInfo();\n        String isTransient = \" : isTransient=\" + exception.isTransient();\n        String isRecoverable = \" : isRecoverable=\" + exception.isRecoverable();\n\n        return diagnosticInformation + isTransient + isRecoverable;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/ExoPlayerErrorMapper.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.error;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.novoda.noplayer.DetailErrorType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.NoPlayerError;\nimport com.novoda.noplayer.PlayerErrorType;\n\npublic final class ExoPlayerErrorMapper {\n\n    private ExoPlayerErrorMapper() {\n        // Static class.\n    }\n\n    public static NoPlayer.PlayerError errorFor(ExoPlaybackException exception) {\n        String message = ErrorFormatter.formatMessage(exception.getCause());\n\n        switch (exception.type) {\n            case ExoPlaybackException.TYPE_SOURCE:\n                return SourceErrorMapper.map(exception.getSourceException(), message);\n            case ExoPlaybackException.TYPE_RENDERER:\n                return RendererErrorMapper.map(exception.getRendererException(), message);\n            case ExoPlaybackException.TYPE_UNEXPECTED:\n                return UnexpectedErrorMapper.map(exception.getUnexpectedException(), message);\n            default:\n                return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, message);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/RendererErrorMapper.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.error;\n\nimport android.media.MediaCodec;\n\nimport com.google.android.exoplayer2.audio.AudioDecoderException;\nimport com.google.android.exoplayer2.audio.AudioProcessor;\nimport com.google.android.exoplayer2.audio.AudioSink;\nimport com.google.android.exoplayer2.drm.DecryptionException;\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionManager;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.KeysExpiredException;\nimport com.google.android.exoplayer2.drm.UnsupportedDrmException;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.novoda.noplayer.DetailErrorType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.NoPlayerError;\nimport com.novoda.noplayer.PlayerErrorType;\n\nfinal class RendererErrorMapper {\n\n    private RendererErrorMapper() {\n        // non-instantiable class\n    }\n\n    @SuppressWarnings({\"PMD.StdCyclomaticComplexity\", \"PMD.CyclomaticComplexity\", \"PMD.ModifiedCyclomaticComplexity\", \"PMD.NPathComplexity\"})\n    static NoPlayer.PlayerError map(Exception rendererException, String message) {\n        if (rendererException instanceof AudioSink.ConfigurationException) {\n            return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_SINK_CONFIGURATION_ERROR, message);\n        }\n\n        if (rendererException instanceof AudioSink.InitializationException) {\n            return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_SINK_INITIALISATION_ERROR, message);\n        }\n\n        if (rendererException instanceof AudioSink.WriteException) {\n            return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_SINK_WRITE_ERROR, message);\n        }\n\n        if (rendererException instanceof AudioProcessor.UnhandledFormatException) {\n            return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_UNHANDLED_FORMAT_ERROR, message);\n        }\n\n        if (rendererException instanceof AudioDecoderException) {\n            return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.AUDIO_DECODER_ERROR, message);\n        }\n\n        if (rendererException instanceof MediaCodecRenderer.DecoderInitializationException) {\n            MediaCodecRenderer.DecoderInitializationException decoderInitializationException =\n                    (MediaCodecRenderer.DecoderInitializationException) rendererException;\n            String fullMessage = \"decoder-name:\" + decoderInitializationException.decoderName + \", \"\n                    + \"mimetype:\" + decoderInitializationException.mimeType + \", \"\n                    + \"secureCodeRequired:\" + decoderInitializationException.secureDecoderRequired + \", \"\n                    + \"diagnosticInfo:\" + decoderInitializationException.diagnosticInfo + \", \"\n                    + \"exceptionMessage:\" + message;\n            return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.INITIALISATION_ERROR, fullMessage);\n        }\n\n        if (rendererException instanceof MediaCodecUtil.DecoderQueryException) {\n            return new NoPlayerError(PlayerErrorType.DEVICE_MEDIA_CAPABILITIES, DetailErrorType.UNKNOWN, message);\n        }\n\n        if (rendererException instanceof SubtitleDecoderException) {\n            return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.DECODING_SUBTITLE_ERROR, message);\n        }\n\n        if (rendererException instanceof UnsupportedDrmException) {\n            return mapUnsupportedDrmException((UnsupportedDrmException) rendererException, message);\n        }\n\n        if (rendererException instanceof DefaultDrmSessionManager.MissingSchemeDataException) {\n            return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.CANNOT_ACQUIRE_DRM_SESSION_MISSING_SCHEME_FOR_REQUIRED_UUID_ERROR, message);\n        }\n\n        if (rendererException instanceof DrmSession.DrmSessionException) {\n            return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.DRM_SESSION_ERROR, message);\n        }\n\n        if (rendererException instanceof KeysExpiredException) {\n            return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.DRM_KEYS_EXPIRED_ERROR, message);\n        }\n\n        if (rendererException instanceof DecryptionException) {\n            return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.FAIL_DECRYPT_DATA_DUE_NON_PLATFORM_COMPONENT_ERROR, message);\n        }\n\n        if (rendererException instanceof MediaCodec.CryptoException) {\n            return mapCryptoException((MediaCodec.CryptoException) rendererException, message);\n        }\n\n        if (rendererException instanceof IllegalStateException) {\n            return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_REQUIRES_DRM_SESSION_MANAGER_ERROR, message);\n        }\n\n        return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, message);\n    }\n\n    private static NoPlayer.PlayerError mapUnsupportedDrmException(UnsupportedDrmException unsupportedDrmException, String message) {\n        switch (unsupportedDrmException.reason) {\n            case UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME:\n                return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.UNSUPPORTED_DRM_SCHEME_ERROR, message);\n            case UnsupportedDrmException.REASON_INSTANTIATION_ERROR:\n                return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.DRM_INSTANTIATION_ERROR, message);\n            default:\n                return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.DRM_UNKNOWN_ERROR, message);\n        }\n    }\n\n    private static NoPlayer.PlayerError mapCryptoException(MediaCodec.CryptoException cryptoException, String message) {\n        switch (cryptoException.getErrorCode()) {\n            case MediaCodec.CryptoException.ERROR_INSUFFICIENT_OUTPUT_PROTECTION:\n                return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.INSUFFICIENT_OUTPUT_PROTECTION_ERROR, message);\n            case MediaCodec.CryptoException.ERROR_KEY_EXPIRED:\n                return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.KEY_EXPIRED_ERROR, message);\n            case MediaCodec.CryptoException.ERROR_NO_KEY:\n                return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.KEY_NOT_FOUND_WHEN_DECRYPTION_ERROR, message);\n            case MediaCodec.CryptoException.ERROR_RESOURCE_BUSY:\n                return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.RESOURCE_BUSY_ERROR_THEN_SHOULD_RETRY, message);\n            case MediaCodec.CryptoException.ERROR_SESSION_NOT_OPENED:\n                return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.ATTEMPTED_ON_CLOSED_SEDDION_ERROR, message);\n            case MediaCodec.CryptoException.ERROR_UNSUPPORTED_OPERATION:\n                return new NoPlayerError(\n                        PlayerErrorType.CONTENT_DECRYPTION,\n                        DetailErrorType.LICENSE_POLICY_REQUIRED_NOT_SUPPORTED_BY_DEVICE_ERROR,\n                        message\n                );\n            default:\n                return new NoPlayerError(PlayerErrorType.CONTENT_DECRYPTION, DetailErrorType.UNKNOWN, message);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/SourceErrorMapper.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.error;\n\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.offline.DownloadException;\nimport com.google.android.exoplayer2.source.ClippingMediaSource;\nimport com.google.android.exoplayer2.source.MergingMediaSource;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\nimport com.google.android.exoplayer2.source.dash.DashManifestStaleException;\nimport com.google.android.exoplayer2.source.hls.SampleQueueMappingException;\nimport com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;\nimport com.google.android.exoplayer2.upstream.AssetDataSource;\nimport com.google.android.exoplayer2.upstream.ContentDataSource;\nimport com.google.android.exoplayer2.upstream.DataSourceException;\nimport com.google.android.exoplayer2.upstream.FileDataSource;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.upstream.Loader;\nimport com.google.android.exoplayer2.upstream.UdpDataSource;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.util.PriorityTaskManager;\nimport com.novoda.noplayer.DetailErrorType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.NoPlayerError;\nimport com.novoda.noplayer.PlayerErrorType;\n\nimport java.io.IOException;\n\nfinal class SourceErrorMapper {\n\n    private SourceErrorMapper() {\n        // non-instantiable class\n    }\n\n    @SuppressWarnings({\"PMD.StdCyclomaticComplexity\", \"PMD.CyclomaticComplexity\", \"PMD.ModifiedCyclomaticComplexity\", \"PMD.NPathComplexity\"})\n    static NoPlayer.PlayerError map(IOException sourceException, String message) {\n        if (sourceException instanceof SampleQueueMappingException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.SAMPLE_QUEUE_MAPPING_ERROR, message);\n        }\n\n        if (sourceException instanceof FileDataSource.FileDataSourceException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.READING_LOCAL_FILE_ERROR, message);\n        }\n\n        if (sourceException instanceof Loader.UnexpectedLoaderException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.UNEXPECTED_LOADING_ERROR, message);\n        }\n\n        if (sourceException instanceof DashManifestStaleException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.LIVE_STALE_MANIFEST_AND_NEW_MANIFEST_COULD_NOT_LOAD_ERROR, message);\n        }\n\n        if (sourceException instanceof DownloadException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.DOWNLOAD_ERROR, message);\n        }\n\n        if (sourceException instanceof AdsMediaSource.AdLoadException) {\n            return mapAdsError((AdsMediaSource.AdLoadException) sourceException, message);\n        }\n\n        if (sourceException instanceof MergingMediaSource.IllegalMergeException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MERGING_MEDIA_SOURCE_CANNOT_MERGE_ITS_SOURCES, message);\n        }\n\n        if (sourceException instanceof ClippingMediaSource.IllegalClippingException) {\n            return mapClippingError((ClippingMediaSource.IllegalClippingException) sourceException, message);\n        }\n\n        if (sourceException instanceof PriorityTaskManager.PriorityTooLowException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.TASK_CANNOT_PROCEED_PRIORITY_TOO_LOW, message);\n        }\n\n        if (sourceException instanceof ParserException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.PARSING_MEDIA_DATA_OR_METADATA_ERROR, message);\n        }\n\n        if (sourceException instanceof Cache.CacheException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.CACHE_WRITING_DATA_ERROR, message);\n        }\n\n        if (sourceException instanceof HlsPlaylistTracker.PlaylistStuckException) {\n            HlsPlaylistTracker.PlaylistStuckException playlistStuckException = (HlsPlaylistTracker.PlaylistStuckException) sourceException;\n            return new NoPlayerError(\n                    PlayerErrorType.CONNECTIVITY,\n                    DetailErrorType.HLS_PLAYLIST_STUCK_SERVER_SIDE_ERROR,\n                    playlistStuckException.url + \" - \" + message\n            );\n        }\n\n        if (sourceException instanceof HlsPlaylistTracker.PlaylistResetException) {\n            HlsPlaylistTracker.PlaylistResetException playlistStuckException = (HlsPlaylistTracker.PlaylistResetException) sourceException;\n            return new NoPlayerError(\n                    PlayerErrorType.CONNECTIVITY,\n                    DetailErrorType.HLS_PLAYLIST_SERVER_HAS_RESET,\n                    playlistStuckException.url + \" - \" + message\n            );\n        }\n\n        if (sourceException instanceof HttpDataSource.HttpDataSourceException) {\n            return mapHttpDataSourceException((HttpDataSource.HttpDataSourceException) sourceException, message);\n        }\n\n        if (sourceException instanceof AssetDataSource.AssetDataSourceException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.READ_LOCAL_ASSET_ERROR, message);\n        }\n\n        if (sourceException instanceof ContentDataSource.ContentDataSourceException) {\n            return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.READ_CONTENT_URI_ERROR, message);\n        }\n\n        if (sourceException instanceof UdpDataSource.UdpDataSourceException) {\n            return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.READ_FROM_UDP_ERROR, message);\n        }\n\n        if (sourceException instanceof DataSourceException) {\n            return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.DATA_POSITION_OUT_OF_RANGE_ERROR, message);\n        }\n\n        return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, message);\n    }\n\n    private static NoPlayer.PlayerError mapAdsError(AdsMediaSource.AdLoadException adLoadException, String message) {\n        switch (adLoadException.type) {\n            case AdsMediaSource.AdLoadException.TYPE_AD:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.AD_LOAD_ERROR_THEN_WILL_SKIP, message);\n            case AdsMediaSource.AdLoadException.TYPE_AD_GROUP:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.AD_GROUP_LOAD_ERROR_THEN_WILL_SKIP, message);\n            case AdsMediaSource.AdLoadException.TYPE_ALL_ADS:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.ALL_ADS_LOAD_ERROR_THEN_WILL_SKIP, message);\n            case AdsMediaSource.AdLoadException.TYPE_UNEXPECTED:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.ADS_LOAD_UNEXPECTED_ERROR_THEN_WILL_SKIP, message);\n            default:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.UNKNOWN, message);\n        }\n    }\n\n    private static NoPlayer.PlayerError mapClippingError(ClippingMediaSource.IllegalClippingException illegalClippingException, String message) {\n        switch (illegalClippingException.reason) {\n            case ClippingMediaSource.IllegalClippingException.REASON_INVALID_PERIOD_COUNT:\n                return new NoPlayerError(\n                        PlayerErrorType.SOURCE,\n                        DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT,\n                        message\n                );\n            case ClippingMediaSource.IllegalClippingException.REASON_NOT_SEEKABLE_TO_START:\n                return new NoPlayerError(\n                        PlayerErrorType.SOURCE,\n                        DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_NOT_SEEKABLE_TO_START,\n                        message\n                );\n            case ClippingMediaSource.IllegalClippingException.REASON_START_EXCEEDS_END:\n                return new NoPlayerError(\n                        PlayerErrorType.SOURCE,\n                        DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_START_EXCEEDS_END,\n                        message\n                );\n            default:\n                return new NoPlayerError(\n                        PlayerErrorType.SOURCE,\n                        DetailErrorType.UNKNOWN,\n                        message\n                );\n        }\n    }\n\n    private static NoPlayer.PlayerError mapHttpDataSourceException(HttpDataSource.HttpDataSourceException httpDataSourceException, String message) {\n        switch (httpDataSourceException.type) {\n            case HttpDataSource.HttpDataSourceException.TYPE_OPEN:\n                return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.HTTP_CANNOT_OPEN_ERROR, message);\n            case HttpDataSource.HttpDataSourceException.TYPE_READ:\n                return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.HTTP_CANNOT_READ_ERROR, message);\n            case HttpDataSource.HttpDataSourceException.TYPE_CLOSE:\n                return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.HTTP_CANNOT_CLOSE_ERROR, message);\n            default:\n                return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.UNKNOWN, message);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/error/UnexpectedErrorMapper.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.error;\n\nimport android.media.MediaCodec;\nimport android.os.Build;\n\nimport com.google.android.exoplayer2.audio.DefaultAudioSink;\nimport com.google.android.exoplayer2.util.EGLSurfaceTexture;\nimport com.novoda.noplayer.DetailErrorType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.NoPlayerError;\nimport com.novoda.noplayer.PlayerErrorType;\n\nfinal class UnexpectedErrorMapper {\n\n    private UnexpectedErrorMapper() {\n        // non-instantiable class\n    }\n\n    static NoPlayer.PlayerError map(RuntimeException unexpectedException, String message) {\n        if (unexpectedException instanceof EGLSurfaceTexture.GlException) {\n            return new NoPlayerError(PlayerErrorType.UNEXPECTED, DetailErrorType.EGL_OPERATION_ERROR, message);\n        }\n\n        if (unexpectedException instanceof DefaultAudioSink.InvalidAudioTrackTimestampException) {\n            return new NoPlayerError(PlayerErrorType.UNEXPECTED, DetailErrorType.SPURIOUS_AUDIO_TRACK_TIMESTAMP_ERROR, message);\n        }\n\n        if (unexpectedException instanceof IllegalStateException && message.contains(\"Multiple renderer media clocks\")) {\n            return new NoPlayerError(PlayerErrorType.UNEXPECTED, DetailErrorType.MULTIPLE_RENDERER_MEDIA_CLOCK_ENABLED_ERROR, message);\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && unexpectedException instanceof MediaCodec.CodecException) {\n            String errorMessage = ErrorFormatter.formatCodecException((MediaCodec.CodecException) unexpectedException);\n            return new NoPlayerError(PlayerErrorType.UNEXPECTED, DetailErrorType.UNEXPECTED_CODEC_ERROR, errorMessage);\n        }\n\n        return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, message);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/AnalyticsListenerForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport android.view.Surface;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.io.IOException;\nimport java.util.HashMap;\n\nimport static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Methods;\nimport static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Parameters;\n\nclass AnalyticsListenerForwarder implements AnalyticsListener {\n\n    private final NoPlayer.InfoListener infoListeners;\n\n    AnalyticsListenerForwarder(NoPlayer.InfoListener infoListeners) {\n        this.infoListeners = infoListeners;\n    }\n\n    @Override\n    public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.PLAY_WHEN_READY, String.valueOf(playWhenReady));\n        callingMethodParameters.put(Parameters.PLAYBACK_STATE, String.valueOf(playbackState));\n\n        infoListeners.onNewInfo(Methods.ON_PLAYER_STATE_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onTimelineChanged(EventTime eventTime, int reason) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.REASON, String.valueOf(reason));\n\n        infoListeners.onNewInfo(Methods.ON_TIMELINE_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onPositionDiscontinuity(EventTime eventTime, int reason) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.REASON, String.valueOf(reason));\n\n        infoListeners.onNewInfo(Methods.ON_POSITION_DISCONTINUITY, callingMethodParameters);\n    }\n\n    @Override\n    public void onSeekStarted(EventTime eventTime) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n\n        infoListeners.onNewInfo(Methods.ON_SEEK_STARTED, callingMethodParameters);\n    }\n\n    @Override\n    public void onSeekProcessed(EventTime eventTime) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n\n        infoListeners.onNewInfo(Methods.ON_SEEK_PROCESSED, callingMethodParameters);\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.PLAYBACK_PARAMETERS, playbackParameters.toString());\n\n        infoListeners.onNewInfo(Methods.ON_PLAYBACK_PARAMETERS_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.REPEAT_MODE, String.valueOf(repeatMode));\n\n        infoListeners.onNewInfo(Methods.ON_REPEAT_MODE_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.SHUFFLE_MODE_ENABLED, String.valueOf(shuffleModeEnabled));\n\n        infoListeners.onNewInfo(Methods.ON_SHUFFLE_MODE_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadingChanged(EventTime eventTime, boolean isLoading) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.IS_LOADING, String.valueOf(isLoading));\n\n        infoListeners.onNewInfo(Methods.ON_LOADING_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.ERROR, error.toString());\n\n        infoListeners.onNewInfo(Methods.ON_PLAYER_ERROR, callingMethodParameters);\n    }\n\n    @Override\n    public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.TRACK_GROUPS, trackGroups.toString());\n        callingMethodParameters.put(Parameters.TRACK_SELECTIONS, trackSelections.toString());\n\n        infoListeners.onNewInfo(Methods.ON_TRACKS_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadStarted(EventTime eventTime,\n                              MediaSourceEventListener.LoadEventInfo loadEventInfo,\n                              MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n\n        infoListeners.onNewInfo(Methods.ON_LOAD_STARTED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadCompleted(EventTime eventTime,\n                                MediaSourceEventListener.LoadEventInfo loadEventInfo,\n                                MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n\n        infoListeners.onNewInfo(Methods.ON_LOAD_COMPLETED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadCanceled(EventTime eventTime,\n                               MediaSourceEventListener.LoadEventInfo loadEventInfo,\n                               MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n\n        infoListeners.onNewInfo(Methods.ON_LOAD_CANCELED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadError(EventTime eventTime,\n                            MediaSourceEventListener.LoadEventInfo loadEventInfo,\n                            MediaSourceEventListener.MediaLoadData mediaLoadData,\n                            IOException error,\n                            boolean wasCanceled) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n        callingMethodParameters.put(Parameters.ERROR, error.toString());\n        callingMethodParameters.put(Parameters.WAS_CANCELED, String.valueOf(wasCanceled));\n\n        infoListeners.onNewInfo(Methods.ON_LOAD_ERROR, callingMethodParameters);\n    }\n\n    @Override\n    public void onDownstreamFormatChanged(EventTime eventTime, MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n\n        infoListeners.onNewInfo(Methods.ON_DOWNSTREAM_FORMAT_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onUpstreamDiscarded(EventTime eventTime, MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n\n        infoListeners.onNewInfo(Methods.ON_UPSTREAM_DISCARDED, callingMethodParameters);\n    }\n\n    @Override\n    public void onMediaPeriodCreated(EventTime eventTime) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n\n        infoListeners.onNewInfo(Methods.ON_MEDIA_PERIOD_CREATED, callingMethodParameters);\n    }\n\n    @Override\n    public void onMediaPeriodReleased(EventTime eventTime) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n\n        infoListeners.onNewInfo(Methods.ON_MEDIA_PERIOD_RELEASED, callingMethodParameters);\n    }\n\n    @Override\n    public void onReadingStarted(EventTime eventTime) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n\n        infoListeners.onNewInfo(Methods.ON_READING_STARTED, callingMethodParameters);\n    }\n\n    @Override\n    public void onBandwidthEstimate(EventTime eventTime,\n                                    int totalLoadTimeMs,\n                                    long totalBytesLoaded,\n                                    long bitrateEstimate) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.TOTAL_LOAD_TIME_MS, String.valueOf(totalLoadTimeMs));\n        callingMethodParameters.put(Parameters.TOTAL_BYTES_LOADED, String.valueOf(totalBytesLoaded));\n        callingMethodParameters.put(Parameters.BITRATE_ESTIMATE, String.valueOf(bitrateEstimate));\n\n        infoListeners.onNewInfo(Methods.ON_BANDWIDTH_ESTIMATE, callingMethodParameters);\n    }\n\n    @Override\n    public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.WIDTH, String.valueOf(width));\n        callingMethodParameters.put(Parameters.HEIGHT, String.valueOf(height));\n\n        infoListeners.onNewInfo(Methods.ON_SURFACE_SIZE_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onMetadata(EventTime eventTime, Metadata metadata) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.METADATA, metadata.toString());\n\n        infoListeners.onNewInfo(Methods.ON_METADATA, callingMethodParameters);\n    }\n\n    @Override\n    public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.TRACK_TYPE, String.valueOf(trackType));\n        callingMethodParameters.put(Parameters.DECODER_COUNTERS, decoderCounters.toString());\n\n        infoListeners.onNewInfo(Methods.ON_DECODER_ENABLED, callingMethodParameters);\n    }\n\n    @Override\n    public void onDecoderInitialized(EventTime eventTime,\n                                     int trackType,\n                                     String decoderName,\n                                     long initializationDurationMs) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.TRACK_TYPE, String.valueOf(trackType));\n        callingMethodParameters.put(Parameters.DECODER_NAME, decoderName);\n        callingMethodParameters.put(Parameters.INITIALIZATION_DURATION_MS, String.valueOf(initializationDurationMs));\n\n        infoListeners.onNewInfo(Methods.ON_DECODER_INITIALIZED, callingMethodParameters);\n    }\n\n    @Override\n    public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.TRACK_TYPE, String.valueOf(trackType));\n        callingMethodParameters.put(Parameters.FORMAT, format.toString());\n\n        infoListeners.onNewInfo(Methods.ON_DECODER_INPUT_FORMAT_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.TRACK_TYPE, String.valueOf(trackType));\n        callingMethodParameters.put(Parameters.DECODER_COUNTERS, decoderCounters.toString());\n\n        infoListeners.onNewInfo(Methods.ON_DECODER_DISABLED, callingMethodParameters);\n    }\n\n    @Override\n    public void onAudioSessionId(EventTime eventTime, int audioSessionId) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.AUDIO_SESSION_ID, String.valueOf(audioSessionId));\n\n        infoListeners.onNewInfo(Methods.ON_AUDIO_SESSION_ID, callingMethodParameters);\n    }\n\n    @Override\n    public void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.BUFFER_SIZE, String.valueOf(bufferSize));\n        callingMethodParameters.put(Parameters.BUFFER_SIZE_MS, String.valueOf(bufferSizeMs));\n        callingMethodParameters.put(Parameters.ELAPSED_SINCE_LAST_FEED_MS, String.valueOf(elapsedSinceLastFeedMs));\n\n        infoListeners.onNewInfo(Methods.ON_AUDIO_UNDERRUN, callingMethodParameters);\n    }\n\n    @Override\n    public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.DROPPED_FRAMES, String.valueOf(droppedFrames));\n        callingMethodParameters.put(Parameters.ELAPSED_MS, String.valueOf(elapsedMs));\n\n        infoListeners.onNewInfo(Methods.ON_DROPPED_VIDEO_FRAMES, callingMethodParameters);\n    }\n\n    @Override\n    public void onVideoSizeChanged(EventTime eventTime,\n                                   int width,\n                                   int height,\n                                   int unappliedRotationDegrees,\n                                   float pixelWidthHeightRatio) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.WIDTH, String.valueOf(width));\n        callingMethodParameters.put(Parameters.HEIGHT, String.valueOf(height));\n        callingMethodParameters.put(Parameters.UNAPPLIED_ROTATION_DEGREES, String.valueOf(unappliedRotationDegrees));\n        callingMethodParameters.put(Parameters.PIXEL_WIDTH_HEIGHT_RATIO, String.valueOf(pixelWidthHeightRatio));\n\n        infoListeners.onNewInfo(Methods.ON_VIDEO_SIZE_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.SURFACE, surface.toString());\n\n        infoListeners.onNewInfo(Methods.ON_RENDERED_FIRST_FRAME, callingMethodParameters);\n    }\n\n    @Override\n    public void onDrmKeysLoaded(EventTime eventTime) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n\n        infoListeners.onNewInfo(Methods.ON_DRM_KEYS_LOADED, callingMethodParameters);\n    }\n\n    @Override\n    public void onDrmSessionManagerError(EventTime eventTime, Exception error) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n        callingMethodParameters.put(Parameters.ERROR, error.toString());\n\n        infoListeners.onNewInfo(Methods.ON_DRM_SESSION_MANAGER_ERROR, callingMethodParameters);\n    }\n\n    @Override\n    public void onDrmKeysRestored(EventTime eventTime) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n\n        infoListeners.onNewInfo(Methods.ON_DRM_KEYS_RESTORED, callingMethodParameters);\n    }\n\n    @Override\n    public void onDrmKeysRemoved(EventTime eventTime) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.EVENT_TIME, eventTime.toString());\n\n        infoListeners.onNewInfo(Methods.ON_DRM_KEYS_REMOVED, callingMethodParameters);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/BitrateForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport android.support.annotation.Nullable;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.model.Bitrate;\n\nimport java.io.IOException;\n\nclass BitrateForwarder implements MediaSourceEventListener {\n\n    private Bitrate videoBitrate = Bitrate.fromBitsPerSecond(0);\n    private Bitrate audioBitrate = Bitrate.fromBitsPerSecond(0);\n\n    private final NoPlayer.BitrateChangedListener bitrateChangedListener;\n\n    BitrateForwarder(NoPlayer.BitrateChangedListener bitrateChangedListener) {\n        this.bitrateChangedListener = bitrateChangedListener;\n    }\n\n    @Override\n    public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onLoadStarted(int windowIndex,\n                              @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                              LoadEventInfo loadEventInfo,\n                              MediaLoadData mediaLoadData) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onLoadCompleted(int windowIndex,\n                                @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                                LoadEventInfo loadEventInfo,\n                                MediaLoadData mediaLoadData) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onLoadCanceled(int windowIndex,\n                               @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                               LoadEventInfo loadEventInfo,\n                               MediaLoadData mediaLoadData) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onLoadError(int windowIndex,\n                            @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                            LoadEventInfo loadEventInfo,\n                            MediaLoadData mediaLoadData,\n                            IOException error,\n                            boolean wasCanceled) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onUpstreamDiscarded(int windowIndex,\n                                    MediaSource.MediaPeriodId mediaPeriodId,\n                                    MediaLoadData mediaLoadData) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onDownstreamFormatChanged(int windowIndex,\n                                          @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                                          MediaLoadData mediaLoadData) {\n        if (mediaLoadData.trackFormat == null) {\n            return;\n        }\n\n        if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO) {\n            videoBitrate = Bitrate.fromBitsPerSecond(mediaLoadData.trackFormat.bitrate);\n            bitrateChangedListener.onBitrateChanged(audioBitrate, videoBitrate);\n        } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) {\n            audioBitrate = Bitrate.fromBitsPerSecond(mediaLoadData.trackFormat.bitrate);\n            bitrateChangedListener.onBitrateChanged(audioBitrate, videoBitrate);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/BufferStateForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.novoda.noplayer.NoPlayer;\n\nclass BufferStateForwarder implements Player.EventListener {\n\n    private final NoPlayer.BufferStateListener bufferStateListener;\n\n    BufferStateForwarder(NoPlayer.BufferStateListener bufferStateListener) {\n        this.bufferStateListener = bufferStateListener;\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n        if (playbackState == Player.STATE_BUFFERING) {\n            bufferStateListener.onBufferStarted();\n        } else if (playbackState == Player.STATE_READY) {\n            bufferStateListener.onBufferCompleted();\n        }\n    }\n\n    @Override\n    public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onLoadingChanged(boolean isLoading) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n        // Sent by ErrorForwarder.\n    }\n\n    @Override\n    public void onPositionDiscontinuity(int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onSeekProcessed() {\n        // TODO: should we send?\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/DrmSessionInfoForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.Collections;\nimport java.util.HashMap;\n\nimport static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Methods;\nimport static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Parameters;\n\nclass DrmSessionInfoForwarder implements DefaultDrmSessionEventListener {\n\n    private final NoPlayer.InfoListener infoListener;\n\n    DrmSessionInfoForwarder(NoPlayer.InfoListener infoListener) {\n        this.infoListener = infoListener;\n    }\n\n    @Override\n    public void onDrmKeysLoaded() {\n        infoListener.onNewInfo(Methods.ON_DRM_KEYS_LOADED, Collections.<String, String>emptyMap());\n    }\n\n    @Override\n    public void onDrmSessionManagerError(Exception error) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.ERROR, String.valueOf(error));\n\n        infoListener.onNewInfo(Methods.ON_DRM_SESSION_MANAGER_ERROR, callingMethodParameters);\n    }\n\n    @Override\n    public void onDrmKeysRestored() {\n        infoListener.onNewInfo(Methods.ON_DRM_KEYS_RESTORED, Collections.<String, String>emptyMap());\n\n    }\n\n    @Override\n    public void onDrmKeysRemoved() {\n        infoListener.onNewInfo(Methods.ON_DRM_KEYS_REMOVED, Collections.<String, String>emptyMap());\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/EventInfoForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.Collections;\nimport java.util.HashMap;\n\nimport static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Methods;\nimport static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Parameters;\n\nclass EventInfoForwarder implements Player.EventListener {\n\n    private final NoPlayer.InfoListener infoListener;\n\n    EventInfoForwarder(NoPlayer.InfoListener infoListener) {\n        this.infoListener = infoListener;\n    }\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.TIMELINE, String.valueOf(timeline));\n        callingMethodParameters.put(Parameters.MANIFEST, String.valueOf(manifest));\n        callingMethodParameters.put(Parameters.REASON, String.valueOf(reason));\n\n        infoListener.onNewInfo(Methods.ON_TIMELINE_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.TRACK_GROUPS, String.valueOf(trackGroups));\n        callingMethodParameters.put(Parameters.TRACK_SELECTIONS, String.valueOf(trackSelections));\n\n        infoListener.onNewInfo(Methods.ON_TRACKS_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadingChanged(boolean isLoading) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.IS_LOADING, String.valueOf(isLoading));\n\n        infoListener.onNewInfo(Methods.ON_LOADING_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.PLAY_WHEN_READY, String.valueOf(playWhenReady));\n        callingMethodParameters.put(Parameters.PLAYBACK_STATE, String.valueOf(playbackState));\n\n        infoListener.onNewInfo(Methods.ON_PLAYER_STATE_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.REPEAT_MODE, String.valueOf(repeatMode));\n\n        infoListener.onNewInfo(Methods.ON_REPEAT_MODE_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.SHUFFLE_MODE_ENABLED, String.valueOf(shuffleModeEnabled));\n\n        infoListener.onNewInfo(Methods.ON_SHUFFLE_MODE_ENABLED_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.ERROR, String.valueOf(error));\n\n        infoListener.onNewInfo(Methods.ON_PLAYER_ERROR, callingMethodParameters);\n    }\n\n    @Override\n    public void onPositionDiscontinuity(int reason) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.REASON, String.valueOf(reason));\n\n        infoListener.onNewInfo(Methods.ON_POSITION_DISCONTINUITY, callingMethodParameters);\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.PLAYBACK_PARAMETERS, String.valueOf(playbackParameters));\n\n        infoListener.onNewInfo(Methods.ON_PLAYBACK_PARAMETERS_CHANGED, callingMethodParameters);\n    }\n\n    @Override\n    public void onSeekProcessed() {\n        infoListener.onNewInfo(Methods.ON_POSITION_DISCONTINUITY, Collections.<String, String>emptyMap());\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/EventListener.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\nclass EventListener implements Player.EventListener {\n\n    private final List<Player.EventListener> listeners = new CopyOnWriteArrayList<>();\n\n    public void add(Player.EventListener listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {\n        for (Player.EventListener listener : listeners) {\n            listener.onTimelineChanged(timeline, manifest, reason);\n        }\n    }\n\n    @Override\n    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n        for (Player.EventListener listener : listeners) {\n            listener.onTracksChanged(trackGroups, trackSelections);\n        }\n    }\n\n    @Override\n    public void onLoadingChanged(boolean isLoading) {\n        for (Player.EventListener listener : listeners) {\n            listener.onLoadingChanged(isLoading);\n        }\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n        for (Player.EventListener listener : listeners) {\n            listener.onPlayerStateChanged(playWhenReady, playbackState);\n        }\n    }\n\n    @Override\n    public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {\n        for (Player.EventListener listener : listeners) {\n            listener.onRepeatModeChanged(repeatMode);\n        }\n    }\n\n    @Override\n    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n        for (Player.EventListener listener : listeners) {\n            listener.onShuffleModeEnabledChanged(shuffleModeEnabled);\n        }\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n        for (Player.EventListener listener : listeners) {\n            listener.onPlayerError(error);\n        }\n    }\n\n    @Override\n    public void onPositionDiscontinuity(int reason) {\n        for (Player.EventListener listener : listeners) {\n            listener.onPositionDiscontinuity(reason);\n        }\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n        for (Player.EventListener listener : listeners) {\n            listener.onPlaybackParametersChanged(playbackParameters);\n        }\n    }\n\n    @Override\n    public void onSeekProcessed() {\n        for (Player.EventListener listener : listeners) {\n            listener.onSeekProcessed();\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/ExoPlayerDrmSessionEventListener.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\nclass ExoPlayerDrmSessionEventListener implements DefaultDrmSessionEventListener {\n\n    private final List<DefaultDrmSessionEventListener> listeners = new CopyOnWriteArrayList<>();\n\n    void add(DefaultDrmSessionEventListener listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public void onDrmKeysLoaded() {\n        for (DefaultDrmSessionEventListener listener : listeners) {\n            listener.onDrmKeysLoaded();\n        }\n    }\n\n    @Override\n    public void onDrmSessionManagerError(Exception e) {\n        for (DefaultDrmSessionEventListener listener : listeners) {\n            listener.onDrmSessionManagerError(e);\n        }\n    }\n\n    @Override\n    public void onDrmKeysRestored() {\n        for (DefaultDrmSessionEventListener listener : listeners) {\n            listener.onDrmKeysRestored();\n        }\n    }\n\n    @Override\n    public void onDrmKeysRemoved() {\n        for (DefaultDrmSessionEventListener listener : listeners) {\n            listener.onDrmKeysRemoved();\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/ExoPlayerForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.analytics.AnalyticsListener;\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.video.VideoListener;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.PlayerState;\n\npublic class ExoPlayerForwarder {\n\n    private final EventListener exoPlayerEventListener;\n    private final NoPlayerMediaSourceEventListener mediaSourceEventListener;\n    private final NoPlayerAnalyticsListener analyticsListener;\n    private final ExoPlayerVideoListener videoListener;\n    private final ExoPlayerDrmSessionEventListener drmSessionEventListener;\n\n    public ExoPlayerForwarder() {\n        exoPlayerEventListener = new EventListener();\n        mediaSourceEventListener = new NoPlayerMediaSourceEventListener();\n        videoListener = new ExoPlayerVideoListener();\n        analyticsListener = new NoPlayerAnalyticsListener();\n        drmSessionEventListener = new ExoPlayerDrmSessionEventListener();\n    }\n\n    public EventListener exoPlayerEventListener() {\n        return exoPlayerEventListener;\n    }\n\n    public MediaSourceEventListener mediaSourceEventListener() {\n        return mediaSourceEventListener;\n    }\n\n    public VideoListener videoListener() {\n        return videoListener;\n    }\n\n    public DefaultDrmSessionEventListener drmSessionEventListener() {\n        return drmSessionEventListener;\n    }\n\n    public AnalyticsListener analyticsListener() {\n        return analyticsListener;\n    }\n\n    public void bind(NoPlayer.PreparedListener preparedListener, PlayerState playerState) {\n        exoPlayerEventListener.add(new OnPrepareForwarder(preparedListener, playerState));\n    }\n\n    public void bind(NoPlayer.CompletionListener completionListener, NoPlayer.StateChangedListener stateChangedListener) {\n        exoPlayerEventListener.add(new OnCompletionForwarder(completionListener));\n        exoPlayerEventListener.add(new OnCompletionStateChangedForwarder(stateChangedListener));\n    }\n\n    public void bind(NoPlayer.ErrorListener errorListener) {\n        exoPlayerEventListener.add(new PlayerOnErrorForwarder(errorListener));\n    }\n\n    public void bind(NoPlayer.BufferStateListener bufferStateListener) {\n        exoPlayerEventListener.add(new BufferStateForwarder(bufferStateListener));\n    }\n\n    public void bind(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) {\n        videoListener.add(new VideoSizeChangedForwarder(videoSizeChangedListener));\n    }\n\n    public void bind(NoPlayer.BitrateChangedListener bitrateChangedListener) {\n        mediaSourceEventListener.add(new BitrateForwarder(bitrateChangedListener));\n    }\n\n    public void bind(NoPlayer.InfoListener infoListeners) {\n        exoPlayerEventListener.add(new EventInfoForwarder(infoListeners));\n        mediaSourceEventListener.add(new MediaSourceEventForwarder(infoListeners));\n        drmSessionEventListener.add(new DrmSessionInfoForwarder(infoListeners));\n        analyticsListener.add(new AnalyticsListenerForwarder(infoListeners));\n    }\n\n    public void bind(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListeners) {\n        analyticsListener.add(droppedVideoFramesListeners);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/ExoPlayerVideoListener.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.video.VideoListener;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\nclass ExoPlayerVideoListener implements VideoListener {\n\n    private final List<VideoListener> listeners = new CopyOnWriteArrayList<>();\n\n    public void add(VideoListener listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n        for (VideoListener listener : listeners) {\n            listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);\n        }\n    }\n\n    @Override\n    public void onRenderedFirstFrame() {\n        for (VideoListener listener : listeners) {\n            listener.onRenderedFirstFrame();\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/ForwarderInformation.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nclass ForwarderInformation {\n\n    static final class Parameters {\n\n        private Parameters() {\n            // non-instantiable\n        }\n\n        static final String EVENT_TIME = \"eventTime\";\n        static final String PLAY_WHEN_READY = \"playWhenReady\";\n        static final String PLAYBACK_STATE = \"playbackState\";\n        static final String REASON = \"reason\";\n        static final String PLAYBACK_PARAMETERS = \"playbackParameters\";\n        static final String REPEAT_MODE = \"repeatMode\";\n        static final String SHUFFLE_MODE_ENABLED = \"shuffleModeEnabled\";\n        static final String IS_LOADING = \"isLoading\";\n        static final String ERROR = \"error\";\n        static final String TRACK_GROUPS = \"trackGroups\";\n        static final String TRACK_SELECTIONS = \"trackSelections\";\n        static final String LOAD_EVENT_INFO = \"loadEventInfo\";\n        static final String MEDIA_LOAD_DATA = \"mediaLoadData\";\n        static final String WAS_CANCELED = \"wasCanceled\";\n        static final String TOTAL_LOAD_TIME_MS = \"totalLoadTimeMs\";\n        static final String TOTAL_BYTES_LOADED = \"totalBytesLoaded\";\n        static final String BITRATE_ESTIMATE = \"bitrateEstimate\";\n        static final String WIDTH = \"width\";\n        static final String HEIGHT = \"height\";\n        static final String METADATA = \"metadata\";\n        static final String TRACK_TYPE = \"trackType\";\n        static final String DECODER_COUNTERS = \"decoderCounters\";\n        static final String DECODER_NAME = \"decoderName\";\n        static final String INITIALIZATION_DURATION_MS = \"initializationDurationMs\";\n        static final String FORMAT = \"format\";\n        static final String AUDIO_SESSION_ID = \"audioSessionId\";\n        static final String BUFFER_SIZE = \"bufferSize\";\n        static final String BUFFER_SIZE_MS = \"bufferSizeMs\";\n        static final String ELAPSED_SINCE_LAST_FEED_MS = \"elapsedSinceLastFeedMs\";\n        static final String DROPPED_FRAMES = \"droppedFrames\";\n        static final String ELAPSED_MS = \"elapsedMs\";\n        static final String PIXEL_WIDTH_HEIGHT_RATIO = \"pixelWidthHeightRatio\";\n        static final String UNAPPLIED_ROTATION_DEGREES = \"unappliedRotationDegrees\";\n        static final String SURFACE = \"surface\";\n        static final String TIMELINE = \"timeline\";\n        static final String MANIFEST = \"manifest\";\n        static final String WINDOW_INDEX = \"windowIndex\";\n        static final String MEDIA_PERIOD_ID = \"mediaPeriodId\";\n\n    }\n\n    static final class Methods {\n\n        private Methods() {\n            // non-instantiable\n        }\n\n        static final String ON_PLAYER_STATE_CHANGED = \"onPlayerStateChanged\";\n        static final String ON_TIMELINE_CHANGED = \"onTimelineChanged\";\n        static final String ON_POSITION_DISCONTINUITY = \"onPositionDiscontinuity\";\n        static final String ON_SEEK_STARTED = \"onSeekStarted\";\n        static final String ON_SEEK_PROCESSED = \"onSeekProcessed\";\n        static final String ON_PLAYBACK_PARAMETERS_CHANGED = \"onPlaybackParametersChanged\";\n        static final String ON_REPEAT_MODE_CHANGED = \"onRepeatModeChanged\";\n        static final String ON_SHUFFLE_MODE_CHANGED = \"onShuffleModeChanged\";\n        static final String ON_PLAYER_ERROR = \"onPlayerError\";\n        static final String ON_TRACKS_CHANGED = \"onTracksChanged\";\n        static final String ON_LOAD_STARTED = \"onLoadStarted\";\n        static final String ON_LOAD_COMPLETED = \"onLoadCompleted\";\n        static final String ON_LOAD_CANCELED = \"onLoadCanceled\";\n        static final String ON_LOAD_ERROR = \"onLoadError\";\n        static final String ON_DOWNSTREAM_FORMAT_CHANGED = \"onDownstreamFormatChanged\";\n        static final String ON_UPSTREAM_DISCARDED = \"onUpstreamDiscarded\";\n        static final String ON_MEDIA_PERIOD_CREATED = \"onMediaPeriodCreated\";\n        static final String ON_MEDIA_PERIOD_RELEASED = \"onMediaPeriodReleased\";\n        static final String ON_READING_STARTED = \"onReadingStarted\";\n        static final String ON_BANDWIDTH_ESTIMATE = \"onBandwidthEstimate\";\n        static final String ON_SURFACE_SIZE_CHANGED = \"onSurfaceSizeChanged\";\n        static final String ON_METADATA = \"onMetadata\";\n        static final String ON_DECODER_ENABLED = \"onDecoderEnabled\";\n        static final String ON_DECODER_INITIALIZED = \"onDecoderInitialized\";\n        static final String ON_DECODER_INPUT_FORMAT_CHANGED = \"onDecoderInputFormatChanged\";\n        static final String ON_DECODER_DISABLED = \"onDecoderDisabled\";\n        static final String ON_AUDIO_SESSION_ID = \"onAudioSessionId\";\n        static final String ON_AUDIO_UNDERRUN = \"onAudioUnderrun\";\n        static final String ON_DROPPED_VIDEO_FRAMES = \"onDroppedVideoFrames\";\n        static final String ON_VIDEO_SIZE_CHANGED = \"onVideoSizeChanged\";\n        static final String ON_RENDERED_FIRST_FRAME = \"onRenderedFirstFrame\";\n        static final String ON_DRM_KEYS_LOADED = \"onDrmKeysLoaded\";\n        static final String ON_DRM_SESSION_MANAGER_ERROR = \"onDrmSessionManagerError\";\n        static final String ON_DRM_KEYS_RESTORED = \"onDrmKeysRestored\";\n        static final String ON_DRM_KEYS_REMOVED = \"onDrmKeysRemoved\";\n        static final String ON_LOADING_CHANGED = \"onLoadingChanged\";\n        static final String ON_SHUFFLE_MODE_ENABLED_CHANGED = \"onShuffleModeEnabledChanged\";\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/MediaSourceEventForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport android.support.annotation.Nullable;\n\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.io.IOException;\nimport java.util.HashMap;\n\nimport static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Methods;\nimport static com.novoda.noplayer.internal.exoplayer.forwarder.ForwarderInformation.Parameters;\n\n// This implements an interface method defined by ExoPlayer\n@SuppressWarnings({\"PMD.UnusedImports\", \"checkstyle:ParameterNumber\", \"PMD.ExcessiveParameterList\"})\nclass MediaSourceEventForwarder implements MediaSourceEventListener {\n\n    private static final String NO_MEDIA_PERIOD_ID = null;\n\n    private final NoPlayer.InfoListener infoListener;\n\n    MediaSourceEventForwarder(NoPlayer.InfoListener infoListener) {\n        this.infoListener = infoListener;\n    }\n\n    @Override\n    public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex));\n        callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId.toString());\n\n        infoListener.onNewInfo(Methods.ON_MEDIA_PERIOD_CREATED, callingMethodParameters);\n    }\n\n    @Override\n    public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex));\n        callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId.toString());\n\n        infoListener.onNewInfo(Methods.ON_MEDIA_PERIOD_RELEASED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadStarted(int windowIndex,\n                              @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                              LoadEventInfo loadEventInfo,\n                              MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex));\n        callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString());\n        callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n\n        infoListener.onNewInfo(Methods.ON_LOAD_STARTED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadCompleted(int windowIndex,\n                                @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                                LoadEventInfo loadEventInfo,\n                                MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex));\n        callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString());\n        callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n\n        infoListener.onNewInfo(Methods.ON_LOAD_COMPLETED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadCanceled(int windowIndex,\n                               @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                               LoadEventInfo loadEventInfo,\n                               MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex));\n        callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString());\n        callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n\n        infoListener.onNewInfo(Methods.ON_LOAD_CANCELED, callingMethodParameters);\n    }\n\n    @Override\n    public void onLoadError(int windowIndex,\n                            @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                            LoadEventInfo loadEventInfo,\n                            MediaLoadData mediaLoadData,\n                            IOException error,\n                            boolean wasCanceled) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex));\n        callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString());\n        callingMethodParameters.put(Parameters.LOAD_EVENT_INFO, loadEventInfo.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA, mediaLoadData.toString());\n\n        infoListener.onNewInfo(Methods.ON_LOAD_CANCELED, callingMethodParameters);\n    }\n\n    @Override\n    public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex));\n        callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID,  mediaPeriodId.toString());\n\n        infoListener.onNewInfo(Methods.ON_READING_STARTED, callingMethodParameters);\n    }\n\n    @Override\n    public void onUpstreamDiscarded(int windowIndex,\n                                    MediaSource.MediaPeriodId mediaPeriodId,\n                                    MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex));\n        callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID,  mediaPeriodId.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA,  mediaLoadData.toString());\n\n        infoListener.onNewInfo(Methods.ON_UPSTREAM_DISCARDED, callingMethodParameters);\n    }\n\n    @Override\n    public void onDownstreamFormatChanged(int windowIndex,\n                                          @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                                          MediaLoadData mediaLoadData) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(Parameters.WINDOW_INDEX, String.valueOf(windowIndex));\n        callingMethodParameters.put(Parameters.MEDIA_PERIOD_ID, mediaPeriodId == null ? NO_MEDIA_PERIOD_ID : mediaPeriodId.toString());\n        callingMethodParameters.put(Parameters.MEDIA_LOAD_DATA,  mediaLoadData.toString());\n\n        infoListener.onNewInfo(Methods.ON_DOWNSTREAM_FORMAT_CHANGED, callingMethodParameters);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/NoPlayerAnalyticsListener.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport android.view.Surface;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.analytics.AnalyticsListener;\nimport com.google.android.exoplayer2.decoder.DecoderCounters;\nimport com.google.android.exoplayer2.metadata.Metadata;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\nclass NoPlayerAnalyticsListener implements AnalyticsListener {\n\n    private final List<AnalyticsListener> listeners = new CopyOnWriteArrayList<>();\n    private final List<NoPlayer.DroppedVideoFramesListener> droppedVideoFramesListeners = new CopyOnWriteArrayList<>();\n\n    public void add(AnalyticsListener listener) {\n        listeners.add(listener);\n    }\n\n    public void add(NoPlayer.DroppedVideoFramesListener listener) {\n        droppedVideoFramesListeners.add(listener);\n    }\n\n    @Override\n    public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int playbackState) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState);\n        }\n    }\n\n    @Override\n    public void onTimelineChanged(EventTime eventTime, int reason) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onTimelineChanged(eventTime, reason);\n        }\n    }\n\n    @Override\n    public void onPositionDiscontinuity(EventTime eventTime, int reason) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onPositionDiscontinuity(eventTime, reason);\n        }\n    }\n\n    @Override\n    public void onSeekStarted(EventTime eventTime) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onSeekStarted(eventTime);\n        }\n    }\n\n    @Override\n    public void onSeekProcessed(EventTime eventTime) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onSeekProcessed(eventTime);\n        }\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(EventTime eventTime, PlaybackParameters playbackParameters) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onPlaybackParametersChanged(eventTime, playbackParameters);\n        }\n    }\n\n    @Override\n    public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onRepeatModeChanged(eventTime, repeatMode);\n        }\n    }\n\n    @Override\n    public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onShuffleModeChanged(eventTime, shuffleModeEnabled);\n        }\n    }\n\n    @Override\n    public void onLoadingChanged(EventTime eventTime, boolean isLoading) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onLoadingChanged(eventTime, isLoading);\n        }\n    }\n\n    @Override\n    public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onPlayerError(eventTime, error);\n        }\n    }\n\n    @Override\n    public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onTracksChanged(eventTime, trackGroups, trackSelections);\n        }\n    }\n\n    @Override\n    public void onLoadStarted(EventTime eventTime,\n                              MediaSourceEventListener.LoadEventInfo loadEventInfo,\n                              MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData);\n        }\n    }\n\n    @Override\n    public void onLoadCompleted(EventTime eventTime,\n                                MediaSourceEventListener.LoadEventInfo loadEventInfo,\n                                MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData);\n        }\n    }\n\n    @Override\n    public void onLoadCanceled(EventTime eventTime,\n                               MediaSourceEventListener.LoadEventInfo loadEventInfo,\n                               MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData);\n        }\n    }\n\n    @Override\n    public void onLoadError(EventTime eventTime,\n                            MediaSourceEventListener.LoadEventInfo loadEventInfo,\n                            MediaSourceEventListener.MediaLoadData mediaLoadData,\n                            IOException error,\n                            boolean wasCanceled) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled);\n        }\n    }\n\n    @Override\n    public void onDownstreamFormatChanged(EventTime eventTime,\n                                          MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDownstreamFormatChanged(eventTime, mediaLoadData);\n        }\n    }\n\n    @Override\n    public void onUpstreamDiscarded(EventTime eventTime,\n                                    MediaSourceEventListener.MediaLoadData mediaLoadData) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onUpstreamDiscarded(eventTime, mediaLoadData);\n        }\n    }\n\n    @Override\n    public void onMediaPeriodCreated(EventTime eventTime) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onMediaPeriodCreated(eventTime);\n        }\n    }\n\n    @Override\n    public void onMediaPeriodReleased(EventTime eventTime) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onMediaPeriodReleased(eventTime);\n        }\n    }\n\n    @Override\n    public void onReadingStarted(EventTime eventTime) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onReadingStarted(eventTime);\n        }\n    }\n\n    @Override\n    public void onBandwidthEstimate(EventTime eventTime,\n                                    int totalLoadTimeMs,\n                                    long totalBytesLoaded,\n                                    long bitrateEstimate) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onBandwidthEstimate(eventTime, totalLoadTimeMs, totalBytesLoaded, bitrateEstimate);\n        }\n    }\n\n    @Override\n    public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onSurfaceSizeChanged(eventTime, width, height);\n        }\n    }\n\n    @Override\n    public void onMetadata(EventTime eventTime, Metadata metadata) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onMetadata(eventTime, metadata);\n        }\n    }\n\n    @Override\n    public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDecoderEnabled(eventTime, trackType, decoderCounters);\n        }\n    }\n\n    @Override\n    public void onDecoderInitialized(EventTime eventTime,\n                                     int trackType,\n                                     String decoderName,\n                                     long initializationDurationMs) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDecoderInitialized(eventTime, trackType, decoderName, initializationDurationMs);\n        }\n    }\n\n    @Override\n    public void onDecoderInputFormatChanged(EventTime eventTime,\n                                            int trackType,\n                                            Format format) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDecoderInputFormatChanged(eventTime, trackType, format);\n        }\n    }\n\n    @Override\n    public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters decoderCounters) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDecoderDisabled(eventTime, trackType, decoderCounters);\n        }\n    }\n\n    @Override\n    public void onAudioSessionId(EventTime eventTime, int audioSessionId) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onAudioSessionId(eventTime, audioSessionId);\n        }\n    }\n\n    @Override\n    public void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);\n        }\n    }\n\n    @Override\n    public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDroppedVideoFrames(eventTime, droppedFrames, elapsedMs);\n        }\n\n        for (NoPlayer.DroppedVideoFramesListener listener : droppedVideoFramesListeners) {\n            listener.onDroppedVideoFrames(droppedFrames, elapsedMs);\n        }\n    }\n\n    @Override\n    public void onVideoSizeChanged(EventTime eventTime,\n                                   int width,\n                                   int height,\n                                   int unappliedRotationDegrees,\n                                   float pixelWidthHeightRatio) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onVideoSizeChanged(eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio);\n        }\n    }\n\n    @Override\n    public void onRenderedFirstFrame(EventTime eventTime, Surface surface) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onRenderedFirstFrame(eventTime, surface);\n        }\n    }\n\n    @Override\n    public void onDrmKeysLoaded(EventTime eventTime) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDrmKeysLoaded(eventTime);\n        }\n    }\n\n    @Override\n    public void onDrmSessionManagerError(EventTime eventTime, Exception error) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDrmSessionManagerError(eventTime, error);\n        }\n    }\n\n    @Override\n    public void onDrmKeysRestored(EventTime eventTime) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDrmKeysRestored(eventTime);\n        }\n    }\n\n    @Override\n    public void onDrmKeysRemoved(EventTime eventTime) {\n        for (AnalyticsListener listener : listeners) {\n            listener.onDrmKeysRemoved(eventTime);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/NoPlayerMediaSourceEventListener.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport android.support.annotation.Nullable;\n\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\n@SuppressWarnings({\"checkstyle:ParameterNumber\", \"PMD.ExcessiveParameterList\"}) // This implements an interface method defined by ExoPlayer\nclass NoPlayerMediaSourceEventListener implements MediaSourceEventListener {\n\n    private final List<MediaSourceEventListener> listeners = new CopyOnWriteArrayList<>();\n\n    public void add(MediaSourceEventListener listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {\n        for (MediaSourceEventListener listener : listeners) {\n            listener.onMediaPeriodCreated(windowIndex, mediaPeriodId);\n        }\n    }\n\n    @Override\n    public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {\n        for (MediaSourceEventListener listener : listeners) {\n            listener.onMediaPeriodReleased(windowIndex, mediaPeriodId);\n        }\n    }\n\n    @Override\n    public void onLoadStarted(int windowIndex,\n                              @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                              LoadEventInfo loadEventInfo,\n                              MediaLoadData mediaLoadData) {\n        for (MediaSourceEventListener listener : listeners) {\n            listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData);\n        }\n    }\n\n    @Override\n    public void onLoadCompleted(int windowIndex,\n                                @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                                LoadEventInfo loadEventInfo,\n                                MediaLoadData mediaLoadData) {\n        for (MediaSourceEventListener listener : listeners) {\n            listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData);\n        }\n    }\n\n    @Override\n    public void onLoadCanceled(int windowIndex,\n                               @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                               LoadEventInfo loadEventInfo,\n                               MediaLoadData mediaLoadData) {\n        for (MediaSourceEventListener listener : listeners) {\n            listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData);\n        }\n    }\n\n    @Override\n    public void onLoadError(int windowIndex,\n                            @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                            LoadEventInfo loadEventInfo,\n                            MediaLoadData mediaLoadData,\n                            IOException error, boolean wasCanceled) {\n        for (MediaSourceEventListener listener : listeners) {\n            listener.onLoadError(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled);\n        }\n    }\n\n    @Override\n    public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {\n        for (MediaSourceEventListener listener : listeners) {\n            listener.onReadingStarted(windowIndex, mediaPeriodId);\n        }\n    }\n\n    @Override\n    public void onUpstreamDiscarded(int windowIndex,\n                                    MediaSource.MediaPeriodId mediaPeriodId,\n                                    MediaLoadData mediaLoadData) {\n        for (MediaSourceEventListener listener : listeners) {\n            listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData);\n        }\n    }\n\n    @Override\n    public void onDownstreamFormatChanged(int windowIndex,\n                                          @Nullable MediaSource.MediaPeriodId mediaPeriodId,\n                                          MediaLoadData mediaLoadData) {\n        for (MediaSourceEventListener listener : listeners) {\n            listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/OnCompletionForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.novoda.noplayer.NoPlayer;\n\nclass OnCompletionForwarder implements Player.EventListener {\n\n    private final NoPlayer.CompletionListener completionListener;\n\n    OnCompletionForwarder(NoPlayer.CompletionListener completionListener) {\n        this.completionListener = completionListener;\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n        if (playbackState == Player.STATE_ENDED) {\n            completionListener.onCompletion();\n        }\n    }\n\n    @Override\n    public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onLoadingChanged(boolean isLoading) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n        // Sent by ErrorForwarder.\n    }\n\n    @Override\n    public void onPositionDiscontinuity(int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onSeekProcessed() {\n        // TODO: should we send?\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/OnCompletionStateChangedForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.novoda.noplayer.NoPlayer;\n\nclass OnCompletionStateChangedForwarder implements Player.EventListener {\n\n    private final NoPlayer.StateChangedListener stateChangedListener;\n\n    OnCompletionStateChangedForwarder(NoPlayer.StateChangedListener stateChangedListener) {\n        this.stateChangedListener = stateChangedListener;\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n        if (playbackState == Player.STATE_ENDED) {\n            stateChangedListener.onVideoStopped();\n        }\n    }\n\n    @Override\n    public void onRepeatModeChanged(int i) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onLoadingChanged(boolean isLoading) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n        // Sent by ErrorForwarder.\n    }\n\n    @Override\n    public void onPositionDiscontinuity(int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onSeekProcessed() {\n        // TODO: should we send?\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/OnPrepareForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.PlayerState;\n\nclass OnPrepareForwarder implements Player.EventListener {\n\n    private final NoPlayer.PreparedListener preparedListener;\n    private final PlayerState playerState;\n\n    OnPrepareForwarder(NoPlayer.PreparedListener preparedListener, PlayerState playerState) {\n        this.preparedListener = preparedListener;\n        this.playerState = playerState;\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n        if (isReady(playbackState)) {\n            preparedListener.onPrepared(playerState);\n        }\n    }\n\n    @Override\n    public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n        // TODO: should we send?\n    }\n\n    private boolean isReady(int playbackState) {\n        return playbackState == Player.STATE_READY;\n    }\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onLoadingChanged(boolean isLoading) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n        // Sent by ErrorForwarder.\n    }\n\n    @Override\n    public void onPositionDiscontinuity(int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onSeekProcessed() {\n        // TODO: should we send?\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/PlayerOnErrorForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.PlaybackParameters;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.Timeline;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.TrackSelectionArray;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.exoplayer.error.ExoPlayerErrorMapper;\n\nclass PlayerOnErrorForwarder implements Player.EventListener {\n\n    private final NoPlayer.ErrorListener errorListener;\n\n    PlayerOnErrorForwarder(NoPlayer.ErrorListener errorListener) {\n        this.errorListener = errorListener;\n    }\n\n    @Override\n    public void onPlayerError(ExoPlaybackException error) {\n        NoPlayer.PlayerError playerError = ExoPlayerErrorMapper.errorFor(error);\n        errorListener.onError(playerError);\n    }\n\n    @Override\n    public void onPositionDiscontinuity(int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onTimelineChanged(Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onLoadingChanged(boolean isLoading) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {\n        // Handled by OnPrepared and OnCompletion forwarders.\n    }\n\n    @Override\n    public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {\n        // TODO: should we send?\n    }\n\n    @Override\n    public void onSeekProcessed() {\n        // TODO: should we send?\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/forwarder/VideoSizeChangedForwarder.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport com.google.android.exoplayer2.video.VideoListener;\nimport com.novoda.noplayer.NoPlayer;\n\nclass VideoSizeChangedForwarder implements VideoListener {\n\n    private final NoPlayer.VideoSizeChangedListener videoSizeChangedListener;\n\n    VideoSizeChangedForwarder(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) {\n        this.videoSizeChangedListener = videoSizeChangedListener;\n    }\n\n    @Override\n    public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n        videoSizeChangedListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);\n    }\n\n    @Override\n    public void onRenderedFirstFrame() {\n        // TODO: should we send?\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/AudioTrackType.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\npublic enum AudioTrackType {\n\n    MAIN(1),\n    ALTERNATIVE(0),\n    UNKNOWN(-1);\n\n    private final int selectionFlag;\n\n    AudioTrackType(int selectionFlag) {\n        this.selectionFlag = selectionFlag;\n    }\n\n    static AudioTrackType from(int selectionFlag) {\n        for (AudioTrackType audioTrackType : AudioTrackType.values()) {\n            if (audioTrackType.selectionFlag == selectionFlag) {\n                return audioTrackType;\n            }\n        }\n        return UNKNOWN;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerAudioTrackSelector.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.RendererTypeRequester;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.novoda.noplayer.internal.exoplayer.mediasource.TrackType.AUDIO;\n\npublic class ExoPlayerAudioTrackSelector {\n\n    private final ExoPlayerTrackSelector trackSelector;\n\n    public ExoPlayerAudioTrackSelector(ExoPlayerTrackSelector trackSelector) {\n        this.trackSelector = trackSelector;\n    }\n\n    public boolean selectAudioTrack(PlayerAudioTrack audioTrack, RendererTypeRequester rendererTypeRequester) {\n        TrackGroupArray trackGroups = trackSelector.trackGroups(AUDIO, rendererTypeRequester);\n\n        DefaultTrackSelector.SelectionOverride selectionOverride = new DefaultTrackSelector.SelectionOverride(\n                audioTrack.groupIndex(),\n                audioTrack.formatIndex()\n        );\n        return trackSelector.setSelectionOverride(AUDIO, rendererTypeRequester, trackGroups, selectionOverride);\n    }\n\n    public AudioTracks getAudioTracks(RendererTypeRequester rendererTypeRequester) {\n        TrackGroupArray trackGroups = trackSelector.trackGroups(AUDIO, rendererTypeRequester);\n\n        List<PlayerAudioTrack> audioTracks = new ArrayList<>();\n\n        for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {\n            if (trackSelector.supportsTrackSwitching(AUDIO, rendererTypeRequester, trackGroups, groupIndex)) {\n                TrackGroup trackGroup = trackGroups.get(groupIndex);\n\n                for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) {\n                    Format format = trackGroup.getFormat(formatIndex);\n\n                    PlayerAudioTrack playerAudioTrack = new PlayerAudioTrack(\n                            groupIndex,\n                            formatIndex,\n                            format.id,\n                            format.language,\n                            format.sampleMimeType,\n                            format.channelCount,\n                            format.bitrate,\n                            AudioTrackType.from(format.selectionFlags)\n                    );\n                    audioTracks.add(playerAudioTrack);\n                }\n            }\n        }\n\n        return AudioTracks.from(audioTracks);\n    }\n\n    public boolean clearAudioTrack(RendererTypeRequester rendererTypeRequester) {\n        return trackSelector.clearSelectionOverrideFor(AUDIO, rendererTypeRequester);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerMappedTrackInfo.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector;\n\nclass ExoPlayerMappedTrackInfo {\n\n    private final MappingTrackSelector.MappedTrackInfo mappedTrackInfo;\n\n    ExoPlayerMappedTrackInfo(MappingTrackSelector.MappedTrackInfo mappedTrackInfo) {\n        this.mappedTrackInfo = mappedTrackInfo;\n    }\n\n    TrackGroupArray getTrackGroups(int index) {\n        return mappedTrackInfo.getTrackGroups(index);\n    }\n\n    int getAdaptiveSupport(int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) {\n        return mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, includeCapabilitiesExceededTracks);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerSubtitleTrackSelector.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.RendererTypeRequester;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.novoda.noplayer.internal.exoplayer.mediasource.TrackType.TEXT;\n\npublic class ExoPlayerSubtitleTrackSelector {\n\n    private final ExoPlayerTrackSelector trackSelector;\n\n    public ExoPlayerSubtitleTrackSelector(ExoPlayerTrackSelector trackSelector) {\n        this.trackSelector = trackSelector;\n    }\n\n    public boolean selectTextTrack(PlayerSubtitleTrack subtitleTrack, RendererTypeRequester rendererTypeRequester) {\n        TrackGroupArray trackGroups = trackSelector.trackGroups(TEXT, rendererTypeRequester);\n\n        DefaultTrackSelector.SelectionOverride selectionOverride = new DefaultTrackSelector.SelectionOverride(\n                subtitleTrack.groupIndex(),\n                subtitleTrack.formatIndex()\n        );\n        return trackSelector.setSelectionOverride(TEXT, rendererTypeRequester, trackGroups, selectionOverride);\n    }\n\n    public List<PlayerSubtitleTrack> getSubtitleTracks(RendererTypeRequester rendererTypeRequester) {\n        TrackGroupArray trackGroups = trackSelector.trackGroups(TEXT, rendererTypeRequester);\n\n        List<PlayerSubtitleTrack> subtitleTracks = new ArrayList<>();\n\n        for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {\n            TrackGroup trackGroup = trackGroups.get(groupIndex);\n\n            for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) {\n                Format format = trackGroup.getFormat(formatIndex);\n                PlayerSubtitleTrack playerSubtitleTrack = new PlayerSubtitleTrack(\n                        groupIndex,\n                        formatIndex,\n                        format.id,\n                        format.language,\n                        format.sampleMimeType,\n                        format.channelCount,\n                        format.bitrate\n                );\n                subtitleTracks.add(playerSubtitleTrack);\n            }\n        }\n\n        return subtitleTracks;\n    }\n\n    public boolean clearSubtitleTrack(RendererTypeRequester rendererTypeRequester) {\n        return trackSelector.clearSelectionOverrideFor(TEXT, rendererTypeRequester);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerTrackSelector.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.RendererCapabilities;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.MappingTrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.RendererTypeRequester;\nimport com.novoda.noplayer.internal.utils.Optional;\n\n// We cannot make it final as we need to mock it in tests\n@SuppressWarnings({\"checkstyle:FinalClass\", \"PMD.ClassWithOnlyPrivateConstructorsShouldBeFinal\"})\npublic class ExoPlayerTrackSelector {\n\n    private final DefaultTrackSelector trackSelector;\n    private final RendererTrackIndexExtractor rendererTrackIndexExtractor;\n\n    public static ExoPlayerTrackSelector newInstance(DefaultTrackSelector trackSelector) {\n        RendererTrackIndexExtractor rendererTrackIndexExtractor = new RendererTrackIndexExtractor();\n        return new ExoPlayerTrackSelector(trackSelector, rendererTrackIndexExtractor);\n    }\n\n    private ExoPlayerTrackSelector(DefaultTrackSelector trackSelector, RendererTrackIndexExtractor rendererTrackIndexExtractor) {\n        this.trackSelector = trackSelector;\n        this.rendererTrackIndexExtractor = rendererTrackIndexExtractor;\n    }\n\n    TrackGroupArray trackGroups(TrackType trackType, RendererTypeRequester rendererTypeRequester) {\n        Optional<Integer> audioRendererIndex = rendererTrackIndexExtractor.extract(trackType, mappedTrackInfoLength(), rendererTypeRequester);\n        return audioRendererIndex.isAbsent() ? TrackGroupArray.EMPTY : trackInfo().getTrackGroups(audioRendererIndex.get());\n    }\n\n    boolean clearSelectionOverrideFor(TrackType trackType, RendererTypeRequester rendererTypeRequester) {\n        Optional<Integer> rendererIndex = rendererTrackIndexExtractor.extract(trackType, mappedTrackInfoLength(), rendererTypeRequester);\n        if (rendererIndex.isPresent()) {\n            trackSelector.setParameters(trackSelector\n                    .buildUponParameters()\n                    .clearSelectionOverrides(rendererIndex.get())\n            );\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    private ExoPlayerMappedTrackInfo trackInfo() {\n        MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo();\n\n        if (trackInfo == null) {\n            throw new IllegalStateException(\"Track info is not available.\");\n        }\n        return new ExoPlayerMappedTrackInfo(trackInfo);\n    }\n\n    private int mappedTrackInfoLength() {\n        return trackSelector.getCurrentMappedTrackInfo().length;\n    }\n\n    boolean setSelectionOverride(TrackType trackType,\n                                 RendererTypeRequester rendererTypeRequester,\n                                 TrackGroupArray trackGroups,\n                                 DefaultTrackSelector.SelectionOverride selectionOverride) {\n        Optional<Integer> rendererIndex = rendererTrackIndexExtractor.extract(trackType, mappedTrackInfoLength(), rendererTypeRequester);\n        if (rendererIndex.isPresent()) {\n            trackSelector.setParameters(trackSelector\n                    .buildUponParameters()\n                    .setSelectionOverride(rendererIndex.get(), trackGroups, selectionOverride)\n            );\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    boolean supportsTrackSwitching(TrackType trackType,\n                                   RendererTypeRequester rendererTypeRequester,\n                                   TrackGroupArray trackGroups,\n                                   int groupIndex) {\n        Optional<Integer> audioRendererIndex = rendererTrackIndexExtractor.extract(trackType, mappedTrackInfoLength(), rendererTypeRequester);\n        return audioRendererIndex.isPresent()\n                && trackGroups.get(groupIndex).length > 0\n                && trackInfo().getAdaptiveSupport(audioRendererIndex.get(), groupIndex, false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED;\n    }\n\n    void clearMaxVideoBitrate() {\n        setMaxVideoBitrateParameter(Integer.MAX_VALUE);\n    }\n\n    void setMaxVideoBitrate(int maxVideoBitrate) {\n        setMaxVideoBitrateParameter(maxVideoBitrate);\n    }\n\n    private void setMaxVideoBitrateParameter(int maxValue) {\n        trackSelector.setParameters(\n                trackSelector.buildUponParameters()\n                        .setMaxVideoBitrate(maxValue)\n                        .build()\n        );\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelector.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.novoda.noplayer.ContentType;\nimport com.novoda.noplayer.internal.exoplayer.RendererTypeRequester;\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.novoda.noplayer.internal.exoplayer.mediasource.TrackType.VIDEO;\n\npublic class ExoPlayerVideoTrackSelector {\n\n    private final ExoPlayerTrackSelector trackSelector;\n\n    public ExoPlayerVideoTrackSelector(ExoPlayerTrackSelector trackSelector) {\n        this.trackSelector = trackSelector;\n    }\n\n    public boolean selectVideoTrack(PlayerVideoTrack videoTrack, RendererTypeRequester rendererTypeRequester) {\n        TrackGroupArray trackGroups = trackSelector.trackGroups(VIDEO, rendererTypeRequester);\n\n        DefaultTrackSelector.SelectionOverride selectionOverride = new DefaultTrackSelector.SelectionOverride(\n                videoTrack.groupIndex(),\n                videoTrack.formatIndex()\n        );\n        return trackSelector.setSelectionOverride(VIDEO, rendererTypeRequester, trackGroups, selectionOverride);\n    }\n\n    public List<PlayerVideoTrack> getVideoTracks(RendererTypeRequester rendererTypeRequester, ContentType contentType) {\n        TrackGroupArray trackGroups = trackSelector.trackGroups(VIDEO, rendererTypeRequester);\n\n        List<PlayerVideoTrack> videoTracks = new ArrayList<>();\n\n        for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {\n            TrackGroup trackGroup = trackGroups.get(groupIndex);\n\n            for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) {\n                Format format = trackGroup.getFormat(formatIndex);\n\n                PlayerVideoTrack playerVideoTrack = new PlayerVideoTrack(\n                        groupIndex,\n                        formatIndex,\n                        format.id,\n                        contentType,\n                        format.width,\n                        format.height,\n                        (int) format.frameRate,\n                        format.bitrate\n                );\n\n                videoTracks.add(playerVideoTrack);\n            }\n        }\n\n        return videoTracks;\n    }\n\n    public Optional<PlayerVideoTrack> getSelectedVideoTrack(SimpleExoPlayer exoPlayer,\n                                                            RendererTypeRequester rendererTypeRequester,\n                                                            ContentType contentType) {\n        Format selectedVideoFormat = exoPlayer.getVideoFormat();\n\n        if (selectedVideoFormat == null) {\n            return Optional.absent();\n        }\n\n        List<PlayerVideoTrack> videoTracks = getVideoTracks(rendererTypeRequester, contentType);\n        return findSelectedVideoTrack(selectedVideoFormat, videoTracks);\n    }\n\n    private Optional<PlayerVideoTrack> findSelectedVideoTrack(Format selectedVideoFormat, List<PlayerVideoTrack> videoTracks) {\n        for (PlayerVideoTrack videoTrack : videoTracks) {\n            if (videoTrack.id().equals(selectedVideoFormat.id)) {\n                return Optional.of(videoTrack);\n            }\n        }\n        return Optional.absent();\n    }\n\n    public boolean clearVideoTrack(RendererTypeRequester rendererTypeRequester) {\n        return trackSelector.clearSelectionOverrideFor(VIDEO, rendererTypeRequester);\n    }\n\n    public void clearMaxVideoBitrate() {\n        trackSelector.clearMaxVideoBitrate();\n    }\n\n    public void setMaxVideoBitrate(int maxVideoBitrate) {\n        trackSelector.setMaxVideoBitrate(maxVideoBitrate);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/MediaSourceFactory.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Handler;\n\nimport com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;\nimport com.google.android.exoplayer2.source.ExtractorMediaSource;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.source.dash.DashMediaSource;\nimport com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;\nimport com.google.android.exoplayer2.source.hls.HlsMediaSource;\nimport com.google.android.exoplayer2.upstream.DataSource;\nimport com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;\nimport com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;\nimport com.google.android.exoplayer2.upstream.DefaultHttpDataSource;\nimport com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.internal.utils.Optional;\n\npublic class MediaSourceFactory {\n\n    private final Context context;\n    private final Handler handler;\n    private final Optional<DataSource.Factory> dataSourceFactory;\n    private final String userAgent;\n    private final boolean allowCrossProtocolRedirects;\n\n    public MediaSourceFactory(Context context,\n                              String userAgent,\n                              Handler handler,\n                              Optional<DataSource.Factory> dataSourceFactory,\n                              boolean allowCrossProtocolRedirects) {\n        this.context = context;\n        this.handler = handler;\n        this.dataSourceFactory = dataSourceFactory;\n        this.userAgent = userAgent;\n        this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;\n    }\n\n    public MediaSource create(Options options,\n                              Uri uri,\n                              MediaSourceEventListener mediaSourceEventListener,\n                              DefaultBandwidthMeter bandwidthMeter) {\n        DefaultDataSourceFactory defaultDataSourceFactory = createDataSourceFactory(bandwidthMeter);\n        switch (options.contentType()) {\n            case HLS:\n                return createHlsMediaSource(defaultDataSourceFactory, uri, mediaSourceEventListener);\n            case H264:\n                return createH264MediaSource(defaultDataSourceFactory, uri, mediaSourceEventListener);\n            case DASH:\n                return createDashMediaSource(defaultDataSourceFactory, uri, mediaSourceEventListener);\n            default:\n                throw new UnsupportedOperationException(\"Content type: \" + options + \" is not supported.\");\n        }\n    }\n\n    private DefaultDataSourceFactory createDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {\n        if (dataSourceFactory.isPresent()) {\n            return new DefaultDataSourceFactory(context, bandwidthMeter, dataSourceFactory.get());\n        } else {\n            DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory(\n                    userAgent,\n                    bandwidthMeter,\n                    DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,\n                    DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,\n                    allowCrossProtocolRedirects\n            );\n\n            return new DefaultDataSourceFactory(context, bandwidthMeter, httpDataSourceFactory);\n        }\n    }\n\n    private MediaSource createHlsMediaSource(DefaultDataSourceFactory defaultDataSourceFactory,\n                                             Uri uri,\n                                             MediaSourceEventListener mediaSourceEventListener) {\n        HlsMediaSource.Factory factory = new HlsMediaSource.Factory(defaultDataSourceFactory);\n        HlsMediaSource hlsMediaSource = factory.createMediaSource(uri);\n        hlsMediaSource.addEventListener(handler, mediaSourceEventListener);\n        return hlsMediaSource;\n    }\n\n    private MediaSource createH264MediaSource(DefaultDataSourceFactory defaultDataSourceFactory,\n                                              Uri uri,\n                                              MediaSourceEventListener mediaSourceEventListener) {\n        ExtractorMediaSource.Factory factory = new ExtractorMediaSource.Factory(defaultDataSourceFactory);\n        ExtractorMediaSource extractorMediaSource = factory\n                .setExtractorsFactory(new DefaultExtractorsFactory())\n                .createMediaSource(uri);\n        extractorMediaSource.addEventListener(handler, mediaSourceEventListener);\n        return extractorMediaSource;\n    }\n\n    private MediaSource createDashMediaSource(DefaultDataSourceFactory defaultDataSourceFactory,\n                                              Uri uri,\n                                              MediaSourceEventListener mediaSourceEventListener) {\n        DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory(defaultDataSourceFactory);\n        DashMediaSource.Factory factory = new DashMediaSource.Factory(chunkSourceFactory, defaultDataSourceFactory);\n        DashMediaSource mediaSource = factory.createMediaSource(uri);\n        mediaSource.addEventListener(handler, mediaSourceEventListener);\n        return mediaSource;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/RendererTrackIndexExtractor.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.C;\nimport com.novoda.noplayer.internal.exoplayer.RendererTypeRequester;\nimport com.novoda.noplayer.internal.utils.Optional;\n\nclass RendererTrackIndexExtractor {\n\n    Optional<Integer> extract(TrackType trackType, int numberOfTracks, RendererTypeRequester typeRequester) {\n        for (int i = 0; i < numberOfTracks; i++) {\n            int rendererType = typeRequester.getRendererTypeFor(i);\n\n            if ((trackType == TrackType.AUDIO && rendererType == C.TRACK_TYPE_AUDIO)\n                    || (trackType == TrackType.VIDEO && rendererType == C.TRACK_TYPE_VIDEO)\n                    || (trackType == TrackType.TEXT && rendererType == C.TRACK_TYPE_TEXT)) {\n                return Optional.of(i);\n            }\n        }\n\n        return Optional.absent();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/TrackType.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nenum TrackType {\n    AUDIO,\n    VIDEO,\n    TEXT\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/BitrateChangedListeners.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.model.Bitrate;\n\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\nclass BitrateChangedListeners implements NoPlayer.BitrateChangedListener {\n\n    private final Set<NoPlayer.BitrateChangedListener> listeners = new CopyOnWriteArraySet<>();\n\n    void add(NoPlayer.BitrateChangedListener listener) {\n        listeners.add(listener);\n    }\n\n    void remove(NoPlayer.BitrateChangedListener listener) {\n        listeners.remove(listener);\n    }\n\n    void clear() {\n        listeners.clear();\n    }\n\n    @Override\n    public void onBitrateChanged(Bitrate audioBitrate, Bitrate videoBitrate) {\n        for (NoPlayer.BitrateChangedListener listener : listeners) {\n            listener.onBitrateChanged(audioBitrate, videoBitrate);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/BufferStateListeners.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\nclass BufferStateListeners implements NoPlayer.BufferStateListener {\n\n    private final Set<NoPlayer.BufferStateListener> listeners = new CopyOnWriteArraySet<>();\n\n    void add(NoPlayer.BufferStateListener listener) {\n        listeners.add(listener);\n    }\n\n    void remove(NoPlayer.BufferStateListener listener) {\n        listeners.remove(listener);\n    }\n\n    void clear() {\n        listeners.clear();\n    }\n\n    @Override\n    public void onBufferStarted() {\n        for (NoPlayer.BufferStateListener listener : listeners) {\n            listener.onBufferStarted();\n        }\n    }\n\n    @Override\n    public void onBufferCompleted() {\n        for (NoPlayer.BufferStateListener listener : listeners) {\n            listener.onBufferCompleted();\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/CompletionListeners.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\nclass CompletionListeners implements NoPlayer.CompletionListener {\n\n    private final Set<NoPlayer.CompletionListener> listeners = new CopyOnWriteArraySet<>();\n\n    private boolean hasCompleted;\n\n    void add(NoPlayer.CompletionListener listener) {\n        listeners.add(listener);\n    }\n\n    void remove(NoPlayer.CompletionListener listener) {\n        listeners.remove(listener);\n    }\n\n    void clear() {\n        listeners.clear();\n    }\n\n    public void onCompletion() {\n        if (!hasCompleted) {\n            hasCompleted = true;\n            for (NoPlayer.CompletionListener listener : listeners) {\n                listener.onCompletion();\n            }\n        }\n    }\n\n    void resetCompletedState() {\n        hasCompleted = false;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/DroppedFramesListeners.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\npublic class DroppedFramesListeners implements NoPlayer.DroppedVideoFramesListener {\n\n    private final Set<NoPlayer.DroppedVideoFramesListener> listeners = new CopyOnWriteArraySet<>();\n\n    void add(NoPlayer.DroppedVideoFramesListener listener) {\n        listeners.add(listener);\n    }\n\n    void remove(NoPlayer.DroppedVideoFramesListener listener) {\n        listeners.remove(listener);\n    }\n\n    void clear() {\n        listeners.clear();\n    }\n\n    @Override\n    public void onDroppedVideoFrames(int droppedFrames, long elapsedMsSinceLastDroppedFrames) {\n        for (NoPlayer.DroppedVideoFramesListener listener : listeners) {\n            listener.onDroppedVideoFrames(droppedFrames, elapsedMsSinceLastDroppedFrames);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/ErrorListeners.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\nclass ErrorListeners implements NoPlayer.ErrorListener {\n\n    private final Set<NoPlayer.ErrorListener> listeners = new CopyOnWriteArraySet<>();\n\n    void add(NoPlayer.ErrorListener listener) {\n        listeners.add(listener);\n    }\n\n    void remove(NoPlayer.ErrorListener listener) {\n        listeners.remove(listener);\n    }\n\n    void clear() {\n        listeners.clear();\n    }\n\n    @Override\n    public void onError(NoPlayer.PlayerError error) {\n        for (NoPlayer.ErrorListener listener : listeners) {\n            listener.onError(error);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/HeartbeatCallbacks.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\nclass HeartbeatCallbacks implements NoPlayer.HeartbeatCallback {\n\n    private final Set<NoPlayer.HeartbeatCallback> callbacks = new CopyOnWriteArraySet<>();\n\n    void registerCallback(NoPlayer.HeartbeatCallback heartbeatCallback) {\n        callbacks.add(heartbeatCallback);\n    }\n\n    void clear() {\n        callbacks.clear();\n    }\n\n    @Override\n    public void onBeat(NoPlayer player) {\n        for (NoPlayer.HeartbeatCallback callback : callbacks) {\n            callback.onBeat(player);\n        }\n    }\n\n    void unregisterCallback(NoPlayer.HeartbeatCallback heartbeatCallback) {\n        callbacks.remove(heartbeatCallback);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/InfoListeners.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\nclass InfoListeners implements NoPlayer.InfoListener {\n\n    private final Set<NoPlayer.InfoListener> listeners = new CopyOnWriteArraySet<>();\n\n    void add(NoPlayer.InfoListener listener) {\n        listeners.add(listener);\n    }\n\n    void remove(NoPlayer.InfoListener listener) {\n        listeners.remove(listener);\n    }\n\n    void clear() {\n        listeners.clear();\n    }\n\n    @Override\n    public void onNewInfo(String callingMethod, Map<String, String> callingMethodParams) {\n        for (NoPlayer.InfoListener listener : listeners) {\n            listener.onNewInfo(callingMethod, callingMethodParams);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/PlayerListenersHolder.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.Listeners;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.NoPlayer.BitrateChangedListener;\n\npublic class PlayerListenersHolder implements Listeners {\n\n    private final ErrorListeners errorListeners;\n    private final PreparedListeners preparedListeners;\n    private final BufferStateListeners bufferStateListeners;\n    private final CompletionListeners completionListeners;\n    private final StateChangedListeners stateChangedListeners;\n    private final InfoListeners infoListeners;\n    private final VideoSizeChangedListeners videoSizeChangedListeners;\n    private final BitrateChangedListeners bitrateChangedListeners;\n    private final DroppedFramesListeners droppedFramesListeners;\n\n    private final HeartbeatCallbacks heartbeatCallbacks;\n\n    public PlayerListenersHolder() {\n        errorListeners = new ErrorListeners();\n        preparedListeners = new PreparedListeners();\n        bufferStateListeners = new BufferStateListeners();\n        completionListeners = new CompletionListeners();\n        stateChangedListeners = new StateChangedListeners();\n        infoListeners = new InfoListeners();\n        videoSizeChangedListeners = new VideoSizeChangedListeners();\n        bitrateChangedListeners = new BitrateChangedListeners();\n        heartbeatCallbacks = new HeartbeatCallbacks();\n        droppedFramesListeners = new DroppedFramesListeners();\n    }\n\n    @Override\n    public void addErrorListener(NoPlayer.ErrorListener errorListener) {\n        errorListeners.add(errorListener);\n    }\n\n    @Override\n    public void removeErrorListener(NoPlayer.ErrorListener errorListener) {\n        errorListeners.remove(errorListener);\n    }\n\n    @Override\n    public void addPreparedListener(NoPlayer.PreparedListener preparedListener) {\n        preparedListeners.add(preparedListener);\n    }\n\n    @Override\n    public void removePreparedListener(NoPlayer.PreparedListener preparedListener) {\n        preparedListeners.remove(preparedListener);\n    }\n\n    @Override\n    public void addBufferStateListener(NoPlayer.BufferStateListener bufferStateListener) {\n        bufferStateListeners.add(bufferStateListener);\n    }\n\n    @Override\n    public void removeBufferStateListener(NoPlayer.BufferStateListener bufferStateListener) {\n        bufferStateListeners.remove(bufferStateListener);\n    }\n\n    @Override\n    public void addCompletionListener(NoPlayer.CompletionListener completionListener) {\n        completionListeners.add(completionListener);\n    }\n\n    @Override\n    public void removeCompletionListener(NoPlayer.CompletionListener completionListener) {\n        completionListeners.remove(completionListener);\n    }\n\n    @Override\n    public void addStateChangedListener(NoPlayer.StateChangedListener stateChangedListener) {\n        stateChangedListeners.add(stateChangedListener);\n    }\n\n    @Override\n    public void removeStateChangedListener(NoPlayer.StateChangedListener stateChangedListener) {\n        stateChangedListeners.remove(stateChangedListener);\n    }\n\n    @Override\n    public void addInfoListener(NoPlayer.InfoListener infoListener) {\n        infoListeners.add(infoListener);\n    }\n\n    @Override\n    public void removeInfoListener(NoPlayer.InfoListener infoListener) {\n        infoListeners.remove(infoListener);\n    }\n\n    @Override\n    public void addBitrateChangedListener(BitrateChangedListener bitrateChangedListener) {\n        bitrateChangedListeners.add(bitrateChangedListener);\n    }\n\n    @Override\n    public void removeBitrateChangedListener(BitrateChangedListener bitrateChangedListener) {\n        bitrateChangedListeners.remove(bitrateChangedListener);\n    }\n\n    @Override\n    public void addHeartbeatCallback(NoPlayer.HeartbeatCallback heartbeatCallback) {\n        heartbeatCallbacks.registerCallback(heartbeatCallback);\n    }\n\n    @Override\n    public void removeHeartbeatCallback(NoPlayer.HeartbeatCallback heartbeatCallback) {\n        heartbeatCallbacks.unregisterCallback(heartbeatCallback);\n    }\n\n    @Override\n    public void addVideoSizeChangedListener(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) {\n        videoSizeChangedListeners.add(videoSizeChangedListener);\n    }\n\n    @Override\n    public void removeVideoSizeChangedListener(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) {\n        videoSizeChangedListeners.remove(videoSizeChangedListener);\n    }\n\n    @Override\n    public void addDroppedVideoFrames(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListener) {\n        droppedFramesListeners.add(droppedVideoFramesListener);\n    }\n\n    @Override\n    public void removeDroppedVideoFrames(NoPlayer.DroppedVideoFramesListener droppedVideoFramesListener) {\n        droppedFramesListeners.remove(droppedVideoFramesListener);\n    }\n\n    public NoPlayer.ErrorListener getErrorListeners() {\n        return errorListeners;\n    }\n\n    public NoPlayer.PreparedListener getPreparedListeners() {\n        return preparedListeners;\n    }\n\n    public NoPlayer.BufferStateListener getBufferStateListeners() {\n        return bufferStateListeners;\n    }\n\n    public NoPlayer.CompletionListener getCompletionListeners() {\n        return completionListeners;\n    }\n\n    public NoPlayer.StateChangedListener getStateChangedListeners() {\n        return stateChangedListeners;\n    }\n\n    public NoPlayer.InfoListener getInfoListeners() {\n        return infoListeners;\n    }\n\n    public NoPlayer.HeartbeatCallback getHeartbeatCallbacks() {\n        return heartbeatCallbacks;\n    }\n\n    public NoPlayer.VideoSizeChangedListener getVideoSizeChangedListeners() {\n        return videoSizeChangedListeners;\n    }\n\n    public NoPlayer.BitrateChangedListener getBitrateChangedListeners() {\n        return bitrateChangedListeners;\n    }\n\n    public NoPlayer.DroppedVideoFramesListener getDroppedVideoFramesListeners() {\n        return droppedFramesListeners;\n    }\n\n    public void resetState() {\n        preparedListeners.resetPreparedState();\n        completionListeners.resetCompletedState();\n    }\n\n    public void clear() {\n        errorListeners.clear();\n        preparedListeners.clear();\n        bufferStateListeners.clear();\n        completionListeners.clear();\n        stateChangedListeners.clear();\n        infoListeners.clear();\n        videoSizeChangedListeners.clear();\n        bitrateChangedListeners.clear();\n        heartbeatCallbacks.clear();\n        droppedFramesListeners.clear();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/PreparedListeners.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.PlayerState;\n\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\nclass PreparedListeners implements NoPlayer.PreparedListener {\n\n    private final Set<NoPlayer.PreparedListener> listeners = new CopyOnWriteArraySet<>();\n\n    private boolean hasPrepared;\n\n    void add(NoPlayer.PreparedListener listener) {\n        listeners.add(listener);\n    }\n\n    void remove(NoPlayer.PreparedListener listener) {\n        listeners.remove(listener);\n    }\n\n    void clear() {\n        listeners.clear();\n    }\n\n    @Override\n    public void onPrepared(PlayerState playerState) {\n        if (!hasPrepared) {\n            hasPrepared = true;\n            for (NoPlayer.PreparedListener listener : listeners) {\n                listener.onPrepared(playerState);\n            }\n        }\n    }\n\n    void resetPreparedState() {\n        hasPrepared = false;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/StateChangedListeners.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.utils.NoPlayerLog;\n\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\nclass StateChangedListeners implements NoPlayer.StateChangedListener {\n\n    private enum State {\n        PLAYING,\n        PAUSED,\n        STOPPED\n    }\n\n    private State currentState;\n\n    private final Set<NoPlayer.StateChangedListener> listeners = new CopyOnWriteArraySet<>();\n\n    void add(NoPlayer.StateChangedListener listener) {\n        listeners.add(listener);\n    }\n\n    void remove(NoPlayer.StateChangedListener listener) {\n        listeners.remove(listener);\n    }\n\n    void clear() {\n        listeners.clear();\n    }\n\n    @Override\n    public void onVideoPlaying() {\n        if (currentState == State.PLAYING) {\n            NoPlayerLog.e(\"Tried calling onVideoPlaying() but video is already playing.\");\n            return;\n        }\n\n        for (NoPlayer.StateChangedListener listener : listeners) {\n            listener.onVideoPlaying();\n        }\n\n        currentState = State.PLAYING;\n    }\n\n    @Override\n    public void onVideoPaused() {\n        if (currentState == State.PAUSED) {\n            NoPlayerLog.e(\"Tried calling onVideoPaused() but video is already paused.\");\n            return;\n        }\n\n        for (NoPlayer.StateChangedListener listener : listeners) {\n            listener.onVideoPaused();\n        }\n\n        currentState = State.PAUSED;\n    }\n\n    @Override\n    public void onVideoStopped() {\n        if (currentState == State.STOPPED) {\n            NoPlayerLog.e(\"Tried calling onVideoStopped() but video has already stopped.\");\n            return;\n        }\n\n        for (NoPlayer.StateChangedListener listener : listeners) {\n            listener.onVideoStopped();\n        }\n\n        currentState = State.STOPPED;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/listeners/VideoSizeChangedListeners.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\nclass VideoSizeChangedListeners implements NoPlayer.VideoSizeChangedListener {\n\n    private final Set<NoPlayer.VideoSizeChangedListener> listeners = new CopyOnWriteArraySet<>();\n\n    void add(NoPlayer.VideoSizeChangedListener listener) {\n        listeners.add(listener);\n    }\n\n    void remove(NoPlayer.VideoSizeChangedListener listener) {\n        listeners.remove(listener);\n    }\n\n    void clear() {\n        listeners.clear();\n    }\n\n    @Override\n    public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n        for (NoPlayer.VideoSizeChangedListener listener : listeners) {\n            listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerAudioTrackSelector.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nclass AndroidMediaPlayerAudioTrackSelector {\n\n    private static final int NO_FORMAT = 0;\n    private static final int NO_CHANNELS = -1;\n    private static final int NO_FREQUENCY = -1;\n    private static final String NO_MIME_TYPE = \"\";\n\n    private final TrackInfosFactory trackInfosFactory;\n\n    AndroidMediaPlayerAudioTrackSelector(TrackInfosFactory trackInfosFactory) {\n        this.trackInfosFactory = trackInfosFactory;\n    }\n\n    AudioTracks getAudioTracks(MediaPlayer mediaPlayer) {\n        if (mediaPlayer == null) {\n            throw new IllegalStateException(\"You can only call getAudioTracks() when video is prepared.\");\n        }\n\n        List<PlayerAudioTrack> audioTracks = new ArrayList<>();\n        NoPlayerTrackInfos trackInfos = trackInfosFactory.createFrom(mediaPlayer);\n\n        for (int i = 0; i < trackInfos.size(); i++) {\n            NoPlayerTrackInfo trackInfo = trackInfos.get(i);\n            if (trackInfo.type() == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {\n                audioTracks.add(\n                        new PlayerAudioTrack(\n                                i,\n                                NO_FORMAT,\n                                String.valueOf(trackInfo.hashCode()),\n                                trackInfo.language(),\n                                NO_MIME_TYPE,\n                                NO_CHANNELS,\n                                NO_FREQUENCY,\n                                AudioTrackType.MAIN\n                        )\n                );\n            }\n        }\n        return AudioTracks.from(audioTracks);\n    }\n\n    boolean selectAudioTrack(MediaPlayer mediaPlayer, PlayerAudioTrack playerAudioTrack) {\n        if (mediaPlayer == null) {\n            throw new IllegalStateException(\"You can only call selectAudioTrack() when video is prepared.\");\n        }\n\n        mediaPlayer.selectTrack(playerAudioTrack.groupIndex());\n        return true;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.content.Context;\nimport android.media.AudioManager;\nimport android.media.MediaPlayer;\nimport android.net.Uri;\nimport android.support.annotation.Nullable;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState;\nimport com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder;\nimport com.novoda.noplayer.internal.utils.NoPlayerLog;\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.Either;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.IDLE;\nimport static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.PAUSED;\nimport static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.PLAYING;\n\n// Not much we can do, wrapping MediaPlayer is a lot of work\n@SuppressWarnings(\"PMD.GodClass\")\nclass AndroidMediaPlayerFacade {\n\n    private static final Map<String, String> NO_HEADERS = null;\n\n    private final Context context;\n    private final MediaPlayerForwarder forwarder;\n    private final AudioManager audioManager;\n    private final AndroidMediaPlayerAudioTrackSelector trackSelector;\n    private final PlaybackStateChecker playbackStateChecker;\n    private final MediaPlayerCreator mediaPlayerCreator;\n\n    private PlaybackState currentState = IDLE;\n\n    private int currentBufferPercentage;\n    private float volume = 1.0f;\n\n    @Nullable\n    private MediaPlayer mediaPlayer;\n\n    static AndroidMediaPlayerFacade newInstance(Context context, MediaPlayerForwarder forwarder) {\n        TrackInfosFactory trackInfosFactory = new TrackInfosFactory();\n        AndroidMediaPlayerAudioTrackSelector trackSelector = new AndroidMediaPlayerAudioTrackSelector(trackInfosFactory);\n        PlaybackStateChecker playbackStateChecker = new PlaybackStateChecker();\n        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);\n        MediaPlayerCreator mediaPlayerCreator = new MediaPlayerCreator();\n        return new AndroidMediaPlayerFacade(context, forwarder, audioManager, trackSelector, playbackStateChecker, mediaPlayerCreator);\n    }\n\n    AndroidMediaPlayerFacade(Context context,\n                             MediaPlayerForwarder forwarder,\n                             AudioManager audioManager,\n                             AndroidMediaPlayerAudioTrackSelector trackSelector,\n                             PlaybackStateChecker playbackStateChecker,\n                             MediaPlayerCreator mediaPlayerCreator) {\n        this.context = context;\n        this.forwarder = forwarder;\n        this.audioManager = audioManager;\n        this.trackSelector = trackSelector;\n        this.playbackStateChecker = playbackStateChecker;\n        this.mediaPlayerCreator = mediaPlayerCreator;\n    }\n\n    void prepareVideo(Uri videoUri, Either<Surface, SurfaceHolder> surface) {\n        requestAudioFocus();\n        release();\n        try {\n            currentState = PlaybackState.PREPARING;\n            mediaPlayer = createAndBindMediaPlayer(surface, videoUri);\n            mediaPlayer.prepareAsync();\n        } catch (IOException | IllegalArgumentException | IllegalStateException ex) {\n            reportCreationError(ex, videoUri);\n        }\n    }\n\n    private void requestAudioFocus() {\n        audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);\n    }\n\n    private MediaPlayer createAndBindMediaPlayer(Either<Surface, SurfaceHolder> surface,\n                                                 Uri videoUri) throws IOException, IllegalStateException, IllegalArgumentException {\n        MediaPlayer mediaPlayer = mediaPlayerCreator.createMediaPlayer();\n        mediaPlayer.setOnPreparedListener(internalPreparedListener);\n        mediaPlayer.setOnVideoSizeChangedListener(internalSizeChangedListener);\n        mediaPlayer.setOnCompletionListener(internalCompletionListener);\n        mediaPlayer.setOnErrorListener(internalErrorListener);\n        mediaPlayer.setOnBufferingUpdateListener(internalBufferingUpdateListener);\n        mediaPlayer.setDataSource(context, videoUri, NO_HEADERS);\n        attachSurface(mediaPlayer, surface);\n        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);\n        mediaPlayer.setScreenOnWhilePlaying(true);\n\n        currentBufferPercentage = 0;\n        volume = 1.0f;\n\n        return mediaPlayer;\n    }\n\n    private void reportCreationError(Exception ex, Uri videoUri) {\n        NoPlayerLog.w(ex, \"Unable to open content: \" + videoUri);\n        currentState = PlaybackState.ERROR;\n        internalErrorListener.onError(mediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);\n    }\n\n    private final MediaPlayer.OnVideoSizeChangedListener internalSizeChangedListener = new MediaPlayer.OnVideoSizeChangedListener() {\n        @Override\n        public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {\n            MediaPlayer.OnVideoSizeChangedListener onVideoSizeChangedForwarder = forwarder.onSizeChangedListener();\n            if (onVideoSizeChangedForwarder == null) {\n                throw new IllegalStateException(\"Should bind a OnVideoSizeChangedListener. Cannot forward events.\");\n            }\n            onVideoSizeChangedForwarder.onVideoSizeChanged(mp, width, height);\n        }\n    };\n\n    private final MediaPlayer.OnPreparedListener internalPreparedListener = new MediaPlayer.OnPreparedListener() {\n        @Override\n        public void onPrepared(MediaPlayer mp) {\n            currentState = PlaybackState.PREPARED;\n\n            MediaPlayer.OnPreparedListener onPreparedForwarder = forwarder.onPreparedListener();\n            if (onPreparedForwarder == null) {\n                throw new IllegalStateException(\"Should bind a OnPreparedListener. Cannot forward events.\");\n            }\n            onPreparedForwarder.onPrepared(mediaPlayer);\n        }\n    };\n\n    private final MediaPlayer.OnCompletionListener internalCompletionListener = new MediaPlayer.OnCompletionListener() {\n        @Override\n        public void onCompletion(MediaPlayer mp) {\n            currentState = PlaybackState.COMPLETED;\n            MediaPlayer.OnCompletionListener onCompletionForwarder = forwarder.onCompletionListener();\n            if (onCompletionForwarder == null) {\n                throw new IllegalStateException(\"Should bind a OnCompletionListener. Cannot forward events.\");\n            }\n            onCompletionForwarder.onCompletion(mediaPlayer);\n        }\n    };\n\n    private final MediaPlayer.OnErrorListener internalErrorListener = new MediaPlayer.OnErrorListener() {\n        @Override\n        public boolean onError(MediaPlayer mp, int what, int extra) {\n            NoPlayerLog.d(\"Error: \" + what + \",\" + extra);\n            currentState = PlaybackState.ERROR;\n            MediaPlayer.OnErrorListener onErrorForwarder = forwarder.onErrorListener();\n            if (onErrorForwarder == null) {\n                throw new IllegalStateException(\"Should bind a OnErrorListener. Cannot forward events.\");\n            }\n            return onErrorForwarder.onError(mediaPlayer, what, extra);\n        }\n    };\n\n    private final MediaPlayer.OnBufferingUpdateListener internalBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() {\n        @Override\n        public void onBufferingUpdate(MediaPlayer mp, int percent) {\n            currentBufferPercentage = percent;\n        }\n    };\n\n    void release() {\n        if (hasPlayer()) {\n            mediaPlayer.reset();\n            mediaPlayer.release();\n            mediaPlayer = null;\n            currentState = IDLE;\n        }\n    }\n\n    void start(Either<Surface, SurfaceHolder> surface) throws IllegalStateException {\n        assertIsInPlaybackState();\n        attachSurface(mediaPlayer, surface);\n        currentState = PLAYING;\n        mediaPlayer.start();\n    }\n\n    private void attachSurface(final MediaPlayer mediaPlayer, Either<Surface, SurfaceHolder> surface) {\n        Either.Consumer<Surface> setSurface = new Either.Consumer<Surface>() {\n            @Override\n            public void accept(Surface value) {\n                mediaPlayer.setSurface(value);\n            }\n        };\n        Either.Consumer<SurfaceHolder> setDisplay = new Either.Consumer<SurfaceHolder>() {\n            @Override\n            public void accept(SurfaceHolder value) {\n                mediaPlayer.setDisplay(value);\n            }\n        };\n        surface.apply(setSurface, setDisplay);\n    }\n\n    void pause() throws IllegalStateException {\n        assertIsInPlaybackState();\n\n        if (isPlaying()) {\n            mediaPlayer.pause();\n            currentState = PAUSED;\n        }\n    }\n\n    int mediaDurationInMillis() throws IllegalStateException {\n        assertIsInPlaybackState();\n        return mediaPlayer.getDuration();\n    }\n\n    int currentPositionInMillis() throws IllegalStateException {\n        assertIsInPlaybackState();\n        return mediaPlayer.getCurrentPosition();\n    }\n\n    void seekTo(long positionInMillis) throws IllegalStateException {\n        assertIsInPlaybackState();\n        mediaPlayer.seekTo((int) positionInMillis);\n    }\n\n    boolean isPlaying() {\n        return playbackStateChecker.isPlaying(mediaPlayer, currentState);\n    }\n\n    int getBufferPercentage() throws IllegalStateException {\n        assertIsInPlaybackState();\n        return currentBufferPercentage;\n    }\n\n    AudioTracks getAudioTracks() throws IllegalStateException {\n        assertIsInPlaybackState();\n        return trackSelector.getAudioTracks(mediaPlayer);\n    }\n\n    boolean selectAudioTrack(PlayerAudioTrack playerAudioTrack) throws IllegalStateException {\n        assertIsInPlaybackState();\n        return trackSelector.selectAudioTrack(mediaPlayer, playerAudioTrack);\n    }\n\n    boolean clearAudioTrackSelection() {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to clear audio track selection but has not been implemented for MediaPlayer.\");\n        return false;\n    }\n\n    void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener seekToResettingSeekListener) throws IllegalStateException {\n        assertIsInPlaybackState();\n        mediaPlayer.setOnSeekCompleteListener(seekToResettingSeekListener);\n    }\n\n    boolean hasPlayedContent() {\n        return hasPlayer();\n    }\n\n    private boolean hasPlayer() {\n        return mediaPlayer != null;\n    }\n\n    boolean clearSubtitleTrack() throws IllegalStateException {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to hide subtitle track but has not been implemented for MediaPlayer.\");\n        return false;\n    }\n\n    boolean selectSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to select subtitle track but has not been implemented for MediaPlayer.\");\n        return false;\n    }\n\n    List<PlayerSubtitleTrack> getSubtitleTracks() throws IllegalStateException {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to get subtitle tracks but has not been implemented for MediaPlayer.\");\n        return Collections.emptyList();\n    }\n\n    private void assertIsInPlaybackState() throws IllegalStateException {\n        if (!playbackStateChecker.isInPlaybackState(mediaPlayer, currentState)) {\n            throw new IllegalStateException(\"Video must be loaded and not in an error state before trying to interact with the player\");\n        }\n    }\n\n    Optional<PlayerVideoTrack> getSelectedVideoTrack() {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to get the currently playing video track but has not been implemented for MediaPlayer.\");\n        return Optional.absent();\n    }\n\n    List<PlayerVideoTrack> getVideoTracks() {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to get video tracks but has not been implemented for MediaPlayer.\");\n        return Collections.emptyList();\n    }\n\n    boolean selectVideoTrack(PlayerVideoTrack videoTrack) {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to select a video track but has not been implemented for MediaPlayer.\");\n        return false;\n    }\n\n    boolean clearVideoTrackSelection() {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to clear video track selection but has not been implemented for MediaPlayer.\");\n        return false;\n    }\n\n    void setRepeating(boolean repeating) {\n        assertIsInPlaybackState();\n        mediaPlayer.setLooping(repeating);\n    }\n\n    void setVolume(float volume) {\n        assertIsInPlaybackState();\n        this.volume = volume;\n        mediaPlayer.setVolume(volume, volume);\n    }\n\n    float getVolume() {\n        assertIsInPlaybackState();\n        return volume;\n    }\n\n    void clearMaxVideoBitrate() {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to clear max video bitrate but has not been implemented for MediaPlayer.\");\n    }\n\n    void setMaxVideoBitrate(int maxVideoBitrate) {\n        assertIsInPlaybackState();\n        NoPlayerLog.w(\"Tried to set max video bitrate but has not been implemented for MediaPlayer.\");\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerImpl.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\nimport android.net.Uri;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport android.view.View;\n\nimport com.novoda.noplayer.Listeners;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.PlayerInformation;\nimport com.novoda.noplayer.PlayerState;\nimport com.novoda.noplayer.PlayerSurfaceHolder;\nimport com.novoda.noplayer.PlayerView;\nimport com.novoda.noplayer.SurfaceRequester;\nimport com.novoda.noplayer.internal.Heart;\nimport com.novoda.noplayer.internal.listeners.PlayerListenersHolder;\nimport com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder;\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.Either;\nimport com.novoda.noplayer.model.LoadTimeout;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\nimport com.novoda.noplayer.model.Timeout;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n// Not much we can do, wrapping MediaPlayer is a lot of work\n@SuppressWarnings(\"PMD.GodClass\")\nclass AndroidMediaPlayerImpl implements NoPlayer {\n\n    private static final long NO_SEEK_TO_POSITION = -1;\n    private static final long INITIAL_PLAY_SEEK_DELAY_IN_MILLIS = 500;\n\n    private final List<SurfaceRequester.Callback> surfaceHolderRequesterCallbacks = new ArrayList<>();\n\n    private final MediaPlayerInformation mediaPlayerInformation;\n    private final AndroidMediaPlayerFacade mediaPlayer;\n    private final MediaPlayerForwarder forwarder;\n    private final CheckBufferHeartbeatCallback bufferHeartbeatCallback;\n    private final DelayedActionExecutor delayedActionExecutor;\n    private final Heart heart;\n    private final PlayerListenersHolder listenersHolder;\n    private final LoadTimeout loadTimeout;\n    private final BuggyVideoDriverPreventer buggyVideoDriverPreventer;\n\n    private int videoWidth;\n    private int videoHeight;\n    private long seekToPositionInMillis = NO_SEEK_TO_POSITION;\n\n    private boolean seekingWithIntentToPlay;\n    private SurfaceRequester surfaceRequester;\n    private View containerView;\n\n    @SuppressWarnings(\"checkstyle:ParameterNumber\")\n        // We cannot really group these any further\n    AndroidMediaPlayerImpl(MediaPlayerInformation mediaPlayerInformation,\n                           AndroidMediaPlayerFacade mediaPlayer,\n                           MediaPlayerForwarder forwarder,\n                           PlayerListenersHolder listenersHolder,\n                           CheckBufferHeartbeatCallback bufferHeartbeatCallback,\n                           LoadTimeout loadTimeout,\n                           Heart heart,\n                           DelayedActionExecutor delayedActionExecutor,\n                           BuggyVideoDriverPreventer buggyVideoDriverPreventer) {\n        this.mediaPlayerInformation = mediaPlayerInformation;\n        this.mediaPlayer = mediaPlayer;\n        this.forwarder = forwarder;\n        this.listenersHolder = listenersHolder;\n        this.bufferHeartbeatCallback = bufferHeartbeatCallback;\n        this.loadTimeout = loadTimeout;\n        this.heart = heart;\n        this.delayedActionExecutor = delayedActionExecutor;\n        this.buggyVideoDriverPreventer = buggyVideoDriverPreventer;\n    }\n\n    void initialise() {\n        forwarder.bind(listenersHolder.getPreparedListeners(), this);\n        forwarder.bind(listenersHolder.getBufferStateListeners(), listenersHolder.getErrorListeners());\n        forwarder.bind(listenersHolder.getCompletionListeners(), listenersHolder.getStateChangedListeners());\n        forwarder.bind(listenersHolder.getVideoSizeChangedListeners());\n        forwarder.bind(listenersHolder.getInfoListeners());\n\n        bufferHeartbeatCallback.bind(forwarder.onHeartbeatListener());\n\n        heart.bind(new Heart.Heartbeat(listenersHolder.getHeartbeatCallbacks(), this));\n\n        listenersHolder.addHeartbeatCallback(bufferHeartbeatCallback);\n        listenersHolder.addPreparedListener(new PreparedListener() {\n            @Override\n            public void onPrepared(PlayerState playerState) {\n                loadTimeout.cancel();\n                mediaPlayer.setOnSeekCompleteListener(seekToResettingSeekListener);\n            }\n        });\n        listenersHolder.addErrorListener(new ErrorListener() {\n            @Override\n            public void onError(PlayerError error) {\n                reset();\n            }\n        });\n        listenersHolder.addVideoSizeChangedListener(new VideoSizeChangedListener() {\n            @Override\n            public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {\n                videoWidth = width;\n                videoHeight = height;\n            }\n        });\n    }\n\n    private final MediaPlayer.OnSeekCompleteListener seekToResettingSeekListener = new MediaPlayer.OnSeekCompleteListener() {\n        @Override\n        public void onSeekComplete(MediaPlayer mp) {\n            seekToPositionInMillis = NO_SEEK_TO_POSITION;\n\n            if (seekingWithIntentToPlay || isPlaying()) {\n                seekingWithIntentToPlay = false;\n                play();\n            }\n        }\n    };\n\n    @Override\n    public void setRepeating(boolean repeating) {\n        mediaPlayer.setRepeating(repeating);\n    }\n\n    @Override\n    public void setVolume(float volume) {\n        mediaPlayer.setVolume(volume);\n    }\n\n    @Override\n    public float getVolume() {\n        return mediaPlayer.getVolume();\n    }\n\n    @Override\n    public Listeners getListeners() {\n        return listenersHolder;\n    }\n\n    @Override\n    public void play() throws IllegalStateException {\n        heart.startBeatingHeart();\n        requestSurface(new SurfaceRequester.Callback() {\n            @Override\n            public void onSurfaceReady(Either<Surface, SurfaceHolder> surface) {\n                mediaPlayer.start(surface);\n                listenersHolder.getStateChangedListeners().onVideoPlaying();\n            }\n        });\n    }\n\n    @Override\n    public void playAt(final long positionInMillis) throws IllegalStateException {\n        if (playheadPositionInMillis() == positionInMillis) {\n            play();\n        } else {\n            requestSurface(new SurfaceRequester.Callback() {\n                @Override\n                public void onSurfaceReady(Either<Surface, SurfaceHolder> surface) {\n                    initialSeekWorkaround(surface, positionInMillis);\n                }\n            });\n        }\n    }\n\n    /**\n     * Workaround to fix some devices (nexus 7 2013 in particular) from natively crashing the mediaplayer\n     * by starting the mediaplayer before seeking it.\n     */\n    private void initialSeekWorkaround(Either<Surface, SurfaceHolder> surface, final long initialPlayPositionInMillis) throws IllegalStateException {\n        listenersHolder.getBufferStateListeners().onBufferStarted();\n        initialisePlaybackForSeeking(surface);\n        delayedActionExecutor.performAfterDelay(new DelayedActionExecutor.Action() {\n            @Override\n            public void perform() {\n                seekWithIntentToPlay(initialPlayPositionInMillis);\n            }\n        }, INITIAL_PLAY_SEEK_DELAY_IN_MILLIS);\n    }\n\n    private void initialisePlaybackForSeeking(Either<Surface, SurfaceHolder> surface) {\n        mediaPlayer.start(surface);\n        mediaPlayer.pause();\n    }\n\n    private void requestSurface(SurfaceRequester.Callback callback) {\n        if (surfaceRequester == null) {\n            throw new IllegalStateException(\"Must attach a PlayerView before interacting with Player\");\n        }\n        surfaceHolderRequesterCallbacks.add(callback);\n        surfaceRequester.requestSurface(callback);\n    }\n\n    private void seekWithIntentToPlay(long positionInMillis) throws IllegalStateException {\n        seekingWithIntentToPlay = true;\n        seekTo(positionInMillis);\n    }\n\n    @Override\n    public boolean isPlaying() {\n        return mediaPlayer.isPlaying();\n    }\n\n    @Override\n    public void seekTo(long positionInMillis) throws IllegalStateException {\n        seekToPositionInMillis = positionInMillis;\n        mediaPlayer.seekTo(positionInMillis);\n    }\n\n    @Override\n    public void pause() throws IllegalStateException {\n        mediaPlayer.pause();\n        if (heart.isBeating()) {\n            heart.stopBeatingHeart();\n            heart.forceBeat();\n        }\n        listenersHolder.getStateChangedListeners().onVideoPaused();\n    }\n\n    @Override\n    public void loadVideo(final Uri uri, final Options options) {\n        if (mediaPlayer.hasPlayedContent()) {\n            stop();\n        }\n        assertPlayerViewIsAttached();\n        createSurfaceByShowingVideoContainer();\n        listenersHolder.getBufferStateListeners().onBufferStarted();\n        requestSurface(new SurfaceRequester.Callback() {\n            @Override\n            public void onSurfaceReady(Either<Surface, SurfaceHolder> surface) {\n                mediaPlayer.prepareVideo(uri, surface);\n            }\n        });\n    }\n\n    private void createSurfaceByShowingVideoContainer() {\n        containerView.setVisibility(View.VISIBLE);\n    }\n\n    private void assertPlayerViewIsAttached() {\n        if (containerView == null) {\n            throw new IllegalStateException(\"A PlayerView must be attached in order to loadVideo\");\n        }\n    }\n\n    @Override\n    public void loadVideoWithTimeout(Uri uri, Options options, Timeout timeout, LoadTimeoutCallback loadTimeoutCallback) {\n        loadTimeout.start(timeout, loadTimeoutCallback);\n        loadVideo(uri, options);\n    }\n\n    @Override\n    public long playheadPositionInMillis() throws IllegalStateException {\n        return isSeeking() ? seekToPositionInMillis : mediaPlayer.currentPositionInMillis();\n    }\n\n    private boolean isSeeking() {\n        return seekToPositionInMillis != NO_SEEK_TO_POSITION;\n    }\n\n    @Override\n    public long mediaDurationInMillis() throws IllegalStateException {\n        return mediaPlayer.mediaDurationInMillis();\n    }\n\n    @Override\n    public int bufferPercentage() throws IllegalStateException {\n        return mediaPlayer.getBufferPercentage();\n    }\n\n    @Override\n    public int videoWidth() {\n        return videoWidth;\n    }\n\n    @Override\n    public int videoHeight() {\n        return videoHeight;\n    }\n\n    @Override\n    public PlayerInformation getPlayerInformation() {\n        return mediaPlayerInformation;\n    }\n\n    @Override\n    public void attach(PlayerView playerView) {\n        containerView = playerView.getContainerView();\n        buggyVideoDriverPreventer.preventVideoDriverBug(this, containerView);\n        listenersHolder.addVideoSizeChangedListener(playerView.getVideoSizeChangedListener());\n        listenersHolder.addStateChangedListener(playerView.getStateChangedListener());\n        PlayerSurfaceHolder playerSurfaceHolder = playerView.getPlayerSurfaceHolder();\n        surfaceRequester = playerSurfaceHolder.getSurfaceRequester();\n    }\n\n    @Override\n    public void detach(PlayerView playerView) {\n        clearSurfaceHolderCallbacks();\n        listenersHolder.removeStateChangedListener(playerView.getStateChangedListener());\n        listenersHolder.removeVideoSizeChangedListener(playerView.getVideoSizeChangedListener());\n        buggyVideoDriverPreventer.clear(playerView.getContainerView());\n        surfaceRequester = null;\n        containerView = null;\n    }\n\n    private void clearSurfaceHolderCallbacks() {\n        for (SurfaceRequester.Callback callback : surfaceHolderRequesterCallbacks) {\n            surfaceRequester.removeCallback(callback);\n        }\n        surfaceHolderRequesterCallbacks.clear();\n    }\n\n    @Override\n    public boolean selectAudioTrack(PlayerAudioTrack audioTrack) throws IllegalStateException {\n        return mediaPlayer.selectAudioTrack(audioTrack);\n    }\n\n    @Override\n    public boolean clearAudioTrackSelection() throws IllegalStateException {\n        return mediaPlayer.clearAudioTrackSelection();\n    }\n\n    @Override\n    public boolean showSubtitleTrack(PlayerSubtitleTrack subtitleTrack) throws IllegalStateException {\n        return mediaPlayer.selectSubtitleTrack(subtitleTrack);\n    }\n\n    @Override\n    public boolean hideSubtitleTrack() throws IllegalStateException {\n        return mediaPlayer.clearSubtitleTrack();\n    }\n\n    @Override\n    public AudioTracks getAudioTracks() throws IllegalStateException {\n        return mediaPlayer.getAudioTracks();\n    }\n\n    @Override\n    public boolean selectVideoTrack(PlayerVideoTrack videoTrack) throws IllegalStateException {\n        return mediaPlayer.selectVideoTrack(videoTrack);\n    }\n\n    @Override\n    public Optional<PlayerVideoTrack> getSelectedVideoTrack() throws IllegalStateException {\n        return mediaPlayer.getSelectedVideoTrack();\n    }\n\n    @Override\n    public boolean clearVideoTrackSelection() throws IllegalStateException {\n        return mediaPlayer.clearVideoTrackSelection();\n    }\n\n    @Override\n    public List<PlayerVideoTrack> getVideoTracks() throws IllegalStateException {\n        return mediaPlayer.getVideoTracks();\n    }\n\n    @Override\n    public List<PlayerSubtitleTrack> getSubtitleTracks() throws IllegalStateException {\n        return mediaPlayer.getSubtitleTracks();\n    }\n\n    @Override\n    public void clearMaxVideoBitrate() {\n        mediaPlayer.clearMaxVideoBitrate();\n    }\n\n    @Override\n    public void setMaxVideoBitrate(int maxVideoBitrate) {\n        mediaPlayer.setMaxVideoBitrate(maxVideoBitrate);\n    }\n\n    @Override\n    public void stop() {\n        reset();\n        listenersHolder.getStateChangedListeners().onVideoStopped();\n    }\n\n    @Override\n    public void release() {\n        stop();\n        listenersHolder.clear();\n    }\n\n    private void reset() {\n        delayedActionExecutor.clearAllActions();\n        listenersHolder.resetState();\n        loadTimeout.cancel();\n        heart.stopBeatingHeart();\n        mediaPlayer.release();\n        destroySurfaceByHidingVideoContainer();\n    }\n\n    private void destroySurfaceByHidingVideoContainer() {\n        if (containerView != null) {\n            containerView.setVisibility(View.GONE);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerType.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nenum AndroidMediaPlayerType {\n\n    AWESOME(\"AwesomePlayer\"),\n    NU(\"NuPlayer\"),\n    UNKNOWN(\"Unknown\");\n\n    private final String name;\n\n    AndroidMediaPlayerType(String name) {\n        this.name = name;\n    }\n\n    String getName() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/BuggyVideoDriverPreventer.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.view.View;\n\nimport com.novoda.noplayer.NoPlayer;\n\n/**\n * The intent for this component is to workaround a buggy video driver affecting AwesomePlayer on Nexus 5.\n * After inserting the headphones the screen goes black, causing layout changes, after recovering\n * from the freeze subsequent calls to {@link android.media.MediaPlayer#pause()} are ignored, the internal status machine got corrupted.\n * <p>\n * It can be workaround by forcing {@link android.media.MediaPlayer#start()} when it was already playing.\n */\nclass BuggyVideoDriverPreventer {\n\n    private final MediaPlayerTypeReader mediaPlayerTypeReader;\n\n    private OnPotentialBuggyDriverLayoutListener preventerListener;\n\n    BuggyVideoDriverPreventer(MediaPlayerTypeReader mediaPlayerTypeReader) {\n        this.mediaPlayerTypeReader = mediaPlayerTypeReader;\n    }\n\n    void preventVideoDriverBug(NoPlayer player, View containerView) {\n        if (videoDriverCanBeBuggy()) {\n            attemptToCorrectMediaPlayerStatus(player, containerView);\n        }\n    }\n\n    private boolean videoDriverCanBeBuggy() {\n        return mediaPlayerTypeReader.getPlayerType() == AndroidMediaPlayerType.AWESOME;\n    }\n\n    private void attemptToCorrectMediaPlayerStatus(NoPlayer player, View containerView) {\n        preventerListener = new OnPotentialBuggyDriverLayoutListener(player);\n        containerView.addOnLayoutChangeListener(preventerListener);\n    }\n\n    void clear(View containerView) {\n        containerView.removeOnLayoutChangeListener(preventerListener);\n        preventerListener = null;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/CheckBufferHeartbeatCallback.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport com.novoda.noplayer.NoPlayer;\n\npublic class CheckBufferHeartbeatCallback implements NoPlayer.HeartbeatCallback {\n\n    private static final int FORCED_BUFFERING_BEATS_THRESHOLD = 4;\n\n    private BufferListener bufferListener = BufferListener.NULL_IMPL;\n    private long previousPositionInMillis = -1;\n    private int beatsPlayed;\n\n    public void bind(BufferListener bufferListener) {\n        this.bufferListener = bufferListener;\n    }\n\n    @Override\n    public void onBeat(NoPlayer player) {\n        if (mediaPlayerIsUnavailable(player)) {\n            stopBuffering();\n            return;\n        }\n\n        long currentPositionInMillis = player.playheadPositionInMillis();\n        if (positionNotUpdating(currentPositionInMillis)) {\n            beatsPlayed = 0;\n            startBuffering();\n        } else {\n            previousPositionInMillis = currentPositionInMillis;\n            beatsPlayed++;\n            if (beatsPlayed > FORCED_BUFFERING_BEATS_THRESHOLD) {\n                stopBuffering();\n            }\n        }\n    }\n\n    private boolean positionNotUpdating(long currentPositionInMillis) {\n        return currentPositionInMillis == previousPositionInMillis;\n    }\n\n    private void stopBuffering() {\n        bufferListener.onBufferComplete();\n    }\n\n    private void startBuffering() {\n        bufferListener.onBufferStart();\n    }\n\n    private boolean mediaPlayerIsUnavailable(NoPlayer player) {\n        try {\n            return !player.isPlaying();\n        } catch (IllegalStateException e) {\n            // The mediaplayer has not been initialized or has been released\n            return true;\n        }\n    }\n\n    public interface BufferListener {\n\n        void onBufferStart();\n\n        void onBufferComplete();\n\n        BufferListener NULL_IMPL = new BufferListener() {\n            @Override\n            public void onBufferStart() {\n                // do nothing\n            }\n\n            @Override\n            public void onBufferComplete() {\n                // do nothing\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/DelayedActionExecutor.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.os.Handler;\n\nimport java.util.Iterator;\nimport java.util.Map;\n\nclass DelayedActionExecutor {\n\n    private final Handler handler;\n    private final Map<Action, Runnable> runnables;\n\n    DelayedActionExecutor(Handler handler, Map<Action, Runnable> runnables) {\n        this.handler = handler;\n        this.runnables = runnables;\n    }\n\n    void performAfterDelay(final Action action, long delayInMillis) {\n        Runnable actionRunnable = new Runnable() {\n            @Override\n            public void run() {\n                action.perform();\n                runnables.remove(action);\n            }\n        };\n        runnables.put(action, actionRunnable);\n        handler.postDelayed(actionRunnable, delayInMillis);\n    }\n\n    void clearAllActions() {\n        Iterator<Map.Entry<Action, Runnable>> it = runnables.entrySet().iterator();\n        while (it.hasNext()) {\n            Map.Entry<Action, Runnable> entry = it.next();\n            handler.removeCallbacks(entry.getValue());\n            it.remove();\n        }\n    }\n\n    interface Action {\n\n        void perform();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/ErrorFactory.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.DetailErrorType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.NoPlayerError;\nimport com.novoda.noplayer.PlayerErrorType;\n\npublic final class ErrorFactory {\n\n    private ErrorFactory() {\n        // no instances\n    }\n\n    @SuppressWarnings({\"PMD.StdCyclomaticComplexity\", \"PMD.CyclomaticComplexity\"})\n    public static NoPlayer.PlayerError createErrorFrom(int type, int extra) {\n        String message = String.valueOf(extra);\n        switch (type) {\n            case MediaPlayer.MEDIA_ERROR_IO:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_IO, message);\n            case MediaPlayer.MEDIA_ERROR_MALFORMED:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_MALFORMED, message);\n            case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK, message);\n            case MediaPlayer.MEDIA_INFO_NOT_SEEKABLE:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_INFO_NOT_SEEKABLE, message);\n            case MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_SUBTITLE_TIMED_OUT, message);\n            case MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE:\n                return new NoPlayerError(PlayerErrorType.SOURCE, DetailErrorType.MEDIA_PLAYER_UNSUPPORTED_SUBTITLE, message);\n\n            case MediaPlayer.MEDIA_ERROR_SERVER_DIED:\n                return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_PLAYER_SERVER_DIED, message);\n            case MediaPlayer.PREPARE_DRM_STATUS_PREPARATION_ERROR:\n                return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_PLAYER_PREPARE_DRM_STATUS_PREPARATION_ERROR, message);\n            case MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR:\n                return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR, message);\n            case MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR:\n                return new NoPlayerError(PlayerErrorType.DRM, DetailErrorType.MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR, message);\n\n            case MediaPlayer.MEDIA_ERROR_TIMED_OUT:\n                return new NoPlayerError(PlayerErrorType.CONNECTIVITY, DetailErrorType.MEDIA_PLAYER_TIMED_OUT, message);\n\n            case MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING:\n                return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.MEDIA_PLAYER_INFO_AUDIO_NOT_PLAYING, message);\n            case MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING:\n                return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.MEDIA_PLAYER_BAD_INTERLEAVING, message);\n            case MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING:\n                return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.MEDIA_PLAYER_INFO_VIDEO_NOT_PLAYING, message);\n            case MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING:\n                return new NoPlayerError(PlayerErrorType.RENDERER_DECODER, DetailErrorType.MEDIA_PLAYER_INFO_VIDEO_TRACK_LAGGING, message);\n            default:\n                return new NoPlayerError(PlayerErrorType.UNKNOWN, DetailErrorType.MEDIA_PLAYER_UNKNOWN, message);\n\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/ErrorFormatter.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nfinal class ErrorFormatter {\n\n    private ErrorFormatter() {\n    }\n\n    static String formatMessage(int type, int extra) {\n        return \"Type: \" + type + \", \" + \"Extra: \" + extra;\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/MediaPlayerCreator.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\n\nclass MediaPlayerCreator {\n\n    MediaPlayer createMediaPlayer() {\n        return new MediaPlayer();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/MediaPlayerInformation.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.os.Build;\n\nimport com.novoda.noplayer.PlayerInformation;\nimport com.novoda.noplayer.PlayerType;\n\nclass MediaPlayerInformation implements PlayerInformation {\n\n    private final MediaPlayerTypeReader mediaPlayerTypeReader;\n\n    MediaPlayerInformation(MediaPlayerTypeReader mediaPlayerTypeReader) {\n        this.mediaPlayerTypeReader = mediaPlayerTypeReader;\n    }\n\n    @Override\n    public PlayerType getPlayerType() {\n        return PlayerType.MEDIA_PLAYER;\n    }\n\n    @Override\n    public String getVersion() {\n        return Build.VERSION.RELEASE;\n    }\n\n    @Override\n    public String getName() {\n        return \"MediaPlayer: \" + mediaPlayerTypeReader.getPlayerType().getName();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/MediaPlayerTypeReader.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.os.Build;\n\nclass MediaPlayerTypeReader {\n\n    private static final String PROP_USE_NU_PLAYER = \"media.stagefright.use-nuplayer\";\n    private static final String PROP_USE_AWESOME_PLAYER_PERSIST = \"persist.sys.media.use-awesome\";\n    private static final String PROP_USE_AWESOME_PLAYER_MEDIA = \"media.stagefright.use-awesome\";\n\n    private final int deviceOSVersion;\n    private final SystemProperties systemProperties;\n\n    MediaPlayerTypeReader(SystemProperties systemProperties, int deviceOSVersion) {\n        this.systemProperties = systemProperties;\n        this.deviceOSVersion = deviceOSVersion;\n    }\n\n    AndroidMediaPlayerType getPlayerType() {\n        AndroidMediaPlayerType playerType;\n        try {\n            playerType = getMediaPlayerType();\n        } catch (SystemProperties.MissingSystemPropertiesException e) {\n            playerType = AndroidMediaPlayerType.UNKNOWN;\n        }\n        return playerType;\n    }\n\n    private AndroidMediaPlayerType getMediaPlayerType() throws SystemProperties.MissingSystemPropertiesException {\n        return deviceOSVersion >= Build.VERSION_CODES.LOLLIPOP ? getPlayerTypeLollipop() : getPlayerTypePreLollipop();\n    }\n\n    private AndroidMediaPlayerType getPlayerTypeLollipop() throws SystemProperties.MissingSystemPropertiesException {\n        // NuPlayer is enabled if property is false or absent\n        // http://androidxref.com/5.0.0_r2/xref/frameworks/av/media/libmediaplayerservice/MediaPlayerFactory.cpp#63\n        boolean isAwesomePlayerEnabled = getBooleanProp(PROP_USE_AWESOME_PLAYER_PERSIST) || getBooleanProp(PROP_USE_AWESOME_PLAYER_MEDIA);\n        return isAwesomePlayerEnabled ? AndroidMediaPlayerType.AWESOME : AndroidMediaPlayerType.NU;\n    }\n\n    private AndroidMediaPlayerType getPlayerTypePreLollipop() throws SystemProperties.MissingSystemPropertiesException {\n        // NuPlayer is disabled if property is false or absent\n        // http://androidxref.com/4.4.4_r1/xref/frameworks/av/media/libmediaplayerservice/MediaPlayerFactory.cpp#63\n        return getBooleanProp(PROP_USE_NU_PLAYER) ? AndroidMediaPlayerType.NU : AndroidMediaPlayerType.AWESOME;\n    }\n\n    private boolean getBooleanProp(String prop) throws SystemProperties.MissingSystemPropertiesException {\n        String value = systemProperties.get(prop);\n        return \"true\".equalsIgnoreCase(value) || \"1\".equals(value);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/NoPlayerMediaPlayerCreator.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.os.Handler;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.Heart;\nimport com.novoda.noplayer.internal.SystemClock;\nimport com.novoda.noplayer.internal.listeners.PlayerListenersHolder;\nimport com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder;\nimport com.novoda.noplayer.model.LoadTimeout;\n\nimport java.util.HashMap;\n\npublic class NoPlayerMediaPlayerCreator {\n\n    private final InternalCreator internalCreator;\n\n    public static NoPlayerMediaPlayerCreator newInstance(Handler handler) {\n        InternalCreator internalCreator = new InternalCreator(handler);\n        return new NoPlayerMediaPlayerCreator(internalCreator);\n    }\n\n    NoPlayerMediaPlayerCreator(InternalCreator internalCreator) {\n        this.internalCreator = internalCreator;\n    }\n\n    public NoPlayer createMediaPlayer(Context context) {\n        AndroidMediaPlayerImpl player = internalCreator.create(context);\n        player.initialise();\n        return player;\n    }\n\n    static class InternalCreator {\n\n        private final Handler handler;\n\n        InternalCreator(Handler handler) {\n            this.handler = handler;\n        }\n\n        public AndroidMediaPlayerImpl create(Context context) {\n            LoadTimeout loadTimeout = new LoadTimeout(new SystemClock(), handler);\n            MediaPlayerForwarder forwarder = new MediaPlayerForwarder();\n            AndroidMediaPlayerFacade facade = AndroidMediaPlayerFacade.newInstance(context, forwarder);\n            PlayerListenersHolder listenersHolder = new PlayerListenersHolder();\n            CheckBufferHeartbeatCallback bufferHeartbeatCallback = new CheckBufferHeartbeatCallback();\n            Heart heart = Heart.newInstance(handler);\n            MediaPlayerTypeReader mediaPlayerTypeReader = new MediaPlayerTypeReader(new SystemProperties(), Build.VERSION.SDK_INT);\n            DelayedActionExecutor delayedActionExecutor = new DelayedActionExecutor(handler, new HashMap<DelayedActionExecutor.Action, Runnable>());\n            BuggyVideoDriverPreventer preventer = new BuggyVideoDriverPreventer(mediaPlayerTypeReader);\n            MediaPlayerInformation mediaPlayerInformation = new MediaPlayerInformation(mediaPlayerTypeReader);\n            return new AndroidMediaPlayerImpl(\n                    mediaPlayerInformation,\n                    facade,\n                    forwarder,\n                    listenersHolder,\n                    bufferHeartbeatCallback,\n                    loadTimeout,\n                    heart,\n                    delayedActionExecutor,\n                    preventer\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/NoPlayerTrackInfo.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\n\nclass NoPlayerTrackInfo {\n\n    private final MediaPlayer.TrackInfo trackInfo;\n\n    NoPlayerTrackInfo(MediaPlayer.TrackInfo trackInfo) {\n        this.trackInfo = trackInfo;\n    }\n\n    int type() {\n        return trackInfo.getTrackType();\n    }\n\n    String language() {\n        return trackInfo.getLanguage();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/NoPlayerTrackInfos.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport java.util.List;\n\nclass NoPlayerTrackInfos {\n\n    private final List<NoPlayerTrackInfo> trackInfos;\n\n    NoPlayerTrackInfos(List<NoPlayerTrackInfo> trackInfos) {\n        this.trackInfos = trackInfos;\n    }\n\n    NoPlayerTrackInfo get(int index) {\n        return trackInfos.get(index);\n    }\n\n    int size() {\n        return trackInfos.size();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        NoPlayerTrackInfos that = (NoPlayerTrackInfos) o;\n\n        return trackInfos != null ? trackInfos.equals(that.trackInfos) : that.trackInfos == null;\n    }\n\n    @Override\n    public int hashCode() {\n        return trackInfos != null ? trackInfos.hashCode() : 0;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/OnPotentialBuggyDriverLayoutListener.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.view.View;\n\nimport com.novoda.noplayer.NoPlayer;\n\nclass OnPotentialBuggyDriverLayoutListener implements View.OnLayoutChangeListener {\n\n    private final NoPlayer player;\n\n    OnPotentialBuggyDriverLayoutListener(NoPlayer player) {\n        this.player = player;\n    }\n\n    @SuppressWarnings(\"checkstyle:parameternumber\") // Checkstyle should not complain about interface methods. No way to workaround this.\n    @Override\n    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {\n        if (statusMightBeCorrupted()) {\n            forceAlignNativeMediaPlayerStatus();\n        }\n    }\n\n    private boolean statusMightBeCorrupted() {\n        return player.isPlaying();\n    }\n\n    private void forceAlignNativeMediaPlayerStatus() {\n        player.play();\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/PlaybackStateChecker.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\n\nimport static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.ERROR;\nimport static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.IDLE;\nimport static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.PREPARING;\n\nclass PlaybackStateChecker {\n\n    boolean isPlaying(MediaPlayer mediaPlayer, PlaybackState playbackState) {\n        return isInPlaybackState(mediaPlayer, playbackState) && mediaPlayer.isPlaying();\n    }\n\n    boolean isInPlaybackState(MediaPlayer mediaPlayer, PlaybackState playbackState) {\n        return mediaPlayer != null\n                && playbackState != ERROR\n                && playbackState != IDLE\n                && playbackState != PREPARING;\n    }\n\n    enum PlaybackState {\n\n        ERROR,\n        IDLE,\n        PREPARING,\n        PREPARED,\n        PLAYING,\n        PAUSED,\n        COMPLETED\n\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/SystemProperties.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.annotation.SuppressLint;\n\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\nclass SystemProperties {\n\n    private static final String SYSTEM_PROPERTIES_CLASS = \"android.os.SystemProperties\";\n    private static final String SYSTEM_PROPERTIES_METHOD_GET = \"get\";\n\n    private static final Object STATIC_CLASS_INSTANCE = null;\n\n    @SuppressLint(\"PrivateApi\") // This method uses reflection to call android.os.SystemProperties.get(String) since the class is hidden\n    String get(String key) throws MissingSystemPropertiesException {\n        try {\n            Class<?> systemProperties = Class.forName(SYSTEM_PROPERTIES_CLASS);\n            Method getMethod = systemProperties.getMethod(SYSTEM_PROPERTIES_METHOD_GET, String.class);\n            return (String) getMethod.invoke(STATIC_CLASS_INSTANCE, key);\n        } catch (ClassNotFoundException e) {\n            throw new MissingSystemPropertiesException(e);\n        } catch (NoSuchMethodException e) {\n            throw new MissingSystemPropertiesException(e);\n        } catch (InvocationTargetException e) {\n            throw new MissingSystemPropertiesException(e);\n        } catch (IllegalAccessException e) {\n            throw new MissingSystemPropertiesException(e);\n        }\n    }\n\n    static class MissingSystemPropertiesException extends Exception {\n        MissingSystemPropertiesException(Exception e) {\n            super(e);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/TrackInfosFactory.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nclass TrackInfosFactory {\n\n    NoPlayerTrackInfos createFrom(MediaPlayer mediaPlayer) {\n        MediaPlayer.TrackInfo[] mediaPlayerTrackInfos = mediaPlayer.getTrackInfo();\n\n        List<NoPlayerTrackInfo> trackInfos = new ArrayList<>(mediaPlayerTrackInfos.length);\n        for (MediaPlayer.TrackInfo mediaPlayerTrackInfo : mediaPlayerTrackInfos) {\n            trackInfos.add(new NoPlayerTrackInfo(mediaPlayerTrackInfo));\n        }\n\n        return new NoPlayerTrackInfos(trackInfos);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/BufferHeartbeatListener.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.mediaplayer.CheckBufferHeartbeatCallback;\n\nclass BufferHeartbeatListener implements CheckBufferHeartbeatCallback.BufferListener {\n\n    private final NoPlayer.BufferStateListener bufferStateListener;\n\n    BufferHeartbeatListener(NoPlayer.BufferStateListener bufferStateListener) {\n        this.bufferStateListener = bufferStateListener;\n    }\n\n    @Override\n    public void onBufferStart() {\n        bufferStateListener.onBufferStarted();\n    }\n\n    @Override\n    public void onBufferComplete() {\n        bufferStateListener.onBufferCompleted();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/BufferInfoForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.mediaplayer.CheckBufferHeartbeatCallback;\n\nimport java.util.HashMap;\n\nclass BufferInfoForwarder implements CheckBufferHeartbeatCallback.BufferListener {\n\n    private final NoPlayer.InfoListener infoListener;\n\n    BufferInfoForwarder(NoPlayer.InfoListener infoListener) {\n        this.infoListener = infoListener;\n    }\n\n    @Override\n    public void onBufferStart() {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        infoListener.onNewInfo(\"onBufferStart\", callingMethodParameters);\n    }\n\n    @Override\n    public void onBufferComplete() {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        infoListener.onNewInfo(\"onBufferStart\", callingMethodParameters);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/BufferOnPreparedListener.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\n\nclass BufferOnPreparedListener implements MediaPlayer.OnPreparedListener {\n\n    private final NoPlayer.BufferStateListener bufferStateListener;\n\n    BufferOnPreparedListener(NoPlayer.BufferStateListener bufferStateListener) {\n        this.bufferStateListener = bufferStateListener;\n    }\n\n    @Override\n    public void onPrepared(MediaPlayer mp) {\n        bufferStateListener.onBufferCompleted();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/CompletionForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\n\nclass CompletionForwarder implements MediaPlayer.OnCompletionListener {\n\n    private final NoPlayer.CompletionListener completionListener;\n\n    CompletionForwarder(NoPlayer.CompletionListener completionListener) {\n        this.completionListener = completionListener;\n    }\n\n    @Override\n    public void onCompletion(MediaPlayer mp) {\n        completionListener.onCompletion();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/CompletionInfoForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.HashMap;\n\nclass CompletionInfoForwarder implements MediaPlayer.OnCompletionListener {\n\n    private final NoPlayer.InfoListener infoListener;\n\n    CompletionInfoForwarder(NoPlayer.InfoListener infoListener) {\n        this.infoListener = infoListener;\n    }\n\n    @Override\n    public void onCompletion(MediaPlayer mp) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(\"mp\", String.valueOf(mp));\n\n        infoListener.onNewInfo(\"onCompletion\", callingMethodParameters);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/CompletionStateChangedForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\n\nclass CompletionStateChangedForwarder implements MediaPlayer.OnCompletionListener {\n\n    private final NoPlayer.StateChangedListener stateChangedListener;\n\n    CompletionStateChangedForwarder(NoPlayer.StateChangedListener stateChangedListener) {\n        this.stateChangedListener = stateChangedListener;\n    }\n\n    @Override\n    public void onCompletion(MediaPlayer mp) {\n        stateChangedListener.onVideoStopped();\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/ErrorForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.mediaplayer.ErrorFactory;\n\nclass ErrorForwarder implements MediaPlayer.OnErrorListener {\n\n    private final NoPlayer.BufferStateListener bufferStateListener;\n    private final NoPlayer.ErrorListener errorListener;\n\n    ErrorForwarder(NoPlayer.BufferStateListener bufferStateListener, NoPlayer.ErrorListener errorListener) {\n        this.bufferStateListener = bufferStateListener;\n        this.errorListener = errorListener;\n    }\n\n    @Override\n    public boolean onError(MediaPlayer mp, int what, int extra) {\n        bufferStateListener.onBufferCompleted();\n        NoPlayer.PlayerError error = ErrorFactory.createErrorFrom(what, extra);\n        errorListener.onError(error);\n        return true;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/ErrorInfoForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.HashMap;\n\nclass ErrorInfoForwarder implements MediaPlayer.OnErrorListener {\n\n    private final NoPlayer.InfoListener infoListener;\n\n    ErrorInfoForwarder(NoPlayer.InfoListener infoListener) {\n        this.infoListener = infoListener;\n    }\n\n    @Override\n    public boolean onError(MediaPlayer mp, int what, int extra) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(\"mp\", String.valueOf(mp));\n        callingMethodParameters.put(\"what\", String.valueOf(what));\n        callingMethodParameters.put(\"extra\", String.valueOf(extra));\n\n        infoListener.onNewInfo(\"onError\", callingMethodParameters);\n        return false;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/HeartBeatListener.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport com.novoda.noplayer.internal.mediaplayer.CheckBufferHeartbeatCallback;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\nclass HeartBeatListener implements CheckBufferHeartbeatCallback.BufferListener {\n\n    private final List<CheckBufferHeartbeatCallback.BufferListener> listeners = new CopyOnWriteArrayList<>();\n\n    void add(CheckBufferHeartbeatCallback.BufferListener listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public void onBufferStart() {\n        for (CheckBufferHeartbeatCallback.BufferListener listener : listeners) {\n            listener.onBufferStart();\n        }\n    }\n\n    @Override\n    public void onBufferComplete() {\n        for (CheckBufferHeartbeatCallback.BufferListener listener : listeners) {\n            listener.onBufferComplete();\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/MediaPlayerCompletionListener.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\nclass MediaPlayerCompletionListener implements MediaPlayer.OnCompletionListener {\n\n    private final List<MediaPlayer.OnCompletionListener> listeners = new CopyOnWriteArrayList<>();\n\n    void add(MediaPlayer.OnCompletionListener listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public void onCompletion(MediaPlayer mp) {\n        for (MediaPlayer.OnCompletionListener listener : listeners) {\n            listener.onCompletion(mp);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/MediaPlayerErrorListener.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\nclass MediaPlayerErrorListener implements MediaPlayer.OnErrorListener {\n\n    private final List<MediaPlayer.OnErrorListener> listeners = new CopyOnWriteArrayList<>();\n\n    void add(MediaPlayer.OnErrorListener listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public boolean onError(MediaPlayer mp, int what, int extra) {\n        boolean handled = false;\n        for (MediaPlayer.OnErrorListener listener : listeners) {\n            handled = listener.onError(mp, what, extra) || handled;\n        }\n        return handled;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/MediaPlayerForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.PlayerState;\nimport com.novoda.noplayer.internal.mediaplayer.CheckBufferHeartbeatCallback;\n\npublic class MediaPlayerForwarder {\n\n    private final MediaPlayerPreparedListener preparedListener;\n    private final HeartBeatListener heartBeatListener;\n    private final MediaPlayerCompletionListener completionListener;\n    private final MediaPlayerErrorListener errorListener;\n    private final VideoSizeChangedListener videoSizeChangedListener;\n\n    public MediaPlayerForwarder() {\n        preparedListener = new MediaPlayerPreparedListener();\n        heartBeatListener = new HeartBeatListener();\n        completionListener = new MediaPlayerCompletionListener();\n        errorListener = new MediaPlayerErrorListener();\n        videoSizeChangedListener = new VideoSizeChangedListener();\n    }\n\n    public void bind(NoPlayer.PreparedListener preparedListener, PlayerState playerState) {\n        this.preparedListener.add(new OnPreparedForwarder(preparedListener, playerState));\n    }\n\n    public void bind(NoPlayer.BufferStateListener bufferStateListener, NoPlayer.ErrorListener errorListener) {\n        preparedListener.add(new BufferOnPreparedListener(bufferStateListener));\n        heartBeatListener.add(new BufferHeartbeatListener(bufferStateListener));\n        this.errorListener.add(new ErrorForwarder(bufferStateListener, errorListener));\n    }\n\n    public void bind(NoPlayer.CompletionListener completionListener, NoPlayer.StateChangedListener stateChangedListener) {\n        this.completionListener.add(new CompletionForwarder(completionListener));\n        this.completionListener.add(new CompletionStateChangedForwarder(stateChangedListener));\n    }\n\n    public void bind(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) {\n        this.videoSizeChangedListener.add(new VideoSizeChangedForwarder(videoSizeChangedListener));\n    }\n\n    public void bind(NoPlayer.InfoListener infoListener) {\n        preparedListener.add(new OnPreparedInfoForwarder(infoListener));\n        heartBeatListener.add(new BufferInfoForwarder(infoListener));\n        completionListener.add(new CompletionInfoForwarder(infoListener));\n        errorListener.add(new ErrorInfoForwarder(infoListener));\n        videoSizeChangedListener.add(new VideoSizeChangedInfoForwarder(infoListener));\n    }\n\n    public MediaPlayer.OnPreparedListener onPreparedListener() {\n        return preparedListener;\n    }\n\n    public CheckBufferHeartbeatCallback.BufferListener onHeartbeatListener() {\n        return heartBeatListener;\n    }\n\n    public MediaPlayer.OnCompletionListener onCompletionListener() {\n        return completionListener;\n    }\n\n    public MediaPlayer.OnErrorListener onErrorListener() {\n        return errorListener;\n    }\n\n    public MediaPlayer.OnVideoSizeChangedListener onSizeChangedListener() {\n        return videoSizeChangedListener;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/MediaPlayerPreparedListener.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\nclass MediaPlayerPreparedListener implements MediaPlayer.OnPreparedListener {\n\n    private final List<MediaPlayer.OnPreparedListener> listeners = new CopyOnWriteArrayList<>();\n\n    void add(MediaPlayer.OnPreparedListener listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public void onPrepared(MediaPlayer mp) {\n        for (MediaPlayer.OnPreparedListener listener : listeners) {\n            listener.onPrepared(mp);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/OnPreparedForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.PlayerState;\n\nclass OnPreparedForwarder implements MediaPlayer.OnPreparedListener {\n\n    private final NoPlayer.PreparedListener preparedListener;\n    private final PlayerState playerState;\n\n    OnPreparedForwarder(NoPlayer.PreparedListener preparedListener, PlayerState playerState) {\n        this.preparedListener = preparedListener;\n        this.playerState = playerState;\n    }\n\n    @Override\n    public void onPrepared(MediaPlayer mp) {\n        preparedListener.onPrepared(playerState);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/OnPreparedInfoForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.HashMap;\n\nclass OnPreparedInfoForwarder implements MediaPlayer.OnPreparedListener {\n\n    private final NoPlayer.InfoListener infoListener;\n\n    OnPreparedInfoForwarder(NoPlayer.InfoListener infoListener) {\n        this.infoListener = infoListener;\n    }\n\n    @Override\n    public void onPrepared(MediaPlayer mp) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(\"mp\", String.valueOf(mp));\n\n        infoListener.onNewInfo(\"onPrepared\", callingMethodParameters);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/VideoSizeChangedForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.utils.NoPlayerLog;\n\nclass VideoSizeChangedForwarder implements MediaPlayer.OnVideoSizeChangedListener {\n\n    private final NoPlayer.VideoSizeChangedListener videoSizeChangedListener;\n\n    private int previousWidth;\n    private int previousHeight;\n\n    VideoSizeChangedForwarder(NoPlayer.VideoSizeChangedListener videoSizeChangedListener) {\n        this.videoSizeChangedListener = videoSizeChangedListener;\n    }\n\n    @Override\n    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {\n        if (bothDimensionsHaveChanged(width, height)) {\n            videoSizeChangedListener.onVideoSizeChanged(width, height, 0, 1);\n        } else {\n            NoPlayerLog.w(\"Video size changed but we have swallowed the event due to only 1 dimension changing\");\n        }\n        previousWidth = width;\n        previousHeight = height;\n    }\n\n    private boolean bothDimensionsHaveChanged(int width, int height) {\n        boolean widthHasChanged = width != previousWidth;\n        boolean heightHasChanged = height != previousHeight;\n        return widthHasChanged && heightHasChanged;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/VideoSizeChangedInfoForwarder.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport java.util.HashMap;\n\nclass VideoSizeChangedInfoForwarder implements MediaPlayer.OnVideoSizeChangedListener {\n\n    private final NoPlayer.InfoListener infoListener;\n\n    VideoSizeChangedInfoForwarder(NoPlayer.InfoListener infoListener) {\n        this.infoListener = infoListener;\n    }\n\n    @Override\n    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {\n        HashMap<String, String> callingMethodParameters = new HashMap<>();\n\n        callingMethodParameters.put(\"mp\", String.valueOf(mp));\n        callingMethodParameters.put(\"width\", String.valueOf(width));\n        callingMethodParameters.put(\"height\", String.valueOf(height));\n\n        infoListener.onNewInfo(\"onVideoSizeChanged\", callingMethodParameters);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/mediaplayer/forwarder/VideoSizeChangedListener.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer.forwarder;\n\nimport android.media.MediaPlayer;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\npublic class VideoSizeChangedListener implements MediaPlayer.OnVideoSizeChangedListener {\n\n    private final List<MediaPlayer.OnVideoSizeChangedListener> listeners = new CopyOnWriteArrayList<>();\n\n    public void add(MediaPlayer.OnVideoSizeChangedListener listener) {\n        listeners.add(listener);\n    }\n\n    @Override\n    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {\n        for (MediaPlayer.OnVideoSizeChangedListener listener : listeners) {\n            listener.onVideoSizeChanged(mp, width, height);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/utils/AndroidDeviceVersion.java",
    "content": "package com.novoda.noplayer.internal.utils;\n\nimport android.os.Build;\n\npublic class AndroidDeviceVersion {\n\n    private final int sdkInt;\n\n    public static AndroidDeviceVersion newInstance() {\n        return new AndroidDeviceVersion(Build.VERSION.SDK_INT);\n    }\n\n    public AndroidDeviceVersion(int sdkInt) {\n        this.sdkInt = sdkInt;\n    }\n\n    public boolean isJellyBeanEighteenOrAbove() {\n        return sdkInt >= Build.VERSION_CODES.JELLY_BEAN_MR2;\n    }\n\n    public boolean isLollipopTwentyOneOrAbove() {\n        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;\n    }\n\n    public int sdkInt() {\n        return sdkInt;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/utils/NoPlayerLog.java",
    "content": "package com.novoda.noplayer.internal.utils;\n\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.Locale;\n\n@SuppressWarnings(\"PMD.ShortMethodName\")    // This is a logger class, the logging methods are 1-letter\npublic final class NoPlayerLog {\n\n    private static final String TAG = \"No-Player\";\n    private static final int DEPTH = 5;\n    private static final int CLASS_SUFFIX = 5;\n    private static final String DETAILED_LOG_TEMPLATE = \"[%s][%s.%s:%d] %s\";\n\n    private static boolean isEnabled = true;\n\n    private NoPlayerLog() {\n        // Not instantiable\n    }\n\n    public static void setLoggingEnabled(boolean enabled) {\n        isEnabled = enabled;\n    }\n\n    private static String logMessage(String message, Throwable throwable) {\n        StringBuilder detailedMessage = new StringBuilder(getDetailedLog(message));\n        if (throwable != null) {\n            detailedMessage.append('\\n').append(getStackTraceString(throwable));\n        }\n        return detailedMessage.toString();\n    }\n\n    private static String getDetailedLog(String message) {\n        Thread current = Thread.currentThread();\n        final StackTraceElement trace = current.getStackTrace()[DEPTH];\n        final String filename = trace.getFileName();\n        return String.format(Locale.US,\n                DETAILED_LOG_TEMPLATE,\n                current.getName(),\n                filename.substring(0, filename.length() - CLASS_SUFFIX),\n                trace.getMethodName(),\n                trace.getLineNumber(),\n                message);\n    }\n\n    private static String getStackTraceString(Throwable throwable) {\n        StringWriter sw = new StringWriter();\n        PrintWriter pw = new PrintWriter(sw);\n        try {\n            throwable.printStackTrace(pw);\n            return sw.toString().trim();\n        } finally {\n            pw.close();\n        }\n    }\n\n    public static void d(String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.d(TAG, logMessage(msg, null));\n    }\n\n    public static void d(Throwable throwable, String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.d(TAG, logMessage(msg, throwable));\n    }\n\n    public static void e(String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.e(TAG, logMessage(msg, null));\n    }\n\n    public static void e(Throwable throwable, String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.e(TAG, logMessage(msg, throwable));\n    }\n\n    public static void i(String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.i(TAG, logMessage(msg, null));\n    }\n\n    public static void i(Throwable throwable, String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.i(TAG, logMessage(msg, throwable));\n    }\n\n    public static void v(String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.v(TAG, logMessage(msg, null));\n    }\n\n    public static void v(Throwable throwable, String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.v(TAG, logMessage(msg, throwable));\n    }\n\n    public static void w(String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.w(TAG, logMessage(msg, null));\n    }\n\n    public static void w(Throwable throwable, String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.w(TAG, logMessage(msg, throwable));\n    }\n\n    public static void wtf(String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.wtf(TAG, logMessage(msg, null));\n    }\n\n    public static void wtf(Throwable throwable) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.wtf(TAG, logMessage(\"\", throwable));\n    }\n\n    public static void wtf(Throwable throwable, String msg) {\n        if (!isEnabled) {\n            return;\n        }\n        android.util.Log.wtf(TAG, logMessage(msg, throwable));\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/internal/utils/Optional.java",
    "content": "package com.novoda.noplayer.internal.utils;\n\npublic final class Optional<T> {\n\n    @SuppressWarnings(\"unchecked\")  // Type erasure has us covered here, we don't care\n    private static final Optional ABSENT = new Optional(null);\n\n    private final T data;\n\n    @SuppressWarnings(\"unchecked\")  // Type erasure has us covered here, we don't care\n    public static <T> Optional<T> absent() {\n        return ABSENT;\n    }\n\n    public static <T> Optional<T> fromNullable(T data) {\n        if (data == null) {\n            return absent();\n        }\n        return new Optional<>(data);\n    }\n\n    public static <T> Optional<T> of(T data) {\n        if (data == null) {\n            throw new IllegalArgumentException(\"Data cannot be null. Use Optional.fromNullable(maybeNullData).\");\n        }\n        return new Optional<>(data);\n    }\n\n    private Optional(T data) {\n        this.data = data;\n    }\n\n    public boolean isPresent() {\n        return data != null;\n    }\n\n    public boolean isAbsent() {\n        return !isPresent();\n    }\n\n    public T get() {\n        if (!isPresent()) {\n            throw new IllegalStateException(\"You must check if data is present before using get()\");\n        }\n        return data;\n    }\n\n    public T or(T elseCase) {\n        return isPresent() ? get() : elseCase;\n    }\n\n    public Optional<T> or(Optional<T> elseCase) {\n        return isPresent() ? this : elseCase;\n    }\n\n    public Optional<T> or(Func0<Optional<T>> elseFunc) {\n        return isPresent() ? this : elseFunc.call();\n    }\n\n    public interface Func0<V> {\n\n        V call();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        Optional<?> optional = (Optional<?>) o;\n\n        return data != null ? data.equals(optional.data) : optional.data == null;\n    }\n\n    @Override\n    public int hashCode() {\n        return data != null ? data.hashCode() : 0;\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"Optional<%s>\", isAbsent() ? \"Absent\" : data.toString());\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/AudioTracks.java",
    "content": "package com.novoda.noplayer.model;\n\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\n\npublic final class AudioTracks implements Iterable<PlayerAudioTrack> {\n\n    private final List<PlayerAudioTrack> playerAudioTracks;\n\n    public static AudioTracks from(List<PlayerAudioTrack> audioTracks) {\n        return new AudioTracks(Collections.unmodifiableList(audioTracks));\n    }\n\n    private AudioTracks(List<PlayerAudioTrack> playerAudioTracks) {\n        this.playerAudioTracks = playerAudioTracks;\n    }\n\n    public PlayerAudioTrack get(int index) {\n        return playerAudioTracks.get(index);\n    }\n\n    public int size() {\n        return playerAudioTracks.size();\n    }\n\n    @Override\n    public Iterator<PlayerAudioTrack> iterator() {\n        return playerAudioTracks.iterator();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        AudioTracks that = (AudioTracks) o;\n\n        return playerAudioTracks != null ? playerAudioTracks.equals(that.playerAudioTracks) : that.playerAudioTracks == null;\n    }\n\n    @Override\n    public int hashCode() {\n        return playerAudioTracks != null ? playerAudioTracks.hashCode() : 0;\n    }\n\n    @Override\n    public String toString() {\n        return \"AudioTracks{playerAudioTracks=\" + playerAudioTracks + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/Bitrate.java",
    "content": "package com.novoda.noplayer.model;\n\npublic final class Bitrate {\n\n    private static final int KILOBIT = 1000;\n\n    private final long bitsPerSecond;\n\n    public static Bitrate fromBitsPerSecond(long bitsPerSecond) {\n        return new Bitrate(bitsPerSecond);\n    }\n\n    private Bitrate(long bitsPerSecond) {\n        this.bitsPerSecond = bitsPerSecond;\n    }\n\n    public long asKilobits() {\n        return bitsPerSecond / KILOBIT;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        Bitrate bitrate = (Bitrate) o;\n\n        return bitsPerSecond == bitrate.bitsPerSecond;\n    }\n\n    @Override\n    public int hashCode() {\n        return (int) (bitsPerSecond ^ (bitsPerSecond >>> 32));\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/Either.java",
    "content": "package com.novoda.noplayer.model;\n\npublic abstract class Either<L, R> {\n\n    public static <L, R> Either<L, R> left(L left) {\n        return new Left<>(left);\n    }\n\n    public static <L, R> Either<L, R> right(R right) {\n        return new Right<>(right);\n    }\n\n    Either() {\n        // restrict subclasses to the package\n    }\n\n    public abstract void apply(Consumer<L> leftConsumer, Consumer<R> rightConsumer);\n\n    static class Left<L, R> extends Either<L, R> {\n\n        private final L valueLeft;\n\n        Left(L valueLeft) {\n            this.valueLeft = valueLeft;\n        }\n\n        @Override\n        public void apply(Consumer<L> leftConsumer, Consumer<R> rightConsumer) {\n            leftConsumer.accept(valueLeft);\n        }\n    }\n\n    static class Right<L, R> extends Either<L, R> {\n\n        private final R valueRight;\n\n        Right(R valueRight) {\n            this.valueRight = valueRight;\n        }\n\n        @Override\n        public void apply(Consumer<L> leftConsumer, Consumer<R> rightConsumer) {\n            rightConsumer.accept(valueRight);\n        }\n    }\n\n    public interface Consumer<T> {\n        void accept(T value);\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/KeySetId.java",
    "content": "package com.novoda.noplayer.model;\n\nimport java.util.Arrays;\n\npublic final class KeySetId {\n\n    private final byte[] keySetIdBytes;\n\n    public static KeySetId of(byte[] sessionId) {\n        return new KeySetId(Arrays.copyOf(sessionId, sessionId.length));\n    }\n\n    @SuppressWarnings(\"PMD.ArrayIsStoredDirectly\")  // This array can only come from the factory method which does defensive copy\n    private KeySetId(byte[] keySetIdBytes) {\n        this.keySetIdBytes = keySetIdBytes;\n    }\n\n    public byte[] asBytes() {\n        return Arrays.copyOf(keySetIdBytes, keySetIdBytes.length);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        KeySetId sessionId1 = (KeySetId) o;\n\n        return Arrays.equals(keySetIdBytes, sessionId1.keySetIdBytes);\n    }\n\n    @Override\n    public int hashCode() {\n        return Arrays.hashCode(keySetIdBytes);\n    }\n\n    @Override\n    public String toString() {\n        return \"KeySetId{keySetIdBytes=\" + Arrays.toString(keySetIdBytes) + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/LoadTimeout.java",
    "content": "package com.novoda.noplayer.model;\n\nimport android.os.Handler;\n\nimport com.novoda.noplayer.internal.Clock;\nimport com.novoda.noplayer.NoPlayer;\n\npublic class LoadTimeout {\n\n    private static final int DELAY_MILLIS = 1000;\n\n    private final Clock clock;\n    private final Handler handler;\n\n    private long startTime;\n    private long endTime;\n    private NoPlayer.LoadTimeoutCallback loadTimeoutCallback;\n\n    public LoadTimeout(Clock clock, Handler handler) {\n        this.clock = clock;\n        this.handler = handler;\n    }\n\n    public void start(Timeout timeout, NoPlayer.LoadTimeoutCallback loadTimeoutCallback) {\n        cancel();\n        this.loadTimeoutCallback = loadTimeoutCallback;\n        startTime = clock.getCurrentTime();\n        endTime = startTime + timeout.inMillis();\n        handler.post(loadTimeoutCheck);\n    }\n\n    private final Runnable loadTimeoutCheck = new Runnable() {\n        @Override\n        public void run() {\n            if (clock.getCurrentTime() >= endTime) {\n                loadTimeoutCallback.onLoadTimeout();\n                cancel();\n            } else {\n                handler.postDelayed(this, DELAY_MILLIS);\n            }\n        }\n    };\n\n    public void cancel() {\n        startTime = 0;\n        loadTimeoutCallback = NoPlayer.LoadTimeoutCallback.NULL_IMPL;\n        handler.removeCallbacks(loadTimeoutCheck);\n    }\n\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/NoPlayerCue.java",
    "content": "package com.novoda.noplayer.model;\n\nimport android.graphics.Bitmap;\nimport android.text.Layout.Alignment;\n\npublic class NoPlayerCue {\n\n    private final CharSequence text;\n    private final Alignment textAlignment;\n    private final Bitmap bitmap;\n    private final float line;\n    private final int lineType;\n    private final int lineAnchor;\n    private final float position;\n    private final int positionAnchor;\n    private final float size;\n    private final float bitmapHeight;\n    private final boolean windowColorSet;\n    private final int windowColor;\n\n    @SuppressWarnings({\"checkstyle:ParameterNumber\", \"PMD.ExcessiveParameterList\"})     // TODO group parameters into classes\n    public NoPlayerCue(CharSequence text,\n                       Alignment textAlignment,\n                       Bitmap bitmap,\n                       float line,\n                       int lineType,\n                       int lineAnchor,\n                       float position,\n                       int positionAnchor,\n                       float size,\n                       float bitmapHeight,\n                       boolean windowColorSet,\n                       int windowColor) {\n        this.text = text;\n        this.textAlignment = textAlignment;\n        this.bitmap = bitmap;\n        this.line = line;\n        this.lineType = lineType;\n        this.lineAnchor = lineAnchor;\n        this.position = position;\n        this.positionAnchor = positionAnchor;\n        this.size = size;\n        this.bitmapHeight = bitmapHeight;\n        this.windowColorSet = windowColorSet;\n        this.windowColor = windowColor;\n    }\n\n    public CharSequence text() {\n        return text;\n    }\n\n    public Alignment textAlignment() {\n        return textAlignment;\n    }\n\n    public Bitmap bitmap() {\n        return bitmap;\n    }\n\n    public float line() {\n        return line;\n    }\n\n    public int lineType() {\n        return lineType;\n    }\n\n    public int lineAnchor() {\n        return lineAnchor;\n    }\n\n    public float position() {\n        return position;\n    }\n\n    public int positionAnchor() {\n        return positionAnchor;\n    }\n\n    public float size() {\n        return size;\n    }\n\n    public float bitmapHeight() {\n        return bitmapHeight;\n    }\n\n    public boolean windowColorSet() {\n        return windowColorSet;\n    }\n\n    public int windowColor() {\n        return windowColor;\n    }\n\n    @Override\n    public String toString() {\n        return \"NoPlayerCue{\"\n                + \"text=\" + text\n                + \", textAlignment=\" + textAlignment\n                + \", bitmap=\" + bitmap\n                + \", line=\" + line\n                + \", lineType=\" + lineType\n                + \", lineAnchor=\" + lineAnchor\n                + \", position=\" + position\n                + \", positionAnchor=\" + positionAnchor\n                + \", size=\" + size\n                + \", bitmapHeight=\" + bitmapHeight\n                + \", windowColorSet=\" + windowColorSet\n                + \", windowColor=\" + windowColor\n                + '}';\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        NoPlayerCue that = (NoPlayerCue) o;\n\n        if (Float.compare(that.line, line) != 0) {\n            return false;\n        }\n        if (lineType != that.lineType) {\n            return false;\n        }\n        if (lineAnchor != that.lineAnchor) {\n            return false;\n        }\n        if (Float.compare(that.position, position) != 0) {\n            return false;\n        }\n        if (positionAnchor != that.positionAnchor) {\n            return false;\n        }\n        if (Float.compare(that.size, size) != 0) {\n            return false;\n        }\n        if (Float.compare(that.bitmapHeight, bitmapHeight) != 0) {\n            return false;\n        }\n        if (windowColorSet != that.windowColorSet) {\n            return false;\n        }\n        if (windowColor != that.windowColor) {\n            return false;\n        }\n        if (text != null ? !text.equals(that.text) : that.text != null) {\n            return false;\n        }\n        if (textAlignment != that.textAlignment) {\n            return false;\n        }\n        return bitmap != null ? bitmap.equals(that.bitmap) : that.bitmap == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = text != null ? text.hashCode() : 0;\n        result = 31 * result + (textAlignment != null ? textAlignment.hashCode() : 0);\n        result = 31 * result + (bitmap != null ? bitmap.hashCode() : 0);\n        result = 31 * result + (line != +0.0f ? Float.floatToIntBits(line) : 0);\n        result = 31 * result + lineType;\n        result = 31 * result + lineAnchor;\n        result = 31 * result + (position != +0.0f ? Float.floatToIntBits(position) : 0);\n        result = 31 * result + positionAnchor;\n        result = 31 * result + (size != +0.0f ? Float.floatToIntBits(size) : 0);\n        result = 31 * result + (bitmapHeight != +0.0f ? Float.floatToIntBits(bitmapHeight) : 0);\n        result = 31 * result + (windowColorSet ? 1 : 0);\n        result = 31 * result + windowColor;\n        return result;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/PlayerAudioTrack.java",
    "content": "package com.novoda.noplayer.model;\n\nimport com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType;\n\npublic class PlayerAudioTrack {\n\n    private final int groupIndex;\n    private final int formatIndex;\n    private final String trackId;\n    private final String language;\n    private final String mimeType;\n    private final int numberOfChannels;\n    private final int frequency;\n    private final AudioTrackType audioTrackType;\n\n    @SuppressWarnings(\"checkstyle:ParameterNumber\") // TODO group parameters into classes\n    public PlayerAudioTrack(int groupIndex,\n                            int formatIndex,\n                            String trackId,\n                            String language,\n                            String mimeType,\n                            int numberOfChannels,\n                            int frequency,\n                            AudioTrackType audioTrackType) {\n        this.groupIndex = groupIndex;\n        this.formatIndex = formatIndex;\n        this.trackId = trackId;\n        this.language = language;\n        this.mimeType = mimeType;\n        this.numberOfChannels = numberOfChannels;\n        this.frequency = frequency;\n        this.audioTrackType = audioTrackType;\n    }\n\n    public int groupIndex() {\n        return groupIndex;\n    }\n\n    public int formatIndex() {\n        return formatIndex;\n    }\n\n    public String trackId() {\n        return trackId;\n    }\n\n    public String language() {\n        return language;\n    }\n\n    public String mimeType() {\n        return mimeType;\n    }\n\n    public int numberOfChannels() {\n        return numberOfChannels;\n    }\n\n    public int frequency() {\n        return frequency;\n    }\n\n    public AudioTrackType audioTrackType() {\n        return audioTrackType;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        PlayerAudioTrack that = (PlayerAudioTrack) o;\n\n        if (groupIndex != that.groupIndex) {\n            return false;\n        }\n        if (formatIndex != that.formatIndex) {\n            return false;\n        }\n        if (numberOfChannels != that.numberOfChannels) {\n            return false;\n        }\n        if (frequency != that.frequency) {\n            return false;\n        }\n        if (trackId != null ? !trackId.equals(that.trackId) : that.trackId != null) {\n            return false;\n        }\n        if (language != null ? !language.equals(that.language) : that.language != null) {\n            return false;\n        }\n        if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) {\n            return false;\n        }\n        return audioTrackType == that.audioTrackType;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = groupIndex;\n        result = 31 * result + formatIndex;\n        result = 31 * result + (trackId != null ? trackId.hashCode() : 0);\n        result = 31 * result + (language != null ? language.hashCode() : 0);\n        result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);\n        result = 31 * result + numberOfChannels;\n        result = 31 * result + frequency;\n        result = 31 * result + (audioTrackType != null ? audioTrackType.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"PlayerAudioTrack{\"\n                + \"groupIndex=\" + groupIndex\n                + \", formatIndex=\" + formatIndex\n                + \", trackId='\" + trackId + '\\''\n                + \", language='\" + language + '\\''\n                + \", mimeType='\" + mimeType + '\\''\n                + \", numberOfChannels=\" + numberOfChannels\n                + \", frequency=\" + frequency\n                + \", audioTrackType=\" + audioTrackType\n                + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/PlayerSubtitleTrack.java",
    "content": "package com.novoda.noplayer.model;\n\npublic class PlayerSubtitleTrack {\n\n    private final int groupIndex;\n    private final int formatIndex;\n    private final String trackId;\n    private final String language;\n    private final String mimeType;\n    private final int numberOfChannels;\n    private final int frequency;\n\n    public PlayerSubtitleTrack(int groupIndex,\n                               int formatIndex,\n                               String trackId,\n                               String language,\n                               String mimeType,\n                               int numberOfChannels,\n                               int frequency) {\n        this.groupIndex = groupIndex;\n        this.formatIndex = formatIndex;\n        this.trackId = trackId;\n        this.language = language;\n        this.mimeType = mimeType;\n        this.numberOfChannels = numberOfChannels;\n        this.frequency = frequency;\n    }\n\n    public int groupIndex() {\n        return groupIndex;\n    }\n\n    public int formatIndex() {\n        return formatIndex;\n    }\n\n    public String trackId() {\n        return trackId;\n    }\n\n    public String language() {\n        return language;\n    }\n\n    public String mimeType() {\n        return mimeType;\n    }\n\n    public int numberOfChannels() {\n        return numberOfChannels;\n    }\n\n    public int frequency() {\n        return frequency;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (!(o instanceof PlayerSubtitleTrack)) {\n            return false;\n        }\n\n        PlayerSubtitleTrack that = (PlayerSubtitleTrack) o;\n\n        if (groupIndex != that.groupIndex) {\n            return false;\n        }\n        if (formatIndex != that.formatIndex) {\n            return false;\n        }\n        if (numberOfChannels != that.numberOfChannels) {\n            return false;\n        }\n        if (frequency != that.frequency) {\n            return false;\n        }\n        if (trackId != null ? !trackId.equals(that.trackId) : that.trackId != null) {\n            return false;\n        }\n        if (language != null ? !language.equals(that.language) : that.language != null) {\n            return false;\n        }\n        return mimeType != null ? mimeType.equals(that.mimeType) : that.mimeType == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = groupIndex;\n        result = 31 * result + formatIndex;\n        result = 31 * result + (trackId != null ? trackId.hashCode() : 0);\n        result = 31 * result + (language != null ? language.hashCode() : 0);\n        result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);\n        result = 31 * result + numberOfChannels;\n        result = 31 * result + frequency;\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"PlayerSubtitleTrack{\"\n                + \"groupIndex=\" + groupIndex\n                + \", formatIndex=\" + formatIndex\n                + \", trackId='\" + trackId + '\\''\n                + \", language='\" + language + '\\''\n                + \", mimeType='\" + mimeType + '\\''\n                + \", numberOfChannels=\" + numberOfChannels\n                + \", frequency=\" + frequency\n                + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/PlayerVideoTrack.java",
    "content": "package com.novoda.noplayer.model;\n\nimport com.novoda.noplayer.ContentType;\n\npublic class PlayerVideoTrack {\n\n    private final int groupIndex;\n    private final int formatIndex;\n    private final String id;\n    private final ContentType contentType;\n    private final int width;\n    private final int height;\n    private final int fps;\n    private final int bitrate;\n\n    @SuppressWarnings(\"checkstyle:ParameterNumber\") // TODO group parameters into classes\n    public PlayerVideoTrack(int groupIndex, int formatIndex, String id, ContentType contentType, int width, int height, int fps, int bitrate) {\n        this.groupIndex = groupIndex;\n        this.formatIndex = formatIndex;\n        this.id = id;\n        this.contentType = contentType;\n        this.width = width;\n        this.height = height;\n        this.fps = fps;\n        this.bitrate = bitrate;\n    }\n\n    public int groupIndex() {\n        return groupIndex;\n    }\n\n    public int formatIndex() {\n        return formatIndex;\n    }\n\n    public String id() {\n        return id;\n    }\n\n    public ContentType contentType() {\n        return contentType;\n    }\n\n    public int width() {\n        return width;\n    }\n\n    public int height() {\n        return height;\n    }\n\n    public int fps() {\n        return fps;\n    }\n\n    public int bitrate() {\n        return bitrate;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        PlayerVideoTrack that = (PlayerVideoTrack) o;\n\n        if (groupIndex != that.groupIndex) {\n            return false;\n        }\n        if (formatIndex != that.formatIndex) {\n            return false;\n        }\n        if (width != that.width) {\n            return false;\n        }\n        if (height != that.height) {\n            return false;\n        }\n        if (fps != that.fps) {\n            return false;\n        }\n        if (bitrate != that.bitrate) {\n            return false;\n        }\n        if (id != null ? !id.equals(that.id) : that.id != null) {\n            return false;\n        }\n        return contentType == that.contentType;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = groupIndex;\n        result = 31 * result + formatIndex;\n        result = 31 * result + (id != null ? id.hashCode() : 0);\n        result = 31 * result + (contentType != null ? contentType.hashCode() : 0);\n        result = 31 * result + width;\n        result = 31 * result + height;\n        result = 31 * result + fps;\n        result = 31 * result + bitrate;\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"PlayerVideoTrack{\"\n                + \"groupIndex=\" + groupIndex\n                + \", formatIndex=\" + formatIndex\n                + \", id='\" + id + '\\''\n                + \", contentType=\" + contentType\n                + \", width=\" + width\n                + \", height=\" + height\n                + \", fps=\" + fps\n                + \", bitrate=\" + bitrate\n                + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/TextCues.java",
    "content": "package com.novoda.noplayer.model;\n\nimport java.util.Collections;\nimport java.util.List;\n\npublic final class TextCues {\n\n    private final List<NoPlayerCue> cues;\n\n    public static TextCues of(List<NoPlayerCue> cues) {\n        return new TextCues(Collections.unmodifiableList(cues));\n    }\n\n    private TextCues(List<NoPlayerCue> cues) {\n        this.cues = cues;\n    }\n\n    public int size() {\n        return cues.size();\n    }\n\n    public boolean isEmpty() {\n        return cues.isEmpty();\n    }\n\n    public NoPlayerCue get(int position) {\n        return cues.get(position);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) {\n            return true;\n        }\n        if (o == null || getClass() != o.getClass()) {\n            return false;\n        }\n\n        TextCues textCues = (TextCues) o;\n\n        return cues != null ? cues.equals(textCues.cues) : textCues.cues == null;\n    }\n\n    @Override\n    public int hashCode() {\n        return cues != null ? cues.hashCode() : 0;\n    }\n\n    @Override\n    public String toString() {\n        return \"TextCues{\" + \"cues=\" + cues + '}';\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/model/Timeout.java",
    "content": "package com.novoda.noplayer.model;\n\nimport java.util.concurrent.TimeUnit;\n\npublic final class Timeout {\n\n    private static final long ONE_SECOND_IN_MILLIS = TimeUnit.SECONDS.toMillis(1);\n\n    private final long timeoutInMillis;\n\n    public static Timeout fromSeconds(long timeoutInSeconds) {\n        return new Timeout(timeoutInSeconds * ONE_SECOND_IN_MILLIS);\n    }\n\n    private Timeout(long timeoutInMillis) {\n        this.timeoutInMillis = timeoutInMillis;\n    }\n\n    public long inMillis() {\n        return timeoutInMillis;\n    }\n}\n"
  },
  {
    "path": "core/src/main/java/com/novoda/noplayer/text/NoPlayerSubtitleDecoderFactory.java",
    "content": "package com.novoda.noplayer.text;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.text.SubtitleDecoder;\nimport com.google.android.exoplayer2.text.SubtitleDecoderFactory;\nimport com.google.android.exoplayer2.text.cea.Cea608Decoder;\nimport com.google.android.exoplayer2.text.cea.Cea708Decoder;\nimport com.google.android.exoplayer2.text.dvb.DvbDecoder;\nimport com.google.android.exoplayer2.text.pgs.PgsDecoder;\nimport com.google.android.exoplayer2.text.ssa.SsaDecoder;\nimport com.google.android.exoplayer2.text.subrip.SubripDecoder;\nimport com.google.android.exoplayer2.text.ttml.TtmlDecoder;\nimport com.google.android.exoplayer2.text.tx3g.Tx3gDecoder;\nimport com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder;\nimport com.google.android.exoplayer2.util.MimeTypes;\nimport com.novoda.noplayer.external.exoplayer.text.webvtt.WebvttDecoder;\n\n// This is a factory and we need to consider all the supported formats when creating a decoder\n@SuppressWarnings({\"PMD.CyclomaticComplexity\", \"PMD.StdCyclomaticComplexity\"})\npublic class NoPlayerSubtitleDecoderFactory implements SubtitleDecoderFactory {\n\n    @Override\n    public boolean supportsFormat(Format format) {\n        String mimeType = format.sampleMimeType;\n        return MimeTypes.TEXT_VTT.equals(mimeType)\n            || MimeTypes.TEXT_SSA.equals(mimeType)\n            || MimeTypes.APPLICATION_TTML.equals(mimeType)\n            || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)\n            || MimeTypes.APPLICATION_SUBRIP.equals(mimeType)\n            || MimeTypes.APPLICATION_TX3G.equals(mimeType)\n            || MimeTypes.APPLICATION_CEA608.equals(mimeType)\n            || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType)\n            || MimeTypes.APPLICATION_CEA708.equals(mimeType)\n            || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)\n            || MimeTypes.APPLICATION_PGS.equals(mimeType);\n    }\n\n    @Override\n    public SubtitleDecoder createDecoder(Format format) {\n        switch (format.sampleMimeType) {\n            case MimeTypes.TEXT_VTT:\n                return new WebvttDecoder();\n            case MimeTypes.TEXT_SSA:\n                return new SsaDecoder(format.initializationData);\n            case MimeTypes.APPLICATION_MP4VTT:\n                return new Mp4WebvttDecoder();\n            case MimeTypes.APPLICATION_TTML:\n                return new TtmlDecoder();\n            case MimeTypes.APPLICATION_SUBRIP:\n                return new SubripDecoder();\n            case MimeTypes.APPLICATION_TX3G:\n                return new Tx3gDecoder(format.initializationData);\n            case MimeTypes.APPLICATION_CEA608:\n            case MimeTypes.APPLICATION_MP4CEA608:\n                return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel);\n            case MimeTypes.APPLICATION_CEA708:\n                return new Cea708Decoder(format.accessibilityChannel, format.initializationData);\n            case MimeTypes.APPLICATION_DVBSUBS:\n                return new DvbDecoder(format.initializationData);\n            case MimeTypes.APPLICATION_PGS:\n                return new PgsDecoder();\n            default:\n                throw new IllegalArgumentException(\"Attempted to create decoder for unsupported format\");\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/main/res/layout/noplayer_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.exoplayer2.ui.AspectRatioFrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:id=\"@+id/video_frame\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\">\n\n  <SurfaceView\n    android:id=\"@+id/surface_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_gravity=\"center\" />\n\n  <com.novoda.noplayer.SubtitleView\n    android:id=\"@+id/subtitles_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:visibility=\"gone\" />\n\n  <View\n    android:id=\"@+id/shutter\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@android:color/black\" />\n\n</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>\n"
  },
  {
    "path": "core/src/test/java/com/google/android/exoplayer2/ExoPlaybackExceptionFactory.java",
    "content": "package com.google.android.exoplayer2;\n\npublic class ExoPlaybackExceptionFactory {\n\n    public static ExoPlaybackException createForUnexpected(RuntimeException exception) {\n        return ExoPlaybackException.createForUnexpected(exception);\n    }\n\n}\n"
  },
  {
    "path": "core/src/test/java/com/google/android/exoplayer2/drm/FrameworkMediaCryptoFixture.java",
    "content": "package com.google.android.exoplayer2.drm;\n\nimport android.media.MediaCrypto;\nimport android.media.MediaCryptoException;\n\nimport java.util.UUID;\n\npublic final class FrameworkMediaCryptoFixture {\n\n    private MediaCrypto mediaCrypto = new MediaCrypto(UUID.randomUUID(), new byte[0]);\n    private boolean forceAllowInsecureDecoderComponents = true;\n\n    private FrameworkMediaCryptoFixture() throws MediaCryptoException {\n        // Static factory method.\n    }\n\n    public static FrameworkMediaCryptoFixture aFrameworkMediaCrypto() throws MediaCryptoException {\n        return new FrameworkMediaCryptoFixture();\n    }\n\n    public FrameworkMediaCryptoFixture withMediaCrypto(MediaCrypto mediaCrypto) {\n        this.mediaCrypto = mediaCrypto;\n        return this;\n    }\n\n    public FrameworkMediaCryptoFixture withForceAllowInsecureDecoderComponents(boolean forceAllowInsecureDecoderComponents) {\n        this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;\n        return this;\n    }\n\n    public FrameworkMediaCrypto build() {\n        return new FrameworkMediaCrypto(mediaCrypto, forceAllowInsecureDecoderComponents);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/LoadTimeoutTest.java",
    "content": "package com.novoda.noplayer;\n\nimport android.os.Handler;\n\nimport com.novoda.noplayer.internal.Clock;\nimport com.novoda.noplayer.model.LoadTimeout;\nimport com.novoda.noplayer.model.Timeout;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mock;\nimport org.mockito.invocation.InvocationOnMock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.mockito.stubbing.Answer;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\npublic class LoadTimeoutTest {\n\n    private static final long START_TIME = 0L;\n    private static final long END_TIME = 1000L;\n    private static final int RESCHEDULE_DELAY_MILLIS = 1000;\n    private static final Timeout TIMEOUT_NOT_REACHED = Timeout.fromSeconds(5);\n    private static final Timeout TIMEOUT_REACHED = Timeout.fromSeconds(1);\n\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    @Mock\n    Clock clock;\n\n    @Mock\n    Handler handler;\n\n    @Mock\n    NoPlayer.LoadTimeoutCallback loadTimeoutCallback;\n\n    private LoadTimeout loadTimeout;\n\n    @Before\n    public void setUp() {\n        loadTimeout = new LoadTimeout(clock, handler);\n        doAnswer(new Answer<Void>() {\n            @Override\n            public Void answer(InvocationOnMock invocation) throws Throwable {\n                Runnable runnable = invocation.getArgument(0);\n                runnable.run();\n                return null;\n            }\n        }).when(handler).post(any(Runnable.class));\n    }\n\n    @Test\n    public void givenTimeoutIsReached_whenStarting_thenOnLoadTimeoutIsCalled() {\n        when(clock.getCurrentTime()).thenReturn(START_TIME, END_TIME);\n\n        loadTimeout.start(TIMEOUT_REACHED, loadTimeoutCallback);\n\n        verify(loadTimeoutCallback).onLoadTimeout();\n    }\n\n    @Test\n    public void givenTimeoutIsNotReached_whenStarting_thenTimeoutIsRescheduled() {\n        when(clock.getCurrentTime()).thenReturn(START_TIME, END_TIME);\n\n        loadTimeout.start(TIMEOUT_NOT_REACHED, loadTimeoutCallback);\n\n        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);\n        verify(handler).post(captor.capture());\n        verify(handler).postDelayed(captor.getValue(), RESCHEDULE_DELAY_MILLIS);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/NoPlayerCreatorTest.java",
    "content": "package com.novoda.noplayer;\n\nimport android.content.Context;\n\nimport com.novoda.noplayer.drm.DownloadedModularDrm;\nimport com.novoda.noplayer.drm.DrmHandler;\nimport com.novoda.noplayer.drm.DrmType;\nimport com.novoda.noplayer.drm.StreamingModularDrm;\nimport com.novoda.noplayer.internal.exoplayer.NoPlayerExoPlayerCreator;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorException;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreatorFactory;\nimport com.novoda.noplayer.internal.mediaplayer.NoPlayerMediaPlayerCreator;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.experimental.runners.Enclosed;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.mock;\n\n@RunWith(Enclosed.class)\npublic class NoPlayerCreatorTest {\n\n    public abstract static class Base {\n\n        static final boolean USE_SECURE_CODEC = false;\n        static final boolean ALLOW_CROSS_PROTOCOL_REDIRECTS = false;\n        static final StreamingModularDrm STREAMING_MODULAR_DRM = mock(StreamingModularDrm.class);\n        static final DownloadedModularDrm DOWNLOADED_MODULAR_DRM = mock(DownloadedModularDrm.class);\n        static final NoPlayer EXO_PLAYER = mock(NoPlayer.class);\n        static final NoPlayer MEDIA_PLAYER = mock(NoPlayer.class);\n\n        @Rule\n        public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n        @Mock\n        Context context;\n\n        @Mock\n        NoPlayerExoPlayerCreator noPlayerExoPlayerCreator;\n        @Mock\n        NoPlayerMediaPlayerCreator noPlayerMediaPlayerCreator;\n        @Mock\n        DrmSessionCreator drmSessionCreator;\n        @Mock\n        DrmSessionCreatorFactory drmSessionCreatorFactory;\n\n        NoPlayerCreator noPlayerCreator;\n\n        @Before\n        public void setUp() throws DrmSessionCreatorException {\n            given(drmSessionCreatorFactory.createFor(any(DrmType.class), any(DrmHandler.class))).willReturn(drmSessionCreator);\n            given(noPlayerExoPlayerCreator.createExoPlayer(context, drmSessionCreator, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS)).willReturn(EXO_PLAYER);\n            given(noPlayerMediaPlayerCreator.createMediaPlayer(context)).willReturn(MEDIA_PLAYER);\n            noPlayerCreator = new NoPlayerCreator(context, prioritizedPlayerTypes(), noPlayerExoPlayerCreator, noPlayerMediaPlayerCreator, drmSessionCreatorFactory);\n        }\n\n        abstract List<PlayerType> prioritizedPlayerTypes();\n    }\n\n    public static class GivenMediaPlayerPrioritized extends Base {\n\n        @Override\n        List<PlayerType> prioritizedPlayerTypes() {\n            return Arrays.asList(PlayerType.MEDIA_PLAYER, PlayerType.EXO_PLAYER);\n        }\n\n        @Test\n        public void whenCreatingPlayerWithDrmTypeNone_thenReturnsMediaPlayer() {\n            NoPlayer player = noPlayerCreator.create(DrmType.NONE, DrmHandler.NO_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS);\n\n            assertThat(player).isEqualTo(MEDIA_PLAYER);\n        }\n\n        @Test\n        public void whenCreatingPlayerWithDrmTypeWidevineClassic_thenReturnsMediaPlayer() {\n            NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_CLASSIC, DrmHandler.NO_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS);\n\n            assertThat(player).isEqualTo(MEDIA_PLAYER);\n        }\n\n        @Test\n        public void whenCreatingPlayerWithDrmTypeWidevineModularStream_thenReturnsExoPlayer() {\n            NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_MODULAR_STREAM, STREAMING_MODULAR_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS);\n\n            assertThat(player).isEqualTo(EXO_PLAYER);\n        }\n\n        @Test\n        public void whenCreatingPlayerWithDrmTypeWidevineModularDownload_thenReturnsExoPlayer() {\n            NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_MODULAR_DOWNLOAD, DOWNLOADED_MODULAR_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS);\n\n            assertThat(player).isEqualTo(EXO_PLAYER);\n        }\n    }\n\n    public static class GivenExoPlayerPlayerPrioritized extends Base {\n\n        @Override\n        List<PlayerType> prioritizedPlayerTypes() {\n            return Arrays.asList(PlayerType.EXO_PLAYER, PlayerType.MEDIA_PLAYER);\n        }\n\n        @Test\n        public void whenCreatingPlayerWithDrmTypeNone_thenReturnsExoPlayer() {\n            NoPlayer player = noPlayerCreator.create(DrmType.NONE, DrmHandler.NO_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS);\n\n            assertThat(player).isEqualTo(EXO_PLAYER);\n        }\n\n        @Test\n        public void whenCreatingPlayerWithDrmTypeWidevineClassic_thenReturnsMediaPlayer() {\n            NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_CLASSIC, DrmHandler.NO_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS);\n\n            assertThat(player).isEqualTo(MEDIA_PLAYER);\n        }\n\n        @Test\n        public void whenCreatingPlayerWithDrmTypeWidevineModularStream_thenReturnsExoPlayer() {\n            NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_MODULAR_STREAM, STREAMING_MODULAR_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS);\n\n            assertThat(player).isEqualTo(EXO_PLAYER);\n        }\n\n        @Test\n        public void whenCreatingPlayerWithDrmTypeWidevineModularDownload_thenReturnsExoPlayer() {\n            NoPlayer player = noPlayerCreator.create(DrmType.WIDEVINE_MODULAR_DOWNLOAD, DOWNLOADED_MODULAR_DRM, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS);\n\n            assertThat(player).isEqualTo(EXO_PLAYER);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/PlayerSurfaceHolderTest.java",
    "content": "package com.novoda.noplayer;\n\nimport android.view.SurfaceHolder;\nimport android.view.SurfaceView;\nimport android.view.TextureView;\nimport com.google.android.exoplayer2.Player;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\n\n@RunWith(MockitoJUnitRunner.class)\npublic class PlayerSurfaceHolderTest {\n\n    @Mock\n    private SurfaceView surfaceView;\n    @Mock\n    private SurfaceHolder surfaceHolder;\n    @Mock\n    private TextureView textureView;\n    @Mock\n    private Player.VideoComponent videoPlayer;\n\n    @Rule\n    public ExpectedException thrown = ExpectedException.none();\n\n    @Before\n    public void setUp() {\n        surfaceHolder = mock(SurfaceHolder.class);\n        given(surfaceView.getHolder()).willReturn(surfaceHolder);\n    }\n\n    @Test\n    public void whenCreatingPlayerSurfaceHolderWithSurfaceView_thenAttachCallbackToSurfaceHolder() {\n\n        PlayerSurfaceHolder.create(surfaceView);\n\n        verify(surfaceHolder).addCallback(any(PlayerViewSurfaceHolder.class));\n    }\n\n    @Test\n    public void whenCreatingPlayerSurfaceHolderWithTextureView_thenAttachSurfaceTextureListenerToTextureView() {\n\n        PlayerSurfaceHolder.create(textureView);\n\n        verify(textureView).setSurfaceTextureListener(any(PlayerViewSurfaceHolder.class));\n    }\n\n    @Test\n    public void givenPlayerSurfaceHolderContainsSurfaceView_whenAttachingVideoPlayer_thenSetsVideoSurfaceView() {\n        PlayerSurfaceHolder playerSurfaceHolder = PlayerSurfaceHolder.create(surfaceView);\n\n        playerSurfaceHolder.attach(videoPlayer);\n\n        verify(videoPlayer).setVideoSurfaceView(surfaceView);\n    }\n\n    @Test\n    public void givenPlayerSurfaceHolderContainsTextureView_whenAttachingVideoPlayer_thenSetsVideoTextureView() {\n        PlayerSurfaceHolder playerSurfaceHolder = PlayerSurfaceHolder.create(textureView);\n\n        playerSurfaceHolder.attach(videoPlayer);\n\n        verify(videoPlayer).setVideoTextureView(textureView);\n    }\n\n    @Test\n    public void givenPlayerSurfaceHolderContainsNoView_whenAttachingVideoPlayer_thenThrowsException() {\n        thrown.expect(IllegalArgumentException.class);\n\n        PlayerSurfaceHolder playerSurfaceHolder = new PlayerSurfaceHolder(null, null, null);\n\n        playerSurfaceHolder.attach(videoPlayer);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/PlayerTypeTest.java",
    "content": "package com.novoda.noplayer;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\n\npublic class PlayerTypeTest {\n\n    @Rule\n    public ExpectedException thrown = ExpectedException.none();\n\n    @Test\n    public void givenUnknownPlayerType_thenThrows() throws Exception {\n        thrown.expect(PlayerType.UnknownPlayerTypeException.class);\n\n        PlayerType.from(\"unknown player type 1234___\");\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/HeartTest.java",
    "content": "package com.novoda.noplayer.internal;\n\nimport android.os.Handler;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.invocation.InvocationOnMock;\nimport org.mockito.stubbing.Answer;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyLong;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.then;\nimport static org.mockito.BDDMockito.will;\nimport static org.mockito.Mockito.mock;\n\npublic class HeartTest {\n\n    private final NoPlayer.HeartbeatCallback heartbeatCallback = mock(NoPlayer.HeartbeatCallback.class);\n    private final NoPlayer noPlayer = mock(NoPlayer.class);\n    private final Handler handler = mock(Handler.class);\n\n    private Heart heart;\n\n    @Before\n    public void setUp() {\n        will(new Answer<Void>() {\n            @Override\n            public Void answer(InvocationOnMock invocation) {\n                Runnable runnable = invocation.getArgument(0);\n                runnable.run();\n                return null;\n            }\n        }).given(handler).post(any(Runnable.class));\n\n        heart = Heart.newInstance(handler);\n    }\n\n    @Test(expected = IllegalStateException.class)\n    public void throwsException_whenStartingHeartWithoutBindingAction() {\n        heart.startBeatingHeart();\n    }\n\n    @Test(expected = IllegalStateException.class)\n    public void throwsException_whenForcingBeatWithoutBindingAction() {\n        heart.forceBeat();\n    }\n\n    @Test\n    public void removesCallbacks_whenStartingHeart() {\n        Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer);\n        heart.bind(onHeartbeat);\n\n        heart.startBeatingHeart();\n\n        then(handler).should().removeCallbacks(any(Runnable.class));\n    }\n\n    @Test\n    public void schedulesNextBeat_whenStartingHeart() {\n        Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer);\n        heart.bind(onHeartbeat);\n\n        heart.startBeatingHeart();\n\n        then(handler).should().postDelayed(any(Runnable.class), anyLong());\n    }\n\n    @Test\n    public void doesNotEmitOnBeat_whenPlayerIsNotPlaying() {\n        Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer);\n        heart.bind(onHeartbeat);\n\n        heart.startBeatingHeart();\n\n        then(heartbeatCallback).shouldHaveZeroInteractions();\n    }\n\n    @Test\n    public void emitsOnBeat_whenPlayerIsPlaying() {\n        given(noPlayer.isPlaying()).willReturn(true);\n        Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer);\n        heart.bind(onHeartbeat);\n\n        heart.startBeatingHeart();\n\n        then(heartbeatCallback).should().onBeat(noPlayer);\n    }\n\n    @Test\n    public void emitsOnBeat_whenForcingBeat() {\n        given(noPlayer.isPlaying()).willReturn(true);\n        Heart.Heartbeat onHeartbeat = new Heart.Heartbeat(heartbeatCallback, noPlayer);\n        heart.bind(onHeartbeat);\n\n        heart.forceBeat();\n\n        then(heartbeatCallback).should().onBeat(noPlayer);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/drm/provision/HttpPostingProvisionExecutorTest.java",
    "content": "package com.novoda.noplayer.internal.drm.provision;\n\nimport com.novoda.noplayer.drm.ModularDrmProvisionRequest;\n\nimport java.io.IOException;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static com.novoda.noplayer.internal.drm.provision.ProvisioningCapabilitiesFixtures.aProvisioningCapabilities;\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.Mockito.verify;\n\npublic class HttpPostingProvisionExecutorTest {\n\n    private static final String PROVISION_URL = \"http://provisionurl.com\";\n    private static final byte[] PROVISION_DATA = \"provision-payload\".getBytes();\n    private static final ModularDrmProvisionRequest A_PROVISION_REQUEST = new ModularDrmProvisionRequest(PROVISION_URL, PROVISION_DATA);\n\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    private HttpPostingProvisionExecutor httpPostingProvisionExecutor;\n    private ArgumentCaptor<String> provisionUrlCaptor;\n\n    @Mock\n    private HttpUrlConnectionPoster httpPoster;\n\n    @Before\n    public void setUp() {\n        provisionUrlCaptor = ArgumentCaptor.forClass(String.class);\n    }\n\n    @Test(expected = UnableToProvisionException.class)\n    public void givenNonCapableProvisionCapabilities_whenProvisioning_thenAnUnableToProvisionExceptionIsThrown() throws IOException, UnableToProvisionException {\n        ProvisioningCapabilities capabilities = aProvisioningCapabilities().thatCannotProvision();\n        httpPostingProvisionExecutor = new HttpPostingProvisionExecutor(httpPoster, capabilities);\n\n        httpPostingProvisionExecutor.execute(A_PROVISION_REQUEST);\n    }\n\n    @Test\n    public void givenCapableProvisionCapabilities_whenProvisioning_thenTheRequestUrlIsExpected() throws IOException, UnableToProvisionException {\n        ProvisioningCapabilities capabilities = aProvisioningCapabilities().thatCanProvision();\n        httpPostingProvisionExecutor = new HttpPostingProvisionExecutor(httpPoster, capabilities);\n\n        String expectedProvisionUrl = PROVISION_URL + \"&signedRequest=\" + new String(PROVISION_DATA);\n\n        httpPostingProvisionExecutor.execute(A_PROVISION_REQUEST);\n        verify(httpPoster).post(provisionUrlCaptor.capture());\n\n        assertThat(provisionUrlCaptor.getValue()).isEqualTo(expectedProvisionUrl);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/drm/provision/ProvisioningCapabilitiesFixtures.java",
    "content": "package com.novoda.noplayer.internal.drm.provision;\n\nimport android.os.Build;\n\npublic final class ProvisioningCapabilitiesFixtures {\n\n    public static ProvisioningCapabilitiesFixtures aProvisioningCapabilities() {\n        return new ProvisioningCapabilitiesFixtures();\n    }\n\n    private ProvisioningCapabilitiesFixtures() {\n        // Not instantiable\n    }\n\n    public ProvisioningCapabilities thatCanProvision() {\n        return new ProvisioningCapabilities(Build.VERSION_CODES.JELLY_BEAN_MR2);\n    }\n\n    public ProvisioningCapabilities thatCannotProvision() {\n        return new ProvisioningCapabilities(Build.VERSION_CODES.JELLY_BEAN_MR1);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacadeTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport android.net.Uri;\nimport android.view.SurfaceHolder;\nimport android.view.SurfaceView;\nimport android.view.TextureView;\n\nimport com.google.android.exoplayer2.C;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.audio.AudioAttributes;\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.google.android.exoplayer2.source.MediaSourceEventListener;\nimport com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;\nimport com.novoda.noplayer.ContentType;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.OptionsBuilder;\nimport com.novoda.noplayer.PlayerSurfaceHolder;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator;\nimport com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder;\nimport com.novoda.noplayer.internal.exoplayer.mediasource.MediaSourceFactory;\nimport com.novoda.noplayer.internal.utils.AndroidDeviceVersion;\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerAudioTrackFixture;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\nimport com.novoda.noplayer.model.PlayerVideoTrackFixture;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.experimental.runners.Enclosed;\nimport org.junit.rules.ExpectedException;\nimport org.junit.runner.RunWith;\nimport org.mockito.InOrder;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport utils.ExceptionMatcher;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.anyLong;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.BDDMockito.willDoNothing;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\n\n@RunWith(Enclosed.class)\npublic class ExoPlayerFacadeTest {\n\n    private static final boolean SELECTED = true;\n\n    private static final long TWENTY_FIVE_SECONDS_IN_MILLIS = 25000;\n    private static final long TWO_MINUTES_IN_MILLIS = 120000;\n    private static final long TEN_MINUTES_IN_MILLIS = 600000;\n\n    private static final int TEN_PERCENT = 10;\n\n    private static final boolean IS_PLAYING = true;\n    private static final boolean PLAY_WHEN_READY = true;\n    private static final boolean DO_NOT_PLAY_WHEN_READY = false;\n    private static final boolean RESET_POSITION = true;\n    private static final boolean DO_NOT_RESET_POSITION = false;\n    private static final boolean DO_NOT_RESET_STATE = false;\n\n    private static final Options OPTIONS = new OptionsBuilder()\n            .withContentType(ContentType.DASH)\n            .build();\n\n    public static class GivenVideoNotLoaded extends Base {\n\n        private static final long ANY_POSITION = 1000;\n        private static final PlayerAudioTrack PLAYER_AUDIO_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().build();\n        private static final AudioTracks AUDIO_TRACKS = AudioTracks.from(Collections.singletonList(PLAYER_AUDIO_TRACK));\n\n        @Rule\n        public ExpectedException thrown = ExpectedException.none();\n\n        @Test\n        public void whenResetting_thenReleasesUnderlyingPlayer() {\n\n            facade.release();\n\n            verify(exoPlayer, never()).release();\n        }\n\n        @Test\n        public void whenLoadingVideo_thenAddsPlayerEventListener() {\n\n            facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector);\n\n            verify(exoPlayer).addListener(exoPlayerForwarder.exoPlayerEventListener());\n        }\n\n        @Test\n        public void whenLoadingVideo_thenSetsAnalyticsListener() {\n\n            facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector);\n\n            verify(exoPlayer).addAnalyticsListener(exoPlayerForwarder.analyticsListener());\n        }\n\n        @Test\n        public void givenSurfaceContainerContainsSurfaceView_whenLoadingVideo_thenSetsSurfaceViewOnExoPlayer() {\n\n            facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector);\n\n            verify(exoPlayer).setVideoSurfaceView(surfaceView);\n        }\n\n        @Test\n        public void givenSurfaceContainerContainsTextureView_whenLoadingVideo_thenSetsTextureViewOnExoPlayer() {\n\n            facade.loadVideo(textureViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector);\n\n            verify(exoPlayer).setVideoTextureView(textureView);\n        }\n\n        @Test\n        public void givenLollipopDevice_whenLoadingVideo_thenSetsMovieAudioAttributesOnExoPlayer() {\n            given(androidDeviceVersion.isLollipopTwentyOneOrAbove()).willReturn(true);\n\n            facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector);\n\n            AudioAttributes expectedMovieAudioAttributes = new AudioAttributes.Builder()\n                    .setContentType(C.CONTENT_TYPE_MOVIE)\n                    .build();\n            verify(exoPlayer).setAudioAttributes(expectedMovieAudioAttributes);\n        }\n\n        @Test\n        public void givenNonLollipopDevice_whenLoadingVideo_thenDoesNotSetAudioAttributesOnExoPlayer() {\n            given(androidDeviceVersion.isLollipopTwentyOneOrAbove()).willReturn(false);\n\n            facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector);\n\n            verify(exoPlayer, never()).setAudioAttributes(any(AudioAttributes.class));\n        }\n\n        @Test\n        public void givenMediaSource_whenLoadingVideo_thenPreparesInternalExoPlayer() {\n            MediaSource mediaSource = givenMediaSource(OPTIONS);\n\n            facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector);\n\n            verify(exoPlayer).prepare(mediaSource, RESET_POSITION, DO_NOT_RESET_STATE);\n        }\n\n        @Test\n        public void givenInitialPosition_whenLoadingVideo_thenPerformsSeekBeforePreparing() {\n            Options options = OPTIONS.toOptionsBuilder()\n                    .withInitialPositionInMillis(TWENTY_FIVE_SECONDS_IN_MILLIS)\n                    .build();\n            MediaSource mediaSource = givenMediaSource(options);\n\n            facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, options, exoPlayerForwarder, mediaCodecSelector);\n\n            InOrder inOrder = inOrder(exoPlayer);\n            inOrder.verify(exoPlayer).seekTo(TWENTY_FIVE_SECONDS_IN_MILLIS);\n            inOrder.verify(exoPlayer).prepare(mediaSource, DO_NOT_RESET_POSITION, DO_NOT_RESET_STATE);\n        }\n\n        @Test\n        public void givenNoInitialPosition_whenLoadingVideo_thenDoesNotPerformSeekBeforePreparing() {\n            MediaSource mediaSource = givenMediaSource(OPTIONS);\n\n            facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector);\n\n            InOrder inOrder = inOrder(exoPlayer);\n            inOrder.verify(exoPlayer, never()).seekTo(TWENTY_FIVE_SECONDS_IN_MILLIS);\n            inOrder.verify(exoPlayer).prepare(mediaSource, RESET_POSITION, DO_NOT_RESET_STATE);\n        }\n\n        @Test\n        public void whenQueryingIsPlaying_thenReturnsFalse() {\n\n            boolean isPlaying = facade.isPlaying();\n\n            assertThat(isPlaying).isFalse();\n        }\n\n        @Test\n        public void whenQueryingPlayheadPosition_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            facade.playheadPositionInMillis();\n        }\n\n        @Test\n        public void whenQueryingMediaDuration_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            facade.mediaDurationInMillis();\n        }\n\n        @Test\n        public void whenQueryingBufferPercentage_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            facade.bufferPercentage();\n        }\n\n        @Test\n        public void whenPausing_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            facade.pause();\n        }\n\n        @Test\n        public void whenSeeking_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            facade.seekTo(ANY_POSITION);\n        }\n\n        @Test\n        public void whenSelectingAudioTrack_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class);\n\n            facade.selectAudioTrack(audioTrack);\n        }\n\n        @Test\n        public void whenGettingAudioTracks_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            given(trackSelector.getAudioTracks(any(RendererTypeRequester.class))).willReturn(AUDIO_TRACKS);\n\n            facade.getAudioTracks();\n        }\n\n        @Test\n        public void selectSubtitleTrack_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            PlayerSubtitleTrack subtitleTrack = mock(PlayerSubtitleTrack.class);\n\n            facade.selectSubtitleTrack(subtitleTrack);\n        }\n\n        @Test\n        public void whenSetVolume_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            facade.setVolume(ANY_VOLUME);\n        }\n\n        @Test\n        public void whenGetVolume_thenThrowsIllegalStateException() {\n            thrown.expect(ExceptionMatcher.matches(\"Video must be loaded before trying to interact with the player\", IllegalStateException.class));\n\n            facade.getVolume();\n        }\n    }\n\n    public static class GivenVideoIsLoaded extends Base {\n\n        private static final PlayerAudioTrack PLAYER_AUDIO_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().build();\n        private static final AudioTracks AUDIO_TRACKS = AudioTracks.from(Collections.singletonList(PLAYER_AUDIO_TRACK));\n        private static final PlayerVideoTrack PLAYER_VIDEO_TRACK = PlayerVideoTrackFixture.aPlayerVideoTrack().build();\n        private static final List<PlayerVideoTrack> VIDEO_TRACKS = Collections.singletonList(PLAYER_VIDEO_TRACK);\n\n        @Override\n        public void setUp() {\n            super.setUp();\n            givenPlayerIsLoaded();\n        }\n\n        private void givenPlayerIsLoaded() {\n            givenMediaSource(OPTIONS);\n            facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector);\n        }\n\n        @Test\n        public void whenResetting_thenReleasesUnderlyingPlayer() {\n            facade.release();\n\n            verify(exoPlayer).release();\n        }\n\n        @Test\n        public void whenPausing_thenSetsPlayWhenReadyToFalse() {\n\n            facade.pause();\n\n            verify(exoPlayer).setPlayWhenReady(DO_NOT_PLAY_WHEN_READY);\n        }\n\n        @Test\n        public void whenSeeking_thenSeeksToPosition() {\n            long videoPositionInMillis = TWO_MINUTES_IN_MILLIS;\n\n            facade.seekTo(videoPositionInMillis);\n\n            verify(exoPlayer).seekTo(videoPositionInMillis);\n        }\n\n        @Test\n        public void whenStartingPlay_thenSetsPlayWhenReadyToTrue() {\n\n            facade.play();\n\n            verify(exoPlayer).setPlayWhenReady(PLAY_WHEN_READY);\n        }\n\n        @Test\n        public void whenStartingPlayAtVideoPosition_thenSeeksToPosition() {\n            facade.play(TWO_MINUTES_IN_MILLIS);\n\n            verify(exoPlayer).seekTo(TWO_MINUTES_IN_MILLIS);\n        }\n\n        @Test\n        public void whenStartingPlayAtVideoPosition_thenSetsPlayWhenReadyToTrue() {\n            facade.play(TWO_MINUTES_IN_MILLIS);\n\n            verify(exoPlayer).setPlayWhenReady(PLAY_WHEN_READY);\n        }\n\n        @Test\n        public void givenExoPlayerIsReadyToPlay_whenQueryingIsPlaying_thenReturnsTrue() {\n            given(exoPlayer.getPlayWhenReady()).willReturn(IS_PLAYING);\n\n            boolean isPlaying = facade.isPlaying();\n\n            assertThat(isPlaying).isTrue();\n        }\n\n        @Test\n        public void whenGettingPlayheadPosition_thenReturnsCurrentPosition() {\n            given(exoPlayer.getCurrentPosition()).willReturn(TWO_MINUTES_IN_MILLIS);\n\n            long playheadPositionInMillis = facade.playheadPositionInMillis();\n\n            assertThat(playheadPositionInMillis).isEqualTo(TWO_MINUTES_IN_MILLIS);\n        }\n\n        @Test\n        public void whenGettingMediaDuration_thenReturnsDuration() {\n            given(exoPlayer.getDuration()).willReturn(TEN_MINUTES_IN_MILLIS);\n\n            long videoDurationInMillis = facade.mediaDurationInMillis();\n\n            assertThat(videoDurationInMillis).isEqualTo(TEN_MINUTES_IN_MILLIS);\n        }\n\n        @Test\n        public void whenGettingBufferPercentage_thenReturnsBufferPercentage() {\n            given(exoPlayer.getBufferedPercentage()).willReturn(TEN_PERCENT);\n\n            int bufferPercentage = facade.bufferPercentage();\n\n            assertThat(bufferPercentage).isEqualTo(TEN_PERCENT);\n        }\n\n        @Test\n        public void whenSelectingAudioTrack_thenDelegatesToTrackSelector() {\n            PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class);\n\n            facade.selectAudioTrack(audioTrack);\n\n            verify(trackSelector).selectAudioTrack(audioTrack, rendererTypeRequester);\n        }\n\n        @Test\n        public void givenSelectingAudioTrackSuceeds_whenSelectingAudioTrack_thenReturnsTrue() {\n            PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class);\n            given(trackSelector.selectAudioTrack(audioTrack, rendererTypeRequester)).willReturn(true);\n\n            boolean success = facade.selectAudioTrack(audioTrack);\n\n            assertThat(success).isTrue();\n        }\n\n        @Test\n        public void givenSelectingAudioTrackFails_whenSelectingAudioTrack_thenReturnsFalse() {\n            PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class);\n            given(trackSelector.selectAudioTrack(audioTrack, rendererTypeRequester)).willReturn(false);\n\n            boolean success = facade.selectAudioTrack(audioTrack);\n\n            assertThat(success).isFalse();\n        }\n\n        @Test\n        public void whenSelectingSubtitlesTrack_thenDelegatesToTrackSelector() {\n            PlayerSubtitleTrack subtitleTrack = mock(PlayerSubtitleTrack.class);\n\n            facade.selectSubtitleTrack(subtitleTrack);\n\n            verify(trackSelector).selectTextTrack(subtitleTrack, rendererTypeRequester);\n        }\n\n        @Test\n        public void givenSelectingTextTrackSuceeds_whenSelectingSubtitlesTrack_thenReturnsTrue() {\n            PlayerSubtitleTrack subtitleTrack = mock(PlayerSubtitleTrack.class);\n            given(trackSelector.selectTextTrack(subtitleTrack, rendererTypeRequester)).willReturn(true);\n\n            boolean success = facade.selectSubtitleTrack(subtitleTrack);\n\n            assertThat(success).isTrue();\n        }\n\n        @Test\n        public void givenSelectingTextTrackFails_whenSelectingSubtitlesTrack_thenReturnsFalse() {\n            PlayerSubtitleTrack subtitleTrack = mock(PlayerSubtitleTrack.class);\n            given(trackSelector.selectTextTrack(subtitleTrack, rendererTypeRequester)).willReturn(false);\n\n            boolean success = facade.selectSubtitleTrack(subtitleTrack);\n\n            assertThat(success).isFalse();\n        }\n\n        @Test\n        public void whenGettingAudioTracks_thenDelegatesToTrackSelector() {\n            given(trackSelector.getAudioTracks(any(RendererTypeRequester.class))).willReturn(AUDIO_TRACKS);\n\n            AudioTracks audioTracks = facade.getAudioTracks();\n\n            assertThat(audioTracks).isEqualTo(AUDIO_TRACKS);\n        }\n\n        @Test\n        public void whenGettingSelectedVideoTrack_thenDelegatesTrackSelector() {\n            given(trackSelector.getSelectedVideoTrack(eq(exoPlayer), any(RendererTypeRequester.class), any(ContentType.class))).willReturn(Optional.of(PLAYER_VIDEO_TRACK));\n\n            Optional<PlayerVideoTrack> selectedVideoTrack = facade.getSelectedVideoTrack();\n\n            assertThat(selectedVideoTrack).isEqualTo(Optional.of(PLAYER_VIDEO_TRACK));\n        }\n\n        @Test\n        public void whenSelectingVideoTrack_thenDelegatesToTrackSelector() {\n            given(trackSelector.selectVideoTrack(eq(PLAYER_VIDEO_TRACK), any(RendererTypeRequester.class))).willReturn(SELECTED);\n\n            boolean selectedVideoTrack = facade.selectVideoTrack(PLAYER_VIDEO_TRACK);\n\n            assertThat(selectedVideoTrack).isTrue();\n        }\n\n        @Test\n        public void whenGettingVideoTracks_thenDelegatesToTrackSelector() {\n            given(trackSelector.getVideoTracks(any(RendererTypeRequester.class), any(ContentType.class))).willReturn(VIDEO_TRACKS);\n\n            List<PlayerVideoTrack> videoTracks = facade.getVideoTracks();\n\n            assertThat(videoTracks).isEqualTo(VIDEO_TRACKS);\n        }\n\n        @Test\n        public void whenSetRepeatingTrue_thenSetsRepeatModeAll() {\n            facade.setRepeating(true);\n\n            verify(exoPlayer).setRepeatMode(Player.REPEAT_MODE_ALL);\n        }\n\n        @Test\n        public void whenSetRepeatingFalse_thenSetsRepeatModeOff() {\n            facade.setRepeating(false);\n\n            verify(exoPlayer).setRepeatMode(Player.REPEAT_MODE_OFF);\n        }\n\n        @Test\n        public void whenSetVolume_thenSetsPlayerVolume() {\n            facade.setVolume(ANY_VOLUME);\n\n            verify(exoPlayer).setVolume(ANY_VOLUME);\n        }\n\n        @Test\n        public void whenGetVolume_thenGetsPlayerVolume() {\n            given(exoPlayer.getVolume()).willReturn(ANY_VOLUME);\n\n            float currentVolume = facade.getVolume();\n\n            assertThat(currentVolume).isEqualTo(ANY_VOLUME);\n        }\n\n    }\n\n    public abstract static class Base {\n\n        static final float ANY_VOLUME = 0.5f;\n\n        @Rule\n        public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n        @Mock\n        BandwidthMeterCreator bandwidthMeterCreator;\n        @Mock\n        DefaultBandwidthMeter defaultBandwidthMeter;\n        @Mock\n        AndroidDeviceVersion androidDeviceVersion;\n        @Mock\n        SimpleExoPlayer exoPlayer;\n        @Mock\n        MediaSourceFactory mediaSourceFactory;\n        @Mock\n        ExoPlayerForwarder exoPlayerForwarder;\n        @Mock\n        CompositeTrackSelectorCreator trackSelectorCreator;\n        @Mock\n        CompositeTrackSelector trackSelector;\n        @Mock\n        Uri uri;\n        @Mock\n        RendererTypeRequester rendererTypeRequester;\n        @Mock\n        RendererTypeRequesterCreator rendererTypeRequesterCreator;\n        @Mock\n        DrmSessionCreator drmSessionCreator;\n        @Mock\n        DefaultDrmSessionEventListener drmSessionEventListener;\n        @Mock\n        MediaSourceEventListener mediaSourceEventListener;\n        @Mock\n        MediaCodecSelector mediaCodecSelector;\n        @Mock\n        SurfaceView surfaceView;\n        @Mock\n        TextureView textureView;\n        PlayerSurfaceHolder surfaceViewHolder;\n        PlayerSurfaceHolder textureViewHolder;\n\n        ExoPlayerFacade facade;\n\n        @Before\n        public void setUp() {\n            ExoPlayerCreator exoPlayerCreator = mock(ExoPlayerCreator.class);\n            given(exoPlayerForwarder.drmSessionEventListener()).willReturn(drmSessionEventListener);\n            given(exoPlayerForwarder.mediaSourceEventListener()).willReturn(mediaSourceEventListener);\n            given(bandwidthMeterCreator.create(anyLong())).willReturn(defaultBandwidthMeter);\n            given(trackSelectorCreator.create(any(Options.class), eq(defaultBandwidthMeter))).willReturn(trackSelector);\n            given(exoPlayerCreator.create(drmSessionCreator, drmSessionEventListener, mediaCodecSelector, trackSelector.trackSelector())).willReturn(exoPlayer);\n            willDoNothing().given(exoPlayer).seekTo(anyInt());\n            given(rendererTypeRequesterCreator.createfrom(exoPlayer)).willReturn(rendererTypeRequester);\n            facade = new ExoPlayerFacade(\n                    bandwidthMeterCreator,\n                    androidDeviceVersion,\n                    mediaSourceFactory,\n                    trackSelectorCreator,\n                    exoPlayerCreator,\n                    rendererTypeRequesterCreator\n            );\n            given(surfaceView.getHolder()).willReturn(mock(SurfaceHolder.class));\n            surfaceViewHolder = PlayerSurfaceHolder.create(surfaceView);\n            textureViewHolder = PlayerSurfaceHolder.create(textureView);\n        }\n\n        MediaSource givenMediaSource(Options options) {\n            MediaSource mediaSource = mock(MediaSource.class);\n            given(\n                    mediaSourceFactory.create(\n                            options,\n                            uri,\n                            mediaSourceEventListener,\n                            defaultBandwidthMeter\n                    )\n            ).willReturn(mediaSource);\n\n            return mediaSource;\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerInformationTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.novoda.noplayer.PlayerType;\n\nimport org.junit.Before;\nimport org.junit.Test;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\n\npublic class ExoPlayerInformationTest {\n\n    private ExoPlayerInformation playerInformation;\n\n    @Before\n    public void setUp() {\n        playerInformation = new ExoPlayerInformation();\n    }\n\n    @Test\n    public void whenReadingName_thenReturnsExoPlayer() {\n\n        String name = playerInformation.getName();\n\n        assertThat(name).isEqualTo(\"ExoPlayer\");\n    }\n\n    @Test\n    public void whenReadingVersion_thenReturnsExoPlayerLibraryVersion() {\n\n        String version = playerInformation.getVersion();\n\n        assertThat(version).isEqualTo(ExoPlayerLibraryInfo.VERSION);\n    }\n\n    @Test\n    public void whenPlayerType_thenReturnsExoPlayer() {\n\n        PlayerType playerType = playerInformation.getPlayerType();\n\n        assertThat(playerType).isEqualTo(PlayerType.EXO_PLAYER);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerTwoImplTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport android.net.Uri;\nimport android.view.View;\nimport com.google.android.exoplayer2.ExoPlayerLibraryInfo;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecSelector;\nimport com.google.android.exoplayer2.text.Cue;\nimport com.novoda.noplayer.ContentType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.NoPlayer.StateChangedListener;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.OptionsBuilder;\nimport com.novoda.noplayer.PlayerInformation;\nimport com.novoda.noplayer.PlayerSurfaceHolder;\nimport com.novoda.noplayer.PlayerType;\nimport com.novoda.noplayer.PlayerView;\nimport com.novoda.noplayer.internal.Heart;\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator;\nimport com.novoda.noplayer.internal.exoplayer.forwarder.ExoPlayerForwarder;\nimport com.novoda.noplayer.internal.listeners.PlayerListenersHolder;\nimport com.novoda.noplayer.model.LoadTimeout;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport com.novoda.noplayer.model.TextCues;\nimport com.novoda.noplayer.model.Timeout;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.experimental.runners.Enclosed;\nimport org.junit.rules.ExpectedException;\nimport org.junit.runner.RunWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mock;\nimport org.mockito.invocation.InvocationOnMock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.mockito.stubbing.Answer;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static android.provider.CalendarContract.CalendarCache.URI;\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\n@RunWith(Enclosed.class)\npublic class ExoPlayerTwoImplTest {\n\n    private static final long TWO_MINUTES_IN_MILLIS = 120000;\n    private static final long TEN_SECONDS = 10;\n\n    private static final int WIDTH = 120;\n    private static final int HEIGHT = 160;\n    private static final int ANY_ROTATION_DEGREES = 30;\n    private static final int ANY_PIXEL_WIDTH_HEIGHT = 75;\n\n    private static final boolean IS_BEATING = true;\n    private static final boolean IS_NOT_BEATING = false;\n\n    private static final Options OPTIONS = new OptionsBuilder().withContentType(ContentType.DASH).build();\n    private static final Timeout ANY_TIMEOUT = Timeout.fromSeconds(TEN_SECONDS);\n    private static final NoPlayer.LoadTimeoutCallback ANY_LOAD_TIMEOUT_CALLBACK = new NoPlayer.LoadTimeoutCallback() {\n        @Override\n        public void onLoadTimeout() {\n\n        }\n    };\n    private static final int INDEX_INTERNAL_VIDEO_SIZE_CHANGED_LISTENER = 0;\n\n    public static class GivenVideoNotLoaded extends Base {\n\n        @Rule\n        public ExpectedException thrown = ExpectedException.none();\n\n        @Test\n        public void whenInitialisingPlayer_thenBindsListenersToForwarder() {\n            player.initialise();\n\n            verify(forwarder).bind(preparedListener, player);\n            verify(forwarder).bind(completionListener, stateChangedListener);\n            verify(forwarder).bind(errorListener);\n            verify(forwarder).bind(bufferStateListener);\n            verify(forwarder).bind(videoSizeChangedListener);\n            verify(forwarder).bind(bitrateChangedListener);\n            verify(forwarder).bind(infoListener);\n        }\n\n        @Test\n        public void whenInitialisingPlayer_thenBindsHeart() {\n            player.initialise();\n\n            verify(listenersHolder).getHeartbeatCallbacks();\n            verify(heart).bind(any(Heart.Heartbeat.class));\n        }\n\n        @Test\n        public void givenPlayerIsInitialised_whenVideoIsPrepared_thenCancelsTimeout() {\n            player.initialise();\n\n            ArgumentCaptor<NoPlayer.PreparedListener> argumentCaptor = ArgumentCaptor.forClass(NoPlayer.PreparedListener.class);\n\n            verify(listenersHolder).addPreparedListener(argumentCaptor.capture());\n            NoPlayer.PreparedListener preparedListener = argumentCaptor.getValue();\n            preparedListener.onPrepared(player);\n\n            verify(loadTimeout).cancel();\n        }\n\n        @Test\n        public void givenPlayerIsInitialised_whenVideoHasError_thenPlayerResourcesAreReleased_andNotListeners() {\n            player.initialise();\n\n            ArgumentCaptor<NoPlayer.ErrorListener> argumentCaptor = ArgumentCaptor.forClass(NoPlayer.ErrorListener.class);\n\n            verify(listenersHolder).addErrorListener(argumentCaptor.capture());\n            NoPlayer.ErrorListener errorListener = argumentCaptor.getValue();\n            errorListener.onError(mock(NoPlayer.PlayerError.class));\n\n            verify(listenersHolder).resetState();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(exoPlayerFacade).release();\n            verify(listenersHolder, never()).clear();\n            verify(stateChangedListener, never()).onVideoStopped();\n        }\n\n        @Test\n        public void givenPlayerIsInitialised_andPlayerViewIsAttached_whenVideoSizeChanges_thenPlayerVideoWidthAndHeightMatches() {\n            player.initialise();\n            player.attach(playerView);\n\n            ArgumentCaptor<NoPlayer.VideoSizeChangedListener> argumentCaptor = ArgumentCaptor.forClass(NoPlayer.VideoSizeChangedListener.class);\n            verify(listenersHolder, times(2)).addVideoSizeChangedListener(argumentCaptor.capture());\n\n            NoPlayer.VideoSizeChangedListener videoSizeChangedListener = argumentCaptor.getAllValues().get(INDEX_INTERNAL_VIDEO_SIZE_CHANGED_LISTENER);\n            videoSizeChangedListener.onVideoSizeChanged(WIDTH, HEIGHT, ANY_ROTATION_DEGREES, ANY_PIXEL_WIDTH_HEIGHT);\n\n            int actualWidth = player.videoWidth();\n            int actualHeight = player.videoHeight();\n\n            assertThat(actualWidth).isEqualTo(WIDTH);\n            assertThat(actualHeight).isEqualTo(HEIGHT);\n        }\n\n        @Test\n        public void givenPlayerIsInitialised_whenAttachingPlayerView_thenAddsPlayerViewVideoSizeChangedListenerToListenersHolder() {\n            player.initialise();\n\n            player.attach(playerView);\n\n            ArgumentCaptor<NoPlayer.VideoSizeChangedListener> argumentCaptor = ArgumentCaptor.forClass(NoPlayer.VideoSizeChangedListener.class);\n            verify(listenersHolder, times(2)).addVideoSizeChangedListener(argumentCaptor.capture());\n            NoPlayer.VideoSizeChangedListener videoSizeChangedListener = argumentCaptor.getAllValues().get(1);\n            assertThat(videoSizeChangedListener).isSameAs(playerView.getVideoSizeChangedListener());\n        }\n\n        @Test\n        public void whenStopping_thenPlayerResourcesAreReleased() {\n\n            player.stop();\n\n            verify(listenersHolder).resetState();\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(exoPlayerFacade).release();\n        }\n\n        @Test\n        public void whenReleasing_thenPlayerResourcesAreReleased() {\n\n            player.release();\n\n            verify(listenersHolder).resetState();\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(exoPlayerFacade).release();\n            verify(listenersHolder).clear();\n        }\n\n        @Test\n        public void givenAttachedPlayerView_whenStopping_thenPlayerResourcesAreReleased() {\n            player.attach(playerView);\n\n            player.stop();\n\n            verify(listenersHolder).resetState();\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(containerView).setVisibility(View.GONE);\n            verify(exoPlayerFacade).release();\n        }\n\n        @Test\n        public void givenAttachedPlayerView_whenReleasing_thenPlayerResourcesAreReleased() {\n            player.attach(playerView);\n\n            player.release();\n\n            verify(listenersHolder).resetState();\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(containerView).setVisibility(View.GONE);\n            verify(exoPlayerFacade).release();\n            verify(listenersHolder).clear();\n        }\n\n        @Test\n        public void whenLoadingVideo_thenDelegatesLoadingToFacade() {\n            player.attach(playerView);\n\n            player.loadVideo(uri, OPTIONS);\n\n            verify(exoPlayerFacade).loadVideo(playerView.getPlayerSurfaceHolder(), drmSessionCreator, uri, OPTIONS, forwarder, mediaCodecSelector);\n        }\n\n        @Test\n        public void whenLoadingVideoWithTimeout_thenDelegatesLoadingToFacade() {\n            player.attach(playerView);\n\n            player.loadVideoWithTimeout(uri, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n\n            verify(exoPlayerFacade).loadVideo(playerView.getPlayerSurfaceHolder(), drmSessionCreator, uri, OPTIONS, forwarder, mediaCodecSelector);\n        }\n\n        @Test\n        public void whenLoadingVideoWithTimeout_thenStartsLoadTimeout() {\n            player.attach(playerView);\n\n            player.loadVideoWithTimeout(uri, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n\n            verify(loadTimeout).start(ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n        }\n\n        @Test\n        public void whenGettingPlayerInformation_thenReturnsPlayerInformation() {\n            PlayerInformation playerInformation = player.getPlayerInformation();\n\n            assertThat(playerInformation.getPlayerType()).isEqualTo(PlayerType.EXO_PLAYER);\n            assertThat(playerInformation.getVersion()).isEqualTo(ExoPlayerLibraryInfo.VERSION);\n        }\n\n        @Test\n        public void whenQueryingIsPlaying_thenReturnsFalse() {\n\n            boolean isPlaying = player.isPlaying();\n\n            assertThat(isPlaying).isFalse();\n        }\n\n        @Test\n        public void whenAttachingPlayerView_thenAddsVideoSizeChangedListener() {\n\n            player.attach(playerView);\n\n            verify(listenersHolder).addVideoSizeChangedListener(videoSizeChangedListener);\n        }\n\n        @Test\n        public void whenAttachingPlayerView_thenAddsStateChangedListener() {\n\n            player.attach(playerView);\n\n            verify(listenersHolder).addStateChangedListener(stateChangeListener);\n        }\n\n        @Test\n        public void givenAttachedPlayerView_whenDetachingPlayerView_thenRemovesVideoSizeChangedListener() {\n            player.attach(playerView);\n\n            player.detach(playerView);\n\n            verify(listenersHolder).removeVideoSizeChangedListener(videoSizeChangedListener);\n        }\n\n        @Test\n        public void givenAttachedPlayerView_whenDetachingPlayerView_thenRemovesStateChangedListener() {\n            player.attach(playerView);\n\n            player.detach(playerView);\n\n            verify(listenersHolder).removeStateChangedListener(stateChangeListener);\n        }\n\n        @Test\n        public void givenAttachedPlayerView_whenLoadingVideo_thenMakesContainerVisible() {\n            player.attach(playerView);\n\n            player.loadVideo(uri, OPTIONS);\n\n            verify(containerView).setVisibility(View.VISIBLE);\n        }\n\n        @Test\n        public void givenPlayerHasPlayedVideo_whenLoadingVideo_thenPlayerIsReleased_andNotListeners() {\n            given(exoPlayerFacade.hasPlayedContent()).willReturn(true);\n            player.attach(playerView);\n\n            player.loadVideo(URI, OPTIONS);\n\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(exoPlayerFacade).release();\n            verify(listenersHolder, never()).clear();\n        }\n\n        @Test\n        public void givenPlayerHasPlayedVideo_whenLoadingVideoWithTimeout_thenPlayerResourcesAreReleased_andNotListeners() {\n            given(exoPlayerFacade.hasPlayedContent()).willReturn(true);\n            player.attach(playerView);\n\n            player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(exoPlayerFacade).release();\n            verify(listenersHolder, never()).clear();\n        }\n\n        @Test\n        public void givenPlayerHasNotPlayedVideo_whenLoadingVideo_thenPlayerResourcesAreNotReleased() {\n            given(exoPlayerFacade.hasPlayedContent()).willReturn(false);\n            player.attach(playerView);\n\n            player.loadVideo(URI, OPTIONS);\n\n            verify(stateChangedListener, never()).onVideoStopped();\n            verify(loadTimeout, never()).cancel();\n            verify(heart, never()).stopBeatingHeart();\n            verify(exoPlayerFacade, never()).release();\n        }\n\n        @Test\n        public void givenPlayerHasNotPlayedVideo_whenLoadingVideoWithTimeout_thenPlayerResourcesAreNotReleased() {\n            given(exoPlayerFacade.hasPlayedContent()).willReturn(false);\n            player.attach(playerView);\n\n            player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n\n            verify(stateChangedListener, never()).onVideoStopped();\n            verify(loadTimeout, never()).cancel();\n            verify(heart, never()).stopBeatingHeart();\n            verify(exoPlayerFacade, never()).release();\n        }\n    }\n\n    public static class GivenAttachedAndVideoIsLoaded extends Base {\n\n        private static final float ANY_VOLUME = 0.4f;\n\n        @Override\n        public void setUp() {\n            super.setUp();\n            player.attach(playerView);\n            player.loadVideo(uri, OPTIONS);\n        }\n\n        @Test\n        public void whenLoadingVideo_thenAddsStateChangedListenerToListenersHolder() {\n\n            player.loadVideo(uri, OPTIONS);\n\n            verify(listenersHolder).addStateChangedListener(playerView.getStateChangedListener());\n        }\n\n        @Test\n        public void whenLoadingVideo_thenAddsVideoSizeChangedListenerToListenersHolder() {\n\n            player.loadVideo(uri, OPTIONS);\n\n            verify(listenersHolder).addVideoSizeChangedListener(playerView.getVideoSizeChangedListener());\n        }\n\n        @Test\n        public void whenReleasing_thenResetsFacade() {\n            player.release();\n\n            verify(exoPlayerFacade).release();\n        }\n\n        @Test\n        public void whenStartingPlayback_thenStartsBeatingHeart() {\n\n            player.play();\n\n            verify(heart).startBeatingHeart();\n        }\n\n        @Test\n        public void whenPausing_thenNotifiesStateListenersThatVideoIsPaused() {\n            player.pause();\n\n            verify(stateChangedListener).onVideoPaused();\n        }\n\n        @Test\n        public void givenHeartIsBeating_whenPausing_thenStopsBeatingHeart() {\n            given(heart.isBeating()).willReturn(IS_BEATING);\n\n            player.pause();\n\n            verify(heart).stopBeatingHeart();\n        }\n\n        @Test\n        public void givenHeartIsBeating_whenPausing_thenForcesHeartBeat() {\n            given(heart.isBeating()).willReturn(IS_BEATING);\n\n            player.pause();\n\n            verify(heart).forceBeat();\n        }\n\n        @Test\n        public void givenHeartIsNotBeating_whenPausing_thenDoesNotStopBeatingHeart() {\n            given(heart.isBeating()).willReturn(IS_NOT_BEATING);\n\n            player.pause();\n\n            verify(heart, never()).stopBeatingHeart();\n        }\n\n        @Test\n        public void givenHeartIsNotBeating_whenPausing_thenDoesNotForceHeartBeat() {\n            given(heart.isBeating()).willReturn(IS_NOT_BEATING);\n\n            player.pause();\n\n            verify(heart, never()).forceBeat();\n        }\n\n        @Test\n        public void whenSeeking_thenSeeksToPosition() {\n            long seekPositionInMillis = TWO_MINUTES_IN_MILLIS;\n\n            player.seekTo(seekPositionInMillis);\n\n            verify(exoPlayerFacade).seekTo(seekPositionInMillis);\n        }\n\n        @Test\n        public void whenStartingPlayback_andSurfaceHolderIsReady_thenPlaysFacadeWithSurfaceHolder() {\n            player.play();\n\n            verify(exoPlayerFacade).play();\n        }\n\n        @Test\n        public void whenStartingPlayAtVideoPosition_thenSeeksToPosition() {\n            player.playAt(TWO_MINUTES_IN_MILLIS);\n\n            verify(exoPlayerFacade).seekTo(TWO_MINUTES_IN_MILLIS);\n        }\n\n        @Test\n        public void whenStartingPlayAtVideoPosition_thenStartsBeatingHeart() {\n            player.playAt(TWO_MINUTES_IN_MILLIS);\n\n            verify(heart).startBeatingHeart();\n        }\n\n        @Test\n        public void whenStartingPlay_thenNotifiesStateListenersThatVideoIsPlaying() {\n\n            player.play();\n\n            verify(stateChangedListener).onVideoPlaying();\n        }\n\n        @Test\n        public void whenStartingPlayAtVideoPosition_thenNotifiesStateListenersThatVideoIsPlaying() {\n            player.playAt(TWO_MINUTES_IN_MILLIS);\n\n            verify(stateChangedListener).onVideoPlaying();\n        }\n\n        @Test\n        public void whenSelectingSubtitlesTrack_thenShowsPlayerSubtitlesView() {\n            PlayerSubtitleTrack playerSubtitleTrack = PlayerSubtitleTrackFixture.anInstance().build();\n\n            player.showSubtitleTrack(playerSubtitleTrack);\n\n            verify(playerView).showSubtitles();\n        }\n\n        @Test\n        public void givenSelectingSubtitleTrackSuceeds_whenSelectingSubtitlesTrack_thenReturnsTrue() {\n            PlayerSubtitleTrack playerSubtitleTrack = mock(PlayerSubtitleTrack.class);\n            given(exoPlayerFacade.selectSubtitleTrack(playerSubtitleTrack)).willReturn(true);\n\n            boolean success = player.showSubtitleTrack(playerSubtitleTrack);\n\n            assertThat(success).isTrue();\n        }\n\n        @Test\n        public void givenSelectingSubtitleTrackFails_whenSelectingSubtitlesTrack_thenReturnsFalse() {\n            PlayerSubtitleTrack playerSubtitleTrack = mock(PlayerSubtitleTrack.class);\n            given(exoPlayerFacade.selectSubtitleTrack(playerSubtitleTrack)).willReturn(false);\n\n            boolean success = player.showSubtitleTrack(playerSubtitleTrack);\n\n            assertThat(success).isFalse();\n        }\n\n        @Test\n        public void givenPlayerHasLoadedSubtitleCues_whenSelectingSubtitlesTrack_thenSetsSubtitleCuesOnView() {\n            TextCues textCues = givenPlayerHasLoadedSubtitleCues();\n\n            PlayerSubtitleTrack playerSubtitleTrack = PlayerSubtitleTrackFixture.anInstance().build();\n\n            player.showSubtitleTrack(playerSubtitleTrack);\n\n            verify(playerView).setSubtitleCue(textCues);\n        }\n\n        private TextCues givenPlayerHasLoadedSubtitleCues() {\n            final List<Cue> cueList = Arrays.asList(new Cue(\"first cue\"), new Cue(\"secondCue\"));\n            doAnswer(new Answer() {\n                @Override\n                public Object answer(InvocationOnMock invocation) {\n                    TextRendererOutput output = invocation.getArgument(0);\n                    output.output().onCues(cueList);\n                    return null;\n                }\n            }).when(exoPlayerFacade).setSubtitleRendererOutput(any(TextRendererOutput.class));\n            return ExoPlayerCueMapper.map(cueList);\n        }\n\n        @Test\n        public void whenClearingSubtitles_thenHidesPlayerSubtitlesView() {\n            player.hideSubtitleTrack();\n\n            verify(playerView).hideSubtitles();\n        }\n\n        @Test\n        public void whenSetRepeating_thenSetRepeating() {\n            player.setRepeating(false);\n\n            verify(exoPlayerFacade).setRepeating(false);\n        }\n\n        @Test\n        public void whenSetVolume_thenSetVolumeOnExoPlayer() {\n            player.setVolume(ANY_VOLUME);\n\n            verify(exoPlayerFacade).setVolume(ANY_VOLUME);\n        }\n\n        @Test\n        public void whenGetVolume_thenReturnVolumeFromExoPlayer() {\n            given(exoPlayerFacade.getVolume()).willReturn(ANY_VOLUME);\n\n            float currentVolume = player.getVolume();\n\n            assertThat(currentVolume).isEqualTo(ANY_VOLUME);\n        }\n    }\n\n    public abstract static class Base {\n\n        @Rule\n        public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n        @Mock\n        ExoPlayerForwarder forwarder;\n        @Mock\n        LoadTimeout loadTimeout;\n        @Mock\n        Heart heart;\n        @Mock\n        Uri uri;\n        @Mock\n        PlayerView playerView;\n        @Mock\n        StateChangedListener stateChangeListener;\n        @Mock\n        NoPlayer.VideoSizeChangedListener videoSizeChangedListener;\n        @Mock\n        PlayerListenersHolder listenersHolder;\n        @Mock\n        NoPlayer.ErrorListener errorListener;\n        @Mock\n        NoPlayer.PreparedListener preparedListener;\n        @Mock\n        NoPlayer.BufferStateListener bufferStateListener;\n        @Mock\n        NoPlayer.CompletionListener completionListener;\n        @Mock\n        NoPlayer.StateChangedListener stateChangedListener;\n        @Mock\n        NoPlayer.InfoListener infoListener;\n        @Mock\n        NoPlayer.BitrateChangedListener bitrateChangedListener;\n        @Mock\n        ExoPlayerFacade exoPlayerFacade;\n        @Mock\n        DrmSessionCreator drmSessionCreator;\n        @Mock\n        MediaCodecSelector mediaCodecSelector;\n        @Mock\n        View containerView;\n        @Mock\n        PlayerSurfaceHolder playerSurfaceHolder;\n\n        ExoPlayerTwoImpl player;\n\n        @Before\n        public void setUp() {\n            given(playerView.getPlayerSurfaceHolder()).willReturn(playerSurfaceHolder);\n            given(playerView.getStateChangedListener()).willReturn(stateChangeListener);\n            given(playerView.getVideoSizeChangedListener()).willReturn(videoSizeChangedListener);\n            given(playerView.getContainerView()).willReturn(containerView);\n\n            given(listenersHolder.getErrorListeners()).willReturn(errorListener);\n            given(listenersHolder.getPreparedListeners()).willReturn(preparedListener);\n            given(listenersHolder.getBufferStateListeners()).willReturn(bufferStateListener);\n            given(listenersHolder.getCompletionListeners()).willReturn(completionListener);\n            given(listenersHolder.getStateChangedListeners()).willReturn(stateChangedListener);\n            given(listenersHolder.getInfoListeners()).willReturn(infoListener);\n            given(listenersHolder.getVideoSizeChangedListeners()).willReturn(videoSizeChangedListener);\n            given(listenersHolder.getBitrateChangedListeners()).willReturn(bitrateChangedListener);\n\n            player = new ExoPlayerTwoImpl(\n                    exoPlayerFacade,\n                    listenersHolder,\n                    forwarder,\n                    loadTimeout,\n                    heart,\n                    drmSessionCreator,\n                    mediaCodecSelector\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/NoPlayerExoPlayerCreatorTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport android.content.Context;\n\nimport com.novoda.noplayer.internal.exoplayer.drm.DrmSessionCreator;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.verify;\n\npublic class NoPlayerExoPlayerCreatorTest {\n\n    private static final boolean USE_SECURE_CODEC = true;\n    private static final boolean ALLOW_CROSS_PROTOCOL_REDIRECTS = true;\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    private ExoPlayerTwoImpl player;\n    @Mock\n    private Context context;\n    @Mock\n    private DrmSessionCreator drmSessionCreator;\n    @Mock\n    private NoPlayerExoPlayerCreator.InternalCreator internalCreator;\n\n    private NoPlayerExoPlayerCreator creator;\n\n    @Before\n    public void setUp() {\n        given(internalCreator.create(context, drmSessionCreator, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS)).willReturn(player);\n        creator = new NoPlayerExoPlayerCreator(internalCreator);\n    }\n\n    @Test\n    public void whenCreatingExoPlayerTwo_thenInitialisesPlayer() {\n        creator.createExoPlayer(context, drmSessionCreator, USE_SECURE_CODEC, ALLOW_CROSS_PROTOCOL_REDIRECTS);\n\n        verify(player).initialise();\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/PlayerSubtitleTrackFixture.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\n\nclass PlayerSubtitleTrackFixture {\n\n    private int groupIndex = 0;\n    private int formatIndex = 0;\n    private String trackId = \"trackId\";\n    private String language = \"language\";\n    private String mimeType = \"text/vtt\";\n    private int numberOfChannels = 1;\n    private int frequency = 4;\n\n    private PlayerSubtitleTrackFixture() {\n        // use anInstance() to get an instance\n    }\n\n    static PlayerSubtitleTrackFixture anInstance() {\n        return new PlayerSubtitleTrackFixture();\n    }\n\n    PlayerSubtitleTrackFixture withGroupIndex(int groupIndex) {\n        this.groupIndex = groupIndex;\n        return this;\n    }\n\n    PlayerSubtitleTrackFixture withFormatIndex(int formatIndex) {\n        this.formatIndex = formatIndex;\n        return this;\n    }\n\n    PlayerSubtitleTrackFixture withTrackId(String trackId) {\n        this.trackId = trackId;\n        return this;\n    }\n\n    PlayerSubtitleTrackFixture withLanguage(String language) {\n        this.language = language;\n        return this;\n    }\n\n    PlayerSubtitleTrackFixture withMimeType(String mimeType) {\n        this.mimeType = mimeType;\n        return this;\n    }\n\n    PlayerSubtitleTrackFixture withNumberOfChannels(int numberOfChannels) {\n        this.numberOfChannels = numberOfChannels;\n        return this;\n    }\n\n    PlayerSubtitleTrackFixture withFrequency(int frequency) {\n        this.frequency = frequency;\n        return this;\n    }\n\n    PlayerSubtitleTrack build() {\n        return new PlayerSubtitleTrack(groupIndex, formatIndex, trackId, language, mimeType, numberOfChannels, frequency);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/SecurityDowngradingCodecSelectorTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer;\n\nimport com.google.android.exoplayer2.mediacodec.MediaCodecUtil;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.verify;\n\npublic class SecurityDowngradingCodecSelectorTest {\n\n    private static final String ANY_MIME_TYPE = \"mimeType\";\n\n    private static final boolean CONTENT_SECURE = true;\n    private static final boolean CONTENT_INSECURE = false;\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    private SecurityDowngradingCodecSelector.InternalMediaCodecUtil internalMediaCodecUtil;\n\n    @Test\n    public void whenContentIsSecure_thenRequiresSecureDecoderIsFalse() throws MediaCodecUtil.DecoderQueryException {\n        SecurityDowngradingCodecSelector securityDowngradingCodecSelector = new SecurityDowngradingCodecSelector(internalMediaCodecUtil);\n\n        securityDowngradingCodecSelector.getDecoderInfos(ANY_MIME_TYPE, CONTENT_SECURE);\n\n        ArgumentCaptor<Boolean> argumentCaptor = ArgumentCaptor.forClass(Boolean.class);\n        verify(internalMediaCodecUtil).getDecoderInfos(eq(ANY_MIME_TYPE), argumentCaptor.capture());\n        assertThat(argumentCaptor.getValue()).isFalse();\n    }\n\n    @Test\n    public void whenContentIsInsecure_thenRequiresSecureDecoderIsFalse() throws MediaCodecUtil.DecoderQueryException {\n        SecurityDowngradingCodecSelector securityDowngradingCodecSelector = new SecurityDowngradingCodecSelector(internalMediaCodecUtil);\n\n        securityDowngradingCodecSelector.getDecoderInfos(ANY_MIME_TYPE, CONTENT_INSECURE);\n\n        ArgumentCaptor<Boolean> argumentCaptor = ArgumentCaptor.forClass(Boolean.class);\n        verify(internalMediaCodecUtil).getDecoderInfos(eq(ANY_MIME_TYPE), argumentCaptor.capture());\n        assertThat(argumentCaptor.getValue()).isFalse();\n    }\n\n    @Test\n    public void whenGettingPassthroughDecoderInfo_thenDelegates() throws MediaCodecUtil.DecoderQueryException {\n        SecurityDowngradingCodecSelector securityDowngradingCodecSelector = new SecurityDowngradingCodecSelector(internalMediaCodecUtil);\n\n        securityDowngradingCodecSelector.getPassthroughDecoderInfo();\n\n        verify(internalMediaCodecUtil).getPassthroughDecoderInfo();\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/drm/DrmSessionCreatorFactoryTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.os.Handler;\n\nimport com.novoda.noplayer.UnableToCreatePlayerException;\nimport com.novoda.noplayer.drm.DownloadedModularDrm;\nimport com.novoda.noplayer.drm.DrmHandler;\nimport com.novoda.noplayer.drm.DrmType;\nimport com.novoda.noplayer.drm.StreamingModularDrm;\nimport com.novoda.noplayer.internal.drm.provision.ProvisionExecutorCreator;\nimport com.novoda.noplayer.internal.utils.AndroidDeviceVersion;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport utils.ExceptionMatcher;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\n\npublic class DrmSessionCreatorFactoryTest {\n\n    private static final AndroidDeviceVersion UNSUPPORTED_MEDIA_DRM_DEVICE_VERSION = new AndroidDeviceVersion(17);\n    private static final DrmHandler IGNORED_DRM_HANDLER = DrmHandler.NO_DRM;\n    private static final AndroidDeviceVersion SUPPORTED_MEDIA_DRM_DEVICE = new AndroidDeviceVersion(18);\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n    @Rule\n    public ExpectedException thrown = ExpectedException.none();\n\n    @Mock\n    private Handler handler;\n    @Mock\n    private DownloadedModularDrm downloadedModularDrm;\n    @Mock\n    private StreamingModularDrm streamingModularDrm;\n    @Mock\n    private ProvisionExecutorCreator provisionExecutorCreator;\n\n    private DrmSessionCreatorFactory drmSessionCreatorFactory;\n\n    @Before\n    public void setUp() {\n        drmSessionCreatorFactory = new DrmSessionCreatorFactory(SUPPORTED_MEDIA_DRM_DEVICE, provisionExecutorCreator, handler);\n    }\n\n    @Test\n    public void givenDrmTypeNone_whenCreatingDrmSessionCreator_thenReturnsNoDrmSession() throws DrmSessionCreatorException {\n        DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(DrmType.NONE, IGNORED_DRM_HANDLER);\n\n        assertThat(drmSessionCreator).isInstanceOf(NoDrmSessionCreator.class);\n    }\n\n    @Test\n    public void givenDrmTypeWidevineClassic_whenCreatingDrmSessionCreator_thenReturnsNoDrmSession() throws DrmSessionCreatorException {\n        DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_CLASSIC, IGNORED_DRM_HANDLER);\n\n        assertThat(drmSessionCreator).isInstanceOf(NoDrmSessionCreator.class);\n    }\n\n    @Test\n    public void givenDrmTypeWidevineModularStream_whenCreatingDrmSessionCreator_thenReturnsStreaming() throws DrmSessionCreatorException {\n        DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_MODULAR_STREAM, streamingModularDrm);\n\n        assertThat(drmSessionCreator).isInstanceOf(StreamingDrmSessionCreator.class);\n    }\n\n    @Test\n    public void givenDrmTypeWidevineModularStream_andAndroidVersionDoesNotSupportMediaDrmApis_whenCreatingDrmSessionCreator_thenThrowsUnableToCreatePlayerException() throws DrmSessionCreatorException {\n        drmSessionCreatorFactory = new DrmSessionCreatorFactory(UNSUPPORTED_MEDIA_DRM_DEVICE_VERSION, provisionExecutorCreator, handler);\n\n        String message = \"Device must be target: 18 but was: 17 for DRM type: WIDEVINE_MODULAR_STREAM\";\n        thrown.expect(ExceptionMatcher.matches(message, UnableToCreatePlayerException.class));\n\n        drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_MODULAR_STREAM, IGNORED_DRM_HANDLER);\n    }\n\n    @Test\n    public void givenDrmTypeWidevineModularDownload_whenCreatingDrmSessionCreator_thenReturnsDownload() throws DrmSessionCreatorException {\n        DrmSessionCreator drmSessionCreator = drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_MODULAR_DOWNLOAD, downloadedModularDrm);\n\n        assertThat(drmSessionCreator).isInstanceOf(DownloadDrmSessionCreator.class);\n    }\n\n    @Test\n    public void givenDrmTypeWidevineDownloadStream_andAndroidVersionDoesNotSupportMediaDrmApis_whenCreatingDrmSessionCreator_thenThrowsUnableToCreatePlayerException() throws DrmSessionCreatorException {\n        drmSessionCreatorFactory = new DrmSessionCreatorFactory(UNSUPPORTED_MEDIA_DRM_DEVICE_VERSION, provisionExecutorCreator, handler);\n\n        String message = \"Device must be target: 18 but was: 17 for DRM type: WIDEVINE_MODULAR_DOWNLOAD\";\n        thrown.expect(ExceptionMatcher.matches(message, UnableToCreatePlayerException.class));\n\n        drmSessionCreatorFactory.createFor(DrmType.WIDEVINE_MODULAR_DOWNLOAD, IGNORED_DRM_HANDLER);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/drm/LocalDrmSessionManagerTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.drm;\n\nimport android.media.MediaCryptoException;\nimport android.media.MediaDrmException;\nimport android.media.ResourceBusyException;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.support.annotation.RequiresApi;\n\nimport com.google.android.exoplayer2.drm.DefaultDrmSessionManager;\nimport com.google.android.exoplayer2.drm.DrmInitData;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.ExoMediaDrm;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCrypto;\nimport com.google.android.exoplayer2.drm.FrameworkMediaCryptoFixture;\nimport com.novoda.noplayer.model.KeySetId;\n\nimport java.util.Collections;\nimport java.util.UUID;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.verify;\n\npublic class LocalDrmSessionManagerTest {\n\n    private static final Looper IGNORED_LOOPER = null;\n    private static final DrmInitData IGNORED_DRM_DATA = null;\n\n    private static final KeySetId KEY_SET_ID_TO_RESTORE = KeySetId.of(new byte[12]);\n    private static final SessionId SESSION_ID = SessionId.of(new byte[10]);\n    private static final UUID DRM_SCHEME = UUID.randomUUID();\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n    @Rule\n    public ExpectedException thrown = ExpectedException.none();\n\n    @Mock\n    private ExoMediaDrm<FrameworkMediaCrypto> mediaDrm;\n    @Mock\n    private Handler handler;\n    @Mock\n    private DefaultDrmSessionManager.EventListener eventListener;\n\n    private LocalDrmSessionManager localDrmSessionManager;\n    private FrameworkMediaCrypto frameworkMediaCrypto;\n\n    @Before\n    public void setUp() throws MediaDrmException, MediaCryptoException {\n        frameworkMediaCrypto = FrameworkMediaCryptoFixture.aFrameworkMediaCrypto().build();\n        given(mediaDrm.openSession()).willReturn(SESSION_ID.asBytes());\n\n        localDrmSessionManager = new LocalDrmSessionManager(\n                KEY_SET_ID_TO_RESTORE,\n                mediaDrm,\n                DRM_SCHEME,\n                handler,\n                eventListener\n        );\n    }\n\n    @Test\n    public void givenDrmDataContainsDrmScheme_whenCheckingCanAcquireSession_thenReturnsTrue() {\n        DrmInitData.SchemeData recognisedSchemeData = new DrmInitData.SchemeData(\n                DRM_SCHEME, \"ANY_MIME_TYPE\", new byte[]{}\n        );\n        DrmInitData drmInitData = new DrmInitData(Collections.singletonList(recognisedSchemeData));\n\n        boolean canAcquireSession = localDrmSessionManager.canAcquireSession(drmInitData);\n\n        assertThat(canAcquireSession).isTrue();\n    }\n\n    @Test\n    public void givenDrmDataDoesNotContainDrmScheme_whenCheckingCanAcquireSession_thenReturnsFalse() {\n        DrmInitData.SchemeData unrecognisedSchemeData = new DrmInitData.SchemeData(\n                UUID.randomUUID(), \"ANY_MIME_TYPE\", new byte[]{}\n        );\n        DrmInitData drmInitData = new DrmInitData(Collections.singletonList(unrecognisedSchemeData));\n\n        boolean canAcquireSession = localDrmSessionManager.canAcquireSession(drmInitData);\n\n        assertThat(canAcquireSession).isFalse();\n    }\n\n    @Test\n    public void givenValidMediaDrm_whenAcquiringSession_thenRestoresKeys() throws MediaCryptoException {\n        given(mediaDrm.createMediaCrypto(SESSION_ID.asBytes())).willReturn(frameworkMediaCrypto);\n\n        localDrmSessionManager.acquireSession(IGNORED_LOOPER, IGNORED_DRM_DATA);\n\n        verify(mediaDrm).restoreKeys(SESSION_ID.asBytes(), KEY_SET_ID_TO_RESTORE.asBytes());\n    }\n\n    @Test\n    public void givenValidMediaDrm_whenAcquiringSession_thenReturnsLocalDrmSession() throws MediaCryptoException {\n        given(mediaDrm.createMediaCrypto(SESSION_ID.asBytes())).willReturn(frameworkMediaCrypto);\n\n        DrmSession<FrameworkMediaCrypto> drmSession = localDrmSessionManager.acquireSession(IGNORED_LOOPER, IGNORED_DRM_DATA);\n\n        LocalDrmSession localDrmSession = new LocalDrmSession(frameworkMediaCrypto, KEY_SET_ID_TO_RESTORE, SESSION_ID);\n        assertThat(drmSession).isEqualTo(localDrmSession);\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.KITKAT)\n    @Test\n    public void givenOpeningSessionError_whenAcquiringSession_thenNotifiesErrorEventListenerOnHandler() throws MediaDrmException {\n        given(mediaDrm.openSession()).willThrow(new ResourceBusyException(\"resource is busy\"));\n\n        localDrmSessionManager.acquireSession(IGNORED_LOOPER, IGNORED_DRM_DATA);\n\n        ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class);\n        verify(handler).post(argumentCaptor.capture());\n        argumentCaptor.getValue().run();\n        verify(eventListener).onDrmSessionManagerError(any(DrmSession.DrmSessionException.class));\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.KITKAT)\n    @Test\n    public void givenOpeningSessionError_whenAcquiringSession_thenReturnsInvalidDrmSession() throws MediaDrmException {\n        ResourceBusyException resourceBusyException = new ResourceBusyException(\"resource is busy\");\n        given(mediaDrm.openSession()).willThrow(resourceBusyException);\n\n        DrmSession<FrameworkMediaCrypto> drmSession = localDrmSessionManager.acquireSession(IGNORED_LOOPER, IGNORED_DRM_DATA);\n\n        assertThat(drmSession).isInstanceOf(InvalidDrmSession.class);\n        assertThat(drmSession.getError().getCause()).isEqualTo(resourceBusyException);\n    }\n\n    @Test\n    public void givenAcquiredSession_whenReleasingSession_thenClosesCurrentSession() {\n        DrmSession<FrameworkMediaCrypto> drmSession = new LocalDrmSession(frameworkMediaCrypto, KEY_SET_ID_TO_RESTORE, SESSION_ID);\n\n        localDrmSessionManager.releaseSession(drmSession);\n\n        verify(mediaDrm).closeSession(SESSION_ID.asBytes());\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/error/ErrorFormatterTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.error;\n\nimport android.media.MediaCodec;\n\nimport org.junit.Test;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.mock;\n\npublic class ErrorFormatterTest {\n\n    private static final String MESSAGE = \"message\";\n\n    @Test\n    public void givenThrowable_whenFormattingMessage_thenReturnsExpectedMessageFormat() {\n        String expectedFormat = \"com.novoda.noplayer.internal.exoplayer.error.ErrorFormatterTest$IncorrectFormatThrowable: message\";\n\n        String actualFormat = ErrorFormatter.formatMessage(new IncorrectFormatThrowable(MESSAGE));\n\n        assertThat(actualFormat).isEqualTo(expectedFormat);\n    }\n\n    @Test\n    public void givenMediaCodecException_whenFormattingMessage_thenReturnsExpectedMessageFormat() {\n        MediaCodec.CodecException codecException = mock(MediaCodec.CodecException.class);\n        given(codecException.getDiagnosticInfo()).willReturn(\"android.media.MediaCodec.error_+1234\");\n        given(codecException.isTransient()).willReturn(true);\n        given(codecException.isRecoverable()).willReturn(false);\n\n        String expectedFormat = \"diagnosticInformation=android.media.MediaCodec.error_+1234 : isTransient=true : isRecoverable=false\";\n        String actualFormat = ErrorFormatter.formatCodecException(codecException);\n\n        assertThat(actualFormat).isEqualTo(expectedFormat);\n    }\n\n    private class IncorrectFormatThrowable extends Throwable {\n\n        IncorrectFormatThrowable(String message) {\n            super(message);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/forwarder/ExoPlayerErrorMapperTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.forwarder;\n\nimport android.net.Uri;\n\nimport com.google.android.exoplayer2.ExoPlaybackException;\nimport com.google.android.exoplayer2.ExoPlaybackExceptionFactory;\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.ParserException;\nimport com.google.android.exoplayer2.audio.AudioDecoderException;\nimport com.google.android.exoplayer2.audio.AudioProcessor;\nimport com.google.android.exoplayer2.audio.AudioSink;\nimport com.google.android.exoplayer2.drm.DecryptionException;\nimport com.google.android.exoplayer2.drm.DrmSession;\nimport com.google.android.exoplayer2.drm.KeysExpiredException;\nimport com.google.android.exoplayer2.drm.UnsupportedDrmException;\nimport com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;\nimport com.google.android.exoplayer2.offline.DownloadException;\nimport com.google.android.exoplayer2.source.ClippingMediaSource;\nimport com.google.android.exoplayer2.source.MergingMediaSource;\nimport com.google.android.exoplayer2.source.ads.AdsMediaSource;\nimport com.google.android.exoplayer2.source.dash.DashManifestStaleException;\nimport com.google.android.exoplayer2.source.hls.SampleQueueMappingException;\nimport com.google.android.exoplayer2.text.SubtitleDecoderException;\nimport com.google.android.exoplayer2.upstream.AssetDataSource;\nimport com.google.android.exoplayer2.upstream.ContentDataSource;\nimport com.google.android.exoplayer2.upstream.DataSourceException;\nimport com.google.android.exoplayer2.upstream.DataSpec;\nimport com.google.android.exoplayer2.upstream.FileDataSource;\nimport com.google.android.exoplayer2.upstream.HttpDataSource;\nimport com.google.android.exoplayer2.upstream.Loader;\nimport com.google.android.exoplayer2.upstream.UdpDataSource;\nimport com.google.android.exoplayer2.upstream.cache.Cache;\nimport com.google.android.exoplayer2.util.PriorityTaskManager;\nimport com.novoda.noplayer.DetailErrorType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.PlayerErrorType;\nimport com.novoda.noplayer.internal.exoplayer.error.ExoPlayerErrorMapper;\n\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Collection;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.junit.runners.Parameterized;\n\nimport static com.novoda.noplayer.DetailErrorType.ADS_LOAD_UNEXPECTED_ERROR_THEN_WILL_SKIP;\nimport static com.novoda.noplayer.DetailErrorType.AD_GROUP_LOAD_ERROR_THEN_WILL_SKIP;\nimport static com.novoda.noplayer.DetailErrorType.AD_LOAD_ERROR_THEN_WILL_SKIP;\nimport static com.novoda.noplayer.DetailErrorType.ALL_ADS_LOAD_ERROR_THEN_WILL_SKIP;\nimport static com.novoda.noplayer.DetailErrorType.AUDIO_DECODER_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.AUDIO_SINK_CONFIGURATION_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.AUDIO_SINK_INITIALISATION_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.AUDIO_SINK_WRITE_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.AUDIO_UNHANDLED_FORMAT_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.CACHE_WRITING_DATA_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT;\nimport static com.novoda.noplayer.DetailErrorType.CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_NOT_SEEKABLE_TO_START;\nimport static com.novoda.noplayer.DetailErrorType.DATA_POSITION_OUT_OF_RANGE_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.DECODING_SUBTITLE_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.DOWNLOAD_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.DRM_INSTANTIATION_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.DRM_KEYS_EXPIRED_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.DRM_SESSION_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.FAIL_DECRYPT_DATA_DUE_NON_PLATFORM_COMPONENT_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.HTTP_CANNOT_CLOSE_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.HTTP_CANNOT_OPEN_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.HTTP_CANNOT_READ_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.INITIALISATION_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.LIVE_STALE_MANIFEST_AND_NEW_MANIFEST_COULD_NOT_LOAD_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.MEDIA_REQUIRES_DRM_SESSION_MANAGER_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.MERGING_MEDIA_SOURCE_CANNOT_MERGE_ITS_SOURCES;\nimport static com.novoda.noplayer.DetailErrorType.MULTIPLE_RENDERER_MEDIA_CLOCK_ENABLED_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.PARSING_MEDIA_DATA_OR_METADATA_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.READING_LOCAL_FILE_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.READ_CONTENT_URI_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.READ_FROM_UDP_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.READ_LOCAL_ASSET_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.SAMPLE_QUEUE_MAPPING_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.TASK_CANNOT_PROCEED_PRIORITY_TOO_LOW;\nimport static com.novoda.noplayer.DetailErrorType.UNEXPECTED_LOADING_ERROR;\nimport static com.novoda.noplayer.DetailErrorType.UNSUPPORTED_DRM_SCHEME_ERROR;\nimport static com.novoda.noplayer.PlayerErrorType.CONNECTIVITY;\nimport static com.novoda.noplayer.PlayerErrorType.CONTENT_DECRYPTION;\nimport static com.novoda.noplayer.PlayerErrorType.DRM;\nimport static com.novoda.noplayer.PlayerErrorType.RENDERER_DECODER;\nimport static com.novoda.noplayer.PlayerErrorType.SOURCE;\nimport static com.novoda.noplayer.PlayerErrorType.UNEXPECTED;\nimport static org.fest.assertions.api.Assertions.assertThat;\n\n@RunWith(Parameterized.class)\npublic class ExoPlayerErrorMapperTest {\n\n    @Parameterized.Parameter\n    public PlayerErrorType playerErrorType;\n    @Parameterized.Parameter(1)\n    public DetailErrorType detailErrorType;\n    @Parameterized.Parameter(2)\n    public ExoPlaybackException exoPlaybackException;\n\n    @Parameterized.Parameters(name = \"{0} with detail {1} is mapped from {2}\")\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(\n                new Object[]{SOURCE, SAMPLE_QUEUE_MAPPING_ERROR, createSource(new SampleQueueMappingException(\"mimetype-sample\"))},\n                new Object[]{SOURCE, READING_LOCAL_FILE_ERROR, createSource(new FileDataSource.FileDataSourceException(new IOException()))},\n                new Object[]{SOURCE, UNEXPECTED_LOADING_ERROR, createSource(new Loader.UnexpectedLoaderException(new Throwable()))},\n                new Object[]{SOURCE, LIVE_STALE_MANIFEST_AND_NEW_MANIFEST_COULD_NOT_LOAD_ERROR, createSource(new DashManifestStaleException())},\n                new Object[]{SOURCE, DOWNLOAD_ERROR, createSource(new DownloadException(\"download-exception\"))},\n                new Object[]{SOURCE, AD_LOAD_ERROR_THEN_WILL_SKIP, createSource(AdsMediaSource.AdLoadException.createForAd(new Exception()))},\n                new Object[]{SOURCE, AD_GROUP_LOAD_ERROR_THEN_WILL_SKIP, createSource(AdsMediaSource.AdLoadException.createForAdGroup(new Exception(), 0))},\n                new Object[]{SOURCE, ALL_ADS_LOAD_ERROR_THEN_WILL_SKIP, createSource(AdsMediaSource.AdLoadException.createForAllAds(new Exception()))},\n                new Object[]{SOURCE, ADS_LOAD_UNEXPECTED_ERROR_THEN_WILL_SKIP, createSource(AdsMediaSource.AdLoadException.createForUnexpected(new RuntimeException()))},\n                new Object[]{SOURCE, MERGING_MEDIA_SOURCE_CANNOT_MERGE_ITS_SOURCES, createSource(new MergingMediaSource.IllegalMergeException(MergingMediaSource.IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH))},\n                new Object[]{SOURCE, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT, createSource(new ClippingMediaSource.IllegalClippingException(ClippingMediaSource.IllegalClippingException.REASON_INVALID_PERIOD_COUNT))},\n                new Object[]{SOURCE, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_NOT_SEEKABLE_TO_START, createSource(new ClippingMediaSource.IllegalClippingException(ClippingMediaSource.IllegalClippingException.REASON_NOT_SEEKABLE_TO_START))},\n                new Object[]{SOURCE, CLIPPING_MEDIA_SOURCE_CANNOT_CLIP_WRAPPED_SOURCE_INVALID_PERIOD_COUNT, createSource(new ClippingMediaSource.IllegalClippingException(ClippingMediaSource.IllegalClippingException.REASON_INVALID_PERIOD_COUNT))},\n                new Object[]{SOURCE, TASK_CANNOT_PROCEED_PRIORITY_TOO_LOW, createSource(new PriorityTaskManager.PriorityTooLowException(1, 2))},\n                new Object[]{SOURCE, PARSING_MEDIA_DATA_OR_METADATA_ERROR, createSource(new ParserException())},\n                new Object[]{SOURCE, CACHE_WRITING_DATA_ERROR, createSource(new Cache.CacheException(\"cache-exception\"))},\n                new Object[]{SOURCE, READ_LOCAL_ASSET_ERROR, createSource(new AssetDataSource.AssetDataSourceException(new IOException()))},\n                new Object[]{SOURCE, DATA_POSITION_OUT_OF_RANGE_ERROR, createSource(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE))},\n\n                new Object[]{CONNECTIVITY, HTTP_CANNOT_OPEN_ERROR, createSource(new HttpDataSource.HttpDataSourceException(new DataSpec(Uri.EMPTY, 0), HttpDataSource.HttpDataSourceException.TYPE_OPEN))},\n                new Object[]{CONNECTIVITY, HTTP_CANNOT_READ_ERROR, createSource(new HttpDataSource.HttpDataSourceException(new DataSpec(Uri.EMPTY, 0), HttpDataSource.HttpDataSourceException.TYPE_READ))},\n                new Object[]{CONNECTIVITY, HTTP_CANNOT_CLOSE_ERROR, createSource(new HttpDataSource.HttpDataSourceException(new DataSpec(Uri.EMPTY, 0), HttpDataSource.HttpDataSourceException.TYPE_CLOSE))},\n                new Object[]{CONNECTIVITY, READ_CONTENT_URI_ERROR, createSource(new ContentDataSource.ContentDataSourceException(new IOException()))},\n                new Object[]{CONNECTIVITY, READ_FROM_UDP_ERROR, createSource(new UdpDataSource.UdpDataSourceException(new IOException()))},\n\n                new Object[]{RENDERER_DECODER, AUDIO_SINK_CONFIGURATION_ERROR, createRenderer(new AudioSink.ConfigurationException(\"configuration-exception\"))},\n                new Object[]{RENDERER_DECODER, AUDIO_SINK_INITIALISATION_ERROR, createRenderer(new AudioSink.InitializationException(0, 0, 0, 0))},\n                new Object[]{RENDERER_DECODER, AUDIO_SINK_WRITE_ERROR, createRenderer(new AudioSink.WriteException(0))},\n                new Object[]{RENDERER_DECODER, AUDIO_UNHANDLED_FORMAT_ERROR, createRenderer(new AudioProcessor.UnhandledFormatException(0, 0, 0))},\n                new Object[]{RENDERER_DECODER, AUDIO_DECODER_ERROR, createRenderer(new AudioDecoderException(\"audio-decoder-exception\"))},\n                new Object[]{RENDERER_DECODER, INITIALISATION_ERROR, createRenderer(new MediaCodecRenderer.DecoderInitializationException(Format.createSampleFormat(\"id\", \"sample-mimety[e\", 0), new Throwable(), true, 0))},\n                new Object[]{RENDERER_DECODER, DECODING_SUBTITLE_ERROR, createRenderer(new SubtitleDecoderException(\"metadata-decoder-exception\"))},\n\n                new Object[]{DRM, UNSUPPORTED_DRM_SCHEME_ERROR, createRenderer(new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))},\n                new Object[]{DRM, DRM_INSTANTIATION_ERROR, createRenderer(new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR))},\n                new Object[]{DRM, DRM_SESSION_ERROR, createRenderer(new DrmSession.DrmSessionException(new Throwable()))},\n                new Object[]{DRM, DRM_KEYS_EXPIRED_ERROR, createRenderer(new KeysExpiredException())},\n                new Object[]{DRM, MEDIA_REQUIRES_DRM_SESSION_MANAGER_ERROR, createRenderer(new IllegalStateException())},\n\n                new Object[]{CONTENT_DECRYPTION, FAIL_DECRYPT_DATA_DUE_NON_PLATFORM_COMPONENT_ERROR, createRenderer(new DecryptionException(0, \"decryption-exception\"))},\n\n                new Object[]{UNEXPECTED, MULTIPLE_RENDERER_MEDIA_CLOCK_ENABLED_ERROR, ExoPlaybackExceptionFactory.createForUnexpected(new IllegalStateException(\"Multiple renderer media clocks enabled.\"))},\n                new Object[]{PlayerErrorType.UNKNOWN, DetailErrorType.UNKNOWN, ExoPlaybackExceptionFactory.createForUnexpected(new IllegalStateException(\"Any other exception\"))}\n                // DefaultAudioSink.InvalidAudioTrackTimestampException is private, cannot create\n                // EGLSurfaceTexture.GlException is private, cannot create\n                // PlaylistStuckException constructor is private, cannot create\n                // PlaylistResetException constructor is private, cannot create\n                // MediaCodecUtil.DecoderQueryException constructor is private, cannot create\n                // DefaultDrmSessionManager.MissingSchemeDataException constructor is private, cannot create\n                // Crypto Exceptions cannot be instantiated, it throws a RuntimeException(\"Stub!\")\n        );\n    }\n\n    private static ExoPlaybackException createSource(IOException exception) {\n        return ExoPlaybackException.createForSource(exception);\n    }\n\n    private static ExoPlaybackException createRenderer(Exception exception) {\n        return ExoPlaybackException.createForRenderer(exception, 0);\n    }\n\n    @Test\n    public void mapErrors() {\n        NoPlayer.PlayerError playerError = ExoPlayerErrorMapper.errorFor(exoPlaybackException);\n        assertThat(playerError.type()).isEqualTo(playerErrorType);\n        assertThat(playerError.detailType()).isEqualTo(detailErrorType);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/AudioFormatFixture.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\n\nimport java.util.Collections;\nimport java.util.List;\n\nclass AudioFormatFixture {\n\n    private String id = \"id\";\n    private String sampleMimeType = \"mime_type\";\n    private String codecs = \"codecs\";\n    private int bitrate = 100;\n    private int maxInputSize = 200;\n    private int channelCount = 2;\n    private int sampleRate = 50;\n    private List<byte[]> initializationData = Collections.emptyList();\n    private DrmInitData drmInitData = new DrmInitData(Collections.<DrmInitData.SchemeData>emptyList());\n    private int selectionFlags = 0;\n    private String language = \"english\";\n\n    static AudioFormatFixture anAudioFormat() {\n        return new AudioFormatFixture();\n    }\n\n    AudioFormatFixture withId(String id) {\n        this.id = id;\n        return this;\n    }\n\n    AudioFormatFixture withSampleMimeType(String sampleMimeType) {\n        this.sampleMimeType = sampleMimeType;\n        return this;\n    }\n\n    AudioFormatFixture withCodecs(String codecs) {\n        this.codecs = codecs;\n        return this;\n    }\n\n    AudioFormatFixture withBitrate(int bitrate) {\n        this.bitrate = bitrate;\n        return this;\n    }\n\n    AudioFormatFixture withMaxInputSize(int maxInputSize) {\n        this.maxInputSize = maxInputSize;\n        return this;\n    }\n\n    AudioFormatFixture withChannelCount(int channelCount) {\n        this.channelCount = channelCount;\n        return this;\n    }\n\n    AudioFormatFixture withSampleRate(int sampleRate) {\n        this.sampleRate = sampleRate;\n        return this;\n    }\n\n    AudioFormatFixture withInitializationData(List<byte[]> initializationData) {\n        this.initializationData = initializationData;\n        return this;\n    }\n\n    AudioFormatFixture withDrmInitData(DrmInitData drmInitData) {\n        this.drmInitData = drmInitData;\n        return this;\n    }\n\n    AudioFormatFixture withSelectionFlags(int selectionFlags) {\n        this.selectionFlags = selectionFlags;\n        return this;\n    }\n\n    AudioFormatFixture withLanguage(String language) {\n        this.language = language;\n        return this;\n    }\n\n    Format build() {\n        return Format.createAudioSampleFormat(\n                id,\n                sampleMimeType,\n                codecs,\n                bitrate,\n                maxInputSize,\n                channelCount,\n                sampleRate,\n                initializationData,\n                drmInitData,\n                selectionFlags,\n                language\n        );\n    }\n\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/AudioTrackTypeTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport org.junit.Test;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\n\npublic class AudioTrackTypeTest {\n\n    private static final int MAIN_SELECTION_FLAG = 1;\n    private static final int ALTERNATIVE_SELECTION_FLAG = 0;\n    private static final int RANDOM_SELECTION_FLAG = 2;\n\n    @Test\n    public void givenSelectionFlagIsZero_whenCreatingAudioTrackType_thenReturnsAlternative() {\n        AudioTrackType audioTrackType = AudioTrackType.from(ALTERNATIVE_SELECTION_FLAG);\n\n        assertThat(audioTrackType).isEqualTo(AudioTrackType.ALTERNATIVE);\n    }\n\n    @Test\n    public void givenSelectionFlagIsOne_whenCreatingAudioTrackType_thenReturnsMain() {\n        AudioTrackType audioTrackType = AudioTrackType.from(MAIN_SELECTION_FLAG);\n\n        assertThat(audioTrackType).isEqualTo(AudioTrackType.MAIN);\n    }\n\n    @Test\n    public void givenAnyOtherSelectionFlag_whenCreatingAudioTrackType_thenReturnsUnknown() {\n        AudioTrackType audioTrackType = AudioTrackType.from(RANDOM_SELECTION_FLAG);\n\n        assertThat(audioTrackType).isEqualTo(AudioTrackType.UNKNOWN);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerAudioTrackSelectorTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.novoda.noplayer.internal.exoplayer.RendererTypeRequester;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\n\nimport java.util.Collections;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.anyInt;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.verify;\n\npublic class ExoPlayerAudioTrackSelectorTest {\n\n    private static final String ANY_TRACK_ID = \"any_track_id\";\n    private static final String ANY_LANGUAGE = \"any_language\";\n    private static final String ANY_MIME_TYPE = \"any_mime_type\";\n\n    private static final int ANY_NUMBER_OF_CHANNELS = 2;\n    private static final int ANY_FREQUENCY = 50;\n    private static final int FIRST_GROUP = 0;\n    private static final int FIRST_TRACK = 0;\n    private static final int SECOND_GROUP = 1;\n    private static final int THIRD_TRACK = 2;\n    private static final int MAIN_AUDIO_TRACK_TYPE = 1;\n\n    private static final Format AUDIO_FORMAT = AudioFormatFixture.anAudioFormat().withId(\"id1\").withSelectionFlags(MAIN_AUDIO_TRACK_TYPE).build();\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    private ExoPlayerTrackSelector trackSelector;\n    @Mock\n    private RendererTypeRequester rendererTypeRequester;\n\n    private ExoPlayerAudioTrackSelector exoPlayerAudioTrackSelector;\n    private static final PlayerAudioTrack AUDIO_TRACK = new PlayerAudioTrack(SECOND_GROUP, THIRD_TRACK, ANY_TRACK_ID, ANY_LANGUAGE, ANY_MIME_TYPE, ANY_NUMBER_OF_CHANNELS, ANY_FREQUENCY, AudioTrackType.MAIN);\n\n    @Before\n    public void setUp() {\n        exoPlayerAudioTrackSelector = new ExoPlayerAudioTrackSelector(trackSelector);\n    }\n\n    @Test\n    public void givenTrackSelectorContainsTracks_whenSelectingAudioTrack_thenSelectsTrack() {\n        TrackGroupArray trackGroups = givenTrackSelectorContainsTracks();\n\n        ArgumentCaptor<DefaultTrackSelector.SelectionOverride> argumentCaptor = whenSelectingAudioTrack(trackGroups);\n\n        DefaultTrackSelector.SelectionOverride selectionOverride = argumentCaptor.getValue();\n        assertThat(selectionOverride.groupIndex).isEqualTo(SECOND_GROUP);\n        assertThat(selectionOverride.tracks).contains(THIRD_TRACK);\n    }\n\n    @Test\n    public void givenTrackSelectorContainsUnsupportedTracks_whenGettingAudioTracks_thenReturnsOnlySupportedTracks() {\n        givenTrackSelectorContainsUnsupportedTracks();\n\n        AudioTracks actualAudioTracks = exoPlayerAudioTrackSelector.getAudioTracks(rendererTypeRequester);\n\n        assertThat(actualAudioTracks).isEqualTo(expectedSupportedAudioTracks());\n    }\n\n    private TrackGroupArray givenTrackSelectorContainsTracks() {\n        TrackGroupArray trackGroups = new TrackGroupArray(\n                new TrackGroup(AudioFormatFixture.anAudioFormat().build()),\n                new TrackGroup(\n                        AudioFormatFixture.anAudioFormat().build(),\n                        AudioFormatFixture.anAudioFormat().build(),\n                        AudioFormatFixture.anAudioFormat().build()\n                )\n        );\n        given(trackSelector.trackGroups(TrackType.AUDIO, rendererTypeRequester)).willReturn(trackGroups);\n\n        return trackGroups;\n    }\n\n    private ArgumentCaptor<DefaultTrackSelector.SelectionOverride> whenSelectingAudioTrack(TrackGroupArray trackGroups) {\n        exoPlayerAudioTrackSelector.selectAudioTrack(AUDIO_TRACK, rendererTypeRequester);\n\n        ArgumentCaptor<DefaultTrackSelector.SelectionOverride> argumentCaptor = ArgumentCaptor.forClass(DefaultTrackSelector.SelectionOverride.class);\n        verify(trackSelector).setSelectionOverride(eq(TrackType.AUDIO), any(RendererTypeRequester.class), eq(trackGroups), argumentCaptor.capture());\n        return argumentCaptor;\n    }\n\n    private void givenTrackSelectorContainsUnsupportedTracks() {\n        TrackGroupArray trackGroups = new TrackGroupArray(\n                new TrackGroup(AUDIO_FORMAT),\n                new TrackGroup(\n                        AudioFormatFixture.anAudioFormat().build(),\n                        AudioFormatFixture.anAudioFormat().build(),\n                        AudioFormatFixture.anAudioFormat().build()\n                )\n        );\n        given(trackSelector.trackGroups(TrackType.AUDIO, rendererTypeRequester)).willReturn(trackGroups);\n        given(trackSelector.supportsTrackSwitching(eq(TrackType.AUDIO), any(RendererTypeRequester.class), any(TrackGroupArray.class), anyInt()))\n                .willReturn(true)\n                .willReturn(false);\n    }\n\n    private AudioTracks expectedSupportedAudioTracks() {\n        return AudioTracks.from(\n                Collections.singletonList(\n                        new PlayerAudioTrack(\n                                FIRST_GROUP,\n                                FIRST_TRACK,\n                                AUDIO_FORMAT.id,\n                                AUDIO_FORMAT.language,\n                                AUDIO_FORMAT.sampleMimeType,\n                                AUDIO_FORMAT.channelCount,\n                                AUDIO_FORMAT.bitrate,\n                                AudioTrackType.from(AUDIO_FORMAT.selectionFlags)\n\n                        )\n                )\n        );\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelectorTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.source.TrackGroup;\nimport com.google.android.exoplayer2.source.TrackGroupArray;\nimport com.google.android.exoplayer2.trackselection.DefaultTrackSelector;\nimport com.google.android.exoplayer2.trackselection.TrackSelection;\nimport com.novoda.noplayer.ContentType;\nimport com.novoda.noplayer.internal.exoplayer.RendererTypeRequester;\nimport com.novoda.noplayer.internal.utils.Optional;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static com.novoda.noplayer.internal.exoplayer.mediasource.VideoFormatFixture.aVideoFormat;\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.verify;\n\npublic class ExoPlayerVideoTrackSelectorTest {\n\n    private static final Format VIDEO_FORMAT = aVideoFormat().withId(\"id1\").build();\n    private static final PlayerVideoTrack PLAYER_VIDEO_TRACK = new PlayerVideoTrack(\n            0,\n            0,\n            VIDEO_FORMAT.id,\n            ContentType.HLS,\n            VIDEO_FORMAT.width,\n            VIDEO_FORMAT.height,\n            (int) VIDEO_FORMAT.frameRate,\n            VIDEO_FORMAT.bitrate\n    );\n\n    private static final Format ADDITIONAL_VIDEO_FORMAT = aVideoFormat().withId(\"id2\").build();\n    private static final int FIRST_GROUP = 0;\n    private static final int SECOND_TRACK = 1;\n    private static final PlayerVideoTrack ADDITIONAL_PLAYER_VIDEO_TRACK = new PlayerVideoTrack(\n            FIRST_GROUP,\n            SECOND_TRACK,\n            ADDITIONAL_VIDEO_FORMAT.id,\n            ContentType.HLS,\n            ADDITIONAL_VIDEO_FORMAT.width,\n            ADDITIONAL_VIDEO_FORMAT.height,\n            (int) ADDITIONAL_VIDEO_FORMAT.frameRate,\n            ADDITIONAL_VIDEO_FORMAT.bitrate\n    );\n\n    private static final List<PlayerVideoTrack> EXPECTED_TRACKS = Arrays.asList(PLAYER_VIDEO_TRACK, ADDITIONAL_PLAYER_VIDEO_TRACK);\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    private ExoPlayerTrackSelector trackSelector;\n    @Mock\n    private TrackSelection.Factory trackSelectionFactory;\n    @Mock\n    private RendererTypeRequester rendererTypeRequester;\n    @Mock\n    private SimpleExoPlayer exoPlayer;\n\n    private ExoPlayerVideoTrackSelector exoPlayerVideoTrackSelector;\n\n    @Before\n    public void setUp() {\n        exoPlayerVideoTrackSelector = new ExoPlayerVideoTrackSelector(trackSelector);\n    }\n\n    @Test\n    public void givenTrackSelectorContainsTracks_whenSelectingVideoTrack_thenSelectsTrack() {\n        givenTrackSelectorContainsTracks();\n\n        ArgumentCaptor<DefaultTrackSelector.SelectionOverride> argumentCaptor = whenSelectingVideoTrack(ADDITIONAL_PLAYER_VIDEO_TRACK);\n\n        DefaultTrackSelector.SelectionOverride selectionOverride = argumentCaptor.getValue();\n        assertThat(selectionOverride.groupIndex).isEqualTo(FIRST_GROUP);\n        assertThat(selectionOverride.tracks).contains(SECOND_TRACK);\n    }\n\n    @Test\n    public void givenTrackSelector_whenGettingVideoTracks_thenReturnsSupportedTracks() {\n        givenTrackSelectorContainsTracks();\n\n        List<PlayerVideoTrack> actualVideoTracks = exoPlayerVideoTrackSelector.getVideoTracks(rendererTypeRequester, ContentType.HLS);\n\n        assertThat(actualVideoTracks).isEqualTo(EXPECTED_TRACKS);\n    }\n\n    @Test\n    public void givenTrackSelector_whenGettingCurrentlySelectedVideoTrack_thenReturnsSelectedTrack() {\n        givenTrackSelectorContainsTracks();\n        given(exoPlayer.getVideoFormat()).willReturn(ADDITIONAL_VIDEO_FORMAT);\n\n        Optional<PlayerVideoTrack> selectedVideoTrack = exoPlayerVideoTrackSelector.getSelectedVideoTrack(exoPlayer, rendererTypeRequester, ContentType.HLS);\n\n        assertThat(selectedVideoTrack).isEqualTo(Optional.of(ADDITIONAL_PLAYER_VIDEO_TRACK));\n    }\n\n    @Test\n    public void givenNoCurrentlySelectedTrack_whenGettingCurrentlySelectedVideoTrack_thenReturnsAbsent() {\n        givenTrackSelectorContainsTracks();\n        given(exoPlayer.getVideoFormat()).willReturn(null);\n\n        Optional<PlayerVideoTrack> selectedVideoTrack = exoPlayerVideoTrackSelector.getSelectedVideoTrack(exoPlayer, rendererTypeRequester, ContentType.HLS);\n\n        assertThat(selectedVideoTrack).isEqualTo(Optional.<PlayerVideoTrack>absent());\n    }\n\n    @Test\n    public void givenTrackSelector_whenClearMaxVideoBitrate_thenClearsMaxVideoBitrate() {\n        givenTrackSelectorContainsTracks();\n\n        exoPlayerVideoTrackSelector.clearMaxVideoBitrate();\n\n        verify(trackSelector).clearMaxVideoBitrate();\n    }\n\n    @Test\n    public void givenTrackSelector_whenSetMaxVideoBitrate1000000_thenSetsMaxVideoBitrate1000000() {\n        givenTrackSelectorContainsTracks();\n\n        exoPlayerVideoTrackSelector.setMaxVideoBitrate(1000000);\n\n        verify(trackSelector).setMaxVideoBitrate(1000000);\n    }\n\n    private void givenTrackSelectorContainsTracks() {\n        TrackGroupArray trackGroups = new TrackGroupArray(\n                new TrackGroup(VIDEO_FORMAT, ADDITIONAL_VIDEO_FORMAT)\n        );\n        given(trackSelector.trackGroups(TrackType.VIDEO, rendererTypeRequester)).willReturn(trackGroups);\n    }\n\n    private ArgumentCaptor<DefaultTrackSelector.SelectionOverride> whenSelectingVideoTrack(PlayerVideoTrack videoTrack) {\n        exoPlayerVideoTrackSelector.selectVideoTrack(videoTrack, rendererTypeRequester);\n\n        ArgumentCaptor<DefaultTrackSelector.SelectionOverride> argumentCaptor = ArgumentCaptor.forClass(DefaultTrackSelector.SelectionOverride.class);\n        verify(trackSelector).setSelectionOverride(eq(TrackType.VIDEO), any(RendererTypeRequester.class), any(TrackGroupArray.class), argumentCaptor.capture());\n        return argumentCaptor;\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/RendererTrackIndexExtractorTest.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.C;\nimport com.novoda.noplayer.internal.exoplayer.RendererTypeRequester;\nimport com.novoda.noplayer.internal.utils.Optional;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\n\npublic class RendererTrackIndexExtractorTest {\n\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    private RendererTrackIndexExtractor extractor;\n\n    @Before\n    public void setUp() throws Exception {\n        extractor = new RendererTrackIndexExtractor();\n    }\n\n    @Test\n    public void givenAudioTrackAtPositionZero_whenExtractingAudioIndex_thenReturnsIndexZero() {\n        Optional<Integer> audioIndex = extractor.extract(TrackType.AUDIO, 1, rendererTypeRequesterAudioTrack);\n        int expectedAudioIndex = 0;\n\n        assertThat(audioIndex.get()).isEqualTo(expectedAudioIndex);\n    }\n\n    @Test\n    public void givenVideoTrackAtPositionZero_whenExtractingVideoIndex_thenReturnsIndexZero() {\n        Optional<Integer> videoIndex = extractor.extract(TrackType.VIDEO, 1, rendererTypeRequesterVideoTrack);\n        int expectedVideoIndex = 0;\n\n        assertThat(videoIndex.get()).isEqualTo(expectedVideoIndex);\n    }\n\n    @Test\n    public void givenSubtitlesTrackAtPositionZero_whenExtractingTextIndex_thenReturnsIndexZero() {\n        Optional<Integer> textIndex = extractor.extract(TrackType.TEXT, 1, rendererTypeRequesterTextTrack);\n        int expectedTextIndex = 0;\n\n        assertThat(textIndex.get()).isEqualTo(expectedTextIndex);\n    }\n\n    @Test\n    public void givenThreeTrackTypes_whenExtractingAudioIndexes_thenReturnsIndexOne() {\n        Optional<Integer> audioIndex = extractor.extract(TrackType.AUDIO, 3, rendererTypeRequesterVideoAudioTextTrack);\n        int expectedAudioIndex = 1;\n\n        assertThat(audioIndex.get()).isEqualTo(expectedAudioIndex);\n    }\n\n    @Test\n    public void givenNoAudioTrack_whenExtractingAudioIndex_thenReturnsEmpty() {\n        Optional<Integer> audioIndex = extractor.extract(TrackType.AUDIO, 1, emptyRendererTypeRequester);\n\n        assertThat(audioIndex.isAbsent()).isTrue();\n    }\n\n    @Test\n    public void givenNoVideoTrack_whenExtractingVideoIndex_thenReturnsEmpty() {\n        Optional<Integer> videoIndex = extractor.extract(TrackType.VIDEO, 1, emptyRendererTypeRequester);\n\n        assertThat(videoIndex.isAbsent()).isTrue();\n    }\n\n    @Test\n    public void givenNoTextTrack_whenExtractingTextIndex_thenReturnsEmpty() {\n        Optional<Integer> textIndex = extractor.extract(TrackType.TEXT, 1, emptyRendererTypeRequester);\n\n        assertThat(textIndex.isAbsent()).isTrue();\n    }\n\n    private RendererTypeRequester rendererTypeRequesterAudioTrack = new RendererTypeRequester() {\n        @Override\n        public int getRendererTypeFor(int index) {\n            if (index == 0) {\n                return C.TRACK_TYPE_AUDIO;\n            }\n\n            return -1;\n        }\n    };\n\n    private RendererTypeRequester rendererTypeRequesterVideoTrack = new RendererTypeRequester() {\n        @Override\n        public int getRendererTypeFor(int index) {\n            if (index == 0) {\n                return C.TRACK_TYPE_VIDEO;\n            }\n\n            return -1;\n        }\n    };\n\n    private RendererTypeRequester rendererTypeRequesterTextTrack = new RendererTypeRequester() {\n        @Override\n        public int getRendererTypeFor(int index) {\n            if (index == 0) {\n                return C.TRACK_TYPE_TEXT;\n            }\n\n            return -1;\n        }\n    };\n\n    private RendererTypeRequester emptyRendererTypeRequester = new RendererTypeRequester() {\n        @Override\n        public int getRendererTypeFor(int index) {\n            return -1;\n        }\n    };\n\n    private RendererTypeRequester rendererTypeRequesterVideoAudioTextTrack = new RendererTypeRequester() {\n        @Override\n        public int getRendererTypeFor(int index) {\n            switch (index) {\n                case 0:\n                    return C.TRACK_TYPE_VIDEO;\n                case 1:\n                    return C.TRACK_TYPE_AUDIO;\n                case 2:\n                    return C.TRACK_TYPE_TEXT;\n                default:\n                    return -1;\n            }\n        }\n    };\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/VideoFormatFixture.java",
    "content": "package com.novoda.noplayer.internal.exoplayer.mediasource;\n\nimport com.google.android.exoplayer2.Format;\nimport com.google.android.exoplayer2.drm.DrmInitData;\n\nimport java.util.Collections;\nimport java.util.List;\n\npublic class VideoFormatFixture {\n\n    private String id = \"id\";\n    private String sampleMimeType = \"mime_type\";\n    private String codecs = \"codecs\";\n    private int bitrate = 100;\n    private int maxInputSize = 200;\n    private int width = 1920;\n    private int height = 1080;\n    private float frameRate;\n    private List<byte[]> initializationData = Collections.emptyList();\n    private DrmInitData drmInitData = new DrmInitData(Collections.<DrmInitData.SchemeData>emptyList());\n\n    public static VideoFormatFixture aVideoFormat() {\n        return new VideoFormatFixture();\n    }\n\n    private VideoFormatFixture() {\n        // Uses static factory method.\n    }\n\n    public VideoFormatFixture withId(String id) {\n        this.id = id;\n        return this;\n    }\n\n    public VideoFormatFixture withSampleMimeType(String sampleMimeType) {\n        this.sampleMimeType = sampleMimeType;\n        return this;\n    }\n\n    public VideoFormatFixture withCodecs(String codecs) {\n        this.codecs = codecs;\n        return this;\n    }\n\n    public VideoFormatFixture withBitrate(int bitrate) {\n        this.bitrate = bitrate;\n        return this;\n    }\n\n    public VideoFormatFixture withMaxInputSize(int maxInputSize) {\n        this.maxInputSize = maxInputSize;\n        return this;\n    }\n\n    public VideoFormatFixture withWidth(int width) {\n        this.width = width;\n        return this;\n    }\n\n    public VideoFormatFixture withHeight(int height) {\n        this.height = height;\n        return this;\n    }\n\n    public VideoFormatFixture withFrameRate(float frameRate) {\n        this.frameRate = frameRate;\n        return this;\n    }\n\n    public VideoFormatFixture withInitializationData(List<byte[]> initializationData) {\n        this.initializationData = initializationData;\n        return this;\n    }\n\n    public VideoFormatFixture withDrmInitData(DrmInitData drmInitData) {\n        this.drmInitData = drmInitData;\n        return this;\n    }\n\n    public Format build() {\n        return Format.createVideoSampleFormat(\n                id,\n                sampleMimeType,\n                codecs,\n                bitrate,\n                maxInputSize,\n                width,\n                height,\n                frameRate,\n                initializationData,\n                drmInitData\n        );\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/listeners/BufferStateListenersTest.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.mockito.Mockito.verify;\n\npublic class BufferStateListenersTest {\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    private NoPlayer.BufferStateListener aBufferStateListener;\n\n    @Mock\n    private NoPlayer.BufferStateListener anotherBufferStateListener;\n\n    private BufferStateListeners bufferStateListeners;\n\n    @Before\n    public void setUp() {\n        bufferStateListeners = new BufferStateListeners();\n        bufferStateListeners.add(aBufferStateListener);\n        bufferStateListeners.add(anotherBufferStateListener);\n    }\n\n    @Test\n    public void givenBufferStateListeners_whenNotifyingOfBufferStarted_thenAllTheListenersAreNotifiedAppropriately() {\n\n        bufferStateListeners.onBufferStarted();\n\n        verify(aBufferStateListener).onBufferStarted();\n        verify(anotherBufferStateListener).onBufferStarted();\n    }\n\n    @Test\n    public void givenBufferStateListeners_whenNotifyingOfBufferCompleted_thenAllTheListenersAreNotifiedAppropriately() {\n\n        bufferStateListeners.onBufferCompleted();\n\n        verify(aBufferStateListener).onBufferCompleted();\n        verify(anotherBufferStateListener).onBufferCompleted();\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/listeners/CompletionListenersTest.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.reset;\nimport static org.mockito.Mockito.verify;\n\npublic class CompletionListenersTest {\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    private NoPlayer.CompletionListener completionListener;\n\n    private CompletionListeners completionListeners;\n\n    @Before\n    public void setUp() {\n        completionListeners = new CompletionListeners();\n        completionListeners.add(completionListener);\n    }\n\n    @Test\n    public void whenCallingOnCompletion_thenNotifiesOnCompletion() {\n        completionListeners.onCompletion();\n\n        verify(completionListener).onCompletion();\n    }\n\n    @Test\n    public void whenCallingOnCompletionTwice_thenDoesNothing() {\n        completionListeners.onCompletion();\n        reset(completionListener);\n\n        completionListeners.onCompletion();\n\n        verify(completionListener, never()).onCompletion();\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/listeners/StateChangedListenersTest.java",
    "content": "package com.novoda.noplayer.internal.listeners;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.utils.NoPlayerLog;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.mockito.Mockito.verify;\n\npublic class StateChangedListenersTest {\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    private NoPlayer.StateChangedListener stateChangedListener;\n\n    private StateChangedListeners stateChangedListeners;\n\n    @Before\n    public void setUp() {\n        NoPlayerLog.setLoggingEnabled(false);\n        stateChangedListeners = new StateChangedListeners();\n        stateChangedListeners.add(stateChangedListener);\n    }\n\n    @Test\n    public void whenDoubleCallingOnVideoPlaying_thenEmitsOnlyFirstOnVideoPlayingEvent() {\n        stateChangedListeners.onVideoPlaying();\n        stateChangedListeners.onVideoPlaying();\n\n        verify(stateChangedListener).onVideoPlaying();\n    }\n\n    @Test\n    public void whenDoubleCallingOnVideoPaused_thenEmitsOnlyFirstOnVideoPausedEvent() {\n        stateChangedListeners.onVideoPaused();\n        stateChangedListeners.onVideoPaused();\n\n        verify(stateChangedListener).onVideoPaused();\n    }\n\n    @Test\n    public void whenDoubleCallingOnVideoStopped_thenEmitsOnlyFirstOnVideoStoppedEvent() {\n        stateChangedListeners.onVideoStopped();\n        stateChangedListeners.onVideoStopped();\n\n        verify(stateChangedListener).onVideoStopped();\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerAudioTrackSelectorTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\n\nimport java.util.Arrays;\nimport java.util.Collections;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport utils.ExceptionMatcher;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.verify;\nimport static utils.ExceptionMatcher.matches;\n\npublic class AndroidMediaPlayerAudioTrackSelectorTest {\n\n    @Rule\n    public ExpectedException thrown = ExpectedException.none();\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    private static final int NO_FORMAT = 0;\n    private static final int NO_CHANNELS = -1;\n    private static final int NO_FREQUENCY = -1;\n    private static final int AUDIO_TRACK_INDEX = 2;\n\n    private static final String NO_MIME_TYPE = \"\";\n    private static final String ANY_LANGUAGE = \"english\";\n\n    private static final NoPlayerTrackInfo AUDIO_TRACK_INFO = mock(NoPlayerTrackInfo.class);\n    private static final NoPlayerTrackInfo VIDEO_TRACK_INFO = mock(NoPlayerTrackInfo.class);\n    private static final NoPlayerTrackInfo UNKNOWN_TRACK_INFO = mock(NoPlayerTrackInfo.class);\n\n    @Mock\n    private TrackInfosFactory trackInfosFactory;\n    @Mock\n    private MediaPlayer mediaPlayer;\n    @Mock\n    private PlayerAudioTrack playerAudioTrack;\n\n    private AndroidMediaPlayerAudioTrackSelector trackSelector;\n\n    @Before\n    public void setUp() {\n        trackSelector = new AndroidMediaPlayerAudioTrackSelector(trackInfosFactory);\n    }\n\n    @Test\n    public void givenNullMediaPlayer_whenGettingAudioTracks_thenThrowsIllegalState() {\n        thrown.expect(ExceptionMatcher.matches(\"You can only call getAudioTracks() when video is prepared.\", IllegalStateException.class));\n\n        trackSelector.getAudioTracks(null);\n    }\n\n    @Test\n    public void givenTrackSelectorContainsUnsupportedTracks_whenGettingAudioTracks_thenReturnsOnlySupportedTracks() {\n        givenTrackSelectorContainsUnsupportedTracks();\n\n        AudioTracks audioTracks = trackSelector.getAudioTracks(mediaPlayer);\n\n        assertThat(audioTracks).isEqualTo(expectedAudioTrack());\n    }\n\n    @Test\n    public void givenNullMediaPlayer_whenSelectingAudioTrack_thenThrowsIllegalState() {\n        thrown.expect(matches(\"You can only call selectAudioTrack() when video is prepared.\", IllegalStateException.class));\n\n        trackSelector.selectAudioTrack(null, mock(PlayerAudioTrack.class));\n    }\n\n    @Test\n    public void whenSelectingAudioTrack_thenMediaPlayerSelectsAudioTrack() {\n        PlayerAudioTrack playerAudioTrack = mock(PlayerAudioTrack.class);\n        given(playerAudioTrack.groupIndex()).willReturn(AUDIO_TRACK_INDEX);\n\n        trackSelector.selectAudioTrack(mediaPlayer, playerAudioTrack);\n\n        verify(mediaPlayer).selectTrack(AUDIO_TRACK_INDEX);\n    }\n\n    private void givenTrackSelectorContainsUnsupportedTracks() {\n        given(AUDIO_TRACK_INFO.type()).willReturn(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO);\n        given(AUDIO_TRACK_INFO.language()).willReturn(ANY_LANGUAGE);\n        given(VIDEO_TRACK_INFO.type()).willReturn(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO);\n        given(VIDEO_TRACK_INFO.language()).willReturn(ANY_LANGUAGE);\n        given(UNKNOWN_TRACK_INFO.type()).willReturn(MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN);\n        given(UNKNOWN_TRACK_INFO.language()).willReturn(ANY_LANGUAGE);\n\n        NoPlayerTrackInfos noPlayerTrackInfos = new NoPlayerTrackInfos(\n                Arrays.asList(\n                        VIDEO_TRACK_INFO,\n                        UNKNOWN_TRACK_INFO,\n                        AUDIO_TRACK_INFO\n                )\n        );\n        given(trackInfosFactory.createFrom(mediaPlayer)).willReturn(noPlayerTrackInfos);\n    }\n\n    private AudioTracks expectedAudioTrack() {\n        return AudioTracks.from(\n                Collections.singletonList(\n                        new PlayerAudioTrack(\n                                AUDIO_TRACK_INDEX,\n                                NO_FORMAT,\n                                String.valueOf(AUDIO_TRACK_INFO.hashCode()),\n                                AUDIO_TRACK_INFO.language(),\n                                NO_MIME_TYPE,\n                                NO_CHANNELS,\n                                NO_FREQUENCY,\n                                AudioTrackType.MAIN\n                        )\n                )\n        );\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacadeTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.content.Context;\nimport android.media.AudioManager;\nimport android.media.MediaPlayer;\nimport android.net.Uri;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport com.novoda.noplayer.SurfaceRequester;\nimport com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder;\nimport com.novoda.noplayer.internal.utils.NoPlayerLog;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.Either;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerAudioTrackFixture;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.rules.ExpectedException;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mock;\nimport org.mockito.invocation.InvocationOnMock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.mockito.stubbing.Answer;\nimport utils.ExceptionMatcher;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.reset;\nimport static org.mockito.Mockito.verify;\n\npublic class AndroidMediaPlayerFacadeTest {\n\n    private static final int ANY_DURATION = 12000;\n    private static final int ANY_POSITION = 60;\n    private static final int ANY_WIDTH = 100;\n    private static final int ANY_HEIGHT = 50;\n    private static final int ANY_ERROR_WHAT = -1;\n    private static final int ANY_ERROR_EXTRA = 404;\n    private static final int TEN_PERCENT = 10;\n    private static final int TEN_SECONDS_IN_MILLIS = 10000;\n    private static final float ANY_VOLUME = 0.5f;\n\n    private static final boolean SCREEN_ON = true;\n    private static final boolean IS_IN_PLAYBACK_STATE = true;\n    private static final boolean IS_NOT_IN_PLAYBACK_STATE = false;\n    private static final boolean IS_PLAYING = true;\n    private static final boolean IS_NOT_PLAYING = false;\n\n    private static final Map<String, String> NO_HEADERS = null;\n    private static final Uri ANY_URI = mock(Uri.class);\n    private static final PlayerAudioTrack PLAYER_AUDIO_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().build();\n    private static final AudioTracks AUDIO_TRACKS = AudioTracks.from(Collections.singletonList(PLAYER_AUDIO_TRACK));\n    private static final String ERROR_MESSAGE = \"Video must be loaded and not in an error state before trying to interact with the player\";\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n    @Rule\n    public ExpectedException thrown = ExpectedException.none();\n\n    @Mock\n    private Context context;\n    @Mock\n    private AndroidMediaPlayerAudioTrackSelector trackSelector;\n    @Mock\n    private PlaybackStateChecker playbackStateChecker;\n    @Mock\n    private MediaPlayerCreator mediaPlayerCreator;\n    @Mock\n    private MediaPlayer mediaPlayer;\n    @Mock\n    private AudioManager audioManager;\n    @Mock\n    private SurfaceRequester surfaceRequester;\n    @Mock\n    private Surface surface;\n    @Mock\n    private MediaPlayer.OnPreparedListener preparedListener;\n    @Mock\n    private MediaPlayer.OnVideoSizeChangedListener videoSizeChangedListener;\n    @Mock\n    private MediaPlayer.OnErrorListener errorListener;\n    @Mock\n    private MediaPlayer.OnCompletionListener completionListener;\n    @Mock\n    private MediaPlayerForwarder forwarder;\n    private Either<Surface, SurfaceHolder> eitherSurface;\n\n    private AndroidMediaPlayerFacade facade;\n\n    @Before\n    public void setUp() {\n        NoPlayerLog.setLoggingEnabled(false);\n\n        facade = new AndroidMediaPlayerFacade(context, forwarder, audioManager, trackSelector, playbackStateChecker, mediaPlayerCreator);\n\n        given(mediaPlayerCreator.createMediaPlayer()).willReturn(mediaPlayer);\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class))).willReturn(IS_IN_PLAYBACK_STATE);\n        eitherSurface = Either.left(surface);\n        givenSurfaceRequesterReturns(eitherSurface);\n\n        given(forwarder.onPreparedListener()).willReturn(preparedListener);\n        given(forwarder.onCompletionListener()).willReturn(completionListener);\n        given(forwarder.onErrorListener()).willReturn(errorListener);\n        given(forwarder.onSizeChangedListener()).willReturn(videoSizeChangedListener);\n    }\n\n    private void givenSurfaceRequesterReturns(final Either<Surface, SurfaceHolder> surface) {\n        doAnswer(new Answer<Void>() {\n            @Override\n            public Void answer(InvocationOnMock invocation) {\n                SurfaceRequester.Callback callback = invocation.getArgument(0);\n                callback.onSurfaceReady(surface);\n                return null;\n            }\n        }).when(surfaceRequester).requestSurface(any(SurfaceRequester.Callback.class));\n    }\n\n    @Test\n    public void whenPreparing_thenRequestsAudioFocus() {\n        givenMediaPlayerIsPrepared();\n\n        verify(audioManager).requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);\n    }\n\n    @Test\n    public void whenPreparing_thenDoesNotReleaseMediaPlayer() {\n        givenMediaPlayerIsPrepared();\n\n        verify(mediaPlayer, never()).reset();\n        verify(mediaPlayer, never()).release();\n    }\n\n    @Test\n    public void whenPreparingMultipleTimes_thenReleasesMediaPlayer() {\n        facade.prepareVideo(ANY_URI, eitherSurface);\n        facade.prepareVideo(ANY_URI, eitherSurface);\n\n        verify(mediaPlayer).reset();\n        verify(mediaPlayer).release();\n    }\n\n    @Test\n    public void whenPreparing_thenSetsDataSource() throws IOException {\n        givenMediaPlayerIsPrepared();\n\n        verify(mediaPlayer).setDataSource(context, ANY_URI, NO_HEADERS);\n    }\n\n    @Test\n    public void givenSurfaceRequesterReturnsSurface_whenPreparing_thenSetsSurface() {\n        Surface surface = mock(Surface.class);\n        Either<Surface, SurfaceHolder> eitherSurface = Either.left(surface);\n        givenSurfaceRequesterReturns(eitherSurface);\n\n        givenMediaPlayerIsPreparedWith(eitherSurface);\n\n        verify(mediaPlayer).setSurface(surface);\n    }\n\n    @Test\n    public void givenSurfaceRequesterReturnsSurfaceHolder_whenPreparing_thenSetsDisplay() {\n        SurfaceHolder surfaceHolder = mock(SurfaceHolder.class);\n        Either<Surface, SurfaceHolder> eitherSurface = Either.right(surfaceHolder);\n        givenSurfaceRequesterReturns(eitherSurface);\n\n        givenMediaPlayerIsPreparedWith(eitherSurface);\n\n        verify(mediaPlayer).setDisplay(surfaceHolder);\n    }\n\n    @Test\n    public void whenPreparing_thenSetsStreamMusicAudioStreamType() {\n        givenMediaPlayerIsPrepared();\n\n        verify(mediaPlayer).setAudioStreamType(AudioManager.STREAM_MUSIC);\n    }\n\n    @Test\n    public void whenPreparing_thenSetsScreenOnWhilePlayerToTrue() {\n        givenMediaPlayerIsPrepared();\n\n        verify(mediaPlayer).setScreenOnWhilePlaying(SCREEN_ON);\n    }\n\n    @Test\n    public void whenPreparing_thenPreparesMediaPlayerAsynchronously() {\n        givenMediaPlayerIsPrepared();\n\n        verify(mediaPlayer).prepareAsync();\n    }\n\n    @Test\n    public void givenExceptionPreparingMediaPlayer_whenPreparingMediaPlayer_thenForwardsOnError() {\n        doAnswer(new Answer<Void>() {\n            @Override\n            public Void answer(InvocationOnMock invocation) {\n                throw new IllegalStateException(\"cannot prepare async\");\n            }\n        }).when(mediaPlayer).prepareAsync();\n\n        givenMediaPlayerIsPrepared();\n        whenErroring();\n\n        verify(errorListener).onError(mediaPlayer, ANY_ERROR_WHAT, ANY_ERROR_EXTRA);\n    }\n\n    @Test\n    public void givenBoundPreparedListener_andMediaPlayerIsPrepared_whenPrepared_thenForwardsOnPrepared() {\n        facade.prepareVideo(ANY_URI, eitherSurface);\n        ArgumentCaptor<MediaPlayer.OnPreparedListener> argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnPreparedListener.class);\n        verify(mediaPlayer).setOnPreparedListener(argumentCaptor.capture());\n        argumentCaptor.getValue().onPrepared(mediaPlayer);\n\n        verify(preparedListener).onPrepared(mediaPlayer);\n    }\n\n    @Test\n    public void givenNoBoundPreparedListener_andMediaPlayerIsPrepared_whenPrepared_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(\"Should bind a OnPreparedListener. Cannot forward events.\", IllegalStateException.class));\n        given(forwarder.onPreparedListener()).willReturn(null);\n        givenMediaPlayerIsPrepared();\n    }\n\n    @Test\n    public void givenBoundVideoSizeChangedListener_andMediaPlayerOnPrepared_whenVideoSizeChanges_thenForwardsSizeChanges() {\n        givenMediaPlayerIsPrepared();\n\n        whenVideoSizeChanges();\n\n        verify(videoSizeChangedListener).onVideoSizeChanged(eq(mediaPlayer), eq(ANY_WIDTH), eq(ANY_HEIGHT));\n    }\n\n    @Test\n    public void givenNoBoundVideoSizeChangedListener_andMediaPlayerIsPrepared_whenVideoSizeChanges_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(\"Should bind a OnVideoSizeChangedListener. Cannot forward events.\", IllegalStateException.class));\n        given(forwarder.onSizeChangedListener()).willReturn(null);\n        givenMediaPlayerIsPrepared();\n\n        whenVideoSizeChanges();\n    }\n\n    @Test\n    public void givenBoundCompletionListener_andMediaPlayerIsPrepared_whenCompleted_thenForwardsCompleted() {\n        givenMediaPlayerIsPrepared();\n\n        whenCompleted();\n\n        verify(completionListener).onCompletion(mediaPlayer);\n    }\n\n    @Test\n    public void givenNoBoundCompletionListener_andMediaPlayerIsPrepared_whenCompleted_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(\"Should bind a OnCompletionListener. Cannot forward events.\", IllegalStateException.class));\n        given(forwarder.onCompletionListener()).willReturn(null);\n        givenMediaPlayerIsPrepared();\n\n        whenCompleted();\n    }\n\n    @Test\n    public void givenBoundErrorListener_andMediaPlayerIsPrepared_whenErroring_thenForwardsError() {\n        givenMediaPlayerIsPrepared();\n\n        whenErroring();\n\n        verify(errorListener).onError(mediaPlayer, ANY_ERROR_WHAT, ANY_ERROR_EXTRA);\n    }\n\n    @Test\n    public void givenNoBoundErrorListener_andMediaPlayerIsPrepared_whenErroring_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(\"Should bind a OnErrorListener. Cannot forward events.\", IllegalStateException.class));\n        given(forwarder.onErrorListener()).willReturn(null);\n        givenMediaPlayerIsPrepared();\n\n        whenErroring();\n    }\n\n    @Test\n    public void givenBoundBufferListener_andMediaPlayerIsPrepared_whenBuffering_thenBufferPercentageIsUpdated() {\n        givenMediaPlayerIsPrepared();\n\n        ArgumentCaptor<MediaPlayer.OnBufferingUpdateListener> argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnBufferingUpdateListener.class);\n        verify(mediaPlayer).setOnBufferingUpdateListener(argumentCaptor.capture());\n        argumentCaptor.getValue().onBufferingUpdate(mediaPlayer, TEN_PERCENT);\n\n        int bufferPercentage = facade.getBufferPercentage();\n        assertThat(bufferPercentage).isEqualTo(TEN_PERCENT);\n    }\n\n    @Test\n    public void givenMediaPlayerIsPrepared_whenReleasing_thenReleasesMediaPlayer() {\n        givenMediaPlayerIsPrepared();\n\n        facade.release();\n\n        verify(mediaPlayer).reset();\n        verify(mediaPlayer).release();\n    }\n\n    @Test\n    public void givenMediaPlayerIsPreparedWithSurface_whenStarting_thenSetsSurface() {\n        givenMediaPlayerIsPrepared();\n        reset(mediaPlayer);\n\n        facade.start(eitherSurface);\n\n        verify(mediaPlayer).setSurface(surface);\n    }\n\n    @Test\n    public void givenMediaPlayerIsPreparedWithSurfaceHolder_whenStarting_thenSetsDisplay() {\n        SurfaceHolder surfaceHolder = mock(SurfaceHolder.class);\n        Either<Surface, SurfaceHolder> eitherSurface = Either.right(surfaceHolder);\n        givenMediaPlayerIsPreparedWith(eitherSurface);\n        reset(mediaPlayer);\n\n        facade.start(eitherSurface);\n\n        verify(mediaPlayer).setDisplay(surfaceHolder);\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotPrepared_whenStarting_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        facade.start(eitherSurface);\n    }\n\n    @Test\n    public void givenMediaPlayerIsPrepared_whenStarting_thenStartsMediaPlayer() {\n        givenMediaPlayerIsPrepared();\n\n        facade.start(eitherSurface);\n\n        verify(mediaPlayer).start();\n    }\n\n    @Test\n    public void givenMediaPlayerIsPlaying_whenPausing_thenPausesMediaPlayer() {\n        givenMediaPlayerIsPrepared();\n        given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_PLAYING);\n\n        facade.pause();\n\n        verify(mediaPlayer).pause();\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotPlaying_whenPausing_thenDoesNotPausesMediaPlayer() {\n        givenMediaPlayerIsPrepared();\n        given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_NOT_PLAYING);\n\n        facade.pause();\n\n        verify(mediaPlayer, never()).pause();\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenPausing_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_NOT_IN_PLAYBACK_STATE);\n\n        facade.pause();\n\n        verify(mediaPlayer).pause();\n    }\n\n    @Test\n    public void givenMediaPlayerIsInPlaybackState_whenGettingDuration_thenReturnsDuration() {\n        givenMediaPlayerIsPrepared();\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_IN_PLAYBACK_STATE);\n        given(mediaPlayer.getDuration()).willReturn(ANY_DURATION);\n\n        int duration = facade.mediaDurationInMillis();\n\n        assertThat(duration).isEqualTo(ANY_DURATION);\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenGettingDuration_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_NOT_IN_PLAYBACK_STATE);\n\n        facade.mediaDurationInMillis();\n    }\n\n    @Test\n    public void givenMediaPlayerIsInPlaybackState_whenGettingPosition_thenReturnsPosition() {\n        givenMediaPlayerIsPrepared();\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_IN_PLAYBACK_STATE);\n        given(mediaPlayer.getCurrentPosition()).willReturn(ANY_POSITION);\n\n        int currentPosition = facade.currentPositionInMillis();\n\n        assertThat(currentPosition).isEqualTo(ANY_POSITION);\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenGettingPosition_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_NOT_IN_PLAYBACK_STATE);\n\n        facade.currentPositionInMillis();\n    }\n\n    @Test\n    public void givenMediaPlayerIsInPlaybackState_whenSeeking_thenSeeksToPosition() {\n        givenMediaPlayerIsPrepared();\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_IN_PLAYBACK_STATE);\n\n        facade.seekTo(TEN_SECONDS_IN_MILLIS);\n\n        verify(mediaPlayer).seekTo(TEN_SECONDS_IN_MILLIS);\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenSeeking_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_NOT_IN_PLAYBACK_STATE);\n\n        facade.seekTo(TEN_SECONDS_IN_MILLIS);\n    }\n\n    @Test\n    public void whenCheckingIsPlaying_thenDelegatesToPlaystateChecker() {\n        givenMediaPlayerIsPrepared();\n        given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_PLAYING);\n\n        boolean playing = facade.isPlaying();\n\n        assertThat(playing).isTrue();\n    }\n\n    @Test\n    public void givenNoMediaPlayer_whenGettingBufferPercentage_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        facade.getBufferPercentage();\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenGettingAudioTracks_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_PLAYING);\n\n        facade.getAudioTracks();\n    }\n\n    @Test\n    public void whenGettingAudioTracks_thenDelegatesToTrackSelector() {\n        givenMediaPlayerIsPrepared();\n        given(trackSelector.getAudioTracks(mediaPlayer)).willReturn(AUDIO_TRACKS);\n\n        AudioTracks audioTracks = facade.getAudioTracks();\n\n        assertThat(audioTracks).isEqualTo(AUDIO_TRACKS);\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenSelectingAudioTracks_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_PLAYING);\n\n        facade.selectAudioTrack(mock(PlayerAudioTrack.class));\n    }\n\n    @Test\n    public void whenSelectingAudioTrack_thenDelegatesToTrackSelector() {\n        givenMediaPlayerIsPrepared();\n        PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class);\n\n        facade.selectAudioTrack(audioTrack);\n\n        verify(trackSelector).selectAudioTrack(mediaPlayer, audioTrack);\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenSelectingSubtitleTrack_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_PLAYING);\n\n        facade.selectSubtitleTrack(mock(PlayerSubtitleTrack.class));\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenClearingSubtitleTrack_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_NOT_IN_PLAYBACK_STATE);\n\n        facade.clearSubtitleTrack();\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenGettingSubtitleTracks_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_NOT_IN_PLAYBACK_STATE);\n\n        facade.getSubtitleTracks();\n    }\n\n    @Test\n    public void whenGettingSubtitleTracks_thenReturnsEmptyList() {\n        givenMediaPlayerIsPrepared();\n\n        List<PlayerSubtitleTrack> subtitleTracks = facade.getSubtitleTracks();\n\n        assertThat(subtitleTracks).isEmpty();\n    }\n\n    @Test\n    public void whenSelectingSubtitleTrack_thenReturnsFalse() {\n        givenMediaPlayerIsPrepared();\n\n        boolean result = facade.selectSubtitleTrack(mock(PlayerSubtitleTrack.class));\n\n        assertThat(result).isFalse();\n    }\n\n    @Test\n    public void whenSettingOnSeekCompleteListener_thenSetsOnSeekCompleteListener() {\n        givenMediaPlayerIsPrepared();\n\n        MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = mock(MediaPlayer.OnSeekCompleteListener.class);\n        facade.setOnSeekCompleteListener(onSeekCompleteListener);\n\n        verify(mediaPlayer).setOnSeekCompleteListener(onSeekCompleteListener);\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenSettingOnSeekCompleteListener_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isInPlaybackState(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_NOT_IN_PLAYBACK_STATE);\n\n        facade.setOnSeekCompleteListener(mock(MediaPlayer.OnSeekCompleteListener.class));\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenGettingVideoTracks_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_PLAYING);\n\n        facade.getVideoTracks();\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenGettingSelectedVideoTrack_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        given(playbackStateChecker.isPlaying(eq(mediaPlayer), any(PlaybackStateChecker.PlaybackState.class)))\n                .willReturn(IS_PLAYING);\n\n        facade.getSelectedVideoTrack();\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenSettingVolume_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        facade.setVolume(ANY_VOLUME);\n    }\n\n    @Test\n    public void whenSettingVolume_thenSetsLeftAndRightVolumeScalars() {\n        givenMediaPlayerIsPrepared();\n\n        facade.setVolume(ANY_VOLUME);\n\n        verify(mediaPlayer).setVolume(ANY_VOLUME, ANY_VOLUME);\n    }\n\n    @Test\n    public void givenMediaPlayerIsNotInPlaybackState_whenGettingVolume_thenThrowsIllegalStateException() {\n        thrown.expect(ExceptionMatcher.matches(ERROR_MESSAGE, IllegalStateException.class));\n\n        facade.getVolume();\n    }\n\n    @Test\n    public void givenNoVolumeWasSet_whenGettingVolume_theReturnsOne() {\n        givenMediaPlayerIsPrepared();\n\n        float currentVolume = facade.getVolume();\n\n        assertThat(currentVolume).isEqualTo(1f);\n    }\n\n    @Test\n    public void givenVolumeWasSet_whenGettingVolume_theReturnsSetVolume() {\n        givenMediaPlayerIsPrepared();\n        facade.setVolume(ANY_VOLUME);\n\n        float currentVolume = facade.getVolume();\n\n        assertThat(currentVolume).isEqualTo(ANY_VOLUME);\n    }\n\n    private void givenMediaPlayerIsPrepared() {\n        givenMediaPlayerIsPreparedWith(eitherSurface);\n    }\n\n    private void givenMediaPlayerIsPreparedWith(Either<Surface, SurfaceHolder> eitherSurface) {\n        facade.prepareVideo(ANY_URI, eitherSurface);\n        ArgumentCaptor<MediaPlayer.OnPreparedListener> argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnPreparedListener.class);\n        verify(mediaPlayer).setOnPreparedListener(argumentCaptor.capture());\n        argumentCaptor.getValue().onPrepared(mediaPlayer);\n    }\n\n    private void whenVideoSizeChanges() {\n        ArgumentCaptor<MediaPlayer.OnVideoSizeChangedListener> argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnVideoSizeChangedListener.class);\n        verify(mediaPlayer).setOnVideoSizeChangedListener(argumentCaptor.capture());\n        argumentCaptor.getValue().onVideoSizeChanged(mediaPlayer, ANY_WIDTH, ANY_HEIGHT);\n    }\n\n    private void whenCompleted() {\n        ArgumentCaptor<MediaPlayer.OnCompletionListener> argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnCompletionListener.class);\n        verify(mediaPlayer).setOnCompletionListener(argumentCaptor.capture());\n        argumentCaptor.getValue().onCompletion(mediaPlayer);\n    }\n\n    private void whenErroring() {\n        ArgumentCaptor<MediaPlayer.OnErrorListener> argumentCaptor = ArgumentCaptor.forClass(MediaPlayer.OnErrorListener.class);\n        verify(mediaPlayer).setOnErrorListener(argumentCaptor.capture());\n        argumentCaptor.getValue().onError(mediaPlayer, ANY_ERROR_WHAT, ANY_ERROR_EXTRA);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerImplTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\nimport android.net.Uri;\nimport android.view.Surface;\nimport android.view.SurfaceHolder;\nimport android.view.View;\nimport com.novoda.noplayer.ContentType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.OptionsBuilder;\nimport com.novoda.noplayer.PlayerInformation;\nimport com.novoda.noplayer.PlayerSurfaceHolder;\nimport com.novoda.noplayer.PlayerView;\nimport com.novoda.noplayer.SurfaceRequester;\nimport com.novoda.noplayer.internal.Heart;\nimport com.novoda.noplayer.internal.listeners.PlayerListenersHolder;\nimport com.novoda.noplayer.internal.mediaplayer.forwarder.MediaPlayerForwarder;\nimport com.novoda.noplayer.internal.utils.NoPlayerLog;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.Either;\nimport com.novoda.noplayer.model.LoadTimeout;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerAudioTrackFixture;\nimport com.novoda.noplayer.model.Timeout;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.experimental.runners.Enclosed;\nimport org.junit.runner.RunWith;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.InOrder;\nimport org.mockito.Mock;\nimport org.mockito.Mockito;\nimport org.mockito.invocation.InvocationOnMock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\nimport org.mockito.stubbing.Answer;\n\nimport java.util.Collections;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.doAnswer;\nimport static org.mockito.Mockito.eq;\nimport static org.mockito.Mockito.inOrder;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.verify;\n\n@RunWith(Enclosed.class)\npublic class AndroidMediaPlayerImplTest {\n\n    public static class GivenPlayer extends Base {\n\n        private static final boolean IS_BEATING = true;\n        private static final boolean IS_NOT_BEATING = false;\n        private static final int WIDTH = 100;\n        private static final int HEIGHT = 200;\n        private static final int ANY_ROTATION_DEGREES = 0;\n        private static final int ANY_PIXEL_WIDTH_HEIGHT = 1;\n        private static final long TWO_MINUTES_IN_MILLIS = 120000;\n        private static final int ONE_SECOND_IN_MILLIS = 1000;\n        private static final boolean IS_PLAYING = true;\n        private static final PlayerAudioTrack PLAYER_AUDIO_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().build();\n        private static final AudioTracks AUDIO_TRACKS = AudioTracks.from(Collections.singletonList(PLAYER_AUDIO_TRACK));\n\n        @Test\n        public void whenInitialising_thenBindsListenersToForwarder() {\n            player.initialise();\n\n            verify(forwarder).bind(preparedListener, player);\n            verify(forwarder).bind(bufferStateListener, errorListener);\n            verify(forwarder).bind(completionListener, stateChangedListener);\n            verify(forwarder).bind(videoSizeChangedListener);\n            verify(forwarder).bind(infoListener);\n        }\n\n        @Test\n        public void whenInitialising_thenBindsListenerToBufferHeartbeatCallback() {\n            player.initialise();\n\n            verify(checkBufferHeartbeatCallback).bind(bufferListener);\n        }\n\n        @Test\n        public void whenInitialising_thenBindsHeartbeatCallbackToListenerHolder() {\n            player.initialise();\n\n            verify(listenersHolder).addHeartbeatCallback(checkBufferHeartbeatCallback);\n        }\n\n        @Test\n        public void givenInitialised_whenCallingOnPrepared_thenCancelsTimeout() {\n            player.initialise();\n\n            ArgumentCaptor<NoPlayer.PreparedListener> preparedListenerCaptor = ArgumentCaptor.forClass(NoPlayer.PreparedListener.class);\n            verify(listenersHolder).addPreparedListener(preparedListenerCaptor.capture());\n\n            NoPlayer.PreparedListener preparedListener = preparedListenerCaptor.getValue();\n            preparedListener.onPrepared(player);\n\n            verify(loadTimeout).cancel();\n        }\n\n        @Test\n        public void whenInitialising_thenBindsHeart() {\n            player.initialise();\n\n            verify(heart).bind(any(Heart.Heartbeat.class));\n        }\n\n        @Test\n        public void givenInitialised_whenCallingOnPrepared_thenSetsOnSeekCompleteListener() {\n            player.initialise();\n            ArgumentCaptor<NoPlayer.PreparedListener> preparedListenerCaptor = ArgumentCaptor.forClass(NoPlayer.PreparedListener.class);\n            verify(listenersHolder).addPreparedListener(preparedListenerCaptor.capture());\n\n            NoPlayer.PreparedListener preparedListener = preparedListenerCaptor.getValue();\n            preparedListener.onPrepared(player);\n\n            verify(mediaPlayer).setOnSeekCompleteListener(any(MediaPlayer.OnSeekCompleteListener.class));\n        }\n\n        @Test\n        public void givenInitialised_whenCallingOnError_thenCancelsTimeout() {\n            player.initialise();\n            ArgumentCaptor<NoPlayer.ErrorListener> errorListenerCaptor = ArgumentCaptor.forClass(NoPlayer.ErrorListener.class);\n            verify(listenersHolder).addErrorListener(errorListenerCaptor.capture());\n\n            NoPlayer.ErrorListener errorListener = errorListenerCaptor.getValue();\n            errorListener.onError(mock(NoPlayer.PlayerError.class));\n\n            verify(loadTimeout).cancel();\n        }\n\n        @Test\n        public void givenInitialised_whenCallingOnError_thenPlayerResourcesAreReleased_andNotListeners() {\n            player.initialise();\n            ArgumentCaptor<NoPlayer.ErrorListener> errorListenerCaptor = ArgumentCaptor.forClass(NoPlayer.ErrorListener.class);\n            verify(listenersHolder).addErrorListener(errorListenerCaptor.capture());\n\n            NoPlayer.ErrorListener errorListener = errorListenerCaptor.getValue();\n            errorListener.onError(mock(NoPlayer.PlayerError.class));\n\n            verify(delayedActionExecutor).clearAllActions();\n            verify(listenersHolder).resetState();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(mediaPlayer).release();\n            verify(listenersHolder, never()).clear();\n            verify(stateChangedListener, never()).onVideoStopped();\n        }\n\n        @Test\n        public void givenInitialised_whenCallingOnVideoSizeChanged_thenVideoWidthAndHeightMatches() {\n            player.initialise();\n            ArgumentCaptor<NoPlayer.VideoSizeChangedListener> videoSizeChangedListenerCaptor = ArgumentCaptor.forClass(NoPlayer.VideoSizeChangedListener.class);\n            verify(listenersHolder).addVideoSizeChangedListener(videoSizeChangedListenerCaptor.capture());\n\n            NoPlayer.VideoSizeChangedListener videoSizeChangedListener = videoSizeChangedListenerCaptor.getValue();\n            videoSizeChangedListener.onVideoSizeChanged(WIDTH, HEIGHT, ANY_ROTATION_DEGREES, ANY_PIXEL_WIDTH_HEIGHT);\n\n            assertThat(player.videoWidth()).isEqualTo(WIDTH);\n            assertThat(player.videoHeight()).isEqualTo(HEIGHT);\n        }\n\n        @Test\n        public void givenAndroidMediaPlayerIsPlaying_whenCallingIsPlaying_thenReturnsTrue() {\n            given(mediaPlayer.isPlaying()).willReturn(IS_PLAYING);\n\n            boolean isPlaying = player.isPlaying();\n\n            assertThat(isPlaying).isTrue();\n        }\n\n        @Test\n        public void whenSeeking_thenSeeksToPosition() {\n            long seekPositionInMillis = TWO_MINUTES_IN_MILLIS;\n\n            player.seekTo(seekPositionInMillis);\n\n            verify(mediaPlayer).seekTo(seekPositionInMillis);\n        }\n\n        @Test\n        public void whenPausing_thenPausesMediaPlayer() {\n            player.pause();\n\n            verify(mediaPlayer).pause();\n        }\n\n        @Test\n        public void givenHeartIsBeating_whenPausing_thenStopsBeatingHeart() {\n            given(heart.isBeating()).willReturn(IS_BEATING);\n\n            player.pause();\n\n            verify(heart).stopBeatingHeart();\n        }\n\n        @Test\n        public void givenHeartIsBeating_whenPausing_thenForcesHeartBeat() {\n            given(heart.isBeating()).willReturn(IS_BEATING);\n\n            player.pause();\n\n            verify(heart).forceBeat();\n        }\n\n        @Test\n        public void givenHeartIsNotBeating_whenPausing_thenDoesNotStopBeatingHeart() {\n            given(heart.isBeating()).willReturn(IS_NOT_BEATING);\n\n            player.pause();\n\n            verify(heart, never()).stopBeatingHeart();\n        }\n\n        @Test\n        public void givenHeartIsNotBeating_whenPausing_thenDoesNotForceHeartBeat() {\n            given(heart.isBeating()).willReturn(IS_NOT_BEATING);\n\n            player.pause();\n\n            verify(heart, never()).forceBeat();\n        }\n\n        @Test\n        public void whenPausing_thenNotifiesStateChangedListenersThatVideoIsPaused() {\n            player.pause();\n\n            verify(stateChangedListener).onVideoPaused();\n        }\n\n        @Test\n        public void givenPlayerIsNotSeeking_whenGettingPlayheadPosition_thenReturnsCurrentMediaPlayerPosition() {\n            given(mediaPlayer.currentPositionInMillis()).willReturn(ONE_SECOND_IN_MILLIS);\n            long playheadPositionInMillis = player.playheadPositionInMillis();\n\n            assertThat(playheadPositionInMillis).isEqualTo(ONE_SECOND_IN_MILLIS);\n        }\n\n        @Test\n        public void givenPlayerIsSeeking_whenGettingPlayheadPosition_thenReturnsSeekPosition() {\n            long seekPositionInMillis = TEN_SECONDS;\n            player.seekTo(seekPositionInMillis);\n\n            long videoPositionInMillis = player.playheadPositionInMillis();\n\n            assertThat(videoPositionInMillis).isEqualTo(seekPositionInMillis);\n        }\n\n        @Test\n        public void whenGettingMediaDuration_thenReturnsMediaPlayerDuration() {\n            given(mediaPlayer.mediaDurationInMillis()).willReturn(ONE_SECOND_IN_MILLIS);\n            long videoDurationInMillis = player.mediaDurationInMillis();\n\n            assertThat(videoDurationInMillis).isEqualTo(ONE_SECOND_IN_MILLIS);\n        }\n\n        @Test\n        public void whenGettingBufferPercentage_thenReturnsMediaPlayerBufferPercentage() {\n            int mediaPlayerBufferPercentage = 10;\n            given(mediaPlayer.getBufferPercentage()).willReturn(mediaPlayerBufferPercentage);\n\n            int bufferPercentage = player.bufferPercentage();\n\n            assertThat(bufferPercentage).isEqualTo(mediaPlayerBufferPercentage);\n        }\n\n        @Test\n        public void whenGettingPlayerInformation_thenReturnsMediaPlayerInformation() {\n            PlayerInformation playerInformation = player.getPlayerInformation();\n\n            assertThat(playerInformation).isEqualTo(mediaPlayerInformation);\n        }\n\n        @Test\n        public void whenAttachingPlayerView_thenAddsVideoSizeChangedListener() {\n            NoPlayer.VideoSizeChangedListener videoSizeChangedListener = mock(NoPlayer.VideoSizeChangedListener.class);\n            given(playerView.getVideoSizeChangedListener()).willReturn(videoSizeChangedListener);\n            player.attach(playerView);\n\n            verify(listenersHolder).addVideoSizeChangedListener(videoSizeChangedListener);\n        }\n\n        @Test\n        public void whenAttachingPlayerView_thenAddsStateChangedListener() {\n            NoPlayer.StateChangedListener stateChangedListener = mock(NoPlayer.StateChangedListener.class);\n            given(playerView.getStateChangedListener()).willReturn(stateChangedListener);\n            player.attach(playerView);\n\n            verify(listenersHolder).addStateChangedListener(stateChangedListener);\n        }\n\n        @Test\n        public void whenAttachingPlayerView_thenPreventsVideoDriverBug() {\n            player.attach(playerView);\n\n            verify(buggyVideoDriverPreventer).preventVideoDriverBug(player, containerView);\n        }\n\n        @Test\n        public void whenDetachingPlayerView_thenRemovesVideoSizeChangedListener() {\n            PlayerView playerView = mock(PlayerView.class);\n            NoPlayer.VideoSizeChangedListener videoSizeChangedListener = mock(NoPlayer.VideoSizeChangedListener.class);\n            given(playerView.getVideoSizeChangedListener()).willReturn(videoSizeChangedListener);\n            player.detach(playerView);\n\n            verify(listenersHolder).removeVideoSizeChangedListener(videoSizeChangedListener);\n        }\n\n        @Test\n        public void whenDetachingPlayerView_thenRemovesStateChangedListener() {\n            PlayerView playerView = mock(PlayerView.class);\n            NoPlayer.StateChangedListener stateChangedListener = mock(NoPlayer.StateChangedListener.class);\n            given(playerView.getStateChangedListener()).willReturn(stateChangedListener);\n            player.detach(playerView);\n\n            verify(listenersHolder).removeStateChangedListener(stateChangedListener);\n        }\n\n        @Test\n        public void whenDetachingPlayerView_thenClearsVideoDriverBugPreventer() {\n            PlayerView playerView = mock(PlayerView.class);\n            View containerView = mock(View.class);\n            given(playerView.getContainerView()).willReturn(containerView);\n            player.detach(playerView);\n\n            verify(buggyVideoDriverPreventer).clear(containerView);\n        }\n\n        @Test\n        public void whenSelectingAudioTrack_thenDelegatesToMediaPlayer() {\n            PlayerAudioTrack audioTrack = mock(PlayerAudioTrack.class);\n\n            player.selectAudioTrack(audioTrack);\n\n            verify(mediaPlayer).selectAudioTrack(audioTrack);\n        }\n\n        @Test\n        public void whenGettingAudioTracks_thenDelegatesToMediaPlayer() {\n            given(mediaPlayer.getAudioTracks()).willReturn(AUDIO_TRACKS);\n            AudioTracks audioTracks = player.getAudioTracks();\n\n            assertThat(audioTracks).isEqualTo(AUDIO_TRACKS);\n        }\n\n        @Test\n        public void whenStopping_thenPlayerResourcesAreReleased_andNotListeners() {\n\n            player.stop();\n\n            verify(delayedActionExecutor).clearAllActions();\n            verify(listenersHolder).resetState();\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(mediaPlayer).release();\n            verify(listenersHolder, never()).clear();\n        }\n\n        @Test\n        public void whenReleasing_thenPlayerResourcesAreReleased() {\n\n            player.release();\n\n            verify(delayedActionExecutor).clearAllActions();\n            verify(listenersHolder).resetState();\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(mediaPlayer).release();\n            verify(listenersHolder).clear();\n        }\n\n        @Test\n        public void givenAttachedPlayerView_whenStopping_thenPlayerResourcesAreReleased_andNotListeners() {\n            player.attach(playerView);\n\n            player.stop();\n\n            verify(delayedActionExecutor).clearAllActions();\n            verify(listenersHolder).resetState();\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(mediaPlayer).release();\n            verify(containerView).setVisibility(View.GONE);\n            verify(listenersHolder, never()).clear();\n        }\n\n        @Test\n        public void givenAttachedPlayerView_whenReleasing_thenPlayerResourcesAreReleased() {\n            player.attach(playerView);\n\n            player.release();\n\n            verify(delayedActionExecutor).clearAllActions();\n            verify(listenersHolder).resetState();\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(mediaPlayer).release();\n            verify(containerView).setVisibility(View.GONE);\n            verify(listenersHolder).clear();\n        }\n    }\n\n    public static class GivenPlayerIsAttached extends Base {\n\n        private static final long DELAY_MILLIS = 500;\n        private static final boolean IS_NOT_PLAYING = false;\n        private static final float ANY_VOLUME = 0.4f;\n\n        @Override\n        public void setUp() {\n            super.setUp();\n            player.attach(playerView);\n        }\n\n        @Test\n        public void whenLoadingVideo_thenNotifiesBufferStateListenersThatBufferStarted() {\n            player.loadVideo(URI, OPTIONS);\n\n            verify(bufferStateListener).onBufferStarted();\n        }\n\n        @Test\n        public void whenLoadingVideo_thenPreparesVideo() {\n            player.loadVideo(URI, OPTIONS);\n\n            verify(mediaPlayer).prepareVideo(URI, surface);\n        }\n\n        @Test\n        public void whenLoadingVideoWithTimeout_thenNotifiesBufferStateListenersThatBufferStarted() {\n            player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n\n            verify(bufferStateListener).onBufferStarted();\n        }\n\n        @Test\n        public void whenLoadingVideoWithTimeout_thenPreparesVideo() {\n            player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n\n            verify(mediaPlayer).prepareVideo(URI, surface);\n        }\n\n        @Test\n        public void whenLoadingVideoWithTimeout_thenStartsTimeout() {\n            player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n\n            verify(loadTimeout).start(ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n        }\n\n        @Test\n        public void whenLoadingVideo_thenShowsContainerView() {\n            player.loadVideo(URI, OPTIONS);\n\n            verify(containerView).setVisibility(View.VISIBLE);\n        }\n\n        @Test\n        public void whenStartingPlay_thenStartsBeatingHeart() {\n            player.play();\n\n            verify(heart).startBeatingHeart();\n        }\n\n        @Test\n        public void whenStartingPlay_thenMediaPlayerStarts() {\n            player.play();\n\n            verify(mediaPlayer).start(surface);\n        }\n\n        @Test\n        public void whenStartingPlay_thenNotifiesStateListenersThatVideoIsPlaying() {\n            player.play();\n\n            verify(stateChangedListener).onVideoPlaying();\n        }\n\n        @Test\n        public void whenStartingPlayAtVideoPosition_thenStartsBeatingHeart() {\n            given(mediaPlayer.currentPositionInMillis()).willReturn((int) BEGINNING_POSITION);\n\n            player.playAt(BEGINNING_POSITION);\n\n            verify(heart).startBeatingHeart();\n        }\n\n        @Test\n        public void whenStartingPlayAtVideoPosition_thenMediaPlayerStarts() {\n            given(mediaPlayer.currentPositionInMillis()).willReturn((int) BEGINNING_POSITION);\n\n            player.playAt(BEGINNING_POSITION);\n\n            verify(mediaPlayer).start(surface);\n        }\n\n        @Test\n        public void whenStartingPlayAtVideoPosition_thenNotifiesStateListenersThatVideoIsPlaying() {\n            given(mediaPlayer.currentPositionInMillis()).willReturn((int) BEGINNING_POSITION);\n\n            player.playAt(BEGINNING_POSITION);\n\n            verify(stateChangedListener).onVideoPlaying();\n        }\n\n        @Test\n        public void givenPlayerHasPlayedVideo_whenLoadingVideo_thenPlayerIsReleased_andNotListeners() {\n            given(mediaPlayer.hasPlayedContent()).willReturn(true);\n\n            player.loadVideo(URI, OPTIONS);\n\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(mediaPlayer).release();\n            verify(listenersHolder, never()).clear();\n        }\n\n        @Test\n        public void givenPlayerHasPlayedVideo_whenLoadingVideoWithTimeout_thenPlayerResourcesAreReleased_andNotListeners() {\n            given(mediaPlayer.hasPlayedContent()).willReturn(true);\n\n            player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n\n            verify(stateChangedListener).onVideoStopped();\n            verify(loadTimeout).cancel();\n            verify(heart).stopBeatingHeart();\n            verify(mediaPlayer).release();\n            verify(listenersHolder, never()).clear();\n        }\n\n        @Test\n        public void givenPlayerHasNotPlayedVideo_whenLoadingVideo_thenPlayerResourcesAreNotReleased() {\n            given(mediaPlayer.hasPlayedContent()).willReturn(false);\n\n            player.loadVideo(URI, OPTIONS);\n\n            verify(stateChangedListener, never()).onVideoStopped();\n            verify(loadTimeout, never()).cancel();\n            verify(heart, never()).stopBeatingHeart();\n            verify(mediaPlayer, never()).release();\n        }\n\n        @Test\n        public void givenPlayerHasNotPlayedVideo_whenLoadingVideoWithTimeout_thenPlayerResourcesAreNotReleased() {\n            given(mediaPlayer.hasPlayedContent()).willReturn(false);\n\n            player.loadVideoWithTimeout(URI, OPTIONS, ANY_TIMEOUT, ANY_LOAD_TIMEOUT_CALLBACK);\n\n            verify(stateChangedListener, never()).onVideoStopped();\n            verify(loadTimeout, never()).cancel();\n            verify(heart, never()).stopBeatingHeart();\n            verify(mediaPlayer, never()).release();\n        }\n\n        @Test\n        public void givenPositionThatDiffersFromPlayheadPosition_whenStartingPlayAtVideoPosition_thenNotifiesBufferStateListenersThatBufferStarted() {\n            long differentPositionInMillis = givenPositionThatDiffersFromPlayheadPosition();\n\n            player.playAt(differentPositionInMillis);\n\n            verify(bufferStateListener).onBufferStarted();\n        }\n\n        @Test\n        public void givenPositionThatDiffersFromPlayheadPosition_whenStartingPlayAtVideoPosition_thenInitialisesPlaybackForSeeking() {\n            long differentPositionInMillis = givenPositionThatDiffersFromPlayheadPosition();\n\n            player.playAt(differentPositionInMillis);\n\n            thenInitialisesPlaybackForSeeking();\n        }\n\n        @Test\n        public void givenPositionThatDiffersFromPlayheadPosition_whenStartingPlayAtVideoPosition_thenSeeksToVideoPosition() {\n            long differentPositionInMillis = givenPositionThatDiffersFromPlayheadPosition();\n\n            player.playAt(differentPositionInMillis);\n            ArgumentCaptor<DelayedActionExecutor.Action> argumentCaptor = ArgumentCaptor.forClass(DelayedActionExecutor.Action.class);\n            verify(delayedActionExecutor).performAfterDelay(argumentCaptor.capture(), eq(DELAY_MILLIS));\n            argumentCaptor.getValue().perform();\n\n            verify(mediaPlayer).seekTo(differentPositionInMillis);\n        }\n\n        @Test\n        public void givenPlayerIsAlreadyPlaying_whenPlaying_thenNotifiesVideoPlaying() {\n            given(mediaPlayer.isPlaying()).willReturn(IS_NOT_PLAYING);\n\n            player.play();\n\n            verify(stateChangedListener).onVideoPlaying();\n        }\n\n        @Test\n        public void whenSetRepeating_thenSetRepeating() {\n            player.setRepeating(false);\n\n            verify(mediaPlayer).setRepeating(false);\n        }\n\n        @Test\n        public void whenSetVolume_thenSetVolumeOnMediaPlayer() {\n            player.setVolume(ANY_VOLUME);\n\n            verify(mediaPlayer).setVolume(ANY_VOLUME);\n        }\n\n        @Test\n        public void whenGetVolume_thenReturnMediaPlayerVolume() {\n            given(mediaPlayer.getVolume()).willReturn(ANY_VOLUME);\n\n            float currentVolume = player.getVolume();\n\n            assertThat(currentVolume).isEqualTo(ANY_VOLUME);\n        }\n\n        private long givenPositionThatDiffersFromPlayheadPosition() {\n            given(mediaPlayer.currentPositionInMillis()).willReturn(0);\n            return 1;\n        }\n\n        private void thenInitialisesPlaybackForSeeking() {\n            InOrder inOrder = inOrder(mediaPlayer);\n\n            inOrder.verify(mediaPlayer).start(surface);\n            inOrder.verify(mediaPlayer).pause();\n            inOrder.verifyNoMoreInteractions();\n        }\n    }\n\n    public abstract static class Base {\n\n        static final Options OPTIONS = new OptionsBuilder().withContentType(ContentType.H264).build();\n        static final long BEGINNING_POSITION = 0;\n        static final Uri URI = Mockito.mock(Uri.class);\n        static final int TEN_SECONDS = 10;\n        static final Timeout ANY_TIMEOUT = Timeout.fromSeconds(TEN_SECONDS);\n        static final NoPlayer.LoadTimeoutCallback ANY_LOAD_TIMEOUT_CALLBACK = new NoPlayer.LoadTimeoutCallback() {\n            @Override\n            public void onLoadTimeout() {\n\n            }\n        };\n\n        @Rule\n        public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n        @Mock\n        MediaPlayerInformation mediaPlayerInformation;\n        @Mock\n        AndroidMediaPlayerFacade mediaPlayer;\n        @Mock\n        MediaPlayerForwarder forwarder;\n        @Mock\n        PlayerListenersHolder listenersHolder;\n        @Mock\n        CheckBufferHeartbeatCallback checkBufferHeartbeatCallback;\n        @Mock\n        LoadTimeout loadTimeout;\n        @Mock\n        Heart heart;\n        @Mock\n        DelayedActionExecutor delayedActionExecutor;\n        @Mock\n        BuggyVideoDriverPreventer buggyVideoDriverPreventer;\n        @Mock\n        NoPlayer.PreparedListener preparedListener;\n        @Mock\n        NoPlayer.BufferStateListener bufferStateListener;\n        @Mock\n        NoPlayer.ErrorListener errorListener;\n        @Mock\n        NoPlayer.CompletionListener completionListener;\n        @Mock\n        NoPlayer.VideoSizeChangedListener videoSizeChangedListener;\n        @Mock\n        NoPlayer.InfoListener infoListener;\n        @Mock\n        NoPlayer.StateChangedListener stateChangedListener;\n        @Mock\n        Either<Surface, SurfaceHolder> surface;\n        @Mock\n        PlayerView playerView;\n        @Mock\n        NoPlayer.StateChangedListener stateChangeListener;\n        @Mock\n        MediaPlayer.OnPreparedListener onPreparedListener;\n        @Mock\n        MediaPlayer.OnCompletionListener onCompletionListener;\n        @Mock\n        MediaPlayer.OnErrorListener onErrorListener;\n        @Mock\n        MediaPlayer.OnVideoSizeChangedListener onSizeChangedListener;\n        @Mock\n        CheckBufferHeartbeatCallback.BufferListener bufferListener;\n        @Mock\n        View containerView;\n        @Mock\n        PlayerSurfaceHolder playerSurfaceHolder;\n\n        AndroidMediaPlayerImpl player;\n\n        @Before\n        public void setUp() {\n            NoPlayerLog.setLoggingEnabled(false);\n            SurfaceRequester surfaceRequester = mock(SurfaceRequester.class);\n            given(playerView.getPlayerSurfaceHolder()).willReturn(playerSurfaceHolder);\n            given(playerSurfaceHolder.getSurfaceRequester()).willReturn(surfaceRequester);\n            given(playerView.getStateChangedListener()).willReturn(stateChangeListener);\n            given(playerView.getVideoSizeChangedListener()).willReturn(videoSizeChangedListener);\n            given(playerView.getContainerView()).willReturn(containerView);\n            doAnswer(new Answer<Void>() {\n                @Override\n                public Void answer(InvocationOnMock invocation) {\n                    SurfaceRequester.Callback callback = invocation.getArgument(0);\n                    callback.onSurfaceReady(surface);\n                    return null;\n                }\n            }).when(surfaceRequester).requestSurface(any(SurfaceRequester.Callback.class));\n\n            given(listenersHolder.getPreparedListeners()).willReturn(preparedListener);\n            given(listenersHolder.getBufferStateListeners()).willReturn(bufferStateListener);\n            given(listenersHolder.getErrorListeners()).willReturn(errorListener);\n            given(listenersHolder.getCompletionListeners()).willReturn(completionListener);\n            given(listenersHolder.getVideoSizeChangedListeners()).willReturn(videoSizeChangedListener);\n            given(listenersHolder.getInfoListeners()).willReturn(infoListener);\n            given(listenersHolder.getStateChangedListeners()).willReturn(stateChangedListener);\n\n            given(forwarder.onPreparedListener()).willReturn(onPreparedListener);\n            given(forwarder.onCompletionListener()).willReturn(onCompletionListener);\n            given(forwarder.onErrorListener()).willReturn(onErrorListener);\n            given(forwarder.onSizeChangedListener()).willReturn(onSizeChangedListener);\n            given(forwarder.onHeartbeatListener()).willReturn(bufferListener);\n\n            player = new AndroidMediaPlayerImpl(\n                    mediaPlayerInformation,\n                    mediaPlayer,\n                    forwarder,\n                    listenersHolder,\n                    checkBufferHeartbeatCallback,\n                    loadTimeout,\n                    heart,\n                    delayedActionExecutor,\n                    buggyVideoDriverPreventer\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/BuggyVideoDriverPreventerTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.view.View;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.mockito.Mockito.*;\n\npublic class BuggyVideoDriverPreventerTest {\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    private View videoContainer;\n\n    @Mock\n    private NoPlayer player;\n\n    @Mock\n    private MediaPlayerTypeReader mediaPlayerTypeReader;\n\n    private BuggyVideoDriverPreventer buggyVideoDriverPreventer;\n\n    @Before\n    public void setUp() {\n        buggyVideoDriverPreventer = new BuggyVideoDriverPreventer(mediaPlayerTypeReader);\n    }\n\n    @Test\n    public void givenVideoDriverIsNotBuggy_whenPreventingVideoDriverBug_thenNothingHappens() {\n        when(mediaPlayerTypeReader.getPlayerType()).thenReturn(AndroidMediaPlayerType.NU);\n\n        buggyVideoDriverPreventer.preventVideoDriverBug(player, videoContainer);\n\n        verifyZeroInteractions(videoContainer);\n    }\n\n    @Test\n    public void givenVideoDriverCanBeBuggy_whenPreventingVideoDriverBug_thenABuggyDriverLayoutListenerIsAddedToTheVideoContainer() {\n        when(mediaPlayerTypeReader.getPlayerType()).thenReturn(AndroidMediaPlayerType.AWESOME);\n\n        buggyVideoDriverPreventer.preventVideoDriverBug(player, videoContainer);\n\n        verify(videoContainer).addOnLayoutChangeListener(any(OnPotentialBuggyDriverLayoutListener.class));\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/DelayedActionExecutorTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.os.Handler;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport org.junit.Test;\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.invocation.InvocationOnMock;\nimport org.mockito.stubbing.Answer;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.BDDMockito.willAnswer;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\n\npublic class DelayedActionExecutorTest {\n\n    private static final long ANY_DELAY_IN_MILLIS = 10;\n\n    private final DelayedActionExecutor.Action action = mock(DelayedActionExecutor.Action.class);\n    private final DelayedActionExecutor.Action secondaryAction = mock(DelayedActionExecutor.Action.class);\n\n    private final Handler immediatelyExecutingHandler = createImmediatelyExecutingHandler();\n    private final Handler nonExecutingHandler = mock(Handler.class);\n\n    private final Map<DelayedActionExecutor.Action, Runnable> runnables = new HashMap<>();\n\n    private DelayedActionExecutor delayedActionExecutor;\n\n    @Test\n    public void whenActionIsNotPerformedYet_thenMapContainsAction() {\n        delayedActionExecutor = new DelayedActionExecutor(nonExecutingHandler, runnables);\n\n        delayedActionExecutor.performAfterDelay(action, ANY_DELAY_IN_MILLIS);\n\n        assertThat(runnables).hasSize(1);\n    }\n\n    @Test\n    public void whenPerformingActionAfterDelay_thenRemovesActionFromMap() {\n        delayedActionExecutor = new DelayedActionExecutor(immediatelyExecutingHandler, runnables);\n\n        delayedActionExecutor.performAfterDelay(action, ANY_DELAY_IN_MILLIS);\n\n        assertThat(runnables).isEmpty();\n    }\n\n    @Test\n    public void whenPerformingActionAfterDelay_thenPerformsAction() {\n        delayedActionExecutor = new DelayedActionExecutor(immediatelyExecutingHandler, runnables);\n\n        delayedActionExecutor.performAfterDelay(action, ANY_DELAY_IN_MILLIS);\n\n        verify(action).perform();\n    }\n\n    @Test\n    public void givenMultipleQueuedActions_whenClearingActions_thenRemovesAllActions() {\n        delayedActionExecutor = new DelayedActionExecutor(nonExecutingHandler, runnables);\n        delayedActionExecutor.performAfterDelay(action, ANY_DELAY_IN_MILLIS);\n        delayedActionExecutor.performAfterDelay(secondaryAction, ANY_DELAY_IN_MILLIS);\n\n        delayedActionExecutor.clearAllActions();\n\n        assertThat(runnables).isEmpty();\n        verify(nonExecutingHandler, times(2)).removeCallbacks(any(Runnable.class));\n    }\n\n    private Handler createImmediatelyExecutingHandler() {\n        Handler handler = mock(Handler.class);\n        final ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class);\n        willAnswer(new Answer<Object>() {\n            @Override\n            public Object answer(InvocationOnMock invocation) throws Throwable {\n                argumentCaptor.getValue().run();\n                return null;\n            }\n        }).given(handler).postDelayed(argumentCaptor.capture(), eq(ANY_DELAY_IN_MILLIS));\n        return handler;\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/ErrorFactoryTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\n\nimport com.novoda.noplayer.DetailErrorType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.PlayerErrorType;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.junit.runners.Parameterized;\n\nimport java.util.Arrays;\nimport java.util.Collection;\n\nimport static com.novoda.noplayer.DetailErrorType.*;\nimport static com.novoda.noplayer.PlayerErrorType.*;\nimport static org.fest.assertions.api.Assertions.assertThat;\n\n@RunWith(Parameterized.class)\npublic class ErrorFactoryTest {\n\n    @Parameterized.Parameter(0)\n    public PlayerErrorType playerErrorType;\n    @Parameterized.Parameter(1)\n    public DetailErrorType detailErrorType;\n    @Parameterized.Parameter(2)\n    public int type;\n\n    @Parameterized.Parameters(name = \"{0} with detail {1} is mapped from {2}\")\n    public static Collection<Object[]> parameters() {\n        return Arrays.asList(\n                new Object[]{SOURCE, MEDIA_PLAYER_MALFORMED, MediaPlayer.MEDIA_ERROR_MALFORMED},\n                new Object[]{SOURCE, MEDIA_PLAYER_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK, MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK},\n                new Object[]{SOURCE, MEDIA_PLAYER_INFO_NOT_SEEKABLE, MediaPlayer.MEDIA_INFO_NOT_SEEKABLE},\n                new Object[]{SOURCE, MEDIA_PLAYER_SUBTITLE_TIMED_OUT, MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT},\n                new Object[]{SOURCE, MEDIA_PLAYER_UNSUPPORTED_SUBTITLE, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE},\n\n                new Object[]{CONNECTIVITY, MEDIA_PLAYER_TIMED_OUT, MediaPlayer.MEDIA_ERROR_TIMED_OUT},\n\n                new Object[]{DRM, MEDIA_PLAYER_SERVER_DIED, MediaPlayer.MEDIA_ERROR_SERVER_DIED},\n                new Object[]{DRM, MEDIA_PLAYER_PREPARE_DRM_STATUS_PREPARATION_ERROR, MediaPlayer.PREPARE_DRM_STATUS_PREPARATION_ERROR},\n                new Object[]{DRM, MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR, MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR},\n                new Object[]{DRM, MEDIA_PLAYER_PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR, MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR},\n\n                new Object[]{RENDERER_DECODER, MEDIA_PLAYER_INFO_AUDIO_NOT_PLAYING, MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING},\n                new Object[]{RENDERER_DECODER, MEDIA_PLAYER_BAD_INTERLEAVING, MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING},\n                new Object[]{RENDERER_DECODER, MEDIA_PLAYER_INFO_VIDEO_NOT_PLAYING, MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING},\n                new Object[]{RENDERER_DECODER, MEDIA_PLAYER_INFO_VIDEO_TRACK_LAGGING, MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING}\n        );\n    }\n\n    @Test\n    public void mapErrors() {\n        NoPlayer.PlayerError playerError = ErrorFactory.createErrorFrom(type, 0);\n        assertThat(playerError.type()).isEqualTo(playerErrorType);\n        assertThat(playerError.detailType()).isEqualTo(detailErrorType);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/ErrorFormatterTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport org.junit.Test;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\n\npublic class ErrorFormatterTest {\n\n    private static final int TYPE_CODE = 202;\n    private static final int EXTRA_CODE = -218;\n\n    @Test\n    public void givenTypeAndExtra_whenFormattingMessage_thenReturnsExpectedMessageFormat() {\n        String expectedFormat = \"Type: 202, Extra: -218\";\n\n        String actualFormat = ErrorFormatter.formatMessage(TYPE_CODE, EXTRA_CODE);\n\n        assertThat(actualFormat).isEqualTo(expectedFormat);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/LoadTimeoutTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.os.Handler;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.internal.Clock;\nimport com.novoda.noplayer.model.LoadTimeout;\nimport com.novoda.noplayer.model.Timeout;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\n\nimport static org.mockito.Matchers.any;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\npublic class LoadTimeoutTest {\n\n    private static final Timeout ANY_TIME = Timeout.fromSeconds(0);\n\n    @Mock\n    private Clock clock;\n\n    @Mock\n    private Handler handler;\n\n    @InjectMocks\n    private LoadTimeout loadTimeout;\n\n    @Before\n    public void setUp() {\n        initMocks(this);\n    }\n\n    @Test\n    public void whenStartingATimeout_thenAnyPreviouslySetTimeoutRunnableAreRemoved() {\n\n        loadTimeout.start(ANY_TIME, any(NoPlayer.LoadTimeoutCallback.class));\n\n        verify(handler).removeCallbacks(any(Runnable.class));\n    }\n\n    @Test\n    public void whenStartingATimeout_thenTheTimeoutRunnableIsPosted() {\n\n        loadTimeout.start(ANY_TIME, any(NoPlayer.LoadTimeoutCallback.class));\n\n        verify(handler).post(any(Runnable.class));\n    }\n\n    @Test\n    public void whenCancelingATimeout_thenTimeoutRunnableIsRemoved() {\n\n        loadTimeout.cancel();\n\n        verify(handler).removeCallbacks(any(Runnable.class));\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/MediaPlayerInformationTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.os.Build;\n\nimport com.novoda.noplayer.PlayerType;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.BDDMockito.given;\n\npublic class MediaPlayerInformationTest {\n\n    @Rule\n    public MockitoRule rule = MockitoJUnit.rule();\n\n    @Mock\n    private MediaPlayerTypeReader playerTypeReader;\n\n    private MediaPlayerInformation playerInformation;\n\n    @Before\n    public void setUp() {\n        playerInformation = new MediaPlayerInformation(playerTypeReader);\n    }\n\n    @Test\n    public void givenInternalNuPlayer_whenReadingName_thenReturnsMediaPlayerNuPlayer() {\n        given(playerTypeReader.getPlayerType()).willReturn(AndroidMediaPlayerType.NU);\n\n        String name = playerInformation.getName();\n\n        assertThat(name).isEqualTo(\"MediaPlayer: NuPlayer\");\n    }\n\n    @Test\n    public void whenReadingVersion_thenReturnsAndroidBuildVersion() {\n\n        String version = playerInformation.getVersion();\n\n        assertThat(version).isEqualTo(Build.VERSION.RELEASE);\n    }\n\n    @Test\n    public void whenPlayerType_thenReturnsMediaPlayer() {\n\n        PlayerType playerType = playerInformation.getPlayerType();\n\n        assertThat(playerType).isEqualTo(PlayerType.MEDIA_PLAYER);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/NoPlayerMediaPlayerCreatorTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.content.Context;\n\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.verify;\n\npublic class NoPlayerMediaPlayerCreatorTest {\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    private NoPlayerMediaPlayerCreator.InternalCreator internalCreator;\n    @Mock\n    private AndroidMediaPlayerImpl player;\n    @Mock\n    private Context context;\n\n    private NoPlayerMediaPlayerCreator creator;\n\n    @Before\n    public void setUp() {\n        creator = new NoPlayerMediaPlayerCreator(internalCreator);\n        given(internalCreator.create(context)).willReturn(player);\n    }\n\n    @Test\n    public void whenCreatingMediaPlayer_thenInitialisesPlayer() {\n        creator.createMediaPlayer(context);\n\n        verify(player).initialise();\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/OnPotentialBuggyDriverLayoutListenerTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.view.View;\n\nimport com.novoda.noplayer.NoPlayer;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.mockito.InjectMocks;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.mockito.Mockito.*;\n\npublic class OnPotentialBuggyDriverLayoutListenerTest {\n\n    private static final int ANY_DIMENSION_VALUE = 0;\n\n    @Rule\n    public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n    @Mock\n    NoPlayer player;\n\n    @InjectMocks\n    OnPotentialBuggyDriverLayoutListener buggyDriverLayoutListener;\n\n    @Test\n    public void givenStatusIsNotCorrupted_whenALayoutChangeOccurs_thenDoNotForceAlignNativeMediaPlayerStatus() {\n        when(player.isPlaying()).thenReturn(false);\n\n        onLayoutChange();\n\n        verify(player, never()).play();\n    }\n\n    @Test\n    public void givenStatusMightBeNotCorrupted_whenALayoutChangeOccurs_thenForceAlignNativeMediaPlayerStatus() {\n        when(player.isPlaying()).thenReturn(true);\n\n        onLayoutChange();\n\n        verify(player, atLeastOnce()).play();\n    }\n\n    private void onLayoutChange() {\n        buggyDriverLayoutListener.onLayoutChange(\n                mock(View.class),\n                ANY_DIMENSION_VALUE,\n                ANY_DIMENSION_VALUE,\n                ANY_DIMENSION_VALUE,\n                ANY_DIMENSION_VALUE,\n                ANY_DIMENSION_VALUE,\n                ANY_DIMENSION_VALUE,\n                ANY_DIMENSION_VALUE,\n                ANY_DIMENSION_VALUE);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/PlaybackStateCheckerTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.media.MediaPlayer;\n\nimport java.util.Arrays;\nimport java.util.Collection;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.experimental.runners.Enclosed;\nimport org.junit.runner.RunWith;\nimport org.junit.runners.Parameterized;\nimport org.mockito.junit.MockitoJUnit;\nimport org.mockito.junit.MockitoRule;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.BDDMockito.given;\nimport static org.mockito.Mockito.mock;\n\n@RunWith(Enclosed.class)\npublic class PlaybackStateCheckerTest {\n\n    private static final MediaPlayer ANY_MEDIA_PLAYER = mock(MediaPlayer.class);\n    private static final MediaPlayer NO_MEDIA_PLAYER = null;\n    private static final boolean IS_IN_PLAYBACK_STATE = true;\n    private static final boolean IS_NOT_IN_PLAYBACK_STATE = false;\n\n    @RunWith(Parameterized.class)\n    public static class CheckingPlaybackState {\n\n        @Rule\n        public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n        private final MediaPlayer mediaPlayer;\n        private final PlaybackStateChecker.PlaybackState playbackState;\n        private final boolean expectedIsInPlaybackState;\n\n        @Parameterized.Parameters(name = \"MediaPlayer: {0} Playback state: {1}, isInPlaybackState: {2}\")\n        public static Collection<Object[]> parameters() {\n            return Arrays.asList(\n                    new Object[]{NO_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.COMPLETED, IS_NOT_IN_PLAYBACK_STATE},\n                    new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.ERROR, IS_NOT_IN_PLAYBACK_STATE},\n                    new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.IDLE, IS_NOT_IN_PLAYBACK_STATE},\n                    new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.PREPARING, IS_NOT_IN_PLAYBACK_STATE},\n                    new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.PREPARED, IS_IN_PLAYBACK_STATE},\n                    new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.PLAYING, IS_IN_PLAYBACK_STATE},\n                    new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.PAUSED, IS_IN_PLAYBACK_STATE},\n                    new Object[]{ANY_MEDIA_PLAYER, PlaybackStateChecker.PlaybackState.COMPLETED, IS_IN_PLAYBACK_STATE}\n            );\n        }\n\n        public CheckingPlaybackState(MediaPlayer mediaPlayer,\n                                     PlaybackStateChecker.PlaybackState playbackState,\n                                     boolean expectedIsInPlaybackState) {\n            this.mediaPlayer = mediaPlayer;\n            this.playbackState = playbackState;\n            this.expectedIsInPlaybackState = expectedIsInPlaybackState;\n        }\n\n        @Test\n        public void whenCheckingIsInPlaybackState_thenReturnsExpectedState() {\n            PlaybackStateChecker playbackStateChecker = new PlaybackStateChecker();\n\n            boolean inPlaybackState = playbackStateChecker.isInPlaybackState(mediaPlayer, playbackState);\n\n            assertThat(inPlaybackState).isEqualTo(expectedIsInPlaybackState);\n        }\n    }\n\n    @RunWith(Parameterized.class)\n    public static class CheckingIsPlaying {\n\n        private static final boolean MEDIA_PLAYER_IS_PLAYING = true;\n        private static final boolean MEDIA_PLAYER_IS_NOT_PLAYING = false;\n        private static final boolean IS_PLAYING = true;\n        private static final boolean IS_NOT_PLAYING = false;\n\n        @Rule\n        public MockitoRule mockitoRule = MockitoJUnit.rule();\n\n        private final MediaPlayer mediaPlayer;\n        private final PlaybackStateChecker.PlaybackState playbackState;\n        private final boolean expectedIsPlaying;\n\n        @Parameterized.Parameters(name = \"MediaPlayer: {0} mediaPlayer.isPlaying(): {1} Playback state: {2}, isPlaying: {3}\")\n        public static Collection<Object[]> parameters() {\n            return Arrays.asList(\n                    new Object[]{NO_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.COMPLETED, IS_NOT_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.ERROR, IS_NOT_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.IDLE, IS_NOT_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.PREPARING, IS_NOT_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.PREPARED, IS_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.PLAYING, IS_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.PAUSED, IS_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_PLAYING, PlaybackStateChecker.PlaybackState.COMPLETED, IS_PLAYING},\n\n                    new Object[]{NO_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.COMPLETED, IS_NOT_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.ERROR, IS_NOT_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.IDLE, IS_NOT_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.PREPARING, IS_NOT_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.PREPARED, IS_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.PLAYING, IS_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.PAUSED, IS_PLAYING},\n                    new Object[]{ANY_MEDIA_PLAYER, MEDIA_PLAYER_IS_NOT_PLAYING, PlaybackStateChecker.PlaybackState.COMPLETED, IS_PLAYING}\n            );\n        }\n\n        public CheckingIsPlaying(MediaPlayer mediaPlayer,\n                                 boolean mediaPlayerIsPlaying,\n                                 PlaybackStateChecker.PlaybackState playbackState,\n                                 boolean expectedIsPlaying) {\n            this.mediaPlayer = mediaPlayer;\n            this.playbackState = playbackState;\n            this.expectedIsPlaying = expectedIsPlaying;\n            if (mediaPlayer != null) {\n                given(mediaPlayer.isPlaying()).willReturn(mediaPlayerIsPlaying);\n            }\n        }\n\n        @Test\n        public void whenCheckingIsPlaying_thenReturnsExpectedState() {\n            PlaybackStateChecker playbackStateChecker = new PlaybackStateChecker();\n\n            boolean inPlaybackState = playbackStateChecker.isInPlaybackState(mediaPlayer, playbackState);\n\n            assertThat(inPlaybackState).isEqualTo(expectedIsPlaying);\n        }\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/internal/mediaplayer/PlayerCheckerTest.java",
    "content": "package com.novoda.noplayer.internal.mediaplayer;\n\nimport android.os.Build;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.mockito.Mock;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\nimport static org.mockito.Matchers.anyString;\nimport static org.mockito.Mockito.when;\nimport static org.mockito.MockitoAnnotations.initMocks;\n\npublic class PlayerCheckerTest {\n\n    private static final String PROP_USE_NU_PLAYER = \"media.stagefright.use-nuplayer\";\n    private static final String PROP_USE_AWESOME_PLAYER_PERSIST = \"persist.sys.media.use-awesome\";\n    private static final String PROP_USE_AWESOME_PLAYER_MEDIA = \"media.stagefright.use-awesome\";\n\n    @Mock\n    SystemProperties systemProperties;\n\n    private MediaPlayerTypeReader checker;\n\n    @Before\n    public void setUp() {\n        initMocks(this);\n    }\n\n    @Test\n    public void givenTheUserIsOnLollipopWhenTheAwesomePlayerPersistPropertyIsPresentThenAwesomePlayerIsDetected() throws Exception {\n        givenTheUserIsOnOSVersion(Build.VERSION_CODES.LOLLIPOP);\n\n        whenPropertyisPresent(PROP_USE_AWESOME_PLAYER_PERSIST);\n\n        thenPlayerTypeIs(AndroidMediaPlayerType.AWESOME);\n    }\n\n    @Test\n    public void givenTheUserIsOnLollipopWhenTheAwesomePlayerMediaPropertyIsPresentThenAwesomePlayerIsDetected() throws Exception {\n        givenTheUserIsOnOSVersion(Build.VERSION_CODES.LOLLIPOP);\n\n        whenPropertyisPresent(PROP_USE_AWESOME_PLAYER_MEDIA);\n\n        thenPlayerTypeIs(AndroidMediaPlayerType.AWESOME);\n    }\n\n    @Test\n    public void givenTheUserIsOnLollipopWhenNoAwesomePlayerMediaPropertyIsPresentThenNuPlayerIsDetected() {\n        givenTheUserIsOnOSVersion(Build.VERSION_CODES.LOLLIPOP);\n\n        whenNoPlayerPropertiesArePresent();\n\n        thenPlayerTypeIs(AndroidMediaPlayerType.NU);\n    }\n\n    @Test\n    public void givenTheUserIsOnKitkatWhenTheNuPlayerPropertyIsPresentThenNuPlayerIsDetected() throws Exception {\n        givenTheUserIsOnOSVersion(Build.VERSION_CODES.KITKAT);\n\n        whenPropertyisPresent(PROP_USE_NU_PLAYER);\n\n        thenPlayerTypeIs(AndroidMediaPlayerType.NU);\n    }\n\n    @Test\n    public void givenTheUserIsOnKitkatWhenNoNuPlayerMediaPropertyIsPresentThenAwesomePlayerIsDetected() {\n        givenTheUserIsOnOSVersion(Build.VERSION_CODES.KITKAT);\n\n        whenNoPlayerPropertiesArePresent();\n\n        thenPlayerTypeIs(AndroidMediaPlayerType.AWESOME);\n    }\n\n    @Test\n    public void givenTheUserIsNotAbleToReadSystemPropertiesWhenFetchingThePlayerTypeThenUnknownPlayerIsDetected() throws Exception {\n        givenTheUserIsOnOSVersion(Build.VERSION_CODES.KITKAT);\n\n        when(systemProperties.get(anyString())).thenThrow(new SystemProperties.MissingSystemPropertiesException(new Exception()));\n\n        thenPlayerTypeIs(AndroidMediaPlayerType.UNKNOWN);\n    }\n\n    private void givenTheUserIsOnOSVersion(int deviceOSVersion) {\n        checker = new MediaPlayerTypeReader(systemProperties, deviceOSVersion);\n    }\n\n    private void whenPropertyisPresent(String property) throws SystemProperties.MissingSystemPropertiesException {\n        when(systemProperties.get(property)).thenReturn(Boolean.TRUE.toString());\n    }\n\n    private void whenNoPlayerPropertiesArePresent() {\n        // no-op because there is no work to do\n    }\n\n    private void thenPlayerTypeIs(AndroidMediaPlayerType playerType) {\n        assertThat(checker.getPlayerType()).isEqualTo(playerType);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/model/AudioTracksTest.java",
    "content": "package com.novoda.noplayer.model;\n\nimport com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType;\n\nimport java.util.Arrays;\nimport java.util.Collections;\n\nimport org.junit.Test;\n\nimport static org.fest.assertions.api.Assertions.assertThat;\n\npublic class AudioTracksTest {\n\n    private static final PlayerAudioTrack MAIN_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().withAudioTrackType(AudioTrackType.MAIN).build();\n    private static final PlayerAudioTrack ALTERNATIVE_TRACK = PlayerAudioTrackFixture.aPlayerAudioTrack().withAudioTrackType(AudioTrackType.ALTERNATIVE).build();\n    private static final int FIRST_INDEX = 0;\n    private static final int EXPECTED_SIZE = 2;\n\n    @Test\n    public void givenAudioTracks_whenGettingTrack_thenReturnsTrack() {\n        AudioTracks audioTracks = AudioTracks.from(Collections.singletonList(MAIN_TRACK));\n\n        PlayerAudioTrack playerAudioTrack = audioTracks.get(FIRST_INDEX);\n\n        assertThat(playerAudioTrack).isEqualTo(MAIN_TRACK);\n    }\n\n    @Test\n    public void givenAudioTracks_whenGettingSize_thenReturnsSize() {\n        AudioTracks audioTracks = AudioTracks.from(Arrays.asList(MAIN_TRACK, ALTERNATIVE_TRACK));\n\n        int size = audioTracks.size();\n\n        assertThat(size).isEqualTo(EXPECTED_SIZE);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/model/EitherTest.java",
    "content": "package com.novoda.noplayer.model;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.MockitoJUnitRunner;\n\nimport static org.mockito.Mockito.verify;\n\n@RunWith(MockitoJUnitRunner.class)\npublic class EitherTest {\n\n    @Mock\n    private Either.Consumer<String> leftConsumer;\n    @Mock\n    private Either.Consumer<Integer> rightConsumer;\n\n    @Test\n    public void givenEitherContainsLeft_whenApplyingConsumers_thenRunsLeftConsumerWithCorrectValue() {\n        String value = \"foo\";\n        Either<String, Integer> either = Either.left(value);\n\n        either.apply(leftConsumer, rightConsumer);\n\n        verify(leftConsumer).accept(value);\n    }\n\n    @Test\n    public void givenEitherContainsRight_whenApplyingConsumers_thenRunsRightConsumerWithCorrectValue() {\n        Integer value = 42;\n        Either<String, Integer> either = Either.right(value);\n\n        either.apply(leftConsumer, rightConsumer);\n\n        verify(rightConsumer).accept(value);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/model/PlayerAudioTrackFixture.java",
    "content": "package com.novoda.noplayer.model;\n\nimport com.novoda.noplayer.internal.exoplayer.mediasource.AudioTrackType;\n\npublic class PlayerAudioTrackFixture {\n\n    private int groupIndex = 0;\n    private int formatIndex = 0;\n    private String trackId = \"id\";\n    private String language = \"english\";\n    private String mimeType = \".mp4\";\n    private int numberOfChannels = 1;\n    private int frequency = 60;\n    private AudioTrackType audioTrackType = AudioTrackType.MAIN;\n\n    public static PlayerAudioTrackFixture aPlayerAudioTrack() {\n        return new PlayerAudioTrackFixture();\n    }\n\n    public PlayerAudioTrackFixture withGroupIndex(int groupIndex) {\n        this.groupIndex = groupIndex;\n        return this;\n    }\n\n    public PlayerAudioTrackFixture withFormatIndex(int formatIndex) {\n        this.formatIndex = formatIndex;\n        return this;\n    }\n\n    public PlayerAudioTrackFixture withTrackId(String trackId) {\n        this.trackId = trackId;\n        return this;\n    }\n\n    public PlayerAudioTrackFixture withLanguage(String language) {\n        this.language = language;\n        return this;\n    }\n\n    public PlayerAudioTrackFixture withMimeType(String mimeType) {\n        this.mimeType = mimeType;\n        return this;\n    }\n\n    public PlayerAudioTrackFixture withNumberOfChannels(int numberOfChannels) {\n        this.numberOfChannels = numberOfChannels;\n        return this;\n    }\n\n    public PlayerAudioTrackFixture withFrequency(int frequency) {\n        this.frequency = frequency;\n        return this;\n    }\n\n    public PlayerAudioTrackFixture withAudioTrackType(AudioTrackType audioTrackType) {\n        this.audioTrackType = audioTrackType;\n        return this;\n    }\n\n    public PlayerAudioTrack build() {\n        return new PlayerAudioTrack(\n                groupIndex,\n                formatIndex,\n                trackId,\n                language,\n                mimeType,\n                numberOfChannels,\n                frequency,\n                audioTrackType\n        );\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/com/novoda/noplayer/model/PlayerVideoTrackFixture.java",
    "content": "package com.novoda.noplayer.model;\n\nimport com.novoda.noplayer.ContentType;\n\npublic class PlayerVideoTrackFixture {\n\n    private int groupIndex = 0;\n    private int formatIndex = 0;\n    private String id = \"id\";\n    private ContentType contentType = ContentType.DASH;\n    private int width = 1920;\n    private int height = 1080;\n    private int fps = 30;\n    private int bitrate = 180000;\n\n    public static PlayerVideoTrackFixture aPlayerVideoTrack() {\n        return new PlayerVideoTrackFixture();\n    }\n\n    private PlayerVideoTrackFixture() {\n        // Uses static factory method.\n    }\n\n    public PlayerVideoTrackFixture withGroupIndex(int groupIndex) {\n        this.groupIndex = groupIndex;\n        return this;\n    }\n\n    public PlayerVideoTrackFixture withFormatIndex(int formatIndex) {\n        this.formatIndex = formatIndex;\n        return this;\n    }\n\n    public PlayerVideoTrackFixture withId(String id) {\n        this.id = id;\n        return this;\n    }\n\n    public PlayerVideoTrackFixture withContentType(ContentType contentType) {\n        this.contentType = contentType;\n        return this;\n    }\n\n    public PlayerVideoTrackFixture withWidth(int width) {\n        this.width = width;\n        return this;\n    }\n\n    public PlayerVideoTrackFixture withHeight(int height) {\n        this.height = height;\n        return this;\n    }\n\n    public PlayerVideoTrackFixture withFps(int fps) {\n        this.fps = fps;\n        return this;\n    }\n\n    public PlayerVideoTrackFixture withBitrate(int bitrate) {\n        this.bitrate = bitrate;\n        return this;\n    }\n\n    public PlayerVideoTrack build() {\n        return new PlayerVideoTrack(groupIndex, formatIndex, id, contentType, width, height, fps, bitrate);\n    }\n}\n"
  },
  {
    "path": "core/src/test/java/utils/ExceptionMatcher.java",
    "content": "package utils;\n\nimport org.hamcrest.BaseMatcher;\nimport org.hamcrest.Description;\n\npublic class ExceptionMatcher extends BaseMatcher<Exception> {\n\n    private final String expectedMessage;\n    private final Class<? extends Exception> expectedExceptionClass;\n\n    public static ExceptionMatcher matches(String message, Class<? extends Exception> expectedExceptionClass) {\n        return new ExceptionMatcher(message, expectedExceptionClass);\n    }\n\n    private ExceptionMatcher(String expectedMessage, Class<? extends Exception> expectedExceptionClass) {\n        this.expectedMessage = expectedMessage;\n        this.expectedExceptionClass = expectedExceptionClass;\n    }\n\n    @Override\n    public boolean matches(Object o) {\n        Exception exception = (Exception) o;\n        return expectedMessage.equals(exception.getMessage()) && exception.getClass().isAssignableFrom(expectedExceptionClass);\n    }\n\n    @Override\n    public void describeTo(Description description) {\n        description.appendText(String.format(\"<%s: %s>\", expectedExceptionClass.getName(), expectedMessage));\n    }\n}\n"
  },
  {
    "path": "core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker",
    "content": "mock-maker-inline\n"
  },
  {
    "path": "demo/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion 28\n    buildToolsVersion '28.0.3'\n\n    defaultConfig {\n        applicationId 'com.novoda.demo'\n        minSdkVersion 16\n        targetSdkVersion 28\n        versionCode 1\n        versionName '1.0'\n    }\n\n    lintOptions {\n        lintConfig teamPropsFile('static-analysis/lint-config.xml')\n        abortOnError true\n        warningsAsErrors true\n    }\n\n    compileOptions {\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n}\ndependencies {\n    implementation project(':core')\n    implementation 'com.android.support:appcompat-v7:28.0.0'\n    testImplementation 'junit:junit:4.12'\n    testImplementation 'com.google.truth:truth:0.44'\n}\n"
  },
  {
    "path": "demo/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  package=\"com.novoda.demo\">\n\n  <application\n    android:allowBackup=\"false\"\n    android:icon=\"@mipmap/ic_launcher\"\n    android:label=\"@string/app_name\"\n    android:roundIcon=\"@mipmap/ic_launcher_round\"\n    android:supportsRtl=\"true\"\n    tools:ignore=\"GoogleAppIndexingWarning\">\n\n    <activity\n      android:name=\"com.novoda.demo.MainActivity\"\n      android:theme=\"@style/Theme.NoPlayer\">\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\" />\n        <category android:name=\"android.intent.category.LAUNCHER\" />\n      </intent-filter>\n    </activity>\n\n  </application>\n\n</manifest>\n"
  },
  {
    "path": "demo/src/main/java/com/novoda/demo/AndroidControllerView.java",
    "content": "package com.novoda.demo;\n\nimport android.content.Context;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.SeekBar;\nimport android.widget.TextView;\n\npublic class AndroidControllerView extends LinearLayout implements ControllerView {\n\n    private ImageView playPauseButton;\n    private TextView elapsedTimeView;\n    private SeekBar progressView;\n    private TextView timeRemainingView;\n    private ImageView volumeOnOffView;\n\n    public AndroidControllerView(@NonNull Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs, 0);\n    }\n\n    public AndroidControllerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        super.setOrientation(HORIZONTAL);\n    }\n\n    @Override\n    public final void setOrientation(int orientation) {\n        throw new IllegalAccessError(\"This layout only supports horizontal orientation\");\n    }\n\n    @Override\n    protected void onFinishInflate() {\n        super.onFinishInflate();\n        LayoutInflater.from(getContext()).inflate(R.layout.merge_player_controls, this, true);\n\n        playPauseButton = (ImageView) findViewById(R.id.player_controls_play_pause);\n        elapsedTimeView = (TextView) findViewById(R.id.player_controls_elapsed_time);\n        progressView = (SeekBar) findViewById(R.id.player_controls_progress);\n        timeRemainingView = (TextView) findViewById(R.id.player_controls_time_remaining);\n        volumeOnOffView = findViewById(R.id.player_controls_volume_on_off);\n    }\n\n    @Override\n    public void setPaused() {\n        playPauseButton.setImageResource(R.drawable.play);\n    }\n\n    @Override\n    public void setPlaying() {\n        playPauseButton.setImageResource(R.drawable.pause);\n    }\n\n    @Override\n    public void updateContentProgress(int progress) {\n        progressView.setProgress(progress);\n    }\n\n    @Override\n    public void updateBufferProgress(int buffer) {\n        progressView.setSecondaryProgress(buffer);\n    }\n\n    @Override\n    public void updateElapsedTime(String elapsedTime) {\n        elapsedTimeView.setText(elapsedTime);\n    }\n\n    @Override\n    public void updateTimeRemaining(String timeRemaining) {\n        timeRemainingView.setText(timeRemaining);\n    }\n\n    @Override\n    public void setTogglePlayPauseAction(final TogglePlayPauseAction togglePlayPauseAction) {\n        playPauseButton.setOnClickListener(new OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                togglePlayPauseAction.perform();\n            }\n        });\n    }\n\n    @Override\n    public void setSeekAction(final SeekAction seekAction) {\n        progressView.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {\n            @Override\n            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {\n                // Not required.\n            }\n\n            @Override\n            public void onStartTrackingTouch(SeekBar seekBar) {\n                // Not required.\n            }\n\n            @Override\n            public void onStopTrackingTouch(SeekBar seekBar) {\n                int progress = seekBar.getProgress();\n                int max = seekBar.getMax();\n                seekAction.perform(progress, max);\n            }\n        });\n    }\n\n    @Override\n    public void setVolumeOn() {\n        volumeOnOffView.setImageResource(R.drawable.volume_on);\n    }\n\n    @Override\n    public void setVolumeOff() {\n        volumeOnOffView.setImageResource(R.drawable.volume_off);\n    }\n\n    @Override\n    public void setToggleVolumeOnOffAction(final ToggleVolumeOnOffAction toggleVolumeOnOffAction) {\n        volumeOnOffView.setOnClickListener(new OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                toggleVolumeOnOffAction.perform();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/com/novoda/demo/ControllerView.java",
    "content": "package com.novoda.demo;\n\ninterface ControllerView {\n\n    void setPaused();\n\n    void setPlaying();\n\n    void updateContentProgress(int progress);\n\n    void updateBufferProgress(int buffer);\n\n    void updateElapsedTime(String elapsedTime);\n\n    void updateTimeRemaining(String timeRemaining);\n\n    void setTogglePlayPauseAction(TogglePlayPauseAction togglePlayPauseAction);\n\n    void setSeekAction(SeekAction seekAction);\n\n    void setVolumeOn();\n\n    void setVolumeOff();\n\n    void setToggleVolumeOnOffAction(ToggleVolumeOnOffAction toggleVolumeOnOffAction);\n\n    interface TogglePlayPauseAction {\n\n        void perform();\n    }\n\n    interface SeekAction {\n\n        void perform(int progress, int max);\n    }\n\n    interface ToggleVolumeOnOffAction {\n\n        void perform();\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/com/novoda/demo/DataPostingModularDrm.java",
    "content": "package com.novoda.demo;\n\nimport com.novoda.noplayer.drm.ModularDrmKeyRequest;\nimport com.novoda.noplayer.drm.StreamingModularDrm;\n\nclass DataPostingModularDrm implements StreamingModularDrm {\n\n    private final String url;\n\n    DataPostingModularDrm(String url) {\n        this.url = url;\n    }\n\n    @Override\n    public byte[] executeKeyRequest(ModularDrmKeyRequest request) throws DrmRequestException {\n        return HttpClient.post(url, request.data());\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/com/novoda/demo/DemoPresenter.java",
    "content": "package com.novoda.demo;\n\nimport android.net.Uri;\n\nimport com.novoda.noplayer.Listeners;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.PlayerState;\nimport com.novoda.noplayer.PlayerView;\n\nclass DemoPresenter {\n\n    private final ControllerView controllerView;\n    private final NoPlayer noPlayer;\n    private final Listeners listeners;\n    private final PlayerView playerView;\n\n    DemoPresenter(ControllerView controllerView, NoPlayer noPlayer, Listeners listeners, PlayerView playerView) {\n        this.controllerView = controllerView;\n        this.noPlayer = noPlayer;\n        this.listeners = listeners;\n        this.playerView = playerView;\n    }\n\n    void startPresenting(Uri uri, Options options) {\n        listeners.addPreparedListener(playOnPrepared);\n        listeners.addStateChangedListener(updatePlayPause);\n        listeners.addHeartbeatCallback(updateProgress);\n\n        controllerView.setTogglePlayPauseAction(onTogglePlayPause);\n        controllerView.setSeekAction(onSeekPerformed);\n        controllerView.setToggleVolumeOnOffAction(onToggleVolume);\n\n        noPlayer.attach(playerView);\n        noPlayer.loadVideo(uri, options);\n    }\n\n    private final NoPlayer.PreparedListener playOnPrepared = new NoPlayer.PreparedListener() {\n        @Override\n        public void onPrepared(PlayerState playerState) {\n            noPlayer.play();\n        }\n    };\n\n    private final NoPlayer.StateChangedListener updatePlayPause = new NoPlayer.StateChangedListener() {\n        @Override\n        public void onVideoPlaying() {\n            controllerView.setPlaying();\n        }\n\n        @Override\n        public void onVideoPaused() {\n            controllerView.setPaused();\n        }\n\n        @Override\n        public void onVideoStopped() {\n            // Not required.\n        }\n    };\n\n    private final NoPlayer.HeartbeatCallback updateProgress = new NoPlayer.HeartbeatCallback() {\n        @Override\n        public void onBeat(NoPlayer player) {\n            long positionInMillis = player.playheadPositionInMillis();\n            long durationInMillis = player.mediaDurationInMillis();\n            int bufferPercentage = player.bufferPercentage();\n\n            updateProgress(positionInMillis, durationInMillis, bufferPercentage);\n            updateTiming(positionInMillis, durationInMillis);\n        }\n    };\n\n    private void updateProgress(long positionInMillis, long durationInMillis, int bufferPercentage) {\n        int progressAsIncrements = ProgressCalculator.progressAsIncrements(positionInMillis, durationInMillis);\n        int bufferAsIncrements = ProgressCalculator.bufferAsIncrements(bufferPercentage);\n\n        controllerView.updateContentProgress(progressAsIncrements);\n        controllerView.updateBufferProgress(bufferAsIncrements);\n    }\n\n    private void updateTiming(long positionInMillis, long durationInMillis) {\n        long remainingDuration = durationInMillis - positionInMillis;\n\n        controllerView.updateElapsedTime(TimeFormatter.asHoursMinutesSeconds(positionInMillis));\n        controllerView.updateTimeRemaining(TimeFormatter.asHoursMinutesSeconds(remainingDuration));\n    }\n\n    private final ControllerView.TogglePlayPauseAction onTogglePlayPause = new ControllerView.TogglePlayPauseAction() {\n        @Override\n        public void perform() {\n            if (noPlayer.isPlaying()) {\n                noPlayer.pause();\n            } else {\n                noPlayer.play();\n            }\n        }\n    };\n\n    private final ControllerView.SeekAction onSeekPerformed = new ControllerView.SeekAction() {\n        @Override\n        public void perform(int progress, int max) {\n            long seekToPosition = ProgressCalculator.seekToPosition(noPlayer.mediaDurationInMillis(), progress, max);\n            noPlayer.seekTo(seekToPosition);\n        }\n    };\n\n    private final ControllerView.ToggleVolumeOnOffAction onToggleVolume = new ControllerView.ToggleVolumeOnOffAction() {\n        @Override\n        public void perform() {\n            boolean isSoundOn = Float.compare(noPlayer.getVolume(), 0f) > 0;\n            if (isSoundOn) {\n                noPlayer.setVolume(0f);\n                controllerView.setVolumeOff();\n            } else {\n                noPlayer.setVolume(1f);\n                controllerView.setVolumeOn();\n            }\n        }\n    };\n\n    void stopPresenting() {\n        noPlayer.stop();\n        noPlayer.release();\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/com/novoda/demo/DialogCreator.java",
    "content": "package com.novoda.demo;\n\nimport android.app.AlertDialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.widget.ArrayAdapter;\n\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.model.AudioTracks;\nimport com.novoda.noplayer.model.PlayerAudioTrack;\nimport com.novoda.noplayer.model.PlayerSubtitleTrack;\nimport com.novoda.noplayer.model.PlayerVideoTrack;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\n\nclass DialogCreator {\n\n    private static final String VIDEO_TRACK_MESSAGE_FORMAT = \"ID: %s Quality: %s\";\n    private static final String AUDIO_TRACK_MESSAGE_FORMAT = \"ID: %s Type: %s\";\n    private static final int AUTO_TRACK_POSITION = 0;\n\n    private final Context context;\n    private final NoPlayer noPlayer;\n\n    DialogCreator(Context context, NoPlayer noPlayer) {\n        this.context = context;\n        this.noPlayer = noPlayer;\n    }\n\n    void showVideoSelectionDialog() {\n        final List<PlayerVideoTrack> videoTracks = noPlayer.getVideoTracks();\n\n        ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.list_item);\n        adapter.addAll(mapVideoTrackToLabel(videoTracks));\n        new AlertDialog.Builder(context)\n                .setTitle(\"Select Video track\")\n                .setAdapter(adapter, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int position) {\n                        if (position == AUTO_TRACK_POSITION) {\n                            noPlayer.clearVideoTrackSelection();\n                        } else {\n                            PlayerVideoTrack videoTrack = videoTracks.get(position - 1);\n                            noPlayer.selectVideoTrack(videoTrack);\n                        }\n                    }\n                })\n                .create()\n                .show();\n    }\n\n    private List<String> mapVideoTrackToLabel(List<PlayerVideoTrack> videoTracks) {\n        List<String> labels = new ArrayList<>();\n        labels.add(\"Auto\");\n        for (PlayerVideoTrack videoTrack : videoTracks) {\n            String message = String.format(VIDEO_TRACK_MESSAGE_FORMAT, videoTrack.id(), videoTrack.height());\n            labels.add(message);\n        }\n        return labels;\n    }\n\n    void showAudioSelectionDialog() {\n        final AudioTracks audioTracks = noPlayer.getAudioTracks();\n        ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.list_item);\n        adapter.addAll(mapAudioTrackToLabel(audioTracks));\n        new AlertDialog.Builder(context)\n                .setTitle(\"Select audio track\")\n                .setAdapter(adapter, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int position) {\n                        if (position == AUTO_TRACK_POSITION) {\n                            noPlayer.clearAudioTrackSelection();\n                        } else {\n                            PlayerAudioTrack audioTrack = audioTracks.get(position - 1);\n                            noPlayer.selectAudioTrack(audioTrack);\n                        }\n                    }\n                })\n                .create()\n                .show();\n    }\n\n    private List<String> mapAudioTrackToLabel(AudioTracks audioTracks) {\n        List<String> labels = new ArrayList<>();\n        labels.add(\"Auto\");\n        for (PlayerAudioTrack audioTrack : audioTracks) {\n            String label = String.format(\n                    Locale.UK,\n                    AUDIO_TRACK_MESSAGE_FORMAT,\n                    audioTrack.trackId(),\n                    audioTrack.audioTrackType()\n            );\n            labels.add(label);\n        }\n        return labels;\n    }\n\n    void showSubtitleSelectionDialog() {\n        final List<PlayerSubtitleTrack> subtitleTracks = noPlayer.getSubtitleTracks();\n        ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.list_item);\n        adapter.addAll(mapSubtitleTrackToLabel(subtitleTracks));\n        new AlertDialog.Builder(context)\n                .setTitle(\"Select subtitle track\")\n                .setAdapter(adapter, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialog, int position) {\n                        if (position == AUTO_TRACK_POSITION) {\n                            noPlayer.hideSubtitleTrack();\n                        } else {\n                            PlayerSubtitleTrack subtitleTrack = subtitleTracks.get(position - 1);\n                            noPlayer.showSubtitleTrack(subtitleTrack);\n                        }\n                    }\n                })\n                .create()\n                .show();\n    }\n\n    private List<String> mapSubtitleTrackToLabel(List<PlayerSubtitleTrack> subtitleTracks) {\n        List<String> labels = new ArrayList<>();\n        labels.add(\"Dismiss subtitles\");\n        for (PlayerSubtitleTrack subtitleTrack : subtitleTracks) {\n            labels.add(\"Group: \" + subtitleTrack.groupIndex() + \" Format: \" + subtitleTrack.formatIndex());\n        }\n        return labels;\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/com/novoda/demo/HttpClient.java",
    "content": "package com.novoda.demo;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.DataOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.net.URLConnection;\n\nfinal class HttpClient {\n\n    private static final String POST = \"POST\";\n    private static final int RESPONSE_BUFFER_SIZE = 16384;\n\n    private HttpClient() {\n        // Not instantiable\n    }\n\n    static byte[] post(String url, byte[] data) {\n        HttpURLConnection connection = null;\n        try {\n            connection = (HttpURLConnection) new URL(url).openConnection();\n            connection.setRequestMethod(POST);\n            connection.setDoOutput(true);\n            connection.setDoInput(true);\n\n            DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());\n            outputStream.write(data);\n            outputStream.flush();\n            outputStream.close();\n\n            return readResponseFrom(connection);\n        } catch (IOException e) {\n            throw new HttpClientException(e);\n        } finally {\n            release(connection);\n        }\n    }\n\n    private static void release(HttpURLConnection connection) {\n        if (connection != null) {\n            connection.disconnect();\n        }\n    }\n\n    private static byte[] readResponseFrom(URLConnection connection) throws IOException {\n        InputStream inputStream = null;\n        ByteArrayOutputStream buffer = null;\n        try {\n            inputStream = connection.getInputStream();\n            buffer = new ByteArrayOutputStream();\n\n            int currentReadPosition;\n            byte[] readHolder = new byte[RESPONSE_BUFFER_SIZE];\n\n            while ((currentReadPosition = inputStream.read(readHolder, 0, readHolder.length)) != -1) {\n                buffer.write(readHolder, 0, currentReadPosition);\n            }\n\n            buffer.flush();\n            return buffer.toByteArray();\n        } catch (IOException e) {\n            throw new HttpClientException(e);\n        } finally {\n            release(inputStream, buffer);\n        }\n    }\n\n    private static void release(InputStream inputStream, ByteArrayOutputStream buffer) throws IOException {\n        if (inputStream != null) {\n            inputStream.close();\n        }\n\n        if (buffer != null) {\n            buffer.close();\n        }\n    }\n\n    private static class HttpClientException extends RuntimeException {\n\n        HttpClientException(Throwable cause) {\n            super(cause);\n        }\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/com/novoda/demo/MainActivity.java",
    "content": "package com.novoda.demo;\n\nimport android.app.Activity;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.View;\nimport android.widget.CheckBox;\nimport android.widget.CompoundButton;\nimport android.widget.Toast;\n\nimport com.novoda.noplayer.ContentType;\nimport com.novoda.noplayer.NoPlayer;\nimport com.novoda.noplayer.Options;\nimport com.novoda.noplayer.OptionsBuilder;\nimport com.novoda.noplayer.PlayerBuilder;\nimport com.novoda.noplayer.PlayerView;\nimport com.novoda.noplayer.internal.utils.NoPlayerLog;\n\npublic class MainActivity extends Activity {\n\n    private static final String URI_VIDEO_WIDEVINE_EXAMPLE_MODULAR_MPD = \"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd\";\n    private static final String EXAMPLE_MODULAR_LICENSE_SERVER_PROXY = \"https://proxy.uat.widevine.com/proxy?provider=widevine_test\";\n    private static final int HALF_A_SECOND_IN_MILLIS = 500;\n    private static final int TWO_MEGABITS = 2000000;\n    private static final int MAX_VIDEO_BITRATE = 800000;\n\n    private NoPlayer player;\n    private DemoPresenter demoPresenter;\n    private DialogCreator dialogCreator;\n    private CheckBox hdSelectionCheckBox;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        NoPlayerLog.setLoggingEnabled(true);\n        setContentView(R.layout.activity_main);\n        PlayerView playerView = findViewById(R.id.player_view);\n        View videoSelectionButton = findViewById(R.id.button_video_selection);\n        View audioSelectionButton = findViewById(R.id.button_audio_selection);\n        View subtitleSelectionButton = findViewById(R.id.button_subtitle_selection);\n        hdSelectionCheckBox = findViewById(R.id.button_hd_selection);\n        ControllerView controllerView = findViewById(R.id.controller_view);\n\n        videoSelectionButton.setOnClickListener(showVideoSelectionDialog);\n        audioSelectionButton.setOnClickListener(showAudioSelectionDialog);\n        subtitleSelectionButton.setOnClickListener(showSubtitleSelectionDialog);\n        hdSelectionCheckBox.setOnCheckedChangeListener(toggleHdSelection);\n\n        DataPostingModularDrm drmHandler = new DataPostingModularDrm(EXAMPLE_MODULAR_LICENSE_SERVER_PROXY);\n\n        player = new PlayerBuilder()\n                .withWidevineModularStreamingDrm(drmHandler)\n                .withDowngradedSecureDecoder()\n                .withUserAgent(\"Android/Linux\")\n                .allowCrossProtocolRedirects()\n                .build(this);\n\n        demoPresenter = new DemoPresenter(controllerView, player, player.getListeners(), playerView);\n        dialogCreator = new DialogCreator(this, player);\n\n        player.getListeners().addDroppedVideoFrames(new NoPlayer.DroppedVideoFramesListener() {\n            @Override\n            public void onDroppedVideoFrames(int droppedFrames, long elapsedMsSinceLastDroppedFrames) {\n                Log.v(getClass().toString(), \"dropped frames: \" + droppedFrames + \" since: \" + elapsedMsSinceLastDroppedFrames + \"ms\");\n            }\n        });\n\n    }\n\n    @Override\n    protected void onStart() {\n        super.onStart();\n        Uri uri = Uri.parse(URI_VIDEO_WIDEVINE_EXAMPLE_MODULAR_MPD);\n        Options options = new OptionsBuilder()\n                .withContentType(ContentType.DASH)\n                .withMinDurationBeforeQualityIncreaseInMillis(HALF_A_SECOND_IN_MILLIS)\n                .withMaxInitialBitrate(TWO_MEGABITS)\n                .withMaxVideoBitrate(getMaxVideoBitrate())\n                .build();\n        demoPresenter.startPresenting(uri, options);\n    }\n\n    private int getMaxVideoBitrate() {\n        if (hdSelectionCheckBox.isChecked()) {\n            return Integer.MAX_VALUE;\n        }\n        return MAX_VIDEO_BITRATE;\n    }\n\n    private final View.OnClickListener showVideoSelectionDialog = new View.OnClickListener() {\n        @Override\n        public void onClick(View v) {\n            if (player.getVideoTracks().isEmpty()) {\n                Toast.makeText(MainActivity.this, \"no additional video tracks available!\", Toast.LENGTH_LONG).show();\n            } else {\n                dialogCreator.showVideoSelectionDialog();\n            }\n        }\n    };\n\n    private final View.OnClickListener showAudioSelectionDialog = new View.OnClickListener() {\n\n        @Override\n        public void onClick(View v) {\n            dialogCreator.showAudioSelectionDialog();\n        }\n    };\n\n    private final View.OnClickListener showSubtitleSelectionDialog = new View.OnClickListener() {\n        @Override\n        public void onClick(View v) {\n            if (player.getSubtitleTracks().isEmpty()) {\n                Toast.makeText(MainActivity.this, \"no subtitles available!\", Toast.LENGTH_LONG).show();\n            } else {\n                dialogCreator.showSubtitleSelectionDialog();\n            }\n        }\n    };\n\n    private final CompoundButton.OnCheckedChangeListener toggleHdSelection = new CompoundButton.OnCheckedChangeListener() {\n        @Override\n        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {\n            if (isChecked) {\n                player.clearMaxVideoBitrate();\n            } else {\n                player.setMaxVideoBitrate(MAX_VIDEO_BITRATE);\n            }\n        }\n    };\n\n    @Override\n    protected void onStop() {\n        demoPresenter.stopPresenting();\n        super.onStop();\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/com/novoda/demo/ProgressCalculator.java",
    "content": "package com.novoda.demo;\n\nfinal class ProgressCalculator {\n\n    private static final int MAX_PROGRESS_INCREMENTS = 100;\n    private static final int MAX_PROGRESS_PERCENT = 100;\n\n    private ProgressCalculator() {\n        // Uses static methods.\n    }\n\n    static int progressAsIncrements(long positionInMillis, long durationInMillis) {\n        double percentageOfDuration = positionInMillis / (float) durationInMillis;\n        return (int) (MAX_PROGRESS_INCREMENTS * percentageOfDuration);\n    }\n\n    static int bufferAsIncrements(int bufferPercentage) {\n        return (bufferPercentage * MAX_PROGRESS_INCREMENTS) / MAX_PROGRESS_PERCENT;\n    }\n\n    static long seekToPosition(long durationInMillis, int progress, int max) {\n        float progressMultiplier = (float) progress / max;\n        return (long) (durationInMillis * progressMultiplier);\n    }\n}\n"
  },
  {
    "path": "demo/src/main/java/com/novoda/demo/TimeFormatter.java",
    "content": "package com.novoda.demo;\n\nimport java.util.Locale;\nimport java.util.concurrent.TimeUnit;\n\nfinal class TimeFormatter {\n\n    private TimeFormatter() {\n        // Uses static methods.\n    }\n\n    static String asHoursMinutesSeconds(long timeInMillis) {\n        long hours = TimeUnit.MILLISECONDS.toHours(timeInMillis);\n        long minutes = TimeUnit.MILLISECONDS.toMinutes(timeInMillis - TimeUnit.HOURS.toMillis(hours));\n        long seconds = TimeUnit.MILLISECONDS.toSeconds(timeInMillis - TimeUnit.HOURS.toMillis(hours) - TimeUnit.MINUTES.toMillis(minutes));\n\n        if (hours > 0) {\n            return String.format(Locale.getDefault(), \"%d:%02d:%02d\", hours, minutes, seconds);\n        } else {\n            return String.format(Locale.getDefault(), \"%02d:%02d\", minutes, seconds);\n        }\n    }\n}\n"
  },
  {
    "path": "demo/src/main/res/drawable/progress.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <item android:id=\"@android:id/background\">\n    <shape>\n      <corners android:radius=\"1dp\" />\n      <solid android:color=\"#FF23282A\" />\n    </shape>\n  </item>\n\n  <item android:id=\"@android:id/secondaryProgress\">\n    <clip>\n      <shape>\n        <corners android:radius=\"1dp\" />\n        <solid android:color=\"#66FFFFFF\" />\n      </shape>\n    </clip>\n  </item>\n\n  <item android:id=\"@android:id/progress\">\n    <clip>\n      <shape>\n        <corners android:radius=\"1dp\" />\n        <solid android:color=\"#FFFFFFFF\" />\n      </shape>\n    </clip>\n  </item>\n\n</layer-list>\n"
  },
  {
    "path": "demo/src/main/res/drawable/thumb.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <item\n    android:top=\"6dp\"\n    android:right=\"6dp\"\n    android:left=\"6dp\"\n    android:bottom=\"6dp\">\n\n    <shape android:shape=\"oval\">\n      <solid android:color=\"@android:color/white\" />\n      <size\n        android:width=\"20dp\" android:height=\"20dp\" />\n    </shape>\n\n  </item>\n\n</layer-list>\n"
  },
  {
    "path": "demo/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n  <LinearLayout\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <LinearLayout\n      style=\"?android:attr/buttonBarStyle\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"wrap_content\"\n      android:orientation=\"horizontal\">\n\n      <Button\n        android:id=\"@+id/button_video_selection\"\n        style=\"?android:attr/buttonBarButtonStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/video\" />\n\n      <Button\n        android:id=\"@+id/button_audio_selection\"\n        style=\"?android:attr/buttonBarButtonStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/audio\" />\n\n      <Button\n        android:id=\"@+id/button_subtitle_selection\"\n        style=\"?android:attr/buttonBarButtonStyle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/subtitle\" />\n\n      <CheckBox\n          android:id=\"@+id/button_hd_selection\"\n          style=\"?android:attr/buttonBarButtonStyle\"\n          android:layout_width=\"wrap_content\"\n          android:layout_height=\"wrap_content\"\n          android:text=\"@string/hd\"/>\n\n    </LinearLayout>\n\n    <com.novoda.noplayer.NoPlayerView\n      android:id=\"@+id/player_view\"\n      android:layout_width=\"wrap_content\"\n      android:layout_height=\"wrap_content\"\n      android:layout_gravity=\"center\" />\n\n    <com.novoda.demo.AndroidControllerView\n      android:id=\"@+id/controller_view\"\n      android:layout_width=\"match_parent\"\n      android:layout_height=\"@dimen/controller_view_height\"\n      android:layout_gravity=\"bottom\" />\n\n  </LinearLayout>\n\n</merge>\n"
  },
  {
    "path": "demo/src/main/res/layout/list_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:id=\"@android:id/text1\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  android:background=\"@color/white\"\n  android:textColor=\"@color/black\"\n  android:textSize=\"@dimen/list_item_text\"\n  android:padding=\"@dimen/list_item_padding\"\n  tools:text=\"Lorem Ipsum Dolor Sit Amet\" />\n"
  },
  {
    "path": "demo/src/main/res/layout/merge_player_controls.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"wrap_content\"\n  tools:parentTag=\"android.widget.LinearLayout\">\n\n  <ImageView\n    android:id=\"@+id/player_controls_play_pause\"\n    android:layout_width=\"@dimen/play_button_size\"\n    android:layout_height=\"@dimen/play_button_size\"\n    android:layout_marginLeft=\"@dimen/player_controls_padding_horizontal\"\n    android:layout_marginStart=\"@dimen/player_controls_padding_horizontal\"\n    android:layout_marginRight=\"@dimen/player_controls_play_button_margin_end\"\n    android:layout_marginEnd=\"@dimen/player_controls_play_button_margin_end\"\n    android:src=\"@drawable/play\"\n    android:contentDescription=\"@string/content_description_play_pause\" />\n\n  <TextView\n    android:id=\"@+id/player_controls_elapsed_time\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_gravity=\"center_vertical\"\n    android:textColor=\"@android:color/white\"\n    tools:text=\"00:00\" />\n\n  <SeekBar\n    android:id=\"@+id/player_controls_progress\"\n    style=\"@style/Controls.SeekBar\"\n    android:layout_width=\"@dimen/use_weight\"\n    android:layout_height=\"wrap_content\"\n    android:layout_weight=\"100\"\n    android:layout_gravity=\"center_vertical\"\n    android:layout_marginLeft=\"@dimen/player_controls_seekbar_margin_horizontal\"\n    android:layout_marginRight=\"@dimen/player_controls_seekbar_margin_horizontal\" />\n\n  <TextView\n    android:id=\"@+id/player_controls_time_remaining\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_gravity=\"center_vertical\"\n    android:layout_marginRight=\"@dimen/player_controls_padding_horizontal\"\n    android:layout_marginEnd=\"@dimen/player_controls_padding_horizontal\"\n    android:textColor=\"@android:color/white\"\n    tools:text=\"01:00\" />\n\n  <ImageView\n    android:id=\"@+id/player_controls_volume_on_off\"\n    android:layout_width=\"@dimen/play_button_size\"\n    android:layout_height=\"@dimen/play_button_size\"\n    android:layout_marginLeft=\"@dimen/player_controls_padding_horizontal\"\n    android:layout_marginStart=\"@dimen/player_controls_padding_horizontal\"\n    android:layout_marginRight=\"@dimen/player_controls_play_button_margin_end\"\n    android:layout_marginEnd=\"@dimen/player_controls_play_button_margin_end\"\n    android:src=\"@drawable/volume_on\"/>\n\n</merge>\n"
  },
  {
    "path": "demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <background android:drawable=\"@mipmap/ic_launcher_background\" />\n  <foreground android:drawable=\"@mipmap/ic_launcher_foreground\" />\n</adaptive-icon>\n"
  },
  {
    "path": "demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <background android:drawable=\"@mipmap/ic_launcher_background\" />\n  <foreground android:drawable=\"@mipmap/ic_launcher_foreground\" />\n</adaptive-icon>\n"
  },
  {
    "path": "demo/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n  <color name=\"colorPrimary\">#6b747b</color>\n  <color name=\"colorPrimaryDark\">#2c3942</color>\n  <color name=\"colorAccent\">#f5bf41</color>\n  <color name=\"textPrimary\">#deffffff</color>\n  <color name=\"windowBackground\">@color/black</color>\n\n  <color name=\"white\">#ffffff</color>\n  <color name=\"black\">#000000</color>\n\n</resources>\n"
  },
  {
    "path": "demo/src/main/res/values/controls_styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n  <style name=\"Controls\" />\n\n  <style name=\"Controls.SeekBar\">\n    <item name=\"android:focusable\">true</item>\n    <item name=\"android:indeterminateDrawable\">@drawable/progress</item>\n    <item name=\"android:indeterminateOnly\">false</item>\n    <item name=\"android:maxHeight\">@dimen/seekbar_height</item>\n    <item name=\"android:minHeight\">@dimen/seekbar_height</item>\n    <item name=\"android:progressDrawable\">@drawable/progress</item>\n    <item name=\"android:thumb\">@drawable/thumb</item>\n    <item name=\"android:thumbOffset\">@dimen/seekbar_thumb_offset</item>\n    <item name=\"android:mirrorForRtl\" tools:ignore=\"NewApi\">true</item> <!-- Only needed on API 18+ -->\n  </style>\n\n</resources>\n"
  },
  {
    "path": "demo/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n  <dimen name=\"play_button_size\">48dp</dimen>\n  <dimen name=\"controller_view_height\">48dp</dimen>\n\n  <dimen name=\"player_controls_padding_horizontal\">12dp</dimen>\n  <dimen name=\"player_controls_play_button_margin_end\">@dimen/player_controls_padding_horizontal</dimen>\n  <dimen name=\"player_controls_seekbar_margin_horizontal\">@dimen/player_controls_padding_horizontal</dimen>\n\n  <dimen name=\"seekbar_height\">2dp</dimen>\n  <dimen name=\"seekbar_thumb_offset\">6dp</dimen>\n\n  <dimen name=\"use_weight\">0dp</dimen>\n  <dimen name=\"list_item_text\">20sp</dimen>\n  <dimen name=\"list_item_padding\">8dp</dimen>\n\n</resources>\n"
  },
  {
    "path": "demo/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n  <string name=\"app_name\">no-player demo</string>\n  \n  <string name=\"video\">Video</string>\n  <string name=\"audio\">Audio</string>\n  <string name=\"subtitle\">Subtitle</string>\n  <string name=\"hd\">HD</string>\n\n  <string name=\"content_description_play_pause\">Play/pause</string>\n\n</resources>\n"
  },
  {
    "path": "demo/src/main/res/values/themes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n  <style name=\"Theme.NoPlayer\" parent=\"Theme.AppCompat.NoActionBar\">\n    <item name=\"colorPrimary\">@color/colorPrimary</item>\n    <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n    <item name=\"colorAccent\">@color/colorAccent</item>\n    <item name=\"android:textColorPrimary\">@color/textPrimary</item>\n    <item name=\"android:windowBackground\">@color/windowBackground</item>\n  </style>\n\n</resources>\n"
  },
  {
    "path": "demo/src/test/java/com/novoda/demo/TimeFormatterTest.java",
    "content": "package com.novoda.demo;\n\nimport org.junit.Test;\n\nimport static com.google.common.truth.Truth.assertThat;\n\npublic class TimeFormatterTest {\n\n    @Test\n    public void givenOverOneHourInMillis_whenFormatting_thenReturnsAsHoursMinutesSeconds() {\n        long oneHourTwelveMinutesAndFifteenSecondsAsMillis = 4335000;\n\n        String asHoursMinutesSeconds = TimeFormatter.asHoursMinutesSeconds(oneHourTwelveMinutesAndFifteenSecondsAsMillis);\n\n        assertThat(asHoursMinutesSeconds).isEqualTo(\"1:12:15\");\n    }\n\n    @Test\n    public void givenLessThanOneHourInMillis_whenFormatting_thenReturnsAsMinutesAndSeconds() {\n        long fiftyOneMinutesAndThirtyFiveSecondsAsMillis = 3095000;\n\n        String asHoursMinutesSeconds = TimeFormatter.asHoursMinutesSeconds(fiftyOneMinutesAndThirtyFiveSecondsAsMillis);\n\n        assertThat(asHoursMinutesSeconds).isEqualTo(\"51:35\");\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Fri Dec 21 08:51:35 GMT 2018\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-5.0-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windows variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':core'\ninclude ':demo'\n"
  },
  {
    "path": "team-props/static-analysis/checkstyle-modules.xml",
    "content": "<!DOCTYPE module PUBLIC\n  \"-//Puppy Crawl//DTD Check Configuration 1.3//EN\"\n  \"http://www.puppycrawl.com/dtds/configuration_1_3.dtd\">\n\n<module name=\"Checker\">\n\n  <module name=\"SuppressionFilter\">\n    <property name=\"file\" value=\"team-props/static-analysis/checkstyle-suppressions.xml\" />\n  </module>\n\n  <module name=\"SuppressWarningsFilter\" />\n\n  <!-- Checks whether files end with a new line.                        -->\n  <!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->\n  <module name=\"NewlineAtEndOfFile\" />\n\n  <!-- Checks that property files contain the same keys.         -->\n  <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->\n  <module name=\"Translation\" />\n\n  <!-- Checks for Size Violations.                    -->\n  <!-- See http://checkstyle.sf.net/config_sizes.html -->\n  <module name=\"FileLength\" />\n\n  <!-- Checks for whitespace                               -->\n  <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n  <module name=\"FileTabCharacter\" />\n\n  <!-- Miscellaneous other checks.                   -->\n  <!-- See http://checkstyle.sf.net/config_misc.html -->\n  <module name=\"RegexpSingleline\">\n    <property name=\"format\" value=\"\\s+$\" />\n    <property name=\"minimum\" value=\"0\" />\n    <property name=\"maximum\" value=\"0\" />\n    <property name=\"message\" value=\"Line has trailing spaces.\" />\n    <property name=\"severity\" value=\"info\" />\n  </module>\n\n  <module name=\"TreeWalker\">\n\n    <module name=\"SuppressWarningsHolder\" />\n\n    <!-- Suppressions -->\n    <module name=\"SuppressionCommentFilter\">\n      <property name=\"offCommentFormat\"\n        value=\"CHECKSTYLE IGNORE\\s+(\\S+)\" />\n      <property name=\"onCommentFormat\"\n        value=\"CHECKSTYLE END IGNORE\\s+(\\S+)\" />\n      <property name=\"checkFormat\"\n        value=\"$1\" />\n    </module>\n\n    <module name=\"SuppressWithNearbyCommentFilter\">\n      <!-- Syntax is \"SUPPRESS CHECKSTYLE name\" -->\n      <property name=\"commentFormat\"\n        value=\"SUPPRESS CHECKSTYLE (\\w+)\" />\n      <property name=\"checkFormat\"\n        value=\"$1\" />\n      <property name=\"influenceFormat\"\n        value=\"1\" />\n    </module>\n\n    <!-- Checks for Naming Conventions.                  -->\n    <!-- See http://checkstyle.sf.net/config_naming.html -->\n    <module name=\"ConstantName\" />\n    <module name=\"LocalFinalVariableName\" />\n    <module name=\"LocalVariableName\" />\n    <module name=\"MemberName\" />\n    <module name=\"MethodName\" />\n    <module name=\"PackageName\" />\n    <module name=\"ParameterName\" />\n    <module name=\"StaticVariableName\" />\n    <module name=\"TypeName\" />\n\n    <!-- Checks for imports                              -->\n    <!-- See http://checkstyle.sf.net/config_import.html -->\n    <module name=\"AvoidStarImport\">\n      <property name=\"allowStaticMemberImports\" value=\"true\" />\n    </module>\n    <module name=\"IllegalImport\" />\n    <!-- defaults to sun.* packages -->\n    <module name=\"RedundantImport\" />\n    <module name=\"UnusedImports\" />\n\n    <!-- Checks for Size Violations.                    -->\n    <!-- See http://checkstyle.sf.net/config_sizes.html -->\n    <module name=\"LineLength\">\n      <!-- what is a good max value? -->\n      <property name=\"max\" value=\"150\" />\n      <!-- ignore lines like \"$File: //depot/... $\" -->\n      <property name=\"ignorePattern\" value=\"\\$File.*\\$\" />\n      <property name=\"severity\" value=\"info\" />\n    </module>\n    <module name=\"MethodLength\" />\n    <module name=\"ParameterNumber\" />\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.sf.net/config_whitespace.html -->\n    <module name=\"EmptyForIteratorPad\" />\n    <module name=\"GenericWhitespace\" />\n    <module name=\"MethodParamPad\" />\n    <module name=\"NoWhitespaceAfter\" />\n    <module name=\"NoWhitespaceBefore\" />\n    <module name=\"OperatorWrap\" />\n    <module name=\"ParenPad\" />\n    <module name=\"TypecastParenPad\" />\n    <module name=\"WhitespaceAfter\" />\n    <module name=\"WhitespaceAround\" />\n\n    <!-- Modifier Checks                                    -->\n    <!-- See http://checkstyle.sf.net/config_modifiers.html -->\n    <module name=\"ModifierOrder\" />\n    <module name=\"RedundantModifier\" />\n\n    <!-- Checks for blocks. You know, those {}'s         -->\n    <!-- See http://checkstyle.sf.net/config_blocks.html -->\n    <module name=\"AvoidNestedBlocks\" />\n    <module name=\"EmptyBlock\">\n      <property name=\"option\" value=\"text\" />\n    </module>\n    <module name=\"LeftCurly\" />\n    <module name=\"NeedBraces\" />\n    <module name=\"RightCurly\" />\n\n    <!-- Checks for common coding problems               -->\n    <!-- See http://checkstyle.sf.net/config_coding.html -->\n    <module name=\"EmptyStatement\" />\n    <module name=\"EqualsHashCode\" />\n    <module name=\"HiddenField\">\n      <property name=\"ignoreConstructorParameter\" value=\"true\" />\n      <property name=\"ignoreSetter\" value=\"true\" />\n      <property name=\"severity\" value=\"ignore\" />\n    </module>\n    <module name=\"IllegalInstantiation\" />\n    <module name=\"InnerAssignment\" />\n    <module name=\"MagicNumber\">\n      <property name=\"severity\" value=\"warning\" />\n      <property name=\"ignoreHashCodeMethod\" value=\"true\" />\n      <property name=\"ignoreAnnotation\" value=\"true\" />\n    </module>\n    <module name=\"MissingSwitchDefault\" />\n    <module name=\"SimplifyBooleanExpression\" />\n    <module name=\"SimplifyBooleanReturn\" />\n\n    <!-- Checks for class design                         -->\n    <!-- See http://checkstyle.sf.net/config_design.html -->\n    <module name=\"FinalClass\" />\n    <module name=\"HideUtilityClassConstructor\" />\n    <module name=\"InterfaceIsType\" />\n    <module name=\"VisibilityModifier\">\n      <!-- Ignore fields injected by Dagger as it requires at least package private visibility -->\n      <property name=\"ignoreAnnotationCanonicalNames\"\n        value=\"javax.inject.Inject\" />\n    </module>\n\n    <!-- Miscellaneous other checks.                   -->\n    <!-- See http://checkstyle.sf.net/config_misc.html -->\n    <module name=\"ArrayTypeStyle\" />\n    <module name=\"UpperEll\" />\n\n  </module>\n\n</module>\n"
  },
  {
    "path": "team-props/static-analysis/checkstyle-suppressions.xml",
    "content": "<?xml version=\"1.0\"?>\n\n<!DOCTYPE suppressions PUBLIC\n  \"-//Puppy Crawl//DTD Suppressions 1.1//EN\"\n  \"http://www.puppycrawl.com/dtds/suppressions_1_1.dtd\">\n\n<suppressions>\n\n  <suppress checks=\".*\" files=\"com[\\\\/]novoda[\\\\/]noplayer[\\\\/]external[\\\\/]exoplayer[\\\\/]\" />\n\n</suppressions>\n"
  },
  {
    "path": "team-props/static-analysis/findbugs-excludes.xml",
    "content": "<FindBugsFilter>\n\n  <Match>\n    <Or>\n      <Class name=\"~.*\\.R\\$.*\" />\n      <Class name=\"~.*\\.R\\$.*\" />\n      <Class name=\"~.*Test\" />\n      <Class name=\"~.*Test\\$.*\" />\n      <Class name=\"~.*\\.Manifest\\$.*\" />\n      <Class name=\"~.*\\.Manifest\\$.*\" />\n      <Package name=\"~com\\.novoda\\.noplayer\\.external\\.exoplayer\\..*\" />\n    </Or>\n  </Match>\n\n</FindBugsFilter>\n"
  },
  {
    "path": "team-props/static-analysis/lint-config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<lint>\n\n  <issue id=\"MissingTranslation\" severity=\"ignore\" />\n  <issue id=\"GoogleAppIndexingWarning\" severity=\"ignore\" />\n\n  <issue id=\"OldTargetApi\" severity=\"informational\" />\n\n  <!-- Ignore RtL warnings since the app doesn't support RtL -->\n  <issue id=\"RtlSymmetry\" severity=\"ignore\" />\n  <issue id=\"RtlHardcoded\" severity=\"ignore\" />\n  <issue id=\"RtlEnabled\" severity=\"ignore\" />\n\n  <!-- Lint only flags only missing content descriptions in XML ImageViews but we rarely put content descriptions here -->\n  <issue id=\"ContentDescription\" severity=\"ignore\" />\n\n  <!-- The Google Services JSON plugin generates some API keys even if we don't actually use them -->\n  <issue id=\"UnusedResources\" severity=\"informational\">\n    <ignore path=\"*generated/res/google-services/*\" />\n  </issue>\n\n  <!-- This issue flags the adaptive icons' background drawables too, which must be square and fully opaque -->\n  <issue id=\"IconLauncherShape\" severity=\"error\">\n    <ignore path=\"*res/mipmap*/ic_launcher_background.png\" />\n  </issue>\n\n</lint>\n"
  },
  {
    "path": "team-props/static-analysis/pmd-rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<ruleset xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n  name=\"No-player PMD rules\"\n  xmlns=\"http://pmd.sourceforge.net/ruleset/2.0.0\"\n  xsi:schemaLocation=\"http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd\">\n\n  <description>PMD rules for the No-player codebase.</description>\n\n  <exclude-pattern>.*\\.R\\$.*</exclude-pattern>\n  <exclude-pattern>.*Test.*</exclude-pattern>\n  <exclude-pattern>.*/noplayer/external/exoplayer/.*</exclude-pattern>\n\n  <rule ref=\"rulesets/java/strictexception.xml\">\n    <!-- Ignored because of the Callable pattern -->\n    <exclude name=\"SignatureDeclareThrowsException\" />\n  </rule>\n  <rule ref=\"rulesets/java/typeresolution.xml\">\n    <!-- Ignored because of the Callable pattern -->\n    <exclude name=\"SignatureDeclareThrowsException\" />\n  </rule>\n\n  <rule ref=\"rulesets/java/naming.xml/ShortMethodName\">\n    <properties>\n      <property name=\"xpath\">\n        <value>\n          //MethodDeclarator[string-length(@Image) &lt; 2]\n        </value>\n      </property>\n    </properties>\n  </rule>\n\n  <rule ref=\"rulesets/java/braces.xml\" />\n\n  <rule ref=\"rulesets/java/design.xml\">\n    <exclude name=\"TooFewBranchesForASwitchStatement\" />\n    <exclude name=\"EmptyMethodInAbstractClassShouldBeAbstract\" />\n    <exclude name=\"FieldDeclarationsShouldBeAtStartOfClass\" />\n    <exclude name=\"ConfusingTernary\" />\n    <exclude name=\"AccessorMethodGeneration\" />\n  </rule>\n\n  <rule ref=\"rulesets/java/design.xml/EmptyMethodInAbstractClassShouldBeAbstract\">\n    <priority>5</priority>\n  </rule>\n\n  <!-- Suppress ImmutableField check for Api models -->\n  <rule ref=\"rulesets/java/design.xml/ImmutableField\">\n    <properties>\n      <property name=\"violationSuppressXPath\"\n        value=\"//ClassOrInterfaceDeclaration[starts-with(@Image, 'Api')]//FieldDeclaration\" />\n    </properties>\n  </rule>\n\n  <rule ref=\"rulesets/java/unusedcode.xml\" />\n  <rule ref=\"rulesets/java/logging-java.xml\" />\n\n  <rule ref=\"rulesets/java/strings.xml\" />\n\n  <!-- Suppress AvoidDuplicateLiterals check for annotations -->\n  <rule ref=\"rulesets/java/strings.xml/AvoidDuplicateLiterals\">\n    <properties>\n      <property name=\"skipAnnotations\" value=\"true\" />\n    </properties>\n  </rule>\n\n  <rule ref=\"rulesets/java/migrating.xml\" />\n  <rule ref=\"rulesets/java/optimizations.xml\">\n    <exclude name=\"LocalVariableCouldBeFinal\" />\n    <exclude name=\"MethodArgumentCouldBeFinal\" />\n    <exclude name=\"AvoidInstantiatingObjectsInLoops\" />\n  </rule>\n  <rule ref=\"rulesets/java/basic.xml\" />\n  <rule ref=\"rulesets/java/sunsecure.xml\" />\n  <rule ref=\"rulesets/java/coupling.xml\">\n    <exclude name=\"LoosePackageCoupling\" />\n    <!-- LawOfDemeter seems to be bugged -->\n    <exclude name=\"LawOfDemeter\" />\n  </rule>\n\n  <rule ref=\"rulesets/java/imports.xml\">\n    <exclude name=\"TooManyStaticImports\" />\n  </rule>\n\n  <rule ref=\"rulesets/java/junit.xml\" />\n  <rule ref=\"rulesets/java/naming.xml\">\n    <exclude name=\"AbstractNaming\" />\n    <exclude name=\"LongVariable\" />\n    <exclude name=\"ShortVariable\" />\n    <exclude name=\"ShortClassName\" />\n    <exclude name=\"AvoidFieldNameMatchingMethodName\" />\n  </rule>\n\n  <rule ref=\"rulesets/java/codesize.xml\">\n    <exclude name=\"TooManyMethods\" />\n    <exclude name=\"TooManyFields\" />\n  </rule>\n\n  <!-- Suppress ExcessiveParameterList check for Api models ctors and factory methods -->\n  <rule ref=\"rulesets/java/codesize.xml/ExcessiveParameterList\">\n    <properties>\n      <property name=\"violationSuppressXPath\"\n        value=\"//ClassOrInterfaceDeclaration[starts-with(@Image, 'Api')]//MethodDeclaration[@Static = 'true' and @Private = 'false'] | //ClassOrInterfaceDeclaration[starts-with(@Image, 'Api')]//ConstructorDeclaration\" />\n    </properties>\n  </rule>\n\n  <!-- Suppress CyclomaticComplexity check for equals and hashCode -->\n  <rule ref=\"rulesets/java/codesize.xml/CyclomaticComplexity\">\n    <properties>\n      <property name=\"violationSuppressXPath\"\n        value=\"//MethodDeclarator[@Image='equals' or @Image='hashCode']\" />\n    </properties>\n  </rule>\n\n  <!-- Suppress ModifiedCyclomaticComplexity check for equals and hashCode -->\n  <rule ref=\"rulesets/java/codesize.xml/ModifiedCyclomaticComplexity\">\n    <properties>\n      <property name=\"violationSuppressXPath\"\n        value=\"//MethodDeclarator[@Image='equals' or @Image='hashCode']\" />\n    </properties>\n  </rule>\n\n  <!-- Suppress StdCyclomaticComplexity check for equals and hashCode -->\n  <rule ref=\"rulesets/java/codesize.xml/StdCyclomaticComplexity\">\n    <properties>\n      <property name=\"violationSuppressXPath\"\n        value=\"//MethodDeclarator[@Image='equals' or @Image='hashCode']\" />\n    </properties>\n  </rule>\n\n  <!-- Suppress NPathComplexity check for equals and hashCode -->\n  <rule ref=\"rulesets/java/codesize.xml/NPathComplexity\">\n    <properties>\n      <property name=\"violationSuppressXPath\"\n        value=\"//MethodDeclarator[@Image='equals' or @Image='hashCode']\" />\n    </properties>\n  </rule>\n\n  <rule ref=\"rulesets/java/finalizers.xml\" />\n  <rule ref=\"rulesets/java/logging-jakarta-commons.xml\" />\n  <rule ref=\"rulesets/java/clone.xml\" />\n  <rule ref=\"rulesets/java/android.xml\" />\n\n</ruleset>\n"
  },
  {
    "path": "team-props/static-analysis.gradle",
    "content": "def configureStaticAnalysis(Project project) {\n    def testSourceSet = project.android.sourceSets.test.java.srcDirs\n\n    apply plugin: 'com.novoda.static-analysis'\n\n    staticAnalysis {\n        penalty {\n            maxErrors = 0\n            maxWarnings = 0\n        }\n\n        checkstyle {\n            toolVersion '8.5'\n            exclude testSourceSet\n            configFile teamPropsFile('static-analysis/checkstyle-modules.xml')\n        }\n\n        pmd {\n            toolVersion '5.8.1'\n            exclude testSourceSet\n            ruleSetFiles = files(teamPropsFile('static-analysis/pmd-rules.xml'))\n            ruleSets = []   // Note: this is a workaround to make the <exclude-pattern>s in pmd-rules.xml actually work\n        }\n\n        findbugs {\n            toolVersion '3.0.1'\n            exclude testSourceSet\n            excludeFilter teamPropsFile('static-analysis/findbugs-excludes.xml')\n        }\n    }\n}\n\nproject.plugins.withId('com.android.library') {\n    configureStaticAnalysis(project)\n}\n\nproject.plugins.withId('com.android.application') {\n    configureStaticAnalysis(project)\n}\n"
  }
]