Full Code of JarvanMo/ExoVideoView for AI

master d53e9442b6cc cached
71 files
254.5 KB
59.8k tokens
365 symbols
1 requests
Download .txt
Showing preview only (280K chars total). Download the full file or copy to clipboard to get everything.
Repository: JarvanMo/ExoVideoView
Branch: master
Commit: d53e9442b6cc
Files: 71
Total size: 254.5 KB

Directory structure:
gitextract_igumccg0/

├── .gitignore
├── LICENSE
├── README.md
├── README_CN.md
├── build.gradle
├── demo/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── jarvanmo/
│       │               └── demo/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── jarvanmo/
│       │   │           └── demo/
│       │   │               ├── MainActivity.java
│       │   │               └── SimpleVideoViewActivity.java
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── ic_arrow_back_white_24dp.xml
│       │       │   └── ic_error_white_24dp.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── activity_simple_video_view.xml
│       │       │   ├── cutom_view_bottom_landscape.xml
│       │       │   ├── cutom_view_top.xml
│       │       │   └── cutom_view_top_landscape.xml
│       │       └── values/
│       │           ├── colors.xml
│       │           ├── strings.xml
│       │           └── styles.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── jarvanmo/
│                       └── demo/
│                           └── ExampleUnitTest.java
├── exoplayerview/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── jarvanmo/
│       │               └── exoplayerview/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── jarvanmo/
│       │   │           └── exoplayerview/
│       │   │               ├── ads/
│       │   │               │   └── ExoAdsLoader.java
│       │   │               ├── extension/
│       │   │               │   └── MultiQualitySelectorAdapter.java
│       │   │               ├── gesture/
│       │   │               │   ├── OnVideoGestureChangeListener.java
│       │   │               │   └── VideoGesture.java
│       │   │               ├── media/
│       │   │               │   ├── EventLogger.java
│       │   │               │   ├── ExoMediaSource.java
│       │   │               │   ├── MediaSourceCreator.java
│       │   │               │   ├── MediaSourceParams.java
│       │   │               │   ├── SimpleMediaSource.java
│       │   │               │   └── SimpleQuality.java
│       │   │               ├── orientation/
│       │   │               │   ├── OnOrientationChangedListener.java
│       │   │               │   └── SensorOrientation.java
│       │   │               ├── ui/
│       │   │               │   ├── ExoVideoPlaybackControlView.java
│       │   │               │   └── ExoVideoView.java
│       │   │               ├── util/
│       │   │               │   ├── AndroidUtil.java
│       │   │               │   └── Permissions.java
│       │   │               └── widget/
│       │   │                   ├── BatteryLevelView.java
│       │   │                   └── BatteryStatusView.java
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── ic_arrow_back_white_24dp.xml
│       │       │   ├── ic_error_outline_white_48dp.xml
│       │       │   ├── stat_sys_battery.xml
│       │       │   └── stat_sys_battery_charge.xml
│       │       ├── layout/
│       │       │   ├── exo_playback_controller_bottom.xml
│       │       │   ├── exo_playback_controller_bottom_landscape.xml
│       │       │   ├── exo_playback_controller_top.xml
│       │       │   ├── exo_playback_controller_top_landscape.xml
│       │       │   ├── exo_player_quality_selector.xml
│       │       │   ├── exo_video_playback_control_view.xml
│       │       │   ├── exo_video_view.xml
│       │       │   └── item_quality.xml
│       │       ├── values/
│       │       │   ├── attrs.xml
│       │       │   ├── colors.xml
│       │       │   ├── dimens.xml
│       │       │   ├── ids.xml
│       │       │   └── strings.xml
│       │       └── values-zh-rCN/
│       │           └── strings.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── jarvanmo/
│                       └── exoplayerview/
│                           └── ExampleUnitTest.java
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
tmp.txt

================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
![logo](./images/default_art.png)
# ExoVideoView
ExoVideoView is based on [ExoPlayer](https://github.com/google/ExoPlayer).

[中文移步至此](./README_CN.md).

![demo](./images/demo.gif)

### Planning new versions

**What's in ExoVideoView**

    1.Process AudioFocus automatically.
    2.Process its orientation by sensor automatically
    3.simple gesture action supported.
    4.multiple video quality supported
    5.you can add custom views to the default controller.
    6.multiple resize-mode supported
    7.custom controller supported.
    8.change the widget's visibility if you like.
## Using ExoVideoView
### 1.Dependency
The easiest way to get started using ExoVideoView is to add it as a gradle dependency. You need to make sure you have the jitpack repositories included in the build.gradle file in the root of your project:
```
allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}
```
Next add a gradle compile dependency to the build.gradle file of your app module:
```
implementation 'com.github.JarvanMo:ExoVideoView:2.1.6'
```
### 2.In Layout
Declare ExoVideoView in your layout file as :
```xml
<com.jarvanmo.exoplayerview.ui.ExoVideoView
     android:id="@+id/videoView"
     android:layout_width="match_parent"
     android:layout_height="300dp"/>
```
### 3.In Java
ExoVideoView provides built-in ```Player``` for convenience,so we can play a video as
```java
SimpleMediaSource mediaSource = new SimpleMediaSource(url);//uri also supported
videoView.play(mediaSource);
videoView.play(mediaSource,where);//play from a particular position
```
Passing a player outside to ExoVideoView:
```java
videoView.setPlayer(player);
```
Note:never forget to release ExoPlayer:
```java
videoView.releasePlayer();
```
see details in [demo]().

### 3.Orientation Management
The ExoVideoView can handle its orientation by sensor automatically only when ExoVideoVIew has a not-null OrientationListener  :
```java
    videoView.setOrientationListener(orientation -> {
            if (orientation == SENSOR_PORTRAIT) {
                //do something
            } else if (orientation == SENSOR_LANDSCAPE) {
                //do something
            }
        });
```
> NOTE:When the ExoVideoView handle its orientation automatically,The ExoVideoView will call ```activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE)``` or ```activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);``` if the ```context``` in controller is an Activity.
The fullscreen management is the same as orientation management.

### 4.Back Events
First,override onKeyDown:
```java
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (keyCode == KeyEvent.KEYCODE_BACK) {
            return videoView.onKeyDown(keyCode, event);
        }
        return super.onKeyDown(keyCode, event);
    }

```
Then passing a backListener to ExoVideoView:
```java
    videoView.setBackListener((view, isPortrait) -> {
            if (isPortrait) {
              //do something
            }
            return false;
        });
```
If return value is ```true```, operation will be interrupted.Otherwise,ExoVideoView handle its orientation by itself and ```OrientationLister.onOrientationChange()``` will be caled.
## Advance
### 1.Multi-Quality
ExoVideoView also provides a built-in multi-quality selector.The multi-quality selector
will be added to ```overlayFrameLayout``` if  multi-quality is enabled and  ```ExoMediaSource``` are given different qualities in current version.
```java
        List<ExoMediaSource.Quality> qualities = new ArrayList<>();
        ExoMediaSource.Quality quality =new SimpleQuality(quality,mediaSource.url());
        qualities.add(quality);
        mediaSource.setQualities(qualities);
```

### 2.Controller Display Mode
```ExoVideoPlaybackController``` are divided into four parts:
```
1.Top
2.Top Landscape
3.Bottom
4.Bottom Landscape
```
Each of them can be hidden or shown:
```xml
 app:controller_display_mode="all|none|top|top_landscape|bottom|bottom_landscape"
```
in java:
```java
  videoView.setControllerDisplayMode(mode);
```
### 3.Add Custom View To Controller
Views can be added to ```ExoVideoPlaybackController``` in java.
```java
  videoView.addCustomView(ExoVideoPlaybackControlView.CUSTOM_VIEW_TOP, view);
  videoView.addCustomView(ExoVideoPlaybackControlView.CUSTOM_VIEW_TOP_LANDSCAPE, view);
  videoView.addCustomView(ExoVideoPlaybackControlView.CUSTOM_VIEW_BOTTOM_LANDSCAPE, view);
```
### 4.Specifying A custom Layout File
Defining your own ```exo_video_playback_control_view.xml``` is useful to customize the layout of ```ExoVideoPlaybackControlView``` throughout your application. It's also possible to customize the layout for asingle instance in a layout file. This is achieved by setting the  controller_layout_id attribute on a ```ExoVideoPlaybackControlView```. This will cause the specified layout to be inflated instead of ```code exo_video_playback_control_view.xml``` for only the instance on which the attribute is set.
```xml
app:controller_layout_id="@layout/my_controller"
```
### 5.Change Visibility
Sometimes,we may not like the back buttons.So let's hide it:
```java
videoView.changeWidgetVisibility(R.id.exo_player_controller_back,View.INVISIBLE);
```
For more widgets you can hide or show,see [IDS_IN_CONTROLLER](./exoplayerview/src/main/res/values/ids.xml).
> NOTE:This is a dangerous operation because I don't know what  will happen to the UI.
## Others

```
  app:controller_background="@android:color/holo_orange_dark"
  app:use_artwork="true"
  app:default_artwork="@drawable/default_art"
```


================================================
FILE: README_CN.md
================================================
![logo](./images/default_art.png)
# ExoVideoView
![demo](./images/demo.gif)

ExoVideoView 是一款基于[ExoPlayer](https://github.com/google/ExoPlayer)开发的视频播放器.

**ExoVideoView可以做什么**
    1.自动处理音频焦点。
    2.根据传感器自动处理方向。
    3.手势支持。
    4.多清晰度选择支持。
    5.为控制器添加自定义布局.
    6.调整显示大小。
    7.自定义controller。
    8.支持调整控件的可见性。
## 使用 ExoVideoView
### 1.依赖
最简单的方式是加入gradle依赖。请确认在工程的build.gradle中添加了JCenter和google()。
```
allprojects {
		repositories {
			...
			maven { url 'https://jitpack.io' }
		}
	}
```
然后在你的项目中添加如下代码:
```
implementation 'com.github.JarvanMo:ExoVideoView:2.1.6'
```
### 2.在xml中定义
在xml中使用 ExoVideoView:
```xml
<com.jarvanmo.exoplayerview.ui.ExoVideoView
     android:id="@+id/videoView"
     android:layout_width="match_parent"
     android:layout_height="300dp"/>
```
### 3.在java代码中
ExoVideoView 提供了内建```Player```:
```java
SimpleMediaSource mediaSource = new SimpleMediaSource(url);//也支持uri
videoView.play(mediaSource);
videoView.play(mediaSource,where);//play from a particular position
```
也可以使用自义的Player:
```java
videoView.setPlayer(player);
```
提示:不要忘记释放ExoPlayer:
```java
videoView.releasePlayer();
```
详情请移步[demo]().

### 3.方向管理
ExoVideoView 可以自动处理方向问题,前提是为ExoVideoView设置一个OrientationListener:
```java
    videoView.setOrientationListener(orientation -> {
            if (orientation == SENSOR_PORTRAIT) {
                //do something
            } else if (orientation == SENSOR_LANDSCAPE) {
                //do something
            }
        });
```
提示:当ExoVideoView自动处理方向问题时,如果在Controller中的context是Activity,那么系统会调用
```activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE)``` or ```activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);```
全屏事件处理也是如此。
### 4返回管理
首先,重写onKeyDown:
```java
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (keyCode == KeyEvent.KEYCODE_BACK) {
            return videoView.onKeyDown(keyCode, event);
        }
        return super.onKeyDown(keyCode, event);
    }

```
为ExoVideoView设置监听:
```java
    videoView.setBackListener((view, isPortrait) -> {
            if (isPortrait) {
              //do something
            }
            return false;
        });
```
如果返回值是 ```true```, 系统后续动作会被中断.否则,ExoVideoView会自动处理方向,并且会回调```OrientationLister.onOrientationChange()``` .
## 高级
### 1.多清晰度支持
ExoVideoView 内置清晰度选择器.如果开启发多清晰度并添加了多清晰度,内置清晰度选择器将被加入```overlayFrameLayout```.
```java
        List<ExoMediaSource.Quality> qualities = new ArrayList<>();
        ExoMediaSource.Quality quality =new SimpleQuality(quality,mediaSource.url());
        qualities.add(quality);
        mediaSource.setQualities(qualities);
```

### 2.Controller显示模式
```ExoVideoPlaybackController``` 被分为四个部分:
```
1.Top
2.Top Landscape
3.Bottom
4.Bottom Landscape
```
每一部分都可以被显示或隐藏:
```xml
 app:controller_display_mode="all|none|top|top_landscape|bottom|bottom_landscape"
```
在java中:
```java
  videoView.setControllerDisplayMode(mode);
```

### 3.为controller添加控件
```ExoVideoPlaybackController``` 允许在java代码中添加控件.
```java
  videoView.addCustomView(ExoVideoPlaybackControlView.CUSTOM_VIEW_TOP, view);
  videoView.addCustomView(ExoVideoPlaybackControlView.CUSTOM_VIEW_TOP_LANDSCAPE, view);
  videoView.addCustomView(ExoVideoPlaybackControlView.CUSTOM_VIEW_BOTTOM_LANDSCAPE, view);
```
### 4.使用自定义controller
```exo_video_playback_control_view.xml```是允许自定义的。其中一些属性在```ExoVideoPlaybackControlView```有定义。具体可看源码。
```xml
app:controller_layout_id="@layout/my_controller"
```
### 5.更改控件可见性
有些时候我们可能不太喜欢返回,所以就让我们隐藏起来吧:
```java
videoView.changeWidgetVisibility(R.id.exo_player_controller_back,View.INVISIBLE);
```
获取更多可隐藏的控件,参看 [ids_in_controller](./exoplayerview/src/main/res/values/ids.xml);
> 注意:这么做可能比较危险,我不敢保证隐藏以后的效果。
## 其他

```
  app:controller_background="@android:color/holo_orange_dark"
  app:use_artwork="true"
  app:default_artwork="@drawable/default_art"
```


================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
        google()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.0.1'
//        classpath 'com.novoda:bintray-release:0.9.2'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
        google()

    }

}

task clean(type: Delete) {
    delete rootProject.buildDir
}

ext {
    compileSdkVersion = 29
    buildToolsVersion = "26.0.2"
    minSdkVersion = 17
    targetSdkVersion = 27

    androidSupportVersion = '27.1.1'
}


//tasks.getByPath(":exoplayerview:javadocRelease").enabled = false

================================================
FILE: demo/.gitignore
================================================
/build


================================================
FILE: demo/build.gradle
================================================
apply plugin: 'com.android.application'

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    defaultConfig {
        applicationId "com.jarvanmo.demo"
        minSdkVersion 17
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables.useSupportLibrary = true
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    lintOptions {
        abortOnError false
    }


    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }


}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
    testImplementation 'junit:junit:4.12'
    implementation project(':exoplayerview')
//    compile 'com.jarvanmo:exoplayerview:0.0.1'
//    implementation 'com.google.android.exoplayer:exoplayer:2.7.3'
    compileOnly 'androidx.recyclerview:recyclerview:1.0.0'
}


================================================
FILE: demo/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/mo/Android/Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class getDisplayName to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file getDisplayName.
#-renamesourcefileattribute SourceFile


================================================
FILE: demo/src/androidTest/java/com/jarvanmo/demo/ExampleInstrumentedTest.java
================================================
package com.jarvanmo.demo;

import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;

/**
 * Instrumentation test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() throws Exception {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();

        assertEquals("com.jarvanmo.demo", appContext.getPackageName());
    }
}


================================================
FILE: demo/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jarvanmo.demo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:theme="@style/FullscreenTheme">

        </activity>
        <activity
            android:name=".SimpleVideoViewActivity"
            android:configChanges="keyboardHidden|orientation|screenSize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

================================================
FILE: demo/src/main/java/com/jarvanmo/demo/MainActivity.java
================================================
package com.jarvanmo.demo;

import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;

import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.jarvanmo.exoplayerview.media.ExoMediaSource;
import com.jarvanmo.exoplayerview.media.SimpleMediaSource;
import com.jarvanmo.exoplayerview.media.SimpleQuality;
import com.jarvanmo.exoplayerview.ui.ExoVideoView;

import java.util.ArrayList;
import java.util.List;

import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_LANDSCAPE;
import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_PORTRAIT;

public class MainActivity extends AppCompatActivity {


    private ExoVideoView videoView;
    private Button modeFit;
    private Button modeNone;
    private Button modeHeight;
    private Button modeWidth;
    private Button modeZoom;
    private View wrapper;
    private Button play;
    private View contentView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        contentView = findViewById(R.id.activity_main);

        videoView = findViewById(R.id.videoView);
        modeFit = findViewById(R.id.mode_fit);
        modeNone = findViewById(R.id.mode_none);
        modeHeight = findViewById(R.id.mode_height);
        modeWidth = findViewById(R.id.mode_width);
        modeZoom = findViewById(R.id.mode_zoom);
        wrapper = findViewById(R.id.wrapper);
        play = findViewById(R.id.changeMode);

        videoView.setBackListener((view, isPortrait) -> {
            if (isPortrait) {
                finish();
            }
            return false;
        });


//
//


        videoView.setOrientationListener(orientation -> {
            if (orientation == SENSOR_PORTRAIT) {
                changeToPortrait();
            } else if (orientation == SENSOR_LANDSCAPE) {
                changeToLandscape();
            }
        });



//

//       final SimpleMediaSource mediaSource = new SimpleMediaSource("http://video19.ifeng.com/video07/2013/11/11/281708-102-007-1138.mp4");
//        SimpleMediaSource mediaSource  = new SimpleMediaSource("https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8");
//        final SimpleMediaSource mediaSource = new SimpleMediaSource("http://playertest.longtailvideo.com/adaptive/bipbop/gear4/prog_index.m3u8");
//        SimpleMediaSource mediaSource  = new SimpleMediaSource(" https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8");
//       SimpleMediaSource mediaSource = new SimpleMediaSource("https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8");
//        SimpleMediaSource mediaSource = new SimpleMediaSource("http://pullhlsbb8f2e48.live.126.net/live/7de213ebb3dc4db2aa2f32f3da0b028d/playlist.m3u8");
//        SimpleMediaSource mediaSource = new SimpleMediaSource("http://rotation.vod.zlive.cc/channel/1234.m3u8");
//        SimpleMediaSource mediaSource = new SimpleMediaSource("https://media.w3.org/2010/05/sintel/trailer.mp4");
        SimpleMediaSource mediaSource = new SimpleMediaSource("http://flv2.bn.netease.com/videolib3/1604/28/fVobI0704/SD/fVobI0704-mobile.mp4");
//        SimpleMediaSource mediaSource = new SimpleMediaSource("http://stream1.hnntv.cn/lywsgq/sd/live.m3u8");

        mediaSource.setDisplayName("VideoPlaying");
        List<ExoMediaSource.Quality> qualities = new ArrayList<>();
        ExoMediaSource.Quality quality;

        for (int i = 0; i < 6; i++) {
            SpannableString spannableString = new SpannableString("Quality" + i);
            if (i % 2 == 0) {
                ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.YELLOW);
                spannableString.setSpan(colorSpan, 0, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

            } else {
                ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.RED);
                spannableString.setSpan(colorSpan, 0, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            }

            quality = new SimpleQuality(spannableString, mediaSource.uri());
            qualities.add(quality);
        }
        mediaSource.setQualities(qualities);

        videoView.play(mediaSource, false);
        play.setOnClickListener(view -> {
            videoView.play(mediaSource);
            play.setVisibility(View.INVISIBLE);
        });


        modeFit.setOnClickListener(v -> videoView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT));
        modeWidth.setOnClickListener(v -> videoView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH));
        modeHeight.setOnClickListener(v -> videoView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT));
        modeNone.setOnClickListener(v -> videoView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL));
        modeZoom.setOnClickListener(v -> videoView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_ZOOM));
    }

    private void changeToPortrait() {

        WindowManager.LayoutParams attr = getWindow().getAttributes();
//        attr.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
        Window window = getWindow();
        window.setAttributes(attr);
        window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
        wrapper.setVisibility(View.VISIBLE);
    }


    private void changeToLandscape() {
        WindowManager.LayoutParams lp = getWindow().getAttributes();
//        lp.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
        Window window = getWindow();
        window.setAttributes(lp);
        window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
        wrapper.setVisibility(View.GONE);
    }


    @Override
    protected void onStart() {
        super.onStart();
        if (Build.VERSION.SDK_INT > 23) {
            videoView.resume();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if ((Build.VERSION.SDK_INT <= 23)) {
            videoView.resume();
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (Build.VERSION.SDK_INT <= 23) {
            videoView.pause();
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (Build.VERSION.SDK_INT > 23) {
            videoView.pause();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        videoView.releasePlayer();
    }


    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (keyCode == KeyEvent.KEYCODE_BACK) {
            return videoView.onKeyDown(keyCode, event);
        }
        return super.onKeyDown(keyCode, event);
    }
}


================================================
FILE: demo/src/main/java/com/jarvanmo/demo/SimpleVideoViewActivity.java
================================================
package com.jarvanmo.demo;

import android.content.res.Configuration;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.Spinner;
import android.widget.Toast;

import com.jarvanmo.exoplayerview.extension.MultiQualitySelectorAdapter;
import com.jarvanmo.exoplayerview.media.ExoMediaSource;
import com.jarvanmo.exoplayerview.media.SimpleMediaSource;
import com.jarvanmo.exoplayerview.media.SimpleQuality;
import com.jarvanmo.exoplayerview.ui.ExoVideoPlaybackControlView;
import com.jarvanmo.exoplayerview.ui.ExoVideoView;

import java.util.ArrayList;
import java.util.List;

import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_LANDSCAPE;
import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_PORTRAIT;

public class SimpleVideoViewActivity extends AppCompatActivity {

    private ExoVideoView videoView;
    private View wrapper;
    private final String[] modes = new String[]{"RESIZE_MODE_FIT", "RESIZE_MODE_FIXED_WIDTH"
            , "RESIZE_MODE_FIXED_HEIGHT", "RESIZE_MODE_FILL", "RESIZE_MODE_ZOOM"};
    private Spinner modeSpinner;
    private ArrayAdapter<String> adapter;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple_video_view);
        wrapper = findViewById(R.id.wrapper);

        initSpinner();
        initControllerMode();
        initVideoView();
        initCustomViews();
    }

    private void initVideoView() {
        videoView = findViewById(R.id.videoView);
        videoView.setPortrait(getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);
        videoView.setBackListener((view, isPortrait) -> {
            if (isPortrait) {
                finish();
            }
            return false;
        });

        videoView.setOrientationListener(orientation -> {
            if (orientation == SENSOR_PORTRAIT) {
                changeToPortrait();
            } else if (orientation == SENSOR_LANDSCAPE) {
                changeToLandscape();
            }
        });

//        videoView.setGestureEnabled(false);
//
//
//        SimpleMediaSource mediaSource = new SimpleMediaSource("http://flv2.bn.netease.com/videolib3/1604/28/fVobI0704/SD/fVobI0704-mobile.mp4");
//        mediaSource.setDisplayName("Apple HLS");

        SimpleMediaSource mediaSource = new SimpleMediaSource("http://vfx.mtime.cn/Video/2019/03/12/mp4/190312083533415853.mp4");
        mediaSource.setDisplayName("Apple HLS");

        //demo only,not real multi quality, urls are the same actually
        List<ExoMediaSource.Quality> qualities = new ArrayList<>();
        ExoMediaSource.Quality quality;
        ForegroundColorSpan colorSpan = new ForegroundColorSpan(Color.YELLOW);
        SpannableString spannableString = new SpannableString("1080p");
        spannableString.setSpan(colorSpan, 0, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        quality = new SimpleQuality(spannableString, mediaSource.uri());
        qualities.add(quality);

        spannableString = new SpannableString("720p");
        colorSpan = new ForegroundColorSpan(Color.LTGRAY);
        spannableString.setSpan(colorSpan, 0, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        quality = new SimpleQuality(spannableString, mediaSource.uri());
        qualities.add(quality);

        mediaSource.setQualities(qualities);
//        videoView.changeWidgetVisibility(R.id.exo_player_controller_back,View.INVISIBLE);
        videoView.setMultiQualitySelectorNavigator(new MultiQualitySelectorAdapter.MultiQualitySelectorNavigator() {
            @Override
            public boolean onQualitySelected(ExoMediaSource.Quality quality) {
                quality.setUri(Uri.parse("https://media.w3.org/2010/05/sintel/trailer.mp4"));
                return false;
            }
        });
        videoView.play(mediaSource, false);

    }

    private void initSpinner() {
        modeSpinner = findViewById(R.id.spinner);
        adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item);
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        modeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                videoView.setResizeMode(position);
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {

            }
        });
        adapter.addAll(modes);
        modeSpinner.setAdapter(adapter);
    }

    private void initControllerMode() {
        CheckBox all = findViewById(R.id.all);
        CheckBox top = findViewById(R.id.top);
        CheckBox topLandscape = findViewById(R.id.topLandscape);
        CheckBox bottom = findViewById(R.id.bottom);
        CheckBox bottomLandscape = findViewById(R.id.bottomLandscape);
        CheckBox none = findViewById(R.id.none);
        findViewById(R.id.applyControllerMode).setOnClickListener(v -> {
            int mode = ExoVideoPlaybackControlView.CONTROLLER_MODE_NONE;
            if (all.isChecked()) {
                mode |= ExoVideoPlaybackControlView.CONTROLLER_MODE_ALL;
            }
            if (top.isChecked()) {
                mode |= ExoVideoPlaybackControlView.CONTROLLER_MODE_TOP;
            }

            if (topLandscape.isChecked()) {
                mode |= ExoVideoPlaybackControlView.CONTROLLER_MODE_TOP_LANDSCAPE;
            }
            if (bottom.isChecked()) {
                mode |= ExoVideoPlaybackControlView.CONTROLLER_MODE_BOTTOM;
            }
            if (bottomLandscape.isChecked()) {
                mode |= ExoVideoPlaybackControlView.CONTROLLER_MODE_BOTTOM_LANDSCAPE;
            }
            if (none.isChecked()) {
                mode |= ExoVideoPlaybackControlView.CONTROLLER_MODE_NONE;
            }

            videoView.setControllerDisplayMode(mode);
            Toast.makeText(SimpleVideoViewActivity.this, "change controller display mode", Toast.LENGTH_SHORT).show();
        });


    }

    private void initCustomViews() {
        findViewById(R.id.addToTop).setOnClickListener(v -> {
            View view = getLayoutInflater().inflate(R.layout.cutom_view_top, null, false);
            videoView.addCustomView(ExoVideoPlaybackControlView.CUSTOM_VIEW_TOP, view);
        });

        findViewById(R.id.addToTopLandscape).setOnClickListener(v -> {
            View view = getLayoutInflater().inflate(R.layout.cutom_view_top_landscape, null, false);
            videoView.addCustomView(ExoVideoPlaybackControlView.CUSTOM_VIEW_TOP_LANDSCAPE, view);
        });


        findViewById(R.id.addToBottomLandscape).setOnClickListener(v -> {
            View view = getLayoutInflater().inflate(R.layout.cutom_view_bottom_landscape, null, false);
            videoView.addCustomView(ExoVideoPlaybackControlView.CUSTOM_VIEW_BOTTOM_LANDSCAPE, view);
        });
    }

    private void changeToPortrait() {

        // WindowManager operation is not necessary
        WindowManager.LayoutParams attr = getWindow().getAttributes();
//        attr.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
        Window window = getWindow();
        window.setAttributes(attr);
        window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);


        wrapper.setVisibility(View.VISIBLE);
    }


    private void changeToLandscape() {

        // WindowManager operation is not necessary

        WindowManager.LayoutParams lp = getWindow().getAttributes();
//        lp.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
        Window window = getWindow();
        window.setAttributes(lp);
        window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
        wrapper.setVisibility(View.GONE);
    }


    @Override
    protected void onStart() {
        super.onStart();
        if (Build.VERSION.SDK_INT > 23) {
            videoView.resume();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if ((Build.VERSION.SDK_INT <= 23)) {
            videoView.resume();
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (Build.VERSION.SDK_INT <= 23) {
            videoView.pause();
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (Build.VERSION.SDK_INT > 23) {
            videoView.pause();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        videoView.releasePlayer();
    }


    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (keyCode == KeyEvent.KEYCODE_BACK) {
            return videoView.onKeyDown(keyCode, event);
        }
        return super.onKeyDown(keyCode, event);
    }
}


================================================
FILE: demo/src/main/res/drawable/ic_arrow_back_white_24dp.xml
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="24dp"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0"
    android:width="24dp">
    <path
        android:fillColor="#FFFFFF"
        android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>


================================================
FILE: demo/src/main/res/drawable/ic_error_white_24dp.xml
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="24dp"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0"
    android:width="24dp">
    <path
        android:fillColor="#FFFFFF"
        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>


================================================
FILE: demo/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.jarvanmo.demo.MainActivity">


    <com.jarvanmo.exoplayerview.ui.ExoVideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1.5"
        app:controller_display_mode="all"
        app:resize_mode="fill"
        app:default_artwork="@drawable/default_art"
        app:use_artwork="true" />

    <androidx.constraintlayout.ConstraintLayout
        android:id="@+id/wrapper"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center">

        <Button
            android:id="@+id/changeMode"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="16dp"
            android:text="play"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/mode_none"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:text="mode_fill"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/changeMode" />

        <Button
            android:id="@+id/mode_fit"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:text="mode_fit"
            app:layout_constraintBottom_toBottomOf="@+id/mode_none"
            app:layout_constraintStart_toEndOf="@+id/mode_none"
            app:layout_constraintTop_toTopOf="@+id/mode_none" />

        <Button
            android:id="@+id/mode_width"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="16dp"
            android:text="mode_fix_width"
            app:layout_constraintBottom_toBottomOf="@+id/mode_height"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="@+id/mode_height" />

        <Button
            android:id="@+id/mode_height"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:text="mode_fix_height"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/mode_none" />

        <Button
            android:id="@+id/mode_zoom"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:text="mode_zoom"
            app:layout_constraintBottom_toBottomOf="@+id/mode_fit"
            app:layout_constraintStart_toEndOf="@+id/mode_fit"
            app:layout_constraintTop_toTopOf="@+id/mode_fit" />
    </androidx.constraintlayout.ConstraintLayout>


</LinearLayout>


================================================
FILE: demo/src/main/res/layout/activity_simple_video_view.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.jarvanmo.demo.MainActivity">

    <com.jarvanmo.exoplayerview.ui.ExoVideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1.5" />

    <ScrollView
        android:id="@+id/wrapper"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/changeMode"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:layout_marginTop="16dp"
                android:text="change resize mode"
                android:textColor="@android:color/holo_orange_dark"
                android:textSize="18sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <Spinner
                android:id="@+id/spinner"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:layout_marginTop="8dp"
                android:spinnerMode="dialog"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/changeMode" />

            <CheckBox
                android:id="@+id/all"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:checked="true"
                android:text="All"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/controllerMode" />

            <TextView
                android:id="@+id/controllerMode"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:text="Controller Display Mode"
                android:textColor="@android:color/holo_orange_dark"
                android:textSize="18sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/spinner" />

            <TextView
                android:id="@+id/applyControllerMode"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                android:text="apply"
                android:textSize="18sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="@+id/controllerMode" />

            <CheckBox
                android:id="@+id/top"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:text="Top"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/all" />

            <CheckBox
                android:id="@+id/topLandscape"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:text="Top Landscape"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/top" />

            <CheckBox
                android:id="@+id/bottom"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:text="Bottom"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/topLandscape" />

            <CheckBox
                android:id="@+id/bottomLandscape"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:text="Bottom Lanscape"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/bottom" />

            <CheckBox
                android:id="@+id/none"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:text="None"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/bottomLandscape" />

            <TextView
                android:id="@+id/customViews"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:text="add custom view to controller"
                android:textColor="@android:color/holo_orange_dark"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/none" />

            <TextView
                android:id="@+id/addToTop"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:drawableEnd="@drawable/ic_playlist_add_green_a700_24dp"
                android:drawablePadding="5dp"
                android:text="add view in to top controller"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/customViews" />

            <TextView
                android:id="@+id/addToTopLandscape"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:drawableEnd="@drawable/ic_playlist_add_green_a700_24dp"
                android:drawablePadding="5dp"
                android:text="add view in to top(landscape) controller"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/addToTop" />

            <TextView
                android:id="@+id/addToBottomLandscape"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:drawableEnd="@drawable/ic_playlist_add_green_a700_24dp"
                android:drawablePadding="5dp"
                android:text="add view in to bottom(landscape) controller"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/addToTopLandscape" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </ScrollView>
</LinearLayout>


================================================
FILE: demo/src/main/res/layout/cutom_view_bottom_landscape.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_message_white_24dp" />
</androidx.constraintlayout.ConstraintLayout>

================================================
FILE: demo/src/main/res/layout/cutom_view_top.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_share_white_24dp" />
</androidx.constraintlayout.ConstraintLayout>

================================================
FILE: demo/src/main/res/layout/cutom_view_top_landscape.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_more_vert_white_24dp" />
</androidx.constraintlayout.ConstraintLayout>

================================================
FILE: demo/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF4081</color>

    <color name="black_overlay">#66000000</color>
</resources>


================================================
FILE: demo/src/main/res/values/strings.xml
================================================
<resources>
    <string name="app_name">ExoPlayerView</string>
</resources>


================================================
FILE: demo/src/main/res/values/styles.xml
================================================
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/default_playback_background</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="FullscreenTheme" parent="AppTheme">
        <item name="android:windowActionBarOverlay">true</item>
    </style>

</resources>


================================================
FILE: demo/src/test/java/com/jarvanmo/demo/ExampleUnitTest.java
================================================
package com.jarvanmo.demo;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
public class ExampleUnitTest {
    @Test
    public void addition_isCorrect() throws Exception {
        assertEquals(4, 2 + 2);
    }
}

================================================
FILE: exoplayerview/.gitignore
================================================
/build


================================================
FILE: exoplayerview/build.gradle
================================================
apply plugin: 'com.android.library'

android {
    compileSdkVersion rootProject.ext.compileSdkVersion

    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 36
        versionName "2.2.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables.useSupportLibrary = true
    }


    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }


    lintOptions {
        abortOnError false
    }



    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta8'
    testImplementation 'junit:junit:4.12'

    api 'com.google.android.exoplayer:exoplayer:2.11.0'


    implementation 'androidx.recyclerview:recyclerview:1.1.0'
}

//tasks.withType(Javadoc) {
//    options.addStringOption('Xdoclint:none', '-quiet')
//    options.addStringOption('encoding', 'UTF-8')
//    options.addStringOption('charSet', 'UTF-8')
//}

================================================
FILE: exoplayerview/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/mo/Android/Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class getDisplayName to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}


================================================
FILE: exoplayerview/src/androidTest/java/com/jarvanmo/exoplayerview/ExampleInstrumentedTest.java
================================================
package com.jarvanmo.exoplayerview;

import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;

/**
 * Instrumentation test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() throws Exception {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();

        assertEquals("com.jarvanmo.exoplayerview.test", appContext.getPackageName());
    }
}


================================================
FILE: exoplayerview/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jarvanmo.exoplayerview">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:label="@string/app_name"
        android:supportsRtl="true">

    </application>

</manifest>


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/ads/ExoAdsLoader.java
================================================
package com.jarvanmo.exoplayerview.ads;

import android.view.ViewGroup;

import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ads.AdsLoader;

import java.io.IOException;

import androidx.annotation.Nullable;

/**
 * Created by mo on 17-11-30.
 * 剑气纵横三万里 一剑光寒十九洲
 */

public class ExoAdsLoader extends Player.DefaultEventListener implements AdsLoader {

    @Override
    public void setSupportedContentTypes(int... contentTypes) {

    }

    @Override
    public void start(EventListener eventListener, AdViewProvider adViewProvider) {

    }

    @Override
    public void stop() {

    }


    @Override
    public void setPlayer(@Nullable Player player) {

    }

    @Override
    public void release() {

    }

    @Override
    public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) {

    }

}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/extension/MultiQualitySelectorAdapter.java
================================================
package com.jarvanmo.exoplayerview.extension;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.jarvanmo.exoplayerview.R;
import com.jarvanmo.exoplayerview.media.ExoMediaSource;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by mo on 18-2-8.
 * 剑气纵横三万里 一剑光寒十九洲
 */

public class MultiQualitySelectorAdapter extends RecyclerView.Adapter<MultiQualitySelectorAdapter.MultiQualitySelectorViewHolder> {

    public interface MultiQualitySelectorNavigator {

        /***
         * @return interrupted if true
         * */
        boolean onQualitySelected(ExoMediaSource.Quality quality);
    }


    public interface VisibilityCallback {
        void shouldChangeVisibility(int visibility);
    }

    private List<ExoMediaSource.Quality> qualities = new ArrayList<>();
    private MultiQualitySelectorNavigator navigator;


    public MultiQualitySelectorAdapter(List<ExoMediaSource.Quality> qualities, MultiQualitySelectorNavigator navigator) {
        this.qualities.addAll(qualities);
        this.navigator = navigator;
    }

    @NonNull
    @Override
    public MultiQualitySelectorViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_quality, parent, false);
        return new MultiQualitySelectorViewHolder(view);
    }

    @Override
    public void onBindViewHolder(MultiQualitySelectorViewHolder holder, int position) {
        holder.rootView.setOnClickListener(view -> navigator.onQualitySelected(qualities.get(position)));
        holder.qualityDes.setText(qualities.get(position).getDisplayName());
    }

    @Override
    public int getItemCount() {
        return qualities.size();
    }

    class MultiQualitySelectorViewHolder extends RecyclerView.ViewHolder {
        View rootView;
        TextView qualityDes;

        MultiQualitySelectorViewHolder(View itemView) {
            super(itemView);
            rootView = itemView;
            qualityDes = itemView.findViewById(R.id.qualityDes);
        }
    }

}

================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/gesture/OnVideoGestureChangeListener.java
================================================
package com.jarvanmo.exoplayerview.gesture;

import androidx.annotation.IntDef;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Created by mo on 18-2-2.
 * 剑气纵横三万里 一剑光寒十九洲
 */

public interface OnVideoGestureChangeListener {

    int VOLUME_CHANGED_REDUCTION = -1;
    int VOLUME_CHANGED_MUTE = VOLUME_CHANGED_REDUCTION + 1;
    int VOLUME_CHANGED_INCREMENT = VOLUME_CHANGED_MUTE + 1;


    @IntDef({VOLUME_CHANGED_REDUCTION, VOLUME_CHANGED_MUTE, VOLUME_CHANGED_INCREMENT})
    @Retention(RetentionPolicy.SOURCE)
    @interface VolumeChangeType {

    }


    void onVolumeChanged(int range, @VolumeChangeType int type);

    void onBrightnessChanged(int brightnessPercent);

    void onNoGesture();

    void onShowSeekSize(long seekSize, boolean fastForward);

}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/gesture/VideoGesture.java
================================================
package com.jarvanmo.exoplayerview.gesture;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.media.AudioManager;
import android.provider.Settings;
import androidx.annotation.NonNull;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.jarvanmo.exoplayerview.ui.ExoVideoPlaybackControlView;
import com.jarvanmo.exoplayerview.util.Permissions;

import static android.content.Context.AUDIO_SERVICE;
import static com.jarvanmo.exoplayerview.gesture.OnVideoGestureChangeListener.VOLUME_CHANGED_INCREMENT;
import static com.jarvanmo.exoplayerview.gesture.OnVideoGestureChangeListener.VOLUME_CHANGED_MUTE;
import static com.jarvanmo.exoplayerview.gesture.OnVideoGestureChangeListener.VOLUME_CHANGED_REDUCTION;

/**
 * Created by mo on 18-2-2.
 * 剑气纵横三万里 一剑光寒十九洲
 */

public class VideoGesture implements View.OnTouchListener {

    private final Context context;
    private final OnVideoGestureChangeListener onVideoGestureChangeListener;
    private final ExoVideoPlaybackControlView.PlayerAccessor playerAccessor;

    private final Timeline.Window window;

    //Touch Events
    private static final int TOUCH_NONE = 0;
    private static final int TOUCH_VOLUME = 1;
    private static final int TOUCH_BRIGHTNESS = 2;
    private static final int TOUCH_SEEK = 3;

    //touch
    private int mTouchAction = TOUCH_NONE;
    private int mSurfaceYDisplayRange;
    private float mInitTouchY;
    private float touchX = -1f;
    private float touchY = -1f;


    //Volume
    private AudioManager mAudioManager;
    private int mAudioMax;
    private float mVol;

    // Brightness
    private boolean mIsFirstBrightnessGesture = true;

    private boolean enabled = true;

    public VideoGesture(Context context, OnVideoGestureChangeListener onVideoGestureChangeListener, @NonNull ExoVideoPlaybackControlView.PlayerAccessor playerAccessor) {
        this.context = context;
        this.onVideoGestureChangeListener = onVideoGestureChangeListener;
        mAudioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
        this.playerAccessor = playerAccessor;
        window = new Timeline.Window();

        initVol();
    }


    private void initVol() {

            /* Services and miscellaneous */
        mAudioManager = (AudioManager) context.getApplicationContext().getSystemService(AUDIO_SERVICE);
        mAudioMax = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);

    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if(!enabled){
            return  false;
        }
        return dispatchCenterWrapperTouchEvent(event);

    }

    private boolean dispatchCenterWrapperTouchEvent(MotionEvent event) {


        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        DisplayMetrics screen = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(screen);

        if (mSurfaceYDisplayRange == 0) {
            mSurfaceYDisplayRange = Math.min(screen.widthPixels, screen.heightPixels);
        }

        float x_changed, y_changed;
        if (touchX != -1f && touchY != -1f) {
            y_changed = event.getRawY() - touchY;
            x_changed = event.getRawX() - touchX;
        } else {
            x_changed = 0f;
            y_changed = 0f;
        }

//        Log.e("tag","x_c=" + x_changed + "screen_x =" + screen.xdpi +" screen_rawx" + event.getRawX());
        float coef = Math.abs(y_changed / x_changed);
        float xgesturesize = (((event.getRawX() - touchX) / screen.xdpi) * 2.54f);//2.54f


        float delta_y = Math.max(1f, (Math.abs(mInitTouchY - event.getRawY()) / screen.xdpi + 0.5f) * 2f);

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                mTouchAction = TOUCH_NONE;
                touchX = event.getRawX();
                mVol = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
                touchY = mInitTouchY = event.getRawY();
                break;

            case MotionEvent.ACTION_MOVE:

                if (mTouchAction != TOUCH_SEEK && coef > 2) {
                    if (Math.abs(y_changed / mSurfaceYDisplayRange) < 0.05) {
                        return false;
                    }

                    touchX = event.getRawX();
                    touchY = event.getRawY();


                    if ((int) touchX > (4 * screen.widthPixels / 7)) {
                        doVolumeTouch(y_changed);

                    }
                    // Brightness (Up or Down - Left side)
                    if ((int) touchX < (3 * screen.widthPixels / 7)) {
                        doBrightnessTouch(y_changed);
                    }

                } else {
                    doSeekTouch(Math.round(delta_y), xgesturesize, false);
                }

                break;

            case MotionEvent.ACTION_UP:
                if (mTouchAction == TOUCH_SEEK) {
                    doSeekTouch(Math.round(delta_y), xgesturesize, true);
                }

                if (mTouchAction != TOUCH_NONE) {
                    hideCenterInfo();
                }

                touchX = -1f;
                touchY = -1f;
                break;
            default:
                break;
        }


        return mTouchAction != TOUCH_NONE;
    }

    private void doVolumeTouch(float y_changed) {
        if (mTouchAction != TOUCH_NONE && mTouchAction != TOUCH_VOLUME) {
            return;
        }

        int oldVol = (int) mVol;
        mTouchAction = TOUCH_VOLUME;
        float delta = -((y_changed / mSurfaceYDisplayRange) * mAudioMax);
        mVol += delta;
        int vol = (int) Math.min(Math.max(mVol, 0), mAudioMax);
        if (delta != 0f) {
            setAudioVolume(vol, vol > oldVol);
        }
    }

    private void setAudioVolume(int vol, boolean isUp) {
        mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, vol, 0);

        /* Since android 4.3, the safe volume warning dialog is displayed only with the FLAG_SHOW_UI flag.
         * We don't want to always show the default UI volume, so show it only when volume is not set. */
        int newVol = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
        if (vol != newVol) {
            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, vol, AudioManager.FLAG_SHOW_UI);
        }

        mTouchAction = TOUCH_VOLUME;
        vol = vol * 100 / mAudioMax;
        int type;
        if (newVol == 0) {
            type = VOLUME_CHANGED_MUTE;
        } else if (isUp) {
            type = VOLUME_CHANGED_INCREMENT;
        } else {
            type = VOLUME_CHANGED_REDUCTION;
        }

        onVolumeChanged(vol, type);
    }

    private void onVolumeChanged(int range, @OnVideoGestureChangeListener.VolumeChangeType int type) {
        if (onVideoGestureChangeListener != null) {
            onVideoGestureChangeListener.onVolumeChanged(range, type);
        }
    }


    private void doBrightnessTouch(float y_changed) {
        if (mTouchAction != TOUCH_NONE && mTouchAction != TOUCH_BRIGHTNESS) {
            return;
        }

        mTouchAction = TOUCH_BRIGHTNESS;
        if (mIsFirstBrightnessGesture) {
            initBrightnessTouch();
        }

        mTouchAction = TOUCH_BRIGHTNESS;
//
        // Set delta : 2f is arbitrary for now, it possibly will change in the future
        float delta = -y_changed / mSurfaceYDisplayRange;
        changeBrightness(delta);
    }

    private void changeBrightness(float delta) {
        // Estimate and adjust Brightness
        if (!(context instanceof Activity)) {
            return;
        }
        Activity activity = (Activity) context;


        WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
        float brightness = Math.min(Math.max(lp.screenBrightness + delta, 0.01f), 1f);

        lp.screenBrightness = brightness;
        // Set Brightness
        activity.getWindow().setAttributes(lp);

        brightness = Math.round(brightness * 100);

        onBrightnessChanged((int) brightness);

    }

    private void onBrightnessChanged(int brightnessPercent) {
        if (onVideoGestureChangeListener != null) {
            onVideoGestureChangeListener.onBrightnessChanged(brightnessPercent);
        }
    }

    private void doSeekTouch(int coef, float gesturesize, boolean seek) {
        if (coef == 0) {
            coef = 1;
        }


        // No seek action if coef > 0.5 and gesturesize < 1cm

        if (Math.abs(gesturesize) < 1 || !canSeek()) {
            return;
        }


        if (mTouchAction != TOUCH_NONE && mTouchAction != TOUCH_SEEK) {
            return;
        }


        mTouchAction = TOUCH_SEEK;
        Player player = playerAccessor.attachPlayer();
        long length = player.getDuration();
        long time = player.getCurrentPosition();

        // Size of the jump, 10 minutes max (600000), with a bi-cubic progression, for a 8cm gesture
        int jump = (int) ((Math.signum(gesturesize) * ((600000 * Math.pow((gesturesize / 8), 4)) + 3000)) / coef);

        // Adjust the jump
        if ((jump > 0) && ((time + jump) > length)) {
            jump = (int) (length - time);
        }


        if ((jump < 0) && ((time + jump) < 0)) {
            jump = (int) -time;
        }

        //Jump !
//        if (seek && length > 0) {
//            jump(time + jump);
//        }

        if (length > 0) {
            //Show the jump's size
            seekAndShowJump(seek, time + jump, jump > 0);
        }
    }

    private void seekAndShowJump(boolean seek, long jumpSize, boolean isFastForward) {
        if (onVideoGestureChangeListener != null) {
            onVideoGestureChangeListener.onShowSeekSize(jumpSize, isFastForward);
        }
    }

    private void hideCenterInfo() {
        if (onVideoGestureChangeListener != null) {
            onVideoGestureChangeListener.onNoGesture();
        }
    }


    private void initBrightnessTouch() {
        if (!(context instanceof Activity)) {
            return;
        }
        Activity activity = (Activity) context;

        WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
        float brightnesstemp = lp.screenBrightness != -1f ? lp.screenBrightness : 0.6f;
        // Initialize the layoutParams screen brightness
        try {
            if (Settings.System.getInt(activity.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE) == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) {
                if (!Permissions.canWriteSettings(activity)) {
                    return;
                }
                Settings.System.putInt(activity.getContentResolver(),
                        Settings.System.SCREEN_BRIGHTNESS_MODE,
                        Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
//                restoreAutoBrightness = android.provider.Settings.System.getInt(activity.getContentResolver(),
//                        android.provider.Settings.System.SCREEN_BRIGHTNESS) / 255.0f;
            } else if (brightnesstemp == 0.6f) {
                brightnesstemp = android.provider.Settings.System.getInt(activity.getContentResolver(),
                        android.provider.Settings.System.SCREEN_BRIGHTNESS) / 255.0f;
            }
        } catch (Settings.SettingNotFoundException e) {
            e.printStackTrace();
        }
        lp.screenBrightness = brightnesstemp;
        activity.getWindow().setAttributes(lp);
        mIsFirstBrightnessGesture = false;
    }

    private boolean canSeek() {

        Player player = playerAccessor.attachPlayer();

        Timeline timeline = player != null ? player.getCurrentTimeline() : null;
        boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty();
        boolean isSeekable = false;
        if (haveNonEmptyTimeline) {
            int windowIndex = player.getCurrentWindowIndex();
            timeline.getWindow(windowIndex, window);
            isSeekable = window.isSeekable;
        }

        return isSeekable;
    }

    public void enable() {
        enabled = true;
    }

    public void disable() {
        enabled = false;
    }
}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/EventLogger.java
================================================
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jarvanmo.exoplayerview.media;

import androidx.annotation.Nullable;
import android.util.Log;
import android.view.Surface;

import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.video.VideoRendererEventListener;

import java.io.IOException;

/**
 * Logs player events using {@link Log}.
 */
/* package */ final class EventLogger implements Player.EventListener, MetadataOutput,
        AudioRendererEventListener, VideoRendererEventListener, MediaSourceEventListener {

    public EventLogger(MappingTrackSelector trackSelector) {
    }

    @Override
    public void onTimelineChanged(Timeline timeline, int reason) {

    }

    @Override
    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {

    }

    @Override
    public void onLoadingChanged(boolean isLoading) {

    }

    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {

    }

    @Override
    public void onRepeatModeChanged(int repeatMode) {

    }

    @Override
    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {

    }

    @Override
    public void onPlayerError(ExoPlaybackException error) {

    }

    @Override
    public void onPositionDiscontinuity(int reason) {

    }

    @Override
    public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {

    }

    @Override
    public void onSeekProcessed() {

    }

    @Override
    public void onAudioEnabled(DecoderCounters counters) {

    }

    @Override
    public void onAudioSessionId(int audioSessionId) {

    }

    @Override
    public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) {

    }

    @Override
    public void onAudioInputFormatChanged(Format format) {

    }

    @Override
    public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {

    }

    @Override
    public void onAudioDisabled(DecoderCounters counters) {

    }


    @Override
    public void onMetadata(Metadata metadata) {

    }


    @Override
    public void onVideoEnabled(DecoderCounters counters) {

    }

    @Override
    public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) {

    }

    @Override
    public void onVideoInputFormatChanged(Format format) {

    }

    @Override
    public void onDroppedFrames(int count, long elapsedMs) {

    }

    @Override
    public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {

    }

    @Override
    public void onRenderedFirstFrame(Surface surface) {

    }

    @Override
    public void onVideoDisabled(DecoderCounters counters) {

    }

    @Override
    public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {

    }

    @Override
    public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {

    }

    @Override
    public void onLoadStarted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {

    }

    @Override
    public void onLoadCompleted(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {

    }

    @Override
    public void onLoadCanceled(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {

    }

    @Override
    public void onLoadError(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) {

    }

    @Override
    public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {

    }

    @Override
    public void onUpstreamDiscarded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {

    }

    @Override
    public void onDownstreamFormatChanged(int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {

    }
}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/ExoMediaSource.java
================================================
package com.jarvanmo.exoplayerview.media;

import android.net.Uri;

import java.util.List;

/**
 * Created by mo on 18-1-11.
 * 剑气纵横三万里 一剑光寒十九洲
 */

public interface ExoMediaSource {

    interface Quality {
        CharSequence getDisplayName();

        Uri getUri();

        void setUri(Uri uri);

        void setDisplayName(CharSequence displayName);

        void setQuality(String quality);

        String getQuality();

    }

    Uri uri();

    String name();

    List<Quality> qualities();

    String extension();
}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/MediaSourceCreator.java
================================================
package com.jarvanmo.exoplayerview.media;

import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.text.TextUtils;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util;

/**
 * Created by mo on 17-11-29.
 * 剑气纵横三万里 一剑光寒十九洲
 */

public class MediaSourceCreator {

    private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();

    private DataSource.Factory mediaDataSourceFactory;

    private Context context;
    private String userAgent;

    private Handler mainHandler;
    private EventLogger eventLogger;

    private DefaultTrackSelector trackSelector;

    public MediaSourceCreator(Context context) {
        this(context, "exo_video_view");
    }

    public MediaSourceCreator(Context context, String userAgent) {
        this.userAgent = userAgent;
        this.context = context;
        mediaDataSourceFactory = buildDataSourceFactory(true);
        mainHandler = new Handler();


        eventLogger = new EventLogger(trackSelector);
    }

    public EventLogger getEventLogger() {
        return eventLogger;
    }


    public MediaSource buildMediaSource(Uri uri, String overrideExtension) {
        @C.ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
                : Util.inferContentType("." + overrideExtension);
        switch (type) {
            case C.TYPE_SS:
                SsMediaSource.Factory factory = new SsMediaSource.Factory(buildDataSourceFactory(false));
                factory.createMediaSource(uri);
                return factory.createMediaSource(uri);
            case C.TYPE_DASH:
                DashMediaSource.Factory factory1 = new DashMediaSource.Factory(buildDataSourceFactory(false));
                return factory1.createMediaSource(uri);
            case C.TYPE_HLS:
                HlsMediaSource.Factory factory2 = new HlsMediaSource.Factory(buildDataSourceFactory(false));
                return factory2.createMediaSource(uri);
            case C.TYPE_OTHER:
                ProgressiveMediaSource.Factory factory3 = new ProgressiveMediaSource.Factory(buildDataSourceFactory(false));
                return factory3.createMediaSource(uri);
            default: {
                throw new IllegalStateException("Unsupported type: " + type);
            }
        }
    }


    private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
        return buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
    }

    public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
        return new DefaultDataSourceFactory(context, bandwidthMeter,
                buildHttpDataSourceFactory(bandwidthMeter));
    }

    public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
        return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
    }

}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/MediaSourceParams.java
================================================
package com.jarvanmo.exoplayerview.media;

/**
 * Created by mo on 18-1-11.
 * 剑气纵横三万里 一剑光寒十九洲
 */

public class MediaSourceParams {
    public boolean playWhenReady = true;
    public long where = 0L;
}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/SimpleMediaSource.java
================================================
package com.jarvanmo.exoplayerview.media;

import android.net.Uri;

import java.util.List;

/**
 * Created by mo on 16-11-30.
 * this package is com.jarvanmo.exoplayerview.ui
 */

public class SimpleMediaSource implements ExoMediaSource {

    private String displayName;

    private Uri uri;

    private List<Quality> qualities;

    public SimpleMediaSource(String url) {
        this.uri = Uri.parse(url);
    }

    public SimpleMediaSource(Uri uri) {
        this.uri = uri;
    }


    @Override
    public String name() {
        return displayName;
    }

    @Override
    public List<Quality> qualities() {
        return qualities;
    }

    @Override
    public String extension() {
        return null;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    @Override
    public Uri uri() {
        return uri;
    }

    public void setQualities(List<Quality> qualities) {
        this.qualities = qualities;
    }

}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/SimpleQuality.java
================================================
package com.jarvanmo.exoplayerview.media;

import android.net.Uri;

/**
 * Created by mo on 18-2-7.
 *
 * @author mo
 */

public class SimpleQuality implements ExoMediaSource.Quality {

    private CharSequence name;
    private Uri uri;
    private String quality;

    public SimpleQuality(CharSequence name, Uri uri) {
        this.name = name;
        this.uri = uri;
    }

    @Override
    public CharSequence getDisplayName() {
        return name;
    }

    @Override
    public Uri getUri() {
        return uri;
    }

    @Override
    public void setUri(Uri uri) {
        this.uri = uri;
    }

    @Override
    public void setDisplayName(CharSequence name) {
        this.name = name;
    }

    @Override
    public void setQuality(String quality) {
        this.quality = quality;
    }

    @Override
    public String getQuality() {
        return quality;
    }


}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/orientation/OnOrientationChangedListener.java
================================================
package com.jarvanmo.exoplayerview.orientation;

import androidx.annotation.IntDef;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Created by mo on 18-2-5.
 * 剑气纵横三万里 一剑光寒十九洲
 */

public interface OnOrientationChangedListener {
    int SENSOR_UNKNOWN = -1;
    int SENSOR_PORTRAIT = SENSOR_UNKNOWN + 1;
    int SENSOR_LANDSCAPE = SENSOR_PORTRAIT + 1;

    @IntDef({SENSOR_UNKNOWN, SENSOR_PORTRAIT, SENSOR_LANDSCAPE})
    @Retention(RetentionPolicy.SOURCE)
    @interface SensorOrientationType {

    }


    void onChanged(@SensorOrientationType int orientation);
}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/orientation/SensorOrientation.java
================================================
package com.jarvanmo.exoplayerview.orientation;

import android.content.Context;
import android.provider.Settings;
import android.util.Log;
import android.view.OrientationEventListener;

import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_LANDSCAPE;
import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_PORTRAIT;
import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_UNKNOWN;

/**
 * Created by mo on 18-2-5.
 * 剑气纵横三万里 一剑光寒十九洲
 */

public class SensorOrientation {

    private int oldScreenOrientation = SENSOR_UNKNOWN;

    private final Context context;
    private final OrientationEventListener screenOrientationEventListener;

    public SensorOrientation(Context context, OnOrientationChangedListener onOrientationChangedListener) {
        this.context = context;
        screenOrientationEventListener = new OrientationEventListener(context) {
            @Override
            public void onOrientationChanged(int orientation) {

                if (onOrientationChangedListener == null || !isScreenOpenRotate()) {
                    return;
                }


                if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
                    onOrientationChangedListener.onChanged(SENSOR_UNKNOWN);
                    return;  //手机平放时,检测不到有效的角度
                }
//只检测是否有四个角度的改变
                if (orientation > 350 || orientation < 10) { //0度
                    orientation = 0;
                } else if (orientation > 80 && orientation < 100) { //90度
                    orientation = 90;
                } else if (orientation > 170 && orientation < 190) { //180度
                    orientation = 180;
                } else if (orientation > 260 && orientation < 280) { //270度
                    orientation = 270;
                } else {
                    return;
                }

                if (oldScreenOrientation == orientation) {
                    onOrientationChangedListener.onChanged(SENSOR_UNKNOWN);
                    return;
                }


                oldScreenOrientation = orientation;

                if (orientation == 0 || orientation == 180) {
                    onOrientationChangedListener.onChanged(SENSOR_PORTRAIT);
                } else {
                    onOrientationChangedListener.onChanged(SENSOR_LANDSCAPE);
                }
            }
        };
    }

    private boolean isScreenOpenRotate() {

        int gravity = 0;
        try {

            gravity = Settings.System.getInt(context.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION);

        } catch (Settings.SettingNotFoundException e) {
            Log.e(getClass().getSimpleName(), e.getMessage() + "");
        }
        return 1 == gravity;

    }

    public void enable() {
        screenOrientationEventListener.enable();
    }

    public void disable() {
        screenOrientationEventListener.disable();
    }
}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/ui/ExoVideoPlaybackControlView.java
================================================
package com.jarvanmo.exoplayerview.ui;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.SystemClock;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.hls.HlsManifest;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import com.google.android.exoplayer2.ui.TimeBar;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util;
import com.jarvanmo.exoplayerview.R;
import com.jarvanmo.exoplayerview.extension.MultiQualitySelectorAdapter;
import com.jarvanmo.exoplayerview.gesture.OnVideoGestureChangeListener;
import com.jarvanmo.exoplayerview.gesture.VideoGesture;
import com.jarvanmo.exoplayerview.media.ExoMediaSource;
import com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener;
import com.jarvanmo.exoplayerview.orientation.SensorOrientation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Formatter;
import java.util.Locale;

import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_LANDSCAPE;
import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_PORTRAIT;
import static com.jarvanmo.exoplayerview.orientation.OnOrientationChangedListener.SENSOR_UNKNOWN;

/**
 * Created by mo on 16-11-7.
 *
 * @author mo
 *         <p>
 *         * <h3>Specifying a custom layout file</h3>
 *         Defining your own {@code exo_video_playback_control_view.xml} is useful to customize the layout of
 *         ExoVideoPlaybackControlView throughout your application. It's also possible to customize the layout for a
 *         single instance in a layout file. This is achieved by setting the {@code controller_layout_id}
 *         attribute on a ExoVideoPlaybackControlView. This will cause the specified layout to be inflated instead
 *         of {@code exo_video_playback_control_view.xml} for only the instance on which the attribute is set.
 */

public class ExoVideoPlaybackControlView extends FrameLayout {

    /**
     * to get  {@link ExoVideoView}
     */
    public interface VideoViewAccessor {

        View attachVideoView();
    }


    /**
     * to get  {@link Player}
     */
    public interface PlayerAccessor {

        Player attachPlayer();
    }


    /**
     * Listener to be notified about changes of the visibility of the UI control.
     */
    public interface VisibilityListener {

        /**
         * Called when the visibility changes.
         *
         * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}.
         */
        void onVisibilityChange(int visibility);

    }


    public interface ExoClickListener {

        /***
         * called when buttons clicked in controller
         * @param view  null when back pressed.
         * @param isPortrait the controller is portrait  or not
         * @return will interrupt operation in controller if return true
         * **/
        boolean onClick(@Nullable View view, boolean isPortrait);

    }


    public interface OrientationListener {
        void onOrientationChanged(@OnOrientationChangedListener.SensorOrientationType int orientation);
    }


    /**
     * The default fast forward increment, in milliseconds.
     */
    public static final int DEFAULT_FAST_FORWARD_MS = 15000;
    /**
     * The default rewind increment, in milliseconds.
     */
    public static final int DEFAULT_REWIND_MS = 5000;
    /**
     * The default show timeout, in milliseconds.
     */
    public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000;
    /**
     * The default repeat toggle modes.
     */
    public static final @RepeatModeUtil.RepeatToggleModes
    int DEFAULT_REPEAT_TOGGLE_MODES =
            RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE;

    /**
     * The maximum number of windows that can be shown in a multi-window time bar.
     */
    public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100;

    private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;


    //        <!--<enum getDisplayName="all" value="0b1111"/>-->
//    <!--<enum getDisplayName="top" value="0b1000"/>-->
//    <!--<enum getDisplayName="top_landscape" value="0b0100"/>-->
//    <!--<enum getDisplayName="bottom" value="0b0010"/>-->
//    <!--<enum getDisplayName="bottom_landscape" value="0b0001"/>-->
    public static final int CONTROLLER_MODE_NONE = 0b0000;
    public static final int CONTROLLER_MODE_ALL = 0b1111;
    public static final int CONTROLLER_MODE_TOP = 0b1000;
    public static final int CONTROLLER_MODE_TOP_LANDSCAPE = 0b0100;
    public static final int CONTROLLER_MODE_BOTTOM = 0b0010;
    public static final int CONTROLLER_MODE_BOTTOM_LANDSCAPE = 0b0001;

    @IntDef({CONTROLLER_MODE_NONE,
            CONTROLLER_MODE_ALL,
            CONTROLLER_MODE_TOP, CONTROLLER_MODE_TOP_LANDSCAPE, CONTROLLER_MODE_BOTTOM, CONTROLLER_MODE_BOTTOM_LANDSCAPE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ControllerModeType {

    }

    public static final int CUSTOM_VIEW_TOP = 1;
    public static final int CUSTOM_VIEW_TOP_LANDSCAPE = CUSTOM_VIEW_TOP + 1;
    public static final int CUSTOM_VIEW_BOTTOM_LANDSCAPE = CUSTOM_VIEW_TOP_LANDSCAPE + 1;

    @IntDef({CUSTOM_VIEW_TOP, CUSTOM_VIEW_TOP_LANDSCAPE, CUSTOM_VIEW_BOTTOM_LANDSCAPE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface CustomViewType {

    }

    private final ComponentListener componentListener;
    private final View previousButton;
    private final View nextButton;
    private final View playButton;
    private final View pauseButton;
    private final View fastForwardButton;
    private final View rewindButton;
    private final ImageView repeatToggleButton;
    private final View shuffleButton;
    private final TextView durationView;
    private final TextView positionView;
    private final TimeBar timeBar;
    private final StringBuilder formatBuilder;
    private final Formatter formatter;
    private final Timeline.Period period;
    private final Timeline.Window window;


    private final Drawable repeatOffButtonDrawable;
    private final Drawable repeatOneButtonDrawable;
    private final Drawable repeatAllButtonDrawable;
    private final String repeatOffButtonContentDescription;
    private final String repeatOneButtonContentDescription;
    private final String repeatAllButtonContentDescription;

    private Player player;
    private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;
    private VisibilityListener visibilityListener;

    private boolean isAttachedToWindow;
    private boolean showMultiWindowTimeBar;
    private boolean multiWindowTimeBar;
    private boolean scrubbing;
    private int rewindMs;
    private int fastForwardMs;
    private int showTimeoutMs;
    private @RepeatModeUtil.RepeatToggleModes
    int repeatToggleModes;
    private boolean showShuffleButton;
    private long hideAtMs;
    private long[] adGroupTimesMs;
    private boolean[] playedAdGroups;
    private long[] extraAdGroupTimesMs;
    private boolean[] extraPlayedAdGroups;

    private final Runnable updateProgressAction = this::updateProgress;

    private final Runnable hideAction = this::hide;


    private final TimeBar timeBarLandscape;
    private final View playButtonLandScape;
    private final View pauseButtonLandScape;
    private final TextView durationViewLandscape;
    private final View enterFullscreen;
    private final View exitFullscreen;


    private final View exoPlayerControllerTop;
    private final View exoPlayerControllerTopLandscape;
    private final View exoPlayerControllerBottom;
    private final View exoPlayerControllerBottomLandscape;

    private final View centerInfoWrapper;
    private final TextView centerInfo;

    private final TextView exoPlayerVideoName;
    private final TextView exoPlayerVideoNameLandscape;

    private final TextView exoPlayerCurrentQualityLandscape;

    private final ViewGroup topCustomView;
    private final ViewGroup topCustomViewLandscape;
    private final ViewGroup bottomCustomViewLandscape;


    private final TextView centerError;
    private final ProgressBar loadingBar;


    private final View back;
    private final View backLandscape;

    private boolean portrait = true;


    private SensorOrientation sensorOrientation;

    private OrientationListener orientationListener;
    private ExoClickListener backListener;


    private boolean isHls;

    private int displayMode = CONTROLLER_MODE_ALL;

    private MultiQualitySelectorAdapter.VisibilityCallback qualityVisibilityCallback;

    private VideoViewAccessor videoViewAccessor;
    private  VideoGesture videoGesture;

    public ExoVideoPlaybackControlView(Context context) {
        this(context, null);
    }

    public ExoVideoPlaybackControlView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ExoVideoPlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, attrs);
    }

    public ExoVideoPlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr,
                                       AttributeSet playbackAttrs) {
        super(context, attrs, defStyleAttr);


        int controllerLayoutId = R.layout.exo_video_playback_control_view;
        rewindMs = DEFAULT_REWIND_MS;
        fastForwardMs = DEFAULT_FAST_FORWARD_MS;
        showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
        repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES;
        showShuffleButton = false;
        boolean enableGesture = true;

        int controllerBackgroundId = 0;

        if (playbackAttrs != null) {
            TypedArray a = context.getTheme().obtainStyledAttributes(playbackAttrs,
                    R.styleable.ExoVideoPlaybackControlView, 0, 0);
            try {
                rewindMs = a.getInt(R.styleable.ExoVideoPlaybackControlView_rewind_increment, rewindMs);
                fastForwardMs = a.getInt(R.styleable.ExoVideoPlaybackControlView_fastforward_increment,
                        fastForwardMs);
                showTimeoutMs = a.getInt(R.styleable.ExoVideoPlaybackControlView_show_timeout, showTimeoutMs);
                controllerLayoutId = a.getResourceId(R.styleable.ExoVideoPlaybackControlView_controller_layout_id,
                        controllerLayoutId);
                repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes);
                showShuffleButton = a.getBoolean(R.styleable.ExoVideoPlaybackControlView_show_shuffle_button,
                        showShuffleButton);
                displayMode = a.getInt(R.styleable.ExoVideoPlaybackControlView_controller_display_mode, CONTROLLER_MODE_ALL);

                controllerBackgroundId = a.getResourceId(R.styleable.ExoVideoPlaybackControlView_controller_background, 0);
                enableGesture = a.getBoolean(R.styleable.ExoVideoPlaybackControlView_enableGesture,enableGesture);
            } finally {
                a.recycle();
            }
        }


        period = new Timeline.Period();
        window = new Timeline.Window();
        formatBuilder = new StringBuilder();
        formatter = new Formatter(formatBuilder, Locale.getDefault());
        adGroupTimesMs = new long[0];
        playedAdGroups = new boolean[0];
        extraAdGroupTimesMs = new long[0];
        extraPlayedAdGroups = new boolean[0];
        componentListener = new ComponentListener();
        controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher();

        LayoutInflater.from(context).inflate(controllerLayoutId, this);
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);

        durationView = findViewById(R.id.exo_player_duration);
        positionView = findViewById(R.id.exo_player_position);
        timeBar = findViewById(R.id.exo_player_progress);
        if (timeBar != null) {
            timeBar.addListener(componentListener);
        }


        playButton = findViewById(R.id.exo_player_play);
        if (playButton != null) {
            playButton.setOnClickListener(componentListener);
        }


        pauseButton = findViewById(R.id.exo_player_pause);
        if (pauseButton != null) {
            pauseButton.setOnClickListener(componentListener);
        }


        previousButton = findViewById(R.id.exo_prev);
        if (previousButton != null) {
            previousButton.setOnClickListener(componentListener);
        }
        nextButton = findViewById(R.id.exo_next);
        if (nextButton != null) {
            nextButton.setOnClickListener(componentListener);
        }
        rewindButton = findViewById(R.id.exo_rew);
        if (rewindButton != null) {
            rewindButton.setOnClickListener(componentListener);
        }
        fastForwardButton = findViewById(R.id.exo_ffwd);
        if (fastForwardButton != null) {
            fastForwardButton.setOnClickListener(componentListener);
        }
        repeatToggleButton = findViewById(R.id.exo_repeat_toggle);
        if (repeatToggleButton != null) {
            repeatToggleButton.setOnClickListener(componentListener);
        }
        shuffleButton = findViewById(R.id.exo_shuffle);
        if (shuffleButton != null) {
            shuffleButton.setOnClickListener(componentListener);
        }
        Resources resources = context.getResources();
        repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off);
        repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one);
        repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all);
        repeatOffButtonContentDescription = resources.getString(
                R.string.exo_controls_repeat_off_description);
        repeatOneButtonContentDescription = resources.getString(
                R.string.exo_controls_repeat_one_description);
        repeatAllButtonContentDescription = resources.getString(
                R.string.exo_controls_repeat_all_description);


        durationViewLandscape = findViewById(R.id.exo_player_position_duration_landscape);

        timeBarLandscape = findViewById(R.id.exo_player_progress_landscape);
        if (timeBarLandscape != null) {
            timeBarLandscape.addListener(componentListener);
        }

        playButtonLandScape = findViewById(R.id.exo_player_play_landscape);
        if (playButtonLandScape != null) {
            playButtonLandScape.setOnClickListener(componentListener);
        }

        pauseButtonLandScape = findViewById(R.id.exo_player_pause_landscape);
        if (pauseButtonLandScape != null) {
            pauseButtonLandScape.setOnClickListener(componentListener);
        }

        enterFullscreen = findViewById(R.id.exo_player_enter_fullscreen);
        if (enterFullscreen != null) {
            enterFullscreen.setOnClickListener(componentListener);
        }


        exitFullscreen = findViewById(R.id.exo_player_exit_fullscreen);
        if (exitFullscreen != null) {
            exitFullscreen.setOnClickListener(componentListener);
        }

        centerInfoWrapper = findViewById(R.id.exo_player_center_info_wrapper);
        centerInfo = findViewById(R.id.exo_player_center_text);


        exoPlayerControllerTop = findViewById(R.id.exo_player_controller_top);
        if (exoPlayerControllerTop != null && controllerBackgroundId != 0) {
            exoPlayerControllerTop.setBackgroundResource(controllerBackgroundId);
        }

        exoPlayerControllerTopLandscape = findViewById(R.id.exo_player_controller_top_landscape);
        if (exoPlayerControllerTopLandscape != null && controllerBackgroundId != 0) {
            exoPlayerControllerTopLandscape.setBackgroundResource(controllerBackgroundId);
        }

        exoPlayerControllerBottom = findViewById(R.id.exo_player_controller_bottom);
        if (exoPlayerControllerBottom != null && controllerBackgroundId != 0) {
            exoPlayerControllerBottom.setBackgroundResource(controllerBackgroundId);
        }

        exoPlayerControllerBottomLandscape = findViewById(R.id.exo_player_controller_bottom_landscape);
        if (exoPlayerControllerBottomLandscape != null && controllerBackgroundId != 0) {
            exoPlayerControllerBottomLandscape.setBackgroundResource(controllerBackgroundId);
        }


        exoPlayerVideoName = findViewById(R.id.exo_player_video_name);
        if (exoPlayerVideoName != null) {
            exoPlayerVideoName.setOnClickListener(componentListener);
        }

        exoPlayerVideoNameLandscape = findViewById(R.id.exo_player_video_name_landscape);
        if (exoPlayerVideoNameLandscape != null) {
            exoPlayerVideoNameLandscape.setOnClickListener(componentListener);
        }

        back = findViewById(R.id.exo_player_controller_back);
        if (back != null) {
            back.setOnClickListener(componentListener);
        }

        backLandscape = findViewById(R.id.exo_player_controller_back_landscape);
        if(backLandscape != null){
            backLandscape.setOnClickListener(componentListener);
        }

        if (centerInfoWrapper != null) {
            setupVideoGesture(enableGesture);
        }

        exoPlayerCurrentQualityLandscape = findViewById(R.id.exo_player_current_quality_landscape);
        if (exoPlayerCurrentQualityLandscape != null) {
            exoPlayerCurrentQualityLandscape.setOnClickListener(componentListener);
        }


        topCustomView = findViewById(R.id.exo_player_controller_top_custom_view);
        topCustomViewLandscape = findViewById(R.id.exo_player_controller_top_custom_view_landscape);
        bottomCustomViewLandscape = findViewById(R.id.exo_player_controller_bottom_custom_view_landscape);

        centerError = findViewById(R.id.exo_player_center_error);
        loadingBar = findViewById(R.id.exo_player_loading);
        sensorOrientation = new SensorOrientation(getContext(), this::changeOrientation);
        showControllerByDisplayMode();

        showUtilHideCalled();
    }


    private void setupVideoGesture(boolean enableGesture) {
        OnVideoGestureChangeListener onVideoGestureChangeListener = new OnVideoGestureChangeListener() {

            @Override
            public void onVolumeChanged(int range, int type) {
                show();
                int drawableId;
                if (type == VOLUME_CHANGED_MUTE) {
                    drawableId = R.drawable.ic_volume_mute_white_36dp;
                } else if (type == VOLUME_CHANGED_INCREMENT) {
                    drawableId = R.drawable.ic_volume_up_white_36dp;
                } else {
                    drawableId = R.drawable.ic_volume_down_white_36dp;
                }

                setVolumeOrBrightnessInfo(getContext().getString(R.string.volume_changing, range), drawableId);
            }

            @Override
            public void onBrightnessChanged(int brightnessPercent) {
                show();
                String info = getContext().getString(R.string.brightness_changing, brightnessPercent);
                int drawable = whichBrightnessImageToUse(brightnessPercent);
                setVolumeOrBrightnessInfo(info, drawable);
            }

            @Override
            public void onNoGesture() {

                if (centerInfo == null) {
                    return;
                }
                centerInfo.startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_out));
                centerInfo.setVisibility(GONE);
            }

            @Override
            public void onShowSeekSize(long seekSize, boolean fastForward) {
                if (isHls) {
                    return;
                }

                show();
                seekTo(seekSize);
                if (centerInfo == null) {
                    return;
                }

                if (centerError != null && centerError.getVisibility() == VISIBLE) {
                    centerError.setVisibility(GONE);
                }
                centerInfo.setVisibility(VISIBLE);
                centerInfo.setText(generateFastForwardOrRewindTxt(seekSize));
                int drawableId = fastForward ? R.drawable.ic_fast_forward_white_36dp : R.drawable.ic_fast_rewind_white_36dp;
                Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
                centerInfo.setCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null);
            }
        };


        videoGesture = new VideoGesture(getContext(), onVideoGestureChangeListener, () -> player);
        if(!enableGesture){
            videoGesture.disable();
        }
        centerInfoWrapper.setOnClickListener(componentListener);
        centerInfoWrapper.setOnTouchListener(videoGesture);

    }


    private CharSequence generateFastForwardOrRewindTxt(long changingTime) {

        long duration = player == null ? 0 : player.getDuration();
        String result = Util.getStringForTime(formatBuilder, formatter, changingTime);
        result = result + "/";
        result = result + Util.getStringForTime(formatBuilder, formatter, duration);

        int index = result.indexOf("/");

        SpannableString spannableString = new SpannableString(result);


        TypedValue typedValue = new TypedValue();
        TypedArray a = getContext().obtainStyledAttributes(typedValue.data, new int[]{R.attr.colorAccent});
        int color = a.getColor(0, 0);
        a.recycle();
        spannableString.setSpan(new ForegroundColorSpan(color), 0, index, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new ForegroundColorSpan(Color.WHITE), index, result.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        return spannableString;
    }

    @DrawableRes
    private int whichBrightnessImageToUse(int brightnessInt) {
        if (brightnessInt <= 15) {
            return R.drawable.ic_brightness_1_white_36dp;
        } else if (brightnessInt <= 30) {
            return R.drawable.ic_brightness_2_white_36dp;
        } else if (brightnessInt <= 45) {
            return R.drawable.ic_brightness_3_white_36dp;
        } else if (brightnessInt <= 60) {
            return R.drawable.ic_brightness_4_white_36dp;
        } else if (brightnessInt <= 75) {
            return R.drawable.ic_brightness_5_white_36dp;
        } else if (brightnessInt <= 90) {
            return R.drawable.ic_brightness_6_white_36dp;
        } else {
            return R.drawable.ic_brightness_7_white_36dp;
        }

    }

    private void setVolumeOrBrightnessInfo(String txt, @DrawableRes int drawableId) {
        if (centerInfo == null) {
            return;
        }

        if (centerError != null && centerError.getVisibility() == VISIBLE) {
            centerError.setVisibility(GONE);
        }

        centerInfo.setVisibility(VISIBLE);
        centerInfo.setText(txt);
        centerInfo.setTextColor(ContextCompat.getColor(getContext(), android.R.color.white));
        centerInfo.setCompoundDrawablesWithIntrinsicBounds(null, ContextCompat.getDrawable(getContext(), drawableId), null, null);
    }


    @SuppressWarnings("ResourceType")
    private static @RepeatModeUtil.RepeatToggleModes
    int getRepeatToggleModes(TypedArray a,
                             @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
        return a.getInt(R.styleable.ExoVideoPlaybackControlView_repeat_toggle_modes, repeatToggleModes);
    }

    /**
     * Returns the {@link Player} currently being controlled by this view, or null if no player is
     * set.
     */
    public Player getPlayer() {
        return player;
    }

    /**
     * Sets the {@link Player} to control.
     *
     * @param player The {@link Player} to control.
     */
    public void setPlayer(Player player) {
        if (this.player == player) {
            return;
        }
        if (this.player != null) {
            this.player.removeListener(componentListener);
        }
        this.player = player;
        if (player != null) {
            player.addListener(componentListener);
        }
        updateAll();
    }

    /**
     * Sets whether the time bar should show all windows, as opposed to just the current one. If the
     * timeline has a period with unknown duration or more than
     * {@link #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a
     * single window.
     *
     * @param showMultiWindowTimeBar Whether the time bar should show all windows.
     */
    public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
        this.showMultiWindowTimeBar = showMultiWindowTimeBar;
        updateTimeBarMode();
    }

    /**
     * Sets the millisecond positions of extra ad markers relative to the start of the window (or
     * timeline, if in multi-window mode) and whether each extra ad has been played or not. The
     * markers are shown in addition to any ad markers for ads in the player's timeline.
     *
     * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or
     *                            {@code null} to show no extra ad markers.
     * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad
     *                            markers.
     */
    public void setExtraAdGroupMarkers(@Nullable long[] extraAdGroupTimesMs,
                                       @Nullable boolean[] extraPlayedAdGroups) {
        if (extraAdGroupTimesMs == null) {
            this.extraAdGroupTimesMs = new long[0];
            this.extraPlayedAdGroups = new boolean[0];
        } else {
            Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length);
            this.extraAdGroupTimesMs = extraAdGroupTimesMs;
            this.extraPlayedAdGroups = extraPlayedAdGroups;
        }
        updateProgress();
    }

    /**
     * Sets the {@link VisibilityListener}.
     *
     * @param listener The listener to be notified about visibility changes.
     */
    public void setVisibilityListener(VisibilityListener listener) {
        this.visibilityListener = listener;
    }

    /**
     * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}.
     *
     * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null
     *                          to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}.
     */
    public void setControlDispatcher(
            @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) {
        this.controlDispatcher = controlDispatcher == null
                ? new com.google.android.exoplayer2.DefaultControlDispatcher() : controlDispatcher;
    }

    /**
     * Sets the rewind increment in milliseconds.
     *
     * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the
     *                 rewind button to be disabled.
     */
    public void setRewindIncrementMs(int rewindMs) {
        this.rewindMs = rewindMs;
        updateNavigation();
    }

    /**
     * Sets the fast forward increment in milliseconds.
     *
     * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will
     *                      cause the fast forward button to be disabled.
     */
    public void setFastForwardIncrementMs(int fastForwardMs) {
        this.fastForwardMs = fastForwardMs;
        updateNavigation();
    }

    /**
     * Returns the playback controls timeout. The playback controls are automatically hidden after
     * this duration of time has elapsed without user input.
     *
     * @return The duration in milliseconds. A non-positive value indicates that the controls will
     * remain visible indefinitely.
     */
    public int getShowTimeoutMs() {
        return showTimeoutMs;
    }

    /**
     * Sets the playback controls timeout. The playback controls are automatically hidden after this
     * duration of time has elapsed without user input.
     *
     * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls
     *                      to remain visible indefinitely.
     */
    public void setShowTimeoutMs(int showTimeoutMs) {
        this.showTimeoutMs = showTimeoutMs;
    }

    /**
     * Returns which repeat toggle modes are enabled.
     *
     * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}.
     */
    public @RepeatModeUtil.RepeatToggleModes
    int getRepeatToggleModes() {
        return repeatToggleModes;
    }

    /**
     * Sets which repeat toggle modes are enabled.
     *
     * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
     */
    public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
        this.repeatToggleModes = repeatToggleModes;
        if (player != null) {
            @Player.RepeatMode int currentMode = player.getRepeatMode();
            if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
                    && currentMode != Player.REPEAT_MODE_OFF) {
                controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF);
            } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE
                    && currentMode == Player.REPEAT_MODE_ALL) {
                controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE);
            } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL
                    && currentMode == Player.REPEAT_MODE_ONE) {
                controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL);
            }
        }
    }

    /**
     * Returns whether the shuffle button is shown.
     */
    public boolean getShowShuffleButton() {
        return showShuffleButton;
    }

    /**
     * Sets whether the shuffle button is shown.
     *
     * @param showShuffleButton Whether the shuffle button is shown.
     */
    public void setShowShuffleButton(boolean showShuffleButton) {
        this.showShuffleButton = showShuffleButton;
        updateShuffleButton();
    }

    /**
     * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will
     * be automatically hidden after this duration of time has elapsed without user input.
     */

    public void show() {

        if (!isVisible()) {
            setVisibility(VISIBLE);
            if (visibilityListener != null) {
                visibilityListener.onVisibilityChange(getVisibility());
            }

            if (portrait) {
                changeSystemUiVisibilityPortrait();
            }

            updateAll();
            requestPlayPauseFocus();
        }
        // Call hideAfterTimeout even if already visible to reset the timeout.
        hideAfterTimeout();

    }

    /**
     * Hides the controller.
     */
    public void hide() {
        if (isVisible()) {
            setVisibility(GONE);
            if (visibilityListener != null) {
                visibilityListener.onVisibilityChange(getVisibility());
            }
            removeCallbacks(updateProgressAction);
            removeCallbacks(hideAction);
            hideAtMs = C.TIME_UNSET;

            if (!portrait) {
                changeSystemUiVisibilityLandscape();
            }
        }
    }

    /**
     * Returns whether the controller is currently visible.
     */
    public boolean isVisible() {
        return getVisibility() == VISIBLE;
    }

    private void hideAfterTimeout() {
        removeCallbacks(hideAction);
        if (showTimeoutMs > 0) {
            hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs;
            if (isAttachedToWindow) {
                postDelayed(hideAction, showTimeoutMs);
            }
        } else {
            hideAtMs = C.TIME_UNSET;
        }
    }

    private void updateAll() {
        updatePlayPauseButton();
        updateNavigation();
        updateRepeatModeButton();
        updateShuffleButton();
        updateProgress();
    }

    private void updateNavigation() {
        if (!isVisible() || !isAttachedToWindow) {
            return;
        }
        Timeline timeline = player != null ? player.getCurrentTimeline() : null;
        boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty();
        boolean isSeekable = false;
        boolean enablePrevious = false;
        boolean enableNext = false;
        if (haveNonEmptyTimeline && !player.isPlayingAd()) {
            int windowIndex = player.getCurrentWindowIndex();
            timeline.getWindow(windowIndex, window);
            isSeekable = window.isSeekable;
            enablePrevious = isSeekable || !window.isDynamic
                    || player.getPreviousWindowIndex() != C.INDEX_UNSET;
            enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET;
        }
        setButtonEnabled(enablePrevious, previousButton);
        setButtonEnabled(enableNext, nextButton);
        setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton);
        setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton);
        if (timeBar != null) {
            timeBar.setEnabled(isSeekable && !isHls);
        }
        if (timeBarLandscape != null) {
            timeBarLandscape.setEnabled(isSeekable && !isHls);
        }
    }

    private void updatePlayPauseButton() {
        if (!isVisible() || !isAttachedToWindow) {
            return;
        }
        boolean requestPlayPauseFocus = false;
        boolean playing = player != null && player.getPlayWhenReady();
        if (playButton != null) {
            requestPlayPauseFocus |= playing && playButton.isFocused();
            playButton.setVisibility(playing ? View.GONE : View.VISIBLE);
        }
        if (pauseButton != null) {
            requestPlayPauseFocus |= !playing && pauseButton.isFocused();
            pauseButton.setVisibility(!playing ? View.GONE : View.VISIBLE);
        }


        if (playButtonLandScape != null) {
            requestPlayPauseFocus |= playing && playButtonLandScape.isFocused();
            playButtonLandScape.setVisibility(playing ? View.GONE : View.VISIBLE);
        }
        if (pauseButtonLandScape != null) {
            requestPlayPauseFocus |= !playing && pauseButtonLandScape.isFocused();
            pauseButtonLandScape.setVisibility(!playing ? View.GONE : View.VISIBLE);
        }


        if (requestPlayPauseFocus) {
            requestPlayPauseFocus();
        }
    }

    private void updateRepeatModeButton() {
        if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) {
            return;
        }
        if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
            repeatToggleButton.setVisibility(View.GONE);
            return;
        }
        if (player == null) {
            setButtonEnabled(false, repeatToggleButton);
            return;
        }
        setButtonEnabled(true, repeatToggleButton);
        switch (player.getRepeatMode()) {
            case Player.REPEAT_MODE_OFF:
                repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);
                repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);
                break;
            case Player.REPEAT_MODE_ONE:
                repeatToggleButton.setImageDrawable(repeatOneButtonDrawable);
                repeatToggleButton.setContentDescription(repeatOneButtonContentDescription);
                break;
            case Player.REPEAT_MODE_ALL:
                repeatToggleButton.setImageDrawable(repeatAllButtonDrawable);
                repeatToggleButton.setContentDescription(repeatAllButtonContentDescription);
                break;
        }
        repeatToggleButton.setVisibility(View.VISIBLE);
    }

    private void updateShuffleButton() {
        if (!isVisible() || !isAttachedToWindow || shuffleButton == null) {
            return;
        }
        if (!showShuffleButton) {
            shuffleButton.setVisibility(View.GONE);
        } else if (player == null) {
            setButtonEnabled(false, shuffleButton);
        } else {
            shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f);
            shuffleButton.setEnabled(true);
            shuffleButton.setVisibility(View.VISIBLE);
        }
    }

    private void updateTimeBarMode() {
        if (player == null) {
            return;
        }
        multiWindowTimeBar = showMultiWindowTimeBar
                && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window);
    }

    private void updateProgress() {
        if (!isVisible() || !isAttachedToWindow) {
            return;
        }

        long position = 0;
        long bufferedPosition = 0;
        long duration = 0;
        if (player != null) {
            long currentWindowTimeBarOffsetUs = 0;
            long durationUs = 0;
            int adGroupCount = 0;
            Timeline timeline = player.getCurrentTimeline();
            if (!timeline.isEmpty()) {
                int currentWindowIndex = player.getCurrentWindowIndex();
                int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex;
                int lastWindowIndex =
                        multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex;
                for (int i = firstWindowIndex; i <= lastWindowIndex; i++) {
                    if (i == currentWindowIndex) {
                        currentWindowTimeBarOffsetUs = durationUs;
                    }
                    timeline.getWindow(i, window);
                    if (window.durationUs == C.TIME_UNSET) {
                        Assertions.checkState(!multiWindowTimeBar);
                        break;
                    }
                    for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) {
                        timeline.getPeriod(j, period);
                        int periodAdGroupCount = period.getAdGroupCount();
                        for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) {
                            long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex);
                            if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) {
                                if (period.durationUs == C.TIME_UNSET) {
                                    // Don't show ad markers for postrolls in periods with unknown duration.
                                    continue;
                                }
                                adGroupTimeInPeriodUs = period.durationUs;
                            }
                            long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs();
                            if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) {
                                if (adGroupCount == adGroupTimesMs.length) {
                                    int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2;
                                    adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength);
                                    playedAdGroups = Arrays.copyOf(playedAdGroups, newLength);
                                }
                                adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs);
                                playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex);
                                adGroupCount++;
                            }
                        }
                    }
                    durationUs += window.durationUs;
                }
            }
            duration = C.usToMs(durationUs);
            position = C.usToMs(currentWindowTimeBarOffsetUs);
            bufferedPosition = position;
            if (player.isPlayingAd()) {
                position += player.getContentPosition();
                bufferedPosition = position;
            } else {
                position += player.getCurrentPosition();
                bufferedPosition += player.getBufferedPosition();
            }
            if (timeBar != null) {
                int extraAdGroupCount = extraAdGroupTimesMs.length;
                int totalAdGroupCount = adGroupCount + extraAdGroupCount;
                if (totalAdGroupCount > adGroupTimesMs.length) {
                    adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount);
                    playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount);
                }
                System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount);
                System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount);
                timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount);
            }
        }
        if (durationView != null && !isHls) {
            durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration));
        }

        if (durationViewLandscape != null && !isHls) {
            String positionStr = Util.getStringForTime(formatBuilder, formatter, position);
            String durationStr = Util.getStringForTime(formatBuilder, formatter, duration);
            durationViewLandscape.setText(positionStr.concat("/").concat(durationStr));
        }

        if (positionView != null && !scrubbing && !isHls) {
            positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
        }


        if (timeBar != null && !isHls) {
            timeBar.setPosition(position);
            timeBar.setBufferedPosition(bufferedPosition);
            timeBar.setDuration(duration);
        }

        if (timeBarLandscape != null && !isHls) {
            timeBarLandscape.setPosition(position);
            timeBarLandscape.setBufferedPosition(bufferedPosition);
            timeBarLandscape.setDuration(duration);
        }


        // Cancel any pending updates and schedule a new one if necessary.
        removeCallbacks(updateProgressAction);
        int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState();
        if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) {
            long delayMs;
            if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) {
                float playbackSpeed = player.getPlaybackParameters().speed;
                if (playbackSpeed <= 0.1f) {
                    delayMs = 1000;
                } else if (playbackSpeed <= 5f) {
                    long mediaTimeUpdatePeriodMs = 1000 / Math.max(1, Math.round(1 / playbackSpeed));
                    long mediaTimeDelayMs = mediaTimeUpdatePeriodMs - (position % mediaTimeUpdatePeriodMs);
                    if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) {
                        mediaTimeDelayMs += mediaTimeUpdatePeriodMs;
                    }
                    delayMs = playbackSpeed == 1 ? mediaTimeDelayMs
                            : (long) (mediaTimeDelayMs / playbackSpeed);
                } else {
                    delayMs = 200;
                }
            } else {
                delayMs = 1000;
            }
            postDelayed(updateProgressAction, delayMs);
        }
    }

    private void requestPlayPauseFocus() {
        boolean playing = player != null && player.getPlayWhenReady();
        if (!playing && playButton != null) {
            playButton.requestFocus();
        } else if (playing && pauseButton != null) {
            pauseButton.requestFocus();

        }

        if (!playing && playButtonLandScape != null) {
            playButtonLandScape.requestFocus();
        } else if (playing && pauseButtonLandScape != null) {
            pauseButtonLandScape.requestFocus();

        }
    }

    private void setButtonEnabled(boolean enabled, View view) {
        if (view == null) {
            return;
        }
        view.setEnabled(enabled);
        view.setAlpha(enabled ? 1f : 0.3f);
        view.setVisibility(VISIBLE);
    }

    private void previous() {
        Timeline timeline = player.getCurrentTimeline();
        if (timeline.isEmpty()) {
            return;
        }
        int windowIndex = player.getCurrentWindowIndex();
        timeline.getWindow(windowIndex, window);
        int previousWindowIndex = player.getPreviousWindowIndex();
        if (previousWindowIndex != C.INDEX_UNSET
                && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
                || (window.isDynamic && !window.isSeekable))) {
            seekTo(previousWindowIndex, C.TIME_UNSET);
        } else {
            seekTo(0);
        }
    }

    private void next() {
        Timeline timeline = player.getCurrentTimeline();
        if (timeline.isEmpty()) {
            return;
        }
        int windowIndex = player.getCurrentWindowIndex();
        int nextWindowIndex = player.getNextWindowIndex();
        if (nextWindowIndex != C.INDEX_UNSET) {
            seekTo(nextWindowIndex, C.TIME_UNSET);
        } else if (timeline.getWindow(windowIndex, window, false).isDynamic) {
            seekTo(windowIndex, C.TIME_UNSET);
        }
    }

    private void rewind() {
        if (rewindMs <= 0) {
            return;
        }
        seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0));
    }

    private void fastForward() {
        if (fastForwardMs <= 0) {
            return;
        }
        long durationMs = player.getDuration();
        long seekPositionMs = player.getCurrentPosition() + fastForwardMs;
        if (durationMs != C.TIME_UNSET) {
            seekPositionMs = Math.min(seekPositionMs, durationMs);
        }
        seekTo(seekPositionMs);
    }

    private void seekTo(long positionMs) {
        seekTo(player.getCurrentWindowIndex(), positionMs);
    }

    private void seekTo(int windowIndex, long positionMs) {
        boolean dispatched = controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
        if (!dispatched) {
            // The seek wasn't dispatched. If the progress bar was dragged by the user to perform the
            // seek then it'll now be in the wrong position. Trigger a progress update to snap it back.
            updateProgress();
        }
    }

    private void seekToTimeBarPosition(long positionMs) {
        int windowIndex;
        Timeline timeline = player.getCurrentTimeline();
        if (multiWindowTimeBar && !timeline.isEmpty()) {
            int windowCount = timeline.getWindowCount();
            windowIndex = 0;
            while (true) {
                long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs();
                if (positionMs < windowDurationMs) {
                    break;
                } else if (windowIndex == windowCount - 1) {
                    // Seeking past the end of the last window should seek to the end of the timeline.
                    positionMs = windowDurationMs;
                    break;
                }
                positionMs -= windowDurationMs;
                windowIndex++;
            }
        } else {
            windowIndex = player.getCurrentWindowIndex();
        }
        seekTo(windowIndex, positionMs);
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        sensorOrientation.enable();
        isAttachedToWindow = true;
        if (hideAtMs != C.TIME_UNSET) {
            long delayMs = hideAtMs - SystemClock.uptimeMillis();
            if (delayMs <= 0) {
                hide();
            } else {
                postDelayed(hideAction, delayMs);
            }
        }
        updateAll();
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        sensorOrientation.disable();
        isAttachedToWindow = false;
        removeCallbacks(updateProgressAction);
        removeCallbacks(hideAction);
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
    }

    /**
     * Called to process media key events. Any {@link KeyEvent} can be passed but only media key
     * events will be handled.
     *
     * @param event A key event.
     * @return Whether the key event was handled.
     */
    public boolean dispatchMediaKeyEvent(KeyEvent event) {
        int keyCode = event.getKeyCode();
        if (player == null || !isHandledMediaKey(keyCode)) {
            return false;
        }
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
                fastForward();
            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
                rewind();
            } else if (event.getRepeatCount() == 0) {
                switch (keyCode) {
                    case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
                        controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
                        break;
                    case KeyEvent.KEYCODE_MEDIA_PLAY:
                        controlDispatcher.dispatchSetPlayWhenReady(player, true);
                        break;
                    case KeyEvent.KEYCODE_MEDIA_PAUSE:
                        controlDispatcher.dispatchSetPlayWhenReady(player, false);
                        break;
                    case KeyEvent.KEYCODE_MEDIA_NEXT:
                        next();
                        break;
                    case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
                        previous();
                        break;
                    default:
                        break;
                }
            }
        }
        return true;
    }


    public void setBackListener(ExoClickListener backListener) {
        this.backListener = backListener;
    }


    public void setPortrait(boolean portrait) {
        this.portrait = portrait;
        showControllerByDisplayMode();
    }

    public boolean isPortrait() {
        return portrait;
    }

    private void toggleControllerOrientation() {
        if (orientationListener == null) {
            setPortrait(!portrait);
        } else {
            changeOrientation(portrait ? SENSOR_LANDSCAPE : SENSOR_PORTRAIT);
        }

    }


    public void setOrientationListener(OrientationListener orientationListener) {
        this.orientationListener = orientationListener;
    }


    public void setMediaSource(ExoMediaSource exoMediaSource) {
        if (exoPlayerVideoName != null) {
            exoPlayerVideoName.setText(exoMediaSource.name());
        }

        if (exoPlayerVideoNameLandscape != null) {
            exoPlayerVideoNameLandscape.setText(exoMediaSource.name());
        }

        if (centerError != null) {
            centerError.setText(null);
            centerError.setVisibility(GONE);
        }
    }


    public void setControllerDisplayMode(int displayMode) {
        this.displayMode = displayMode;
        showControllerByDisplayMode();
    }

    private void showControllerByDisplayMode() {

        if (exoPlayerControllerTop != null) {
            boolean showByMode = (displayMode & CONTROLLER_MODE_TOP) == CONTROLLER_MODE_TOP;
            if (portrait) {
                int visibility = showByMode ? VISIBLE : INVISIBLE;
                exoPlayerControllerTop.setVisibility(visibility);
            } else {
                exoPlayerControllerTop.setVisibility(INVISIBLE);
            }

        }

        if (exoPlayerControllerTopLandscape != null) {
            boolean showByMode = (displayMode & CONTROLLER_MODE_TOP_LANDSCAPE) == CONTROLLER_MODE_TOP_LANDSCAPE;
            if (portrait) {
                exoPlayerControllerTopLandscape.setVisibility(INVISIBLE);
            } else {
                int visibility = showByMode ? VISIBLE : INVISIBLE;
                exoPlayerControllerTopLandscape.setVisibility(visibility);
            }
        }

        if (exoPlayerControllerBottom != null) {
            boolean showByMode = (displayMode & CONTROLLER_MODE_BOTTOM) == CONTROLLER_MODE_BOTTOM;
            if (portrait) {
                int visibility = showByMode ? VISIBLE : INVISIBLE;
                exoPlayerControllerBottom.setVisibility(visibility);
            } else {

                exoPlayerControllerBottom.setVisibility(INVISIBLE);
            }
        }

        if (exoPlayerControllerBottomLandscape != null) {
            boolean showByMode = (displayMode & CONTROLLER_MODE_BOTTOM_LANDSCAPE) == CONTROLLER_MODE_BOTTOM_LANDSCAPE;
            if (portrait) {
                exoPlayerControllerBottomLandscape.setVisibility(INVISIBLE);
            } else {
                int visibility = showByMode ? VISIBLE : INVISIBLE;
                exoPlayerControllerBottomLandscape.setVisibility(visibility);
            }
        }


        if (qualityVisibilityCallback != null) {
            qualityVisibilityCallback.shouldChangeVisibility(GONE);
        }
    }

    private synchronized void changeOrientation(@OnOrientationChangedListener.SensorOrientationType int orientation) {
        Context context = getContext();
        Activity activity;
        if (!(context instanceof Activity)) {
            return;
        }

        if (orientationListener == null) {
            return;
        }


        activity = (Activity) context;
        switch (orientation) {
            case SENSOR_PORTRAIT:
                setPortrait(true);
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
                changeSystemUiVisibilityPortrait();
                break;
            case SENSOR_LANDSCAPE:
                setPortrait(false);
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
                changeSystemUiVisibilityLandscape();
                break;
            case SENSOR_UNKNOWN:
            default:
                break;
        }

        orientationListener.onOrientationChanged(orientation);
    }


    private void changeSystemUiVisibilityPortrait() {
        videoViewAccessor.attachVideoView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
    }

    private void changeSystemUiVisibilityLandscape() {
        WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        if (windowManager == null) {
            return;
        }

        int flag = View.SYSTEM_UI_FLAG_LOW_PROFILE
                | View.SYSTEM_UI_FLAG_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            flag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
        }

        videoViewAccessor.attachVideoView().setSystemUiVisibility(flag);
    }


    public void setVideoViewAccessor(VideoViewAccessor videoViewAccessor) {
        this.videoViewAccessor = videoViewAccessor;
    }

    public void setVisibilityCallback(MultiQualitySelectorAdapter.VisibilityCallback qualityVisibilityCallback) {
        this.qualityVisibilityCallback = qualityVisibilityCallback;
    }

    public void updateQualityDes(CharSequence qualityDes) {
        if (exoPlayerCurrentQualityLandscape != null) {
            exoPlayerCurrentQualityLandscape.setText(qualityDes);
        }
    }



    /**
     * add your view to controller
     *
     * @param customViewType the target view type
     * @param customView     the view you want to add
     * @param removeViews    remove all views in target view before add  if true
     **/
    public void addCustomView(@CustomViewType int customViewType, View customView, boolean removeViews) {
        ViewGroup viewGroup = null;
        if (customViewType == CUSTOM_VIEW_TOP && topCustomView != null) {
            viewGroup = topCustomView;
        } else if (customViewType == CUSTOM_VIEW_TOP_LANDSCAPE && topCustomView != null) {
            viewGroup = topCustomViewLandscape;
        } else if (customViewType == CUSTOM_VIEW_BOTTOM_LANDSCAPE && topCustomView != null) {
            viewGroup = bottomCustomViewLandscape;
        }

        if (viewGroup != null) {
            if (removeViews) {
                viewGroup.removeAllViews();
            }
            viewGroup.addView(customView);
        }

    }

    public void addCustomView(@CustomViewType int customViewType, View customView) {
        addCustomView(customViewType, customView, false);
    }

    public void changeWidgetVisibility(int id,int visibility){
       View view = findViewById(id);
       if(view != null){
           view.setVisibility(visibility);
       }
    }
    private void showLoading(boolean isLoading) {
        if(loadingBar == null ){
            return;
        }
        if (isLoading) {
            loadingBar.setVisibility(View.VISIBLE);
        } else {
            loadingBar.setVisibility(GONE);
        }
    }

    public void showUtilHideCalled() {
        if (!isVisible()) {
            setVisibility(VISIBLE);
            if (visibilityListener != null) {
                visibilityListener.onVisibilityChange(getVisibility());
            }
            updateAll();
        }
    }

    public void setGestureEnabled(boolean enabled){
        if(centerInfoWrapper == null){
            return;
        }

        if (videoGesture == null) {
            return;
        }

        if(enabled){
            videoGesture.enable();
        }else {
            videoGesture.disable();
        }
    }


    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
            if (backListener != null) {
                if (!backListener.onClick(null, portrait)) {
                    if (portrait) {
                        return super.onKeyDown(keyCode, event);
                    } else {
                        changeOrientation(SENSOR_PORTRAIT);
                        return true;
                    }

                }
            }
        }
        return super.onKeyDown(keyCode, event);
    }

    @SuppressLint("InlinedApi")
    private static boolean isHandledMediaKey(int keyCode) {
        return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
                || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
                || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
                || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
                || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS;
    }

    /**
     * Returns whether the specified {@code timeline} can be shown on a multi-window time bar.
     *
     * @param timeline The {@link Timeline} to check.
     * @param window   A scratch {@link Timeline.Window} instance.
     * @return Whether the specified timeline can be shown on a multi-window time bar.
     */
    private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) {
        if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) {
            return false;
        }
        int windowCount = timeline.getWindowCount();
        for (int i = 0; i < windowCount; i++) {
            if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) {
                return false;
            }
        }
        return true;
    }

    private final class ComponentListener  implements
            TimeBar.OnScrubListener, OnClickListener,Player.EventListener {

        @Override
        public void onScrubStart(TimeBar timeBar, long position) {
            removeCallbacks(hideAction);
            scrubbing = true;
        }

        @Override
        public void onScrubMove(TimeBar timeBar, long position) {
            if (positionView != null) {
                positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
            }
        }

        @Override
        public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
            scrubbing = false;
            if (!canceled && player != null) {
                seekToTimeBarPosition(position);
            }
            hideAfterTimeout();
        }

        @Override
        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {

            if (playbackState != Player.STATE_IDLE && centerError != null && centerError.getVisibility() == VISIBLE) {
                centerError.setVisibility(GONE);
            }

            if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_BUFFERING) {
                removeCallbacks(hideAction);
                showUtilHideCalled();
                showLoading(true);
            } else if (playbackState == Player.STATE_READY && player.getPlayWhenReady() || playbackState == Player.STATE_ENDED) {
                showLoading(false);
                hide();
            }

            updatePlayPauseButton();
            updateProgress();
        }

        @Override
        public void onRepeatModeChanged(int repeatMode) {
            updateRepeatModeButton();
            updateNavigation();
        }

        @Override
        public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
            updateShuffleButton();
            updateNavigation();
        }

        @Override
        public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
            updateNavigation();
            updateProgress();
        }

        @Override
        public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
            if (manifest instanceof HlsManifest) {
                HlsManifest hlsManifest = (HlsManifest) manifest;
                isHls = !hlsManifest.mediaPlaylist.hasEndTag && hlsManifest.mediaPlaylist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
            } else {
                isHls = false;
            }


            updateNavigation();
            updateTimeBarMode();
            updateProgress();
        }


        @Override
        public void onPlayerError(ExoPlaybackException error) {
            if(loadingBar  != null){
                loadingBar.setVisibility(GONE);
            }
            if (centerError != null) {
                String errorText = getResources().getString(R.string.player_error, error.type);
                centerError.setText(errorText);
                centerError.setVisibility(VISIBLE);
            }
        }

        @Override
        public void onClick(View view) {
            if (player != null) {
                if (nextButton == view) {
                    next();
                } else if (previousButton == view) {
                    previous();
                } else if (fastForwardButton == view) {
                    fastForward();
                } else if (rewindButton == view) {
                    rewind();
                } else if (playButton == view || playButtonLandScape == view) {
                    controlDispatcher.dispatchSetPlayWhenReady(player, true);
                } else if (pauseButton == view || pauseButtonLandScape == view) {
                    controlDispatcher.dispatchSetPlayWhenReady(player, false);
                } else if (repeatToggleButton == view) {
                    controlDispatcher.dispatchSetRepeatMode(player, RepeatModeUtil.getNextRepeatMode(
                            player.getRepeatMode(), repeatToggleModes));
                } else if (shuffleButton == view) {
                    controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled());
                } else if (enterFullscreen == view) {
                    changeOrientation(SENSOR_LANDSCAPE);
                } else if (exitFullscreen == view) {
                    changeOrientation(SENSOR_PORTRAIT);
                } else if (exoPlayerVideoName == view || back == view) {
                    if (backListener != null) {
                        if (!backListener.onClick(view, portrait)) {
                            changeOrientation(SENSOR_LANDSCAPE);
                        }
                    }
                } else if (exoPlayerVideoNameLandscape == view || backLandscape == view) {
                    if (backListener != null) {
                        if (!backListener.onClick(view, portrait)) {
                            changeOrientation(SENSOR_PORTRAIT);
                        }
                    }
                } else if (centerInfoWrapper == view) {
                    playOrPause();
                } else if (exoPlayerCurrentQualityLandscape == view && qualityVisibilityCallback != null) {
                    hide();
                    qualityVisibilityCallback.shouldChangeVisibility(View.VISIBLE);
                }
            }
            hideAfterTimeout();
        }

        long[] mHits = new long[2];

        private void playOrPause() {

            System.arraycopy(mHits, 1, mHits, 0, mHits.length - 1);
            mHits[mHits.length - 1] = SystemClock.uptimeMillis();

            if (500 > (SystemClock.uptimeMillis() - mHits[0])) {
                controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
            }

        }


    }


}


================================================
FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/ui/ExoVideoView.java
================================================
package com.jarvanmo.exoplayerview.ui;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.PlaybackControlView;
import com.google.android.exoplayer2.ui.SubtitleView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util;
import com.jarvanmo.exoplayerview.R;
import com.jarvanmo.exoplayerview.extension.MultiQualitySelectorAdapter;
import com.jarvanmo.exoplayerview.media.ExoMediaSource;
import com.jarvanmo.exoplayerview.media.MediaSourceCreator;

import java.util.List;

import static android.content.Context.AUDIO_SERVICE;

/**
 * Created by mo on 16-11-7.
 *
 * @author mo
 */

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public class ExoVideoView extends FrameLayout implements ExoVideoPlaybackControlView.VideoViewAccessor {


    private static final int SURFACE_TYPE_NONE = 0;
    private static final int SURFACE_TYPE_SURFACE_VIEW = 1;
    private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;

    private final AspectRatioFrameLayout contentFrame;
    private final View shutterView;
    private final View surfaceView;
    private final ImageView artworkView;
    private final SubtitleView subtitleView;
    private final ExoVideoPlaybackControlView controller;
    private final ComponentListener componentListener;
    private final FrameLayout overlayFrameLayout;


    private SimpleExoPlayer player;
    private boolean useController;
    private boolean useArtwork;
    private Bitmap defaultArtwork;
    private int controllerShowTimeoutMs;
    private boolean controllerAutoShow;
    private boolean controllerHideOnTouch;


    private boolean pausedFromPlayer = false;

    private boolean enableMultiQuality = true;

    private MultiQualitySelectorAdapter.MultiQualitySelectorNavigator multiQualitySelectorNavigator;

    private final AudioManager audioManager;
    private AudioManager.OnAudioFocusChangeListener afChangeListener = new AudioManager.OnAudioFocusChangeListener() {
        public void onAudioFocusChange(int focusChange) {
            if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
                // Pause playback
                pause();
            } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                // Resume playback
                resume();
            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
                // audioManager.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
                audioManager.abandonAudioFocus(afChangeListener);
                // Stop playback
                stop();

            }
        }
    };

    private long lastPlayedPosition = 0L;
    private long[] mHits = new long[2];

    private int controllerBackgroundId = 0;

    public ExoVideoView(Context context) {
        this(context, null);
    }

    public ExoVideoView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ExoVideoView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        audioManager = (AudioManager) context.getApplicationContext().getSystemService(AUDIO_SERVICE);


        if (isInEditMode()) {
            contentFrame = null;
            shutterView = null;
            surfaceView = null;
            artworkView = null;
            subtitleView = null;
            controller = null;
            componentListener = null;
            overlayFrameLayout = null;
            ImageView logo = new ImageView(context);
            if (Util.SDK_INT >= 23) {
                configureEditModeLogoV23(getResources(), logo);
            } else {
                configureEditModeLogo(getResources(), logo);
            }
            addView(logo);
            return;
        }

        boolean shutterColorSet = false;
        int shutterColor = 0;
        int playerLayoutId = R.layout.exo_video_view;
        boolean useArtwork = true;
        int defaultArtworkId = 0;
        boolean useController = true;
        int surfaceType = SURFACE_TYPE_SURFACE_VIEW;
        int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
        int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS;
        boolean controllerHideOnTouch = true;
        boolean controllerAutoShow = true;
        if (attrs != null) {
            TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
                    R.styleable.ExoVideoView, 0, 0);
            try {
                shutterColorSet = a.hasValue(R.styleable.ExoVideoView_shutter_background_color);
                shutterColor = a.getColor(R.styleable.ExoVideoView_shutter_background_color,
                        shutterColor);
                playerLayoutId = a.getResourceId(R.styleable.ExoVideoView_player_layout_id,
                        playerLayoutId);
                useArtwork = a.getBoolean(R.styleable.ExoVideoView_use_artwork, useArtwork);
                defaultArtworkId = a.getResourceId(R.styleable.ExoVideoView_default_artwork,
                        defaultArtworkId);
                useController = a.getBoolean(R.styleable.ExoVideoView_use_controller, useController);
                surfaceType = a.getInt(R.styleable.ExoVideoView_surface_type, surfaceType);
                resizeMode = a.getInt(R.styleable.ExoVideoView_resize_mode, resizeMode);
                controllerShowTimeoutMs = a.getInt(R.styleable.ExoVideoView_show_timeout,
                        controllerShowTimeoutMs);
                controllerHideOnTouch = a.getBoolean(R.styleable.ExoVideoView_hide_on_touch,
                        controllerHideOnTouch);
                controllerAutoShow = a.getBoolean(R.styleable.ExoVideoView_auto_show,
                        controllerAutoShow);
                enableMultiQuality = a.getBoolean(R.styleable.ExoVideoView_enable_multi_quality, true);

                controllerBackgroundId = a.getResourceId(R.styleable.ExoVideoView_controller_background, 0);
            } finally {
                a.recycle();
            }
        }

        LayoutInflater.from(context).inflate(playerLayoutId, this);
        componentListener = new ComponentListener();
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);

        // Content frame.
        contentFrame = findViewById(R.id.exo_player_content_frame);
        if (contentFrame != null) {
            setResizeModeRaw(contentFrame, resizeMode);
        }

        // Shutter view.
        shutterView = findViewById(R.id.exo_player_shutter);
        if (shutterView != null && shutterColorSet) {
            shutterView.setBackgroundColor(shutterColor);
        }

        // Create a surface view and insert it into the content frame, if there is one.
        if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) {
            ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            surfaceView = surfaceType == SURFACE_TYPE_TEXTURE_VIEW ? new TextureView(context)
                    : new SurfaceView(context);
            surfaceView.setLayoutParams(params);
            contentFrame.addView(surfaceView, 0);
        } else {
            surfaceView = null;
        }

        // Overlay frame layout.
        overlayFrameLayout = findViewById(R.id.exo_player_overlay);

        // Artwork view.
        artworkView = findViewById(R.id.exo_player_artwork);
        this.useArtwork = useArtwork && artworkView != null;
        if (defaultArtworkId != 0) {
            defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId);
            setArtworkFromBitmap(defaultArtwork);
        }

        // Subtitle view.
        subtitleView = findViewById(R.id.exo_player_subtitles);
        if (subtitleView != null) {
            subtitleView.setUserDefaultStyle();
            subtitleView.setUserDefaultTextSize();
        }


        // Playback control view.
        ExoVideoPlaybackControlView customController = findViewById(R.id.exo_player_controller);
        View controllerPlaceholder = findViewById(R.id.exo_player_controller_placeholder);
        if (customController != null) {
            this.controller = customController;
        } else if (controllerPlaceholder != null) {
            // Propagate attrs as playbackAttrs so that PlaybackControlView's custom attributes are
            // transferred, but standard FrameLayout attributes (e.g. background) are not.
            this.controller = new ExoVideoPlaybackControlView(context, null, 0, attrs);
            controller.setLayoutParams(controllerPlaceholder.getLayoutParams());
            ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());
            int controllerIndex = parent.indexOfChild(controllerPlaceholder);
            parent.removeView(controllerPlaceholder);
            parent.addView(controller, controllerIndex);
        } else {
            this.controller = null;
        }
        this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0;
        this.controllerHideOnTouch = controllerHideOnTouch;
        this.controllerAutoShow = controllerAutoShow;
        this.useController = useController && controller != null;


        if (useController && controller != null) {
            controller.show();
            controller.setVideoViewAccessor(this);
        }
        setKeepScreenOn(true);
    }

    /**
     * Switches the view targeted by a given {@link SimpleExoPlayer}.
     *
     * @param player        The player whose target view is being switched.
     * @param oldPlayerView The old view to detach from the player.
     * @param newPlayerView The new view to attach to the player.
     */
    public static void switchTargetView(@NonNull SimpleExoPlayer player,
                                        @Nullable ExoVideoView oldPlayerView, @Nullable ExoVideoView newPlayerView) {
        if (oldPlayerView == newPlayerView) {
            return;
        }
        // We attach the new view before detaching the old one because this ordering allows the player
        // to swap directly from one surface to another, without transitioning through a state where no
        // surface is attached. This is significantly more efficient and achieves a more seamless
        // transition when using platform provided video decoders.
        if (newPlayerView != null) {
            newPlayerView.setPlayer(player);
        }
        if (oldPlayerView != null) {
            oldPlayerView.setPlayer(null);
        }
    }

    /**
     * Returns the player currently set on this view, or null if no player is set.
     */
    public SimpleExoPlayer getPlayer() {
        return player;
    }

    /**
     * Set the {@link SimpleExoPlayer} to use.
     * <p>
     * To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended to
     * use {@link #switchTargetView(SimpleExoPlayer, ExoVideoView, ExoVideoView)} rather
     * than this method. If you do wish to use this method directly, be sure to attach the player to
     * the new view <em>before</em> calling {@code setPlayer(null)} to detach it from the old one.
     * This ordering is significantly more efficient and may allow for more seamless transitions.
     *
     * @param player The {@link SimpleExoPlayer} to use.
     */
    public void setPlayer(SimpleExoPlayer player) {
        if (this.player == player) {
            return;
        }
        if (this.player != null) {
            this.player.removeListener(componentListener);
            this.player.removeTextOutput(componentListener);
            this.player.removeVideoListener(componentListener);
            if (surfaceView instanceof TextureView) {
                this.player.clearVideoTextureView((TextureView) surfaceView);
            } else if (surfaceView instanceof SurfaceView) {
                this.player.clearVideoSurfaceView((SurfaceView) surfaceView);
            }
        }
        this.player = player;
        if (useController) {
            controller.setPlayer(player);
        }
        if (shutterView != null) {
            shutterView.setVisibility(VISIBLE);
        }
        if (player != null) {
            if (surfaceView instanceof TextureView) {
                player.setVideoTextureView((TextureView) surfaceView);
            } else if (surfaceView instanceof SurfaceView) {
                player.setVideoSurfaceView((SurfaceView) surfaceView);
            }
            player.addVideoListener(componentListener);
            player.addTextOutput(componentListener);
            player.addListener(componentListener);
            maybeShowController(false);
            updateForCurrentTrackSelections();
        } else {
            hideController();
            hideArtwork();
        }
    }

    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(visibility);
        if (surfaceView instanceof SurfaceView) {
            // Work around https://github.com/google/ExoPlayer/issues/3160.
            surfaceView.setVisibility(visibility);
        }
    }

    /**
     * Sets the resize mode.
     *
     * @param resizeMode The resize mode.
     */
    public void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
        Assertions.checkState(contentFrame != null);
        contentFrame.setResizeMode(resizeMode);
    }

    /**
     * Returns whether artwork is displayed if present in the media.
     */
    public boolean getUseArtwork() {
        return useArtwork;
    }

    /**
     * Sets whether artwork is displayed if present in the media.
     *
     * @param useArtwork Whether artwork is displayed.
     */
    public void setUseArtwork(boolean useArtwork) {
        Assertions.checkState(!useArtwork || artworkView != null);
        if (this.useArtwork != useArtwork) {
            this.useArtwork = useArtwork;
            updateForCurrentTrackSelections();
        }
    }

    /**
     * Returns the default artwork to display.
     */
    public Bitmap getDefaultArtwork() {
        return defaultArtwork;
    }

    /**
     * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is
     * present in the media.
     *
     * @param defaultArtwork the default artwork to display.
     */
    public void setDefaultArtwork(Bitmap defaultArtwork) {
        if (this.defaultArtwork != defaultArtwork) {
            this.defaultArtwork = defaultArtwork;
            updateForCurrentTrackSelections();
        }
    }

    /**
     * Returns whether the playback controls can be shown.
     */
    public boolean getUseController() {
        return useController;
    }

    /**
     * Sets whether the playback controls can be shown. If set to {@code false} the playback controls
     * are never visible and are disconnected from the player.
     *
     * @param useController Whether the playback controls can be shown.
     */
    public void setUseController(boolean useController) {
        Assertions.checkState(!useController || controller != null);
        if (this.useController == useController) {
            return;
        }
        this.useController = useController;
        if (useController) {
            controller.setPlayer(player);
        } else if (controller != null) {
            controller.hide();
            controller.setPlayer(null);
        }
    }

    /**
     * Sets the background color of the {@code exo_shutter} view.
     *
     * @param color The background color.
     */
    public void setShutterBackgroundColor(int color) {
        if (shutterView != null) {
            shutterView.setBackgroundColor(color);
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (audioManager != null) {
            requestAudioFocus();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (audioManager != null) {
            audioManager.abandonAudioFocus(afChangeListener);
        }
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (player != null && player.isPlayingAd()) {
            // Focus any overlay UI now, in case it's provided by a WebView whose contents may update
            // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using
            // IMA [Internal: b/62371030].
            overlayFrameLayout.requestFocus();
            return super.dispatchKeyEvent(event);
        }
        boolean isDpadWhenControlHidden = isDpadKey(event.getKeyCode()) && useController
                && !controller.isVisible();
        maybeShowController(true);
        return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
    }

    /**
     * Called to process media key events. Any {@link KeyEvent} can be passed but only media key
     * events will be handled. Does nothing if playback controls are disabled.
     *
     * @param event A key event.
     * @return Whether the key event was handled.
     */
    public boolean dispatchMediaKeyEvent(KeyEvent event) {
        return useController && controller.dispatchMediaKeyEvent(event);
    }

    /**
     * Shows the playback controls. Does nothing if playback controls are disabled.
     * <p>
     * <p>The playback controls are automatically hidden during playback after
     * {{@link #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not
     * started yet, is paused, has ended or failed.
     */
    public void showController() {
        showController(shouldShowControllerIndefinitely());
    }

    /**
     * Hides the playback controls. Does nothing if playback controls are disabled.
     */
    public void hideController() {
        if (controller != null) {
            controller.hide();
        }
    }

    /**
     * Returns the playback controls timeout. The playback controls are automatically hidden after
     * this duration of time has elapsed without user input and with playback or buffering in
     * progress.
     *
     * @return The timeout in milliseconds. A non-positive value will cause the controller to remain
     * visible indefinitely.
     */
    public int getControllerShowTimeoutMs() {
        return controllerShowTimeoutMs;
    }

    /**
     * Sets the playback controls timeout. The playback controls are automatically hidden after this
     * duration of time has elapsed without user input and with playback or buffering in progress.
     *
     * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause
     *                                the controller to remain visible indefinitely.
     */
    public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) {
        Assertions.checkState(controller != null);
        this.controllerShowTimeoutMs = controllerShowTimeoutMs;
    }

    /**
     * Returns whether the playback controls are hidden by touch events.
     */
    public boolean getControllerHideOnTouch() {
        return controllerHideOnTouch;
    }

    /**
     * Sets whether the playback controls are hidden by touch events.
     *
     * @param controllerHideOnTouch Whether the playback controls are hidden by touch events.
     */
    public void setControllerHideOnTouch(boolean controllerHideOnTouch) {
        Assertions.checkState(controller != null);
        this.controllerHideOnTouch = controllerHideOnTouch;
    }

    /**
     * Returns whether the playback controls are automatically shown when playback starts, pauses,
     * ends, or fails. If set to false, the playback controls can be manually operated with {@link
     * #showController()} and {@link #hideController()}.
     */
    public boolean getControllerAutoShow() {
        return controllerAutoShow;
    }

    /**
     * Sets whether the playback controls are automatically shown when playback starts, pauses, ends,
     * or fails. If set to false, the playback controls can be manually operated with {@link
     * #showController()} and {@link #hideController()}.
     *
     * @param controllerAutoShow Whether the playback controls are allowed to show automatically.
     */
    public void setControllerAutoShow(boolean controllerAutoShow) {
        this.controllerAutoShow = controllerAutoShow;
    }

    /**
     * Set the {@link PlaybackControlView.VisibilityListener}.
     *
     * @param listener The listener to be notified about visibility changes.
     */
    public void setControllerVisibilityListener(ExoVideoPlaybackControlView.VisibilityListener listener) {
        Assertions.checkState(controller != null);
        controller.setVisibilityListener(listener);
    }

    /**
     * Sets the {@link ControlDispatcher}.
     *
     * @param controlDispatcher The {@link ControlDispatcher}, or null to use
     *                          {@link PlaybackControlView.DefaultControlDispatcher}.
     */
    public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) {
        Assertions.checkState(controller != null);
        controller.setControlDispatcher(controlDispatcher);
    }

    /**
     * Sets the rewind increment in milliseconds.
     *
     * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the
     *                 rewind button to be disabled.
     */
    public void setRewindIncrementMs(int rewindMs) {
        Assertions.checkState(controller != null);
        controller.setRewindIncrementMs(rewindMs);
    }

    /**
     * Sets the fast forward increment in milliseconds.
     *
     * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will
     *                      cause the fast forward button to be disabled.
     */
    public void setFastForwardIncrementMs(int fastForwardMs) {
        Assertions.checkState(controller != null);
        controller.setFastForwardIncrementMs(fastForwardMs);
    }

    /**
     * Sets which repeat toggle modes are enabled.
     *
     * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
     */
    public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
        Assertions.checkState(controller != null);
        controller.setRepeatToggleModes(repeatToggleModes);
    }

    /**
     * Sets whether the shuffle button is shown.
     *
     * @param showShuffleButton Whether the shuffle button is shown.
     */
    public void setShowShuffleButton(boolean showShuffleButton) {
        Assertions.checkState(controller != null);
        controller.setShowShuffleButton(showShuffleButton);
    }

    /**
     * Sets whether the time bar should show all windows, as opposed to just the current one.
     *
     * @param showMultiWindowTimeBar Whether to show all windows.
     */
    public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
        Assertions.checkState(controller != null);
        controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar);
    }

    /**
     * Gets the view onto which video is rendered. This is a:
     * <ul>
     * <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to
     * {@code surface_view}.</li>
     * <li>{@link TextureView} if {@code surface_type} is {@code texture_view}.</li>
     * <li>{@code null} if {@code surface_type} is {@code none}.</li>
     * </ul>
     *
     * @return The {@link SurfaceView}, {@link TextureView} or {@code null}.
     */
    public View getVideoSurfaceView() {
        return surfaceView;
    }

    /**
     * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of
     * the player.
     *
     * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and
     * the overlay is not present.
     */
    public FrameLayout getOverlayFrameLayout() {
        return overlayFrameLayout;
    }

    /**
     * Gets the {@link SubtitleView}.
     *
     * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the
     * subtitle view is not present.
     */
    public SubtitleView getSubtitleView() {
        return subtitleView;
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
            return false;
        }
        if (enableMultiQuality) {
            View v = overlayFrameLayout.findViewById(R.id.exo_player_quality_container);
            if (v != null && overlayFrameLayout.getVisibility() == VISIBLE) {
                return true;
            }

        }

        if (!controller.isVisible()) {
            maybeShowController(true);
        } else if (controllerHideOnTouch) {
            controller.hide();
        }
        return true;
    }

    @Override
    public boolean onTrackballEvent(MotionEvent ev) {
        if (!useController || player == null) {
            return false;
        }
        maybeShowController(true);
        return true;
    }

    /**
     * Shows the playback controls, but only if forced or shown indefinitely.
     */
    private void maybeShowController(boolean isForced) {
        if (isPlayingAd()) {
            // Never show the controller if an ad is currently playing.
            return;
        }
        if (useController) {
            boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0;
            boolean shouldShowIndefinitely = shouldShowControllerIndefinitely();
            if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) {
                showController(shouldShowIndefinitely);
            }
        }
    }

    private boolean shouldShowControllerIndefinitely() {
        if (player == null) {
            return true;
        }
        int playbackState = player.getPlaybackState();
        return controllerAutoShow && (playbackState == Player.STATE_IDLE
                || playbackState == Player.STATE_ENDED || !player.getPlayWhenReady());
    }

    private void showController(boolean showIndefinitely) {
        if (!useController) {
            return;
        }
        controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs);
        controller.show();
    }

    private boolean isPlayingAd() {
        return player != null && player.isPlayingAd() && player.getPlayWhenReady();
    }

    private void updateForCurrentTrackSelections() {
        if (player == null) {
            return;
        }
        TrackSelectionArray selections = player.getCurrentTrackSelections();
        for (int i = 0; i < selections.length; i++) {
            if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
                // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
                // onRenderedFirstFrame().
                hideArtwork();
                return;
            }
        }
        // Video disabled so the shutter must be closed.
        if (shutterView != null) {
            shutterView.setVisibility(VISIBLE);
        }
        // Display artwork if enabled and available, else hide it.
        if (useArtwork) {
            for (int i = 0; i < selections.length; i++) {
                TrackSelection selection = selections.get(i);
                if (selection != null) {
                    for (int j = 0; j < selection.length(); j++) {
                        Metadata metadata = selection.getFormat(j).metadata;
                        if (metadata != null && setArtworkFromMetadata(metadata)) {
                            return;
                        }
                    }
                }
            }
            if (setArtworkFromBitmap(defaultArtwork)) {
                return;
            }
        }
        // Artwork disabled or unavailable.
        hideArtwork();
    }

    private boolean setArtworkFromMetadata(Metadata metadata) {
        for (int i = 0; i < metadata.length(); i++) {
            Metadata.Entry metadataEntry = metadata.get(i);
            if (metadataEntry instanceof ApicFrame) {
                byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData;
                Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length);
                return setArtworkFromBitmap(bitmap);
            }
        }
        return false;
    }

    private boolean setArtworkFromBitmap(Bitmap bitmap) {
        if (bitmap != null) {
            int bitmapWidth = bitmap.getWidth();
            int bitmapHeight = bitmap.getHeight();
            if (bitmapWidth > 0 && bitmapHeight > 0) {
                if (contentFrame != null) {
                    contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight);
                }
                artworkView.setImageBitmap(bitmap);
                artworkView.setVisibility(VISIBLE);
                return true;
            }
        }
        return false;
    }

    private void hideArtwork() {
        if (artworkView != null) {
            artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference.
            artworkView.setVisibility(INVISIBLE);
        }
    }

    @TargetApi(23)
    private static void configureEditModeLogoV23(Resources resources, ImageView logo) {
        logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null));
        logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null));
    }

    @SuppressWarnings("deprecation")
    private static void con
Download .txt
gitextract_igumccg0/

├── .gitignore
├── LICENSE
├── README.md
├── README_CN.md
├── build.gradle
├── demo/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── jarvanmo/
│       │               └── demo/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── jarvanmo/
│       │   │           └── demo/
│       │   │               ├── MainActivity.java
│       │   │               └── SimpleVideoViewActivity.java
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── ic_arrow_back_white_24dp.xml
│       │       │   └── ic_error_white_24dp.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── activity_simple_video_view.xml
│       │       │   ├── cutom_view_bottom_landscape.xml
│       │       │   ├── cutom_view_top.xml
│       │       │   └── cutom_view_top_landscape.xml
│       │       └── values/
│       │           ├── colors.xml
│       │           ├── strings.xml
│       │           └── styles.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── jarvanmo/
│                       └── demo/
│                           └── ExampleUnitTest.java
├── exoplayerview/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── jarvanmo/
│       │               └── exoplayerview/
│       │                   └── ExampleInstrumentedTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── jarvanmo/
│       │   │           └── exoplayerview/
│       │   │               ├── ads/
│       │   │               │   └── ExoAdsLoader.java
│       │   │               ├── extension/
│       │   │               │   └── MultiQualitySelectorAdapter.java
│       │   │               ├── gesture/
│       │   │               │   ├── OnVideoGestureChangeListener.java
│       │   │               │   └── VideoGesture.java
│       │   │               ├── media/
│       │   │               │   ├── EventLogger.java
│       │   │               │   ├── ExoMediaSource.java
│       │   │               │   ├── MediaSourceCreator.java
│       │   │               │   ├── MediaSourceParams.java
│       │   │               │   ├── SimpleMediaSource.java
│       │   │               │   └── SimpleQuality.java
│       │   │               ├── orientation/
│       │   │               │   ├── OnOrientationChangedListener.java
│       │   │               │   └── SensorOrientation.java
│       │   │               ├── ui/
│       │   │               │   ├── ExoVideoPlaybackControlView.java
│       │   │               │   └── ExoVideoView.java
│       │   │               ├── util/
│       │   │               │   ├── AndroidUtil.java
│       │   │               │   └── Permissions.java
│       │   │               └── widget/
│       │   │                   ├── BatteryLevelView.java
│       │   │                   └── BatteryStatusView.java
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── ic_arrow_back_white_24dp.xml
│       │       │   ├── ic_error_outline_white_48dp.xml
│       │       │   ├── stat_sys_battery.xml
│       │       │   └── stat_sys_battery_charge.xml
│       │       ├── layout/
│       │       │   ├── exo_playback_controller_bottom.xml
│       │       │   ├── exo_playback_controller_bottom_landscape.xml
│       │       │   ├── exo_playback_controller_top.xml
│       │       │   ├── exo_playback_controller_top_landscape.xml
│       │       │   ├── exo_player_quality_selector.xml
│       │       │   ├── exo_video_playback_control_view.xml
│       │       │   ├── exo_video_view.xml
│       │       │   └── item_quality.xml
│       │       ├── values/
│       │       │   ├── attrs.xml
│       │       │   ├── colors.xml
│       │       │   ├── dimens.xml
│       │       │   ├── ids.xml
│       │       │   └── strings.xml
│       │       └── values-zh-rCN/
│       │           └── strings.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── jarvanmo/
│                       └── exoplayerview/
│                           └── ExampleUnitTest.java
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
Download .txt
SYMBOL INDEX (365 symbols across 24 files)

FILE: demo/src/androidTest/java/com/jarvanmo/demo/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 17) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 19) | @Test

FILE: demo/src/main/java/com/jarvanmo/demo/MainActivity.java
  class MainActivity (line 28) | public class MainActivity extends AppCompatActivity {
    method onCreate (line 41) | @Override
    method changeToPortrait (line 126) | private void changeToPortrait() {
    method changeToLandscape (line 137) | private void changeToLandscape() {
    method onStart (line 147) | @Override
    method onResume (line 155) | @Override
    method onPause (line 163) | @Override
    method onStop (line 171) | @Override
    method onDestroy (line 179) | @Override
    method onKeyDown (line 186) | @Override

FILE: demo/src/main/java/com/jarvanmo/demo/SimpleVideoViewActivity.java
  class SimpleVideoViewActivity (line 35) | public class SimpleVideoViewActivity extends AppCompatActivity {
    method onCreate (line 45) | @Override
    method initVideoView (line 57) | private void initVideoView() {
    method initSpinner (line 112) | private void initSpinner() {
    method initControllerMode (line 131) | private void initControllerMode() {
    method initCustomViews (line 167) | private void initCustomViews() {
    method changeToPortrait (line 185) | private void changeToPortrait() {
    method changeToLandscape (line 199) | private void changeToLandscape() {
    method onStart (line 212) | @Override
    method onResume (line 220) | @Override
    method onPause (line 228) | @Override
    method onStop (line 236) | @Override
    method onDestroy (line 244) | @Override
    method onKeyDown (line 251) | @Override

FILE: demo/src/test/java/com/jarvanmo/demo/ExampleUnitTest.java
  class ExampleUnitTest (line 12) | public class ExampleUnitTest {
    method addition_isCorrect (line 13) | @Test

FILE: exoplayerview/src/androidTest/java/com/jarvanmo/exoplayerview/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 17) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 19) | @Test

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/ads/ExoAdsLoader.java
  class ExoAdsLoader (line 18) | public class ExoAdsLoader extends Player.DefaultEventListener implements...
    method setSupportedContentTypes (line 20) | @Override
    method start (line 25) | @Override
    method stop (line 30) | @Override
    method setPlayer (line 36) | @Override
    method release (line 41) | @Override
    method handlePrepareError (line 46) | @Override

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/extension/MultiQualitySelectorAdapter.java
  class MultiQualitySelectorAdapter (line 22) | public class MultiQualitySelectorAdapter extends RecyclerView.Adapter<Mu...
    type MultiQualitySelectorNavigator (line 24) | public interface MultiQualitySelectorNavigator {
      method onQualitySelected (line 29) | boolean onQualitySelected(ExoMediaSource.Quality quality);
    type VisibilityCallback (line 33) | public interface VisibilityCallback {
      method shouldChangeVisibility (line 34) | void shouldChangeVisibility(int visibility);
    method MultiQualitySelectorAdapter (line 41) | public MultiQualitySelectorAdapter(List<ExoMediaSource.Quality> qualit...
    method onCreateViewHolder (line 46) | @NonNull
    method onBindViewHolder (line 53) | @Override
    method getItemCount (line 59) | @Override
    class MultiQualitySelectorViewHolder (line 64) | class MultiQualitySelectorViewHolder extends RecyclerView.ViewHolder {
      method MultiQualitySelectorViewHolder (line 68) | MultiQualitySelectorViewHolder(View itemView) {

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/gesture/OnVideoGestureChangeListener.java
  type OnVideoGestureChangeListener (line 13) | public interface OnVideoGestureChangeListener {
    method onVolumeChanged (line 27) | void onVolumeChanged(int range, @VolumeChangeType int type);
    method onBrightnessChanged (line 29) | void onBrightnessChanged(int brightnessPercent);
    method onNoGesture (line 31) | void onNoGesture();
    method onShowSeekSize (line 33) | void onShowSeekSize(long seekSize, boolean fastForward);

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/gesture/VideoGesture.java
  class VideoGesture (line 29) | public class VideoGesture implements View.OnTouchListener {
    method VideoGesture (line 61) | public VideoGesture(Context context, OnVideoGestureChangeListener onVi...
    method initVol (line 72) | private void initVol() {
    method onTouch (line 80) | @SuppressLint("ClickableViewAccessibility")
    method dispatchCenterWrapperTouchEvent (line 90) | private boolean dispatchCenterWrapperTouchEvent(MotionEvent event) {
    method doVolumeTouch (line 173) | private void doVolumeTouch(float y_changed) {
    method setAudioVolume (line 188) | private void setAudioVolume(int vol, boolean isUp) {
    method onVolumeChanged (line 212) | private void onVolumeChanged(int range, @OnVideoGestureChangeListener....
    method doBrightnessTouch (line 219) | private void doBrightnessTouch(float y_changed) {
    method changeBrightness (line 236) | private void changeBrightness(float delta) {
    method onBrightnessChanged (line 257) | private void onBrightnessChanged(int brightnessPercent) {
    method doSeekTouch (line 263) | private void doSeekTouch(int coef, float gesturesize, boolean seek) {
    method seekAndShowJump (line 310) | private void seekAndShowJump(boolean seek, long jumpSize, boolean isFa...
    method hideCenterInfo (line 316) | private void hideCenterInfo() {
    method initBrightnessTouch (line 323) | private void initBrightnessTouch() {
    method canSeek (line 354) | private boolean canSeek() {
    method enable (line 370) | public void enable() {
    method disable (line 374) | public void disable() {

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/EventLogger.java
  class EventLogger (line 47) | final class EventLogger implements Player.EventListener, MetadataOutput,
    method EventLogger (line 50) | public EventLogger(MappingTrackSelector trackSelector) {
    method onTimelineChanged (line 53) | @Override
    method onTracksChanged (line 58) | @Override
    method onLoadingChanged (line 63) | @Override
    method onPlayerStateChanged (line 68) | @Override
    method onRepeatModeChanged (line 73) | @Override
    method onShuffleModeEnabledChanged (line 78) | @Override
    method onPlayerError (line 83) | @Override
    method onPositionDiscontinuity (line 88) | @Override
    method onPlaybackParametersChanged (line 93) | @Override
    method onSeekProcessed (line 98) | @Override
    method onAudioEnabled (line 103) | @Override
    method onAudioSessionId (line 108) | @Override
    method onAudioDecoderInitialized (line 113) | @Override
    method onAudioInputFormatChanged (line 118) | @Override
    method onAudioSinkUnderrun (line 123) | @Override
    method onAudioDisabled (line 128) | @Override
    method onMetadata (line 134) | @Override
    method onVideoEnabled (line 140) | @Override
    method onVideoDecoderInitialized (line 145) | @Override
    method onVideoInputFormatChanged (line 150) | @Override
    method onDroppedFrames (line 155) | @Override
    method onVideoSizeChanged (line 160) | @Override
    method onRenderedFirstFrame (line 165) | @Override
    method onVideoDisabled (line 170) | @Override
    method onMediaPeriodCreated (line 175) | @Override
    method onMediaPeriodReleased (line 180) | @Override
    method onLoadStarted (line 185) | @Override
    method onLoadCompleted (line 190) | @Override
    method onLoadCanceled (line 195) | @Override
    method onLoadError (line 200) | @Override
    method onReadingStarted (line 205) | @Override
    method onUpstreamDiscarded (line 210) | @Override
    method onDownstreamFormatChanged (line 215) | @Override

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/ExoMediaSource.java
  type ExoMediaSource (line 12) | public interface ExoMediaSource {
    type Quality (line 14) | interface Quality {
      method getDisplayName (line 15) | CharSequence getDisplayName();
      method getUri (line 17) | Uri getUri();
      method setUri (line 19) | void setUri(Uri uri);
      method setDisplayName (line 21) | void setDisplayName(CharSequence displayName);
      method setQuality (line 23) | void setQuality(String quality);
      method getQuality (line 25) | String getQuality();
    method uri (line 29) | Uri uri();
    method name (line 31) | String name();
    method qualities (line 33) | List<Quality> qualities();
    method extension (line 35) | String extension();

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/MediaSourceCreator.java
  class MediaSourceCreator (line 31) | public class MediaSourceCreator {
    method MediaSourceCreator (line 45) | public MediaSourceCreator(Context context) {
    method MediaSourceCreator (line 49) | public MediaSourceCreator(Context context, String userAgent) {
    method getEventLogger (line 59) | public EventLogger getEventLogger() {
    method buildMediaSource (line 64) | public MediaSource buildMediaSource(Uri uri, String overrideExtension) {
    method buildDataSourceFactory (line 88) | private DataSource.Factory buildDataSourceFactory(boolean useBandwidth...
    method buildDataSourceFactory (line 92) | public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter...
    method buildHttpDataSourceFactory (line 97) | public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwi...

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/MediaSourceParams.java
  class MediaSourceParams (line 8) | public class MediaSourceParams {

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/SimpleMediaSource.java
  class SimpleMediaSource (line 12) | public class SimpleMediaSource implements ExoMediaSource {
    method SimpleMediaSource (line 20) | public SimpleMediaSource(String url) {
    method SimpleMediaSource (line 24) | public SimpleMediaSource(Uri uri) {
    method name (line 29) | @Override
    method qualities (line 34) | @Override
    method extension (line 39) | @Override
    method setDisplayName (line 44) | public void setDisplayName(String displayName) {
    method uri (line 48) | @Override
    method setQualities (line 53) | public void setQualities(List<Quality> qualities) {

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/SimpleQuality.java
  class SimpleQuality (line 11) | public class SimpleQuality implements ExoMediaSource.Quality {
    method SimpleQuality (line 17) | public SimpleQuality(CharSequence name, Uri uri) {
    method getDisplayName (line 22) | @Override
    method getUri (line 27) | @Override
    method setUri (line 32) | @Override
    method setDisplayName (line 37) | @Override
    method setQuality (line 42) | @Override
    method getQuality (line 47) | @Override

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/orientation/OnOrientationChangedListener.java
  type OnOrientationChangedListener (line 13) | public interface OnOrientationChangedListener {
    method onChanged (line 25) | void onChanged(@SensorOrientationType int orientation);

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/orientation/SensorOrientation.java
  class SensorOrientation (line 17) | public class SensorOrientation {
    method SensorOrientation (line 24) | public SensorOrientation(Context context, OnOrientationChangedListener...
    method isScreenOpenRotate (line 69) | private boolean isScreenOpenRotate() {
    method enable (line 83) | public void enable() {
    method disable (line 87) | public void disable() {

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/ui/ExoVideoPlaybackControlView.java
  class ExoVideoPlaybackControlView (line 76) | public class ExoVideoPlaybackControlView extends FrameLayout {
    type VideoViewAccessor (line 81) | public interface VideoViewAccessor {
      method attachVideoView (line 83) | View attachVideoView();
    type PlayerAccessor (line 90) | public interface PlayerAccessor {
      method attachPlayer (line 92) | Player attachPlayer();
    type VisibilityListener (line 99) | public interface VisibilityListener {
      method onVisibilityChange (line 106) | void onVisibilityChange(int visibility);
    type ExoClickListener (line 111) | public interface ExoClickListener {
      method onClick (line 119) | boolean onClick(@Nullable View view, boolean isPortrait);
    type OrientationListener (line 124) | public interface OrientationListener {
      method onOrientationChanged (line 125) | void onOrientationChanged(@OnOrientationChangedListener.SensorOrient...
    method ExoVideoPlaybackControlView (line 287) | public ExoVideoPlaybackControlView(Context context) {
    method ExoVideoPlaybackControlView (line 291) | public ExoVideoPlaybackControlView(Context context, AttributeSet attrs) {
    method ExoVideoPlaybackControlView (line 295) | public ExoVideoPlaybackControlView(Context context, AttributeSet attrs...
    method ExoVideoPlaybackControlView (line 299) | public ExoVideoPlaybackControlView(Context context, AttributeSet attrs...
    method setupVideoGesture (line 503) | private void setupVideoGesture(boolean enableGesture) {
    method generateFastForwardOrRewindTxt (line 573) | private CharSequence generateFastForwardOrRewindTxt(long changingTime) {
    method whichBrightnessImageToUse (line 594) | @DrawableRes
    method setVolumeOrBrightnessInfo (line 614) | private void setVolumeOrBrightnessInfo(String txt, @DrawableRes int dr...
    method getRepeatToggleModes (line 630) | @SuppressWarnings("ResourceType")
    method getPlayer (line 641) | public Player getPlayer() {
    method setPlayer (line 650) | public void setPlayer(Player player) {
    method setShowMultiWindowTimeBar (line 672) | public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
    method setExtraAdGroupMarkers (line 687) | public void setExtraAdGroupMarkers(@Nullable long[] extraAdGroupTimesMs,
    method setVisibilityListener (line 705) | public void setVisibilityListener(VisibilityListener listener) {
    method setControlDispatcher (line 715) | public void setControlDispatcher(
    method setRewindIncrementMs (line 727) | public void setRewindIncrementMs(int rewindMs) {
    method setFastForwardIncrementMs (line 738) | public void setFastForwardIncrementMs(int fastForwardMs) {
    method getShowTimeoutMs (line 750) | public int getShowTimeoutMs() {
    method setShowTimeoutMs (line 761) | public void setShowTimeoutMs(int showTimeoutMs) {
    method getRepeatToggleModes (line 770) | public @RepeatModeUtil.RepeatToggleModes
    method setRepeatToggleModes (line 780) | public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int...
    method getShowShuffleButton (line 800) | public boolean getShowShuffleButton() {
    method setShowShuffleButton (line 809) | public void setShowShuffleButton(boolean showShuffleButton) {
    method show (line 819) | public void show() {
    method hide (line 842) | public void hide() {
    method isVisible (line 861) | public boolean isVisible() {
    method hideAfterTimeout (line 865) | private void hideAfterTimeout() {
    method updateAll (line 877) | private void updateAll() {
    method updateNavigation (line 885) | private void updateNavigation() {
    method updatePlayPauseButton (line 914) | private void updatePlayPauseButton() {
    method updateRepeatModeButton (line 945) | private void updateRepeatModeButton() {
    method updateShuffleButton (line 975) | private void updateShuffleButton() {
    method updateTimeBarMode (line 990) | private void updateTimeBarMode() {
    method updateProgress (line 998) | private void updateProgress() {
    method requestPlayPauseFocus (line 1130) | private void requestPlayPauseFocus() {
    method setButtonEnabled (line 1147) | private void setButtonEnabled(boolean enabled, View view) {
    method previous (line 1156) | private void previous() {
    method next (line 1173) | private void next() {
    method rewind (line 1187) | private void rewind() {
    method fastForward (line 1194) | private void fastForward() {
    method seekTo (line 1206) | private void seekTo(long positionMs) {
    method seekTo (line 1210) | private void seekTo(int windowIndex, long positionMs) {
    method seekToTimeBarPosition (line 1219) | private void seekToTimeBarPosition(long positionMs) {
    method onAttachedToWindow (line 1243) | @Override
    method onDetachedFromWindow (line 1259) | @Override
    method dispatchKeyEvent (line 1268) | @Override
    method dispatchMediaKeyEvent (line 1280) | public boolean dispatchMediaKeyEvent(KeyEvent event) {
    method setBackListener (line 1316) | public void setBackListener(ExoClickListener backListener) {
    method setPortrait (line 1321) | public void setPortrait(boolean portrait) {
    method isPortrait (line 1326) | public boolean isPortrait() {
    method toggleControllerOrientation (line 1330) | private void toggleControllerOrientation() {
    method setOrientationListener (line 1340) | public void setOrientationListener(OrientationListener orientationList...
    method setMediaSource (line 1345) | public void setMediaSource(ExoMediaSource exoMediaSource) {
    method setControllerDisplayMode (line 1361) | public void setControllerDisplayMode(int displayMode) {
    method showControllerByDisplayMode (line 1366) | private void showControllerByDisplayMode() {
    method changeOrientation (line 1416) | private synchronized void changeOrientation(@OnOrientationChangedListe...
    method changeSystemUiVisibilityPortrait (line 1449) | private void changeSystemUiVisibilityPortrait() {
    method changeSystemUiVisibilityLandscape (line 1453) | private void changeSystemUiVisibilityLandscape() {
    method setVideoViewAccessor (line 1473) | public void setVideoViewAccessor(VideoViewAccessor videoViewAccessor) {
    method setVisibilityCallback (line 1477) | public void setVisibilityCallback(MultiQualitySelectorAdapter.Visibili...
    method updateQualityDes (line 1481) | public void updateQualityDes(CharSequence qualityDes) {
    method addCustomView (line 1496) | public void addCustomView(@CustomViewType int customViewType, View cus...
    method addCustomView (line 1515) | public void addCustomView(@CustomViewType int customViewType, View cus...
    method changeWidgetVisibility (line 1519) | public void changeWidgetVisibility(int id,int visibility){
    method showLoading (line 1525) | private void showLoading(boolean isLoading) {
    method showUtilHideCalled (line 1536) | public void showUtilHideCalled() {
    method setGestureEnabled (line 1546) | public void setGestureEnabled(boolean enabled){
    method onKeyDown (line 1563) | @Override
    method isHandledMediaKey (line 1582) | @SuppressLint("InlinedApi")
    method canShowMultiWindowTimeBar (line 1600) | private static boolean canShowMultiWindowTimeBar(Timeline timeline, Ti...
    class ComponentListener (line 1613) | private final class ComponentListener  implements
      method onScrubStart (line 1616) | @Override
      method onScrubMove (line 1622) | @Override
      method onScrubStop (line 1629) | @Override
      method onPlayerStateChanged (line 1638) | @Override
      method onRepeatModeChanged (line 1658) | @Override
      method onShuffleModeEnabledChanged (line 1664) | @Override
      method onPositionDiscontinuity (line 1670) | @Override
      method onTimelineChanged (line 1676) | @Override
      method onPlayerError (line 1692) | @Override
      method onClick (line 1704) | @Override
      method playOrPause (line 1752) | private void playOrPause() {

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/ui/ExoVideoView.java
  class ExoVideoView (line 67) | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    method onAudioFocusChange (line 102) | public void onAudioFocusChange(int focusChange) {
    method ExoVideoView (line 124) | public ExoVideoView(Context context) {
    method ExoVideoView (line 128) | public ExoVideoView(Context context, AttributeSet attrs) {
    method ExoVideoView (line 132) | public ExoVideoView(Context context, AttributeSet attrs, int defStyleA...
    method switchTargetView (line 281) | public static void switchTargetView(@NonNull SimpleExoPlayer player,
    method getPlayer (line 301) | public SimpleExoPlayer getPlayer() {
    method setPlayer (line 316) | public void setPlayer(SimpleExoPlayer player) {
    method setVisibility (line 354) | @Override
    method setResizeMode (line 368) | public void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resiz...
    method getUseArtwork (line 376) | public boolean getUseArtwork() {
    method setUseArtwork (line 385) | public void setUseArtwork(boolean useArtwork) {
    method getDefaultArtwork (line 396) | public Bitmap getDefaultArtwork() {
    method setDefaultArtwork (line 406) | public void setDefaultArtwork(Bitmap defaultArtwork) {
    method getUseController (line 416) | public boolean getUseController() {
    method setUseController (line 426) | public void setUseController(boolean useController) {
    method setShutterBackgroundColor (line 445) | public void setShutterBackgroundColor(int color) {
    method onAttachedToWindow (line 451) | @Override
    method onDetachedFromWindow (line 459) | @Override
    method dispatchKeyEvent (line 467) | @Override
    method dispatchMediaKeyEvent (line 489) | public boolean dispatchMediaKeyEvent(KeyEvent event) {
    method showController (line 500) | public void showController() {
    method hideController (line 507) | public void hideController() {
    method getControllerShowTimeoutMs (line 521) | public int getControllerShowTimeoutMs() {
    method setControllerShowTimeoutMs (line 532) | public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) {
    method getControllerHideOnTouch (line 540) | public boolean getControllerHideOnTouch() {
    method setControllerHideOnTouch (line 549) | public void setControllerHideOnTouch(boolean controllerHideOnTouch) {
    method getControllerAutoShow (line 559) | public boolean getControllerAutoShow() {
    method setControllerAutoShow (line 570) | public void setControllerAutoShow(boolean controllerAutoShow) {
    method setControllerVisibilityListener (line 579) | public void setControllerVisibilityListener(ExoVideoPlaybackControlVie...
    method setControlDispatcher (line 590) | public void setControlDispatcher(@Nullable ControlDispatcher controlDi...
    method setRewindIncrementMs (line 601) | public void setRewindIncrementMs(int rewindMs) {
    method setFastForwardIncrementMs (line 612) | public void setFastForwardIncrementMs(int fastForwardMs) {
    method setRepeatToggleModes (line 622) | public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int...
    method setShowShuffleButton (line 632) | public void setShowShuffleButton(boolean showShuffleButton) {
    method setShowMultiWindowTimeBar (line 642) | public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
    method getVideoSurfaceView (line 658) | public View getVideoSurfaceView() {
    method getOverlayFrameLayout (line 669) | public FrameLayout getOverlayFrameLayout() {
    method getSubtitleView (line 679) | public SubtitleView getSubtitleView() {
    method onTouchEvent (line 683) | @SuppressLint("ClickableViewAccessibility")
    method onTrackballEvent (line 706) | @Override
    method maybeShowController (line 718) | private void maybeShowController(boolean isForced) {
    method shouldShowControllerIndefinitely (line 732) | private boolean shouldShowControllerIndefinitely() {
    method showController (line 741) | private void showController(boolean showIndefinitely) {
    method isPlayingAd (line 749) | private boolean isPlayingAd() {
    method updateForCurrentTrackSelections (line 753) | private void updateForCurrentTrackSelections() {
    method setArtworkFromMetadata (line 791) | private boolean setArtworkFromMetadata(Metadata metadata) {
    method setArtworkFromBitmap (line 803) | private boolean setArtworkFromBitmap(Bitmap bitmap) {
    method hideArtwork (line 819) | private void hideArtwork() {
    method configureEditModeLogoV23 (line 826) | @TargetApi(23)
    method configureEditModeLogo (line 832) | @SuppressWarnings("deprecation")
    method setResizeModeRaw (line 838) | @SuppressWarnings("ResourceType")
    method isDpadKey (line 843) | @SuppressLint("InlinedApi")
    method play (line 853) | public void play(ExoMediaSource mediaSource) {
    method play (line 857) | public void play(ExoMediaSource mediaSource, boolean playWhenReady) {
    method play (line 861) | public void play(ExoMediaSource mediaSource, long where) {
    method play (line 865) | public void play(ExoMediaSource mediaSource, boolean playWhenReady, lo...
    method pause (line 881) | public void pause() {
    method resume (line 895) | public void resume() {
    method stop (line 911) | public void stop() {
    method playInternal (line 917) | private void playInternal(ExoMediaSource mediaSource, boolean playWhen...
    method createExoPlayer (line 929) | private void createExoPlayer(MediaSourceCreator creator) {
    method releasePlayer (line 951) | public void releasePlayer() {
    method requestAudioFocus (line 960) | private boolean requestAudioFocus() {
    method setBackListener (line 975) | public void setBackListener(ExoVideoPlaybackControlView.ExoClickListen...
    method setOrientationListener (line 982) | public void setOrientationListener(ExoVideoPlaybackControlView.Orienta...
    method setMultiQualitySelectorNavigator (line 989) | public void setMultiQualitySelectorNavigator(MultiQualitySelectorAdapt...
    method isPortrait (line 993) | public boolean isPortrait() {
    method setPortrait (line 997) | public void setPortrait(boolean portrait) {
    method changeWidgetVisibility (line 1004) | public void changeWidgetVisibility(int id, int visibility) {
    method setControllerDisplayMode (line 1010) | public void setControllerDisplayMode(int displayMode) {
    method addMultiQualitySelector (line 1017) | private void addMultiQualitySelector(ExoMediaSource mediaSource) {
    method onKeyDown (line 1082) | @Override
    method attachVideoView (line 1090) | @Override
    method addCustomView (line 1102) | public void addCustomView(@ExoVideoPlaybackControlView.CustomViewType ...
    method addCustomView (line 1108) | public void addCustomView(@ExoVideoPlaybackControlView.CustomViewType ...
    method setGestureEnabled (line 1112) | public void setGestureEnabled(boolean enabled) {
    class ComponentListener (line 1118) | private final class ComponentListener implements TextOutput, Player.Ev...
      method onCues (line 1123) | @Override
      method onVideoSizeChanged (line 1132) | @Override
      method onRenderedFirstFrame (line 1141) | @Override
      method onTracksChanged (line 1148) | @Override
      method onPlayerStateChanged (line 1155) | @Override
      method onPositionDiscontinuity (line 1164) | @Override

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/util/AndroidUtil.java
  class AndroidUtil (line 28) | public class AndroidUtil {
    method isJellyBeanMR1OrLater (line 31) | public static boolean isJellyBeanMR1OrLater() {
    method isJellyBeanMR2OrLater (line 35) | public static boolean isJellyBeanMR2OrLater() {
    method isKitKatOrLater (line 39) | public static boolean isKitKatOrLater() {
    method isLolliPopOrLater (line 43) | public static boolean isLolliPopOrLater() {
    method isMarshMallowOrLater (line 47) | public static boolean isMarshMallowOrLater() {
    method UriToFile (line 51) | public static File UriToFile(Uri uri) {
    method PathToUri (line 61) | public static Uri PathToUri(String path) {
    method LocationToUri (line 65) | public static Uri LocationToUri(String location) {
    method FileToUri (line 72) | public static Uri FileToUri(File file) {

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/util/Permissions.java
  class Permissions (line 36) | public class Permissions {
    method canDrawOverlays (line 50) | @TargetApi(Build.VERSION_CODES.M)
    method canWriteSettings (line 55) | @TargetApi(Build.VERSION_CODES.M)
    method canReadStorage (line 60) | public static boolean canReadStorage(Context context) {
    method requestStoragePermission (line 66) | private static void requestStoragePermission(Activity activity) {

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/widget/BatteryLevelView.java
  class BatteryLevelView (line 21) | public class BatteryLevelView extends View {
    method BatteryLevelView (line 60) | public BatteryLevelView(Context context) {
    method BatteryLevelView (line 64) | public BatteryLevelView(Context context, @Nullable AttributeSet attrs) {
    method BatteryLevelView (line 68) | public BatteryLevelView(Context context, @Nullable AttributeSet attrs,...
    method initView (line 74) | public void initView() {
    method onDraw (line 100) | @Override
    method onMeasure (line 111) | @Override
    method setPower (line 119) | public void setPower(float power) {
    method getPowerColor (line 135) | private int getPowerColor() {
    method onReceive (line 150) | @Override
    method onAttachedToWindow (line 164) | @Override
    method onDetachedFromWindow (line 170) | @Override

FILE: exoplayerview/src/main/java/com/jarvanmo/exoplayerview/widget/BatteryStatusView.java
  class BatteryStatusView (line 19) | public class BatteryStatusView extends AppCompatImageView {
    method BatteryStatusView (line 22) | public BatteryStatusView(Context context) {
    method BatteryStatusView (line 26) | public BatteryStatusView(Context context, @Nullable AttributeSet attrs) {
    method BatteryStatusView (line 30) | public BatteryStatusView(Context context, @Nullable AttributeSet attrs...
    method init (line 35) | private void init() {
    method onReceive (line 42) | @Override
    method onAttachedToWindow (line 61) | @Override
    method onDetachedFromWindow (line 67) | @Override

FILE: exoplayerview/src/test/java/com/jarvanmo/exoplayerview/ExampleUnitTest.java
  class ExampleUnitTest (line 12) | public class ExampleUnitTest {
    method addition_isCorrect (line 13) | @Test
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (278K chars).
[
  {
    "path": ".gitignore",
    "chars": 94,
    "preview": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/build\n/captures\n.externalNativeBuild\ntmp.txt"
  },
  {
    "path": "LICENSE",
    "chars": 11358,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 5646,
    "preview": "![logo](./images/default_art.png)\n# ExoVideoView\nExoVideoView is based on [ExoPlayer](https://github.com/google/ExoPlaye"
  },
  {
    "path": "README_CN.md",
    "chars": 3859,
    "preview": "![logo](./images/default_art.png)\n# ExoVideoView\n![demo](./images/demo.gif)\n\nExoVideoView 是一款基于[ExoPlayer](https://githu"
  },
  {
    "path": "build.gradle",
    "chars": 812,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    r"
  },
  {
    "path": "demo/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "demo/build.gradle",
    "chars": 1404,
    "preview": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion rootProject.ext.compileSdkVersion\n    defaultCo"
  },
  {
    "path": "demo/proguard-rules.pro",
    "chars": 942,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /h"
  },
  {
    "path": "demo/src/androidTest/java/com/jarvanmo/demo/ExampleInstrumentedTest.java",
    "chars": 735,
    "preview": "package com.jarvanmo.demo;\n\nimport android.content.Context;\nimport androidx.test.InstrumentationRegistry;\nimport android"
  },
  {
    "path": "demo/src/main/AndroidManifest.xml",
    "chars": 1014,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package="
  },
  {
    "path": "demo/src/main/java/com/jarvanmo/demo/MainActivity.java",
    "chars": 7136,
    "preview": "package com.jarvanmo.demo;\n\nimport android.graphics.Color;\nimport android.os.Build;\nimport android.os.Bundle;\nimport and"
  },
  {
    "path": "demo/src/main/java/com/jarvanmo/demo/SimpleVideoViewActivity.java",
    "chars": 9400,
    "preview": "package com.jarvanmo.demo;\n\nimport android.content.res.Configuration;\nimport android.graphics.Color;\nimport android.net."
  },
  {
    "path": "demo/src/main/res/drawable/ic_arrow_back_white_24dp.xml",
    "chars": 390,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android"
  },
  {
    "path": "demo/src/main/res/drawable/ic_error_white_24dp.xml",
    "chars": 432,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android"
  },
  {
    "path": "demo/src/main/res/layout/activity_main.xml",
    "chars": 3595,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "demo/src/main/res/layout/activity_simple_video_view.xml",
    "chars": 7937,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "demo/src/main/res/layout/cutom_view_bottom_landscape.xml",
    "chars": 801,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.ConstraintLayout xmlns:android=\"http://schemas.android"
  },
  {
    "path": "demo/src/main/res/layout/cutom_view_top.xml",
    "chars": 799,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.ConstraintLayout xmlns:android=\"http://schemas.android"
  },
  {
    "path": "demo/src/main/res/layout/cutom_view_top_landscape.xml",
    "chars": 803,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.ConstraintLayout xmlns:android=\"http://schemas.android"
  },
  {
    "path": "demo/src/main/res/values/colors.xml",
    "chars": 259,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"color"
  },
  {
    "path": "demo/src/main/res/values/strings.xml",
    "chars": 76,
    "preview": "<resources>\n    <string name=\"app_name\">ExoPlayerView</string>\n</resources>\n"
  },
  {
    "path": "demo/src/main/res/values/styles.xml",
    "chars": 523,
    "preview": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">"
  },
  {
    "path": "demo/src/test/java/com/jarvanmo/demo/ExampleUnitTest.java",
    "chars": 406,
    "preview": "package com.jarvanmo.demo;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * Example local u"
  },
  {
    "path": "exoplayerview/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "exoplayerview/build.gradle",
    "chars": 1514,
    "preview": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion rootProject.ext.compileSdkVersion\n\n    defaultConfi"
  },
  {
    "path": "exoplayerview/proguard-rules.pro",
    "chars": 661,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /h"
  },
  {
    "path": "exoplayerview/src/androidTest/java/com/jarvanmo/exoplayerview/ExampleInstrumentedTest.java",
    "chars": 758,
    "preview": "package com.jarvanmo.exoplayerview;\n\nimport android.content.Context;\nimport androidx.test.InstrumentationRegistry;\nimpor"
  },
  {
    "path": "exoplayerview/src/main/AndroidManifest.xml",
    "chars": 576,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.jarvanmo.exoplayerview\">\n\n    <use"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/ads/ExoAdsLoader.java",
    "chars": 926,
    "preview": "package com.jarvanmo.exoplayerview.ads;\n\nimport android.view.ViewGroup;\n\nimport com.google.android.exoplayer2.ExoPlayer;"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/extension/MultiQualitySelectorAdapter.java",
    "chars": 2236,
    "preview": "package com.jarvanmo.exoplayerview.extension;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.R"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/gesture/OnVideoGestureChangeListener.java",
    "chars": 810,
    "preview": "package com.jarvanmo.exoplayerview.gesture;\n\nimport androidx.annotation.IntDef;\n\nimport java.lang.annotation.Retention;\n"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/gesture/VideoGesture.java",
    "chars": 12421,
    "preview": "package com.jarvanmo.exoplayerview.gesture;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/EventLogger.java",
    "chars": 5924,
    "preview": "/*\n * Copyright (C) 2016 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/ExoMediaSource.java",
    "chars": 531,
    "preview": "package com.jarvanmo.exoplayerview.media;\n\nimport android.net.Uri;\n\nimport java.util.List;\n\n/**\n * Created by mo on 18-1"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/MediaSourceCreator.java",
    "chars": 3997,
    "preview": "package com.jarvanmo.exoplayerview.media;\n\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Han"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/MediaSourceParams.java",
    "chars": 204,
    "preview": "package com.jarvanmo.exoplayerview.media;\n\n/**\n * Created by mo on 18-1-11.\n * 剑气纵横三万里 一剑光寒十九洲\n */\n\npublic class MediaSo"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/SimpleMediaSource.java",
    "chars": 991,
    "preview": "package com.jarvanmo.exoplayerview.media;\n\nimport android.net.Uri;\n\nimport java.util.List;\n\n/**\n * Created by mo on 16-1"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/media/SimpleQuality.java",
    "chars": 888,
    "preview": "package com.jarvanmo.exoplayerview.media;\n\nimport android.net.Uri;\n\n/**\n * Created by mo on 18-2-7.\n *\n * @author mo\n */"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/orientation/OnOrientationChangedListener.java",
    "chars": 612,
    "preview": "package com.jarvanmo.exoplayerview.orientation;\n\nimport androidx.annotation.IntDef;\n\nimport java.lang.annotation.Retenti"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/orientation/SensorOrientation.java",
    "chars": 3008,
    "preview": "package com.jarvanmo.exoplayerview.orientation;\n\nimport android.content.Context;\nimport android.provider.Settings;\nimpor"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/ui/ExoVideoPlaybackControlView.java",
    "chars": 66975,
    "preview": "package com.jarvanmo.exoplayerview.ui;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport andr"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/ui/ExoVideoView.java",
    "chars": 43315,
    "preview": "package com.jarvanmo.exoplayerview.ui;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimp"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/util/AndroidUtil.java",
    "chars": 2555,
    "preview": "/*****************************************************************************\n * AndroidUtil.java\n ********************"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/util/Permissions.java",
    "chars": 2719,
    "preview": "/*\n * *************************************************************************\n *  Permissions.java\n * ****************"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/widget/BatteryLevelView.java",
    "chars": 5506,
    "preview": "package com.jarvanmo.exoplayerview.widget;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nim"
  },
  {
    "path": "exoplayerview/src/main/java/com/jarvanmo/exoplayerview/widget/BatteryStatusView.java",
    "chars": 2211,
    "preview": "package com.jarvanmo.exoplayerview.widget;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nim"
  },
  {
    "path": "exoplayerview/src/main/res/drawable/ic_arrow_back_white_24dp.xml",
    "chars": 390,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android"
  },
  {
    "path": "exoplayerview/src/main/res/drawable/ic_error_outline_white_48dp.xml",
    "chars": 506,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android"
  },
  {
    "path": "exoplayerview/src/main/res/drawable/stat_sys_battery.xml",
    "chars": 1784,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n/* //device/apps/common/res/drawable/stat_sys_battery.xml\n**\n** Copyright 200"
  },
  {
    "path": "exoplayerview/src/main/res/drawable/stat_sys_battery_charge.xml",
    "chars": 1812,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n** Copyright 2007, The Android Open Source Project\n**\n** Licensed under the A"
  },
  {
    "path": "exoplayerview/src/main/res/layout/exo_playback_controller_bottom.xml",
    "chars": 2491,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    andr"
  },
  {
    "path": "exoplayerview/src/main/res/layout/exo_playback_controller_bottom_landscape.xml",
    "chars": 6502,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
  },
  {
    "path": "exoplayerview/src/main/res/layout/exo_playback_controller_top.xml",
    "chars": 2240,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
  },
  {
    "path": "exoplayerview/src/main/res/layout/exo_playback_controller_top_landscape.xml",
    "chars": 2285,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
  },
  {
    "path": "exoplayerview/src/main/res/layout/exo_player_quality_selector.xml",
    "chars": 1349,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
  },
  {
    "path": "exoplayerview/src/main/res/layout/exo_video_playback_control_view.xml",
    "chars": 3502,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
  },
  {
    "path": "exoplayerview/src/main/res/layout/exo_video_view.xml",
    "chars": 1502,
    "preview": "<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <com.google.android.exoplayer2.ui.AspectRatioFra"
  },
  {
    "path": "exoplayerview/src/main/res/layout/item_quality.xml",
    "chars": 978,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
  },
  {
    "path": "exoplayerview/src/main/res/values/attrs.xml",
    "chars": 4243,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <attr name=\"showTimeout\" format=\"integer\" />\n    <attr name=\"rew"
  },
  {
    "path": "exoplayerview/src/main/res/values/colors.xml",
    "chars": 768,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"default_playback_background\">#3D000000</color>\n\n    "
  },
  {
    "path": "exoplayerview/src/main/res/values/dimens.xml",
    "chars": 296,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <dimen name=\"common_zero_dp\">0dp</dimen>\n    <dimen name=\"default"
  },
  {
    "path": "exoplayerview/src/main/res/values/ids.xml",
    "chars": 2384,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <item name=\"exo_player_play\" type=\"id\" />\n    <item name=\"exo_pla"
  },
  {
    "path": "exoplayerview/src/main/res/values/strings.xml",
    "chars": 559,
    "preview": "<resources>\n    <string name=\"app_name\">ExoPlayerView</string>\n    <string name=\"default_video_time\" translatable=\"false"
  },
  {
    "path": "exoplayerview/src/main/res/values-zh-rCN/strings.xml",
    "chars": 371,
    "preview": "<resources>\n    <string name=\"app_name\">ExoPlayerView</string>\n    <string name=\"time_am\">上午</string>\n    <string name=\""
  },
  {
    "path": "exoplayerview/src/test/java/com/jarvanmo/exoplayerview/ExampleUnitTest.java",
    "chars": 415,
    "preview": "package com.jarvanmo.exoplayerview;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.assertEquals;\n\n/**\n * Exampl"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 232,
    "preview": "#Thu Jul 23 09:49:01 CST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
  },
  {
    "path": "gradle.properties",
    "chars": 779,
    "preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
  },
  {
    "path": "gradlew",
    "chars": 4971,
    "preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start "
  },
  {
    "path": "gradlew.bat",
    "chars": 2404,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
  },
  {
    "path": "settings.gradle",
    "chars": 40,
    "preview": "include ':demo'\ninclude ':exoplayerview'"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the JarvanMo/ExoVideoView GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (254.5 KB), approximately 59.8k tokens, and a symbol index with 365 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!