├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle ├── maven-publish.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lowlevel │ │ └── videoviewcompat │ │ ├── MediaController.java │ │ ├── VideoView.java │ │ └── internal │ │ └── PolicyCompat.java │ └── res │ ├── drawable-hdpi │ ├── vvc_ic_media_ff.png │ ├── vvc_ic_media_next.png │ ├── vvc_ic_media_pause.png │ ├── vvc_ic_media_play.png │ ├── vvc_ic_media_previous.png │ └── vvc_ic_media_rew.png │ ├── drawable-ldpi │ ├── vvc_ic_media_ff.png │ ├── vvc_ic_media_next.png │ ├── vvc_ic_media_pause.png │ ├── vvc_ic_media_play.png │ ├── vvc_ic_media_previous.png │ └── vvc_ic_media_rew.png │ ├── drawable-mdpi │ ├── vvc_ic_media_ff.png │ ├── vvc_ic_media_next.png │ ├── vvc_ic_media_pause.png │ ├── vvc_ic_media_play.png │ ├── vvc_ic_media_previous.png │ └── vvc_ic_media_rew.png │ ├── drawable-xhdpi │ ├── vvc_ic_media_ff.png │ ├── vvc_ic_media_next.png │ ├── vvc_ic_media_pause.png │ ├── vvc_ic_media_play.png │ ├── vvc_ic_media_previous.png │ └── vvc_ic_media_rew.png │ ├── drawable-xxhdpi │ ├── vvc_ic_media_ff.png │ ├── vvc_ic_media_next.png │ ├── vvc_ic_media_pause.png │ ├── vvc_ic_media_play.png │ ├── vvc_ic_media_previous.png │ └── vvc_ic_media_rew.png │ ├── layout │ └── vvc_media_controller.xml │ └── values │ ├── colors.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | ### Android template 2 | # Built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # Files for the Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | .DS_Store 33 | .idea 34 | *.iml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | VideoViewCompat 2 | =============== 3 | 4 | VideoViewCompat is an Android library designed to use VideoView and MediaController from the latest Android across all versions (2.1 or higher). 5 | 6 | This implementation also supports the usage of a custom layout for the MediaController by overriding the `makeControllerView()` method, a feature that is not possible when using the original MediaController from the Android framework. 7 | 8 | License 9 | ======= 10 | 11 | Copyright 2012 Lowlevel Studios 12 | 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unless required by applicable law or agreed to in writing, software 20 | distributed under the License is distributed on an "AS IS" BASIS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:2.3.1' 8 | classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:4.4.17' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | mavenCentral() 15 | jcenter() 16 | } 17 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Mar 06 12:45:50 CET 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 8 9 | targetSdkVersion 25 10 | } 11 | 12 | lintOptions { 13 | abortOnError false 14 | } 15 | } 16 | 17 | project.ext.artifactId = 'library' 18 | project.group = 'st.lowlevel.videoviewcompat' 19 | project.version = '1.1.4' 20 | 21 | apply from: 'maven-publish.gradle' -------------------------------------------------------------------------------- /library/maven-publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.jfrog.artifactory' 2 | apply plugin: 'maven-publish' 3 | 4 | publishing { 5 | publications { 6 | aar(MavenPublication) { 7 | groupId project.group 8 | version project.version 9 | artifactId project.ext.artifactId 10 | artifact("$buildDir/outputs/aar/${artifactId}-release.aar") 11 | 12 | pom.withXml { 13 | def dependencies = asNode().appendNode('dependencies') 14 | configurations.getByName("_releaseCompile").getResolvedConfiguration().getFirstLevelModuleDependencies().each { 15 | def dependency = dependencies.appendNode('dependency') 16 | dependency.appendNode('groupId', it.moduleGroup) 17 | dependency.appendNode('artifactId', it.moduleName) 18 | dependency.appendNode('version', it.moduleVersion) 19 | } 20 | } 21 | } 22 | } 23 | } 24 | 25 | artifactory { 26 | contextUrl = 'http://maven.lowlevel.st/artifactory' 27 | 28 | publish { 29 | repository { 30 | repoKey = 'libs-release-local' 31 | 32 | username = lowlevel_artifactory_username 33 | password = lowlevel_artifactory_password 34 | } 35 | defaults { 36 | publications('aar') 37 | publishArtifacts = true 38 | 39 | properties = ['qa.level': 'basic', 'q.os': 'android', 'dev.team': 'core'] 40 | publishPom = true 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/src/main/java/com/lowlevel/videoviewcompat/MediaController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2006 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lowlevel.videoviewcompat; 18 | 19 | import java.util.Formatter; 20 | import java.util.Locale; 21 | 22 | import com.lowlevel.videoviewcompat.internal.PolicyCompat; 23 | 24 | import android.annotation.SuppressLint; 25 | import android.annotation.TargetApi; 26 | import android.content.Context; 27 | import android.graphics.PixelFormat; 28 | import android.media.AudioManager; 29 | import android.os.Build; 30 | import android.os.Build.VERSION; 31 | import android.os.Build.VERSION_CODES; 32 | import android.os.Handler; 33 | import android.os.Message; 34 | import android.util.AttributeSet; 35 | import android.util.Log; 36 | import android.view.Gravity; 37 | import android.view.KeyEvent; 38 | import android.view.LayoutInflater; 39 | import android.view.MotionEvent; 40 | import android.view.View; 41 | import android.view.ViewGroup; 42 | import android.view.Window; 43 | import android.view.WindowManager; 44 | import android.view.accessibility.AccessibilityEvent; 45 | import android.view.accessibility.AccessibilityNodeInfo; 46 | import android.widget.FrameLayout; 47 | import android.widget.ImageButton; 48 | import android.widget.ProgressBar; 49 | import android.widget.SeekBar; 50 | import android.widget.SeekBar.OnSeekBarChangeListener; 51 | import android.widget.TextView; 52 | 53 | /** 54 | * A view containing controls for a MediaPlayer. Typically contains the 55 | * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress 56 | * slider. It takes care of synchronizing the controls with the state 57 | * of the MediaPlayer. 58 | *

59 | * The way to use this class is to instantiate it programatically. 60 | * The MediaController will create a default set of controls 61 | * and put them in a window floating above your application. Specifically, 62 | * the controls will float above the view specified with setAnchorView(). 63 | * The window will disappear if left idle for three seconds and reappear 64 | * when the user touches the anchor view. 65 | *

66 | * Functions like show() and hide() have no effect when MediaController 67 | * is created in an xml layout. 68 | * 69 | * MediaController will hide and 70 | * show the buttons according to these rules: 71 | *

80 | */ 81 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 82 | public class MediaController extends FrameLayout { 83 | 84 | private MediaPlayerControl mPlayer; 85 | private Context mContext; 86 | private View mAnchor; 87 | private View mRoot; 88 | private WindowManager mWindowManager; 89 | private Window mWindow; 90 | private View mDecor; 91 | private WindowManager.LayoutParams mDecorLayoutParams; 92 | private ProgressBar mProgress; 93 | private TextView mEndTime, mCurrentTime; 94 | private boolean mShowing; 95 | private boolean mDragging; 96 | private static final int sDefaultTimeout = 3000; 97 | private static final int FADE_OUT = 1; 98 | private static final int SHOW_PROGRESS = 2; 99 | private boolean mUseFastForward; 100 | private boolean mFromXml; 101 | private boolean mListenersSet; 102 | private View.OnClickListener mNextListener, mPrevListener; 103 | StringBuilder mFormatBuilder; 104 | Formatter mFormatter; 105 | private ImageButton mPauseButton; 106 | private ImageButton mFfwdButton; 107 | private ImageButton mRewButton; 108 | private ImageButton mNextButton; 109 | private ImageButton mPrevButton; 110 | 111 | public MediaController(Context context, AttributeSet attrs) { 112 | super(context, attrs); 113 | mRoot = this; 114 | mContext = context; 115 | mUseFastForward = true; 116 | mFromXml = true; 117 | } 118 | 119 | @Override 120 | public void onFinishInflate() { 121 | if (mRoot != null) 122 | initControllerView(mRoot); 123 | } 124 | 125 | public MediaController(Context context, boolean useFastForward) { 126 | super(context); 127 | mContext = context; 128 | mUseFastForward = useFastForward; 129 | initFloatingWindowLayout(); 130 | initFloatingWindow(); 131 | } 132 | 133 | public MediaController(Context context) { 134 | this(context, true); 135 | } 136 | 137 | private void initFloatingWindow() { 138 | mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); 139 | mWindow = PolicyCompat.createWindow(mContext); 140 | mWindow.setWindowManager(mWindowManager, null, null); 141 | mWindow.requestFeature(Window.FEATURE_NO_TITLE); 142 | mDecor = mWindow.getDecorView(); 143 | mDecor.setOnTouchListener(mTouchListener); 144 | mWindow.setContentView(this); 145 | mWindow.setBackgroundDrawableResource(android.R.color.transparent); 146 | 147 | // While the media controller is up, the volume control keys should 148 | // affect the media stream type 149 | mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC); 150 | 151 | setFocusable(true); 152 | setFocusableInTouchMode(true); 153 | setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); 154 | requestFocus(); 155 | } 156 | 157 | // Allocate and initialize the static parts of mDecorLayoutParams. Must 158 | // also call updateFloatingWindowLayout() to fill in the dynamic parts 159 | // (y and width) before mDecorLayoutParams can be used. 160 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 161 | private void initFloatingWindowLayout() { 162 | mDecorLayoutParams = new WindowManager.LayoutParams(); 163 | WindowManager.LayoutParams p = mDecorLayoutParams; 164 | p.gravity = Gravity.TOP | Gravity.LEFT; 165 | p.height = LayoutParams.WRAP_CONTENT; 166 | p.x = 0; 167 | p.format = PixelFormat.TRANSLUCENT; 168 | p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 169 | p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 170 | | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 171 | | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; 172 | p.token = null; 173 | p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; 174 | } 175 | 176 | // Update the dynamic parts of mDecorLayoutParams 177 | // Must be called with mAnchor != NULL. 178 | private void updateFloatingWindowLayout() { 179 | int [] anchorPos = new int[2]; 180 | mAnchor.getLocationOnScreen(anchorPos); 181 | 182 | // we need to know the size of the controller so we can properly position it 183 | // within its space 184 | mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST), 185 | MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST)); 186 | 187 | WindowManager.LayoutParams p = mDecorLayoutParams; 188 | p.width = mAnchor.getWidth(); 189 | p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; 190 | p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); 191 | } 192 | 193 | // This is called whenever mAnchor's layout bound changes 194 | private OnLayoutChangeListener mLayoutChangeListener = 195 | (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) ? 196 | new OnLayoutChangeListener() { 197 | public void onLayoutChange(View v, int left, int top, int right, 198 | int bottom, int oldLeft, int oldTop, int oldRight, 199 | int oldBottom) { 200 | updateFloatingWindowLayout(); 201 | if (mShowing) { 202 | mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams); 203 | } 204 | } 205 | } : 206 | null; 207 | 208 | private OnTouchListener mTouchListener = new OnTouchListener() { 209 | public boolean onTouch(View v, MotionEvent event) { 210 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 211 | if (mShowing) { 212 | hide(); 213 | } 214 | } 215 | return false; 216 | } 217 | }; 218 | 219 | public void setMediaPlayer(MediaPlayerControl player) { 220 | mPlayer = player; 221 | updatePausePlay(); 222 | } 223 | 224 | /** 225 | * Set the view that acts as the anchor for the control view. 226 | * This can for example be a VideoView, or your Activity's main view. 227 | * When VideoView calls this method, it will use the VideoView's parent 228 | * as the anchor. 229 | * @param view The view to which to anchor the controller when it is visible. 230 | */ 231 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 232 | public void setAnchorView(View view) { 233 | boolean hasOnLayoutChangeListener = (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB); 234 | 235 | if (hasOnLayoutChangeListener && mAnchor != null) { 236 | mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener); 237 | } 238 | mAnchor = view; 239 | if (hasOnLayoutChangeListener && mAnchor != null) { 240 | mAnchor.addOnLayoutChangeListener(mLayoutChangeListener); 241 | } 242 | 243 | FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( 244 | ViewGroup.LayoutParams.MATCH_PARENT, 245 | ViewGroup.LayoutParams.MATCH_PARENT 246 | ); 247 | 248 | removeAllViews(); 249 | mRoot = makeControllerView(); 250 | initControllerView(mRoot); 251 | addView(mRoot, frameParams); 252 | } 253 | 254 | /** 255 | * Create the view that holds the widgets that control playback. 256 | * Derived classes can override this to create their own. 257 | * @return The controller view. 258 | * @hide This doesn't work as advertised 259 | */ 260 | protected View makeControllerView() { 261 | LayoutInflater inflate = LayoutInflater.from(getContext()); 262 | return inflate.inflate(R.layout.vvc_media_controller, null); 263 | } 264 | 265 | private void initControllerView(View v) { 266 | mPauseButton = (ImageButton) v.findViewById(R.id.pause); 267 | if (mPauseButton != null) { 268 | mPauseButton.requestFocus(); 269 | mPauseButton.setOnClickListener(mPauseListener); 270 | } 271 | 272 | mFfwdButton = (ImageButton) v.findViewById(R.id.ffwd); 273 | if (mFfwdButton != null) { 274 | mFfwdButton.setOnClickListener(mFfwdListener); 275 | if (!mFromXml) { 276 | mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); 277 | } 278 | } 279 | 280 | mRewButton = (ImageButton) v.findViewById(R.id.rew); 281 | if (mRewButton != null) { 282 | mRewButton.setOnClickListener(mRewListener); 283 | if (!mFromXml) { 284 | mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); 285 | } 286 | } 287 | 288 | // By default these are hidden. They will be enabled when setPrevNextListeners() is called 289 | mNextButton = (ImageButton) v.findViewById(R.id.next); 290 | if (mNextButton != null && !mFromXml && !mListenersSet) { 291 | mNextButton.setVisibility(View.GONE); 292 | } 293 | mPrevButton = (ImageButton) v.findViewById(R.id.prev); 294 | if (mPrevButton != null && !mFromXml && !mListenersSet) { 295 | mPrevButton.setVisibility(View.GONE); 296 | } 297 | 298 | mProgress = (ProgressBar) v.findViewById(R.id.mediacontroller_progress); 299 | if (mProgress != null) { 300 | if (mProgress instanceof SeekBar) { 301 | SeekBar seeker = (SeekBar) mProgress; 302 | seeker.setOnSeekBarChangeListener(mSeekListener); 303 | } 304 | mProgress.setMax(1000); 305 | } 306 | 307 | mEndTime = (TextView) v.findViewById(R.id.time); 308 | mCurrentTime = (TextView) v.findViewById(R.id.time_current); 309 | mFormatBuilder = new StringBuilder(); 310 | mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); 311 | 312 | installPrevNextListeners(); 313 | } 314 | 315 | /** 316 | * Show the controller on screen. It will go away 317 | * automatically after 3 seconds of inactivity. 318 | */ 319 | public void show() { 320 | show(sDefaultTimeout); 321 | } 322 | 323 | /** 324 | * Disable pause or seek buttons if the stream cannot be paused or seeked. 325 | * This requires the control interface to be a MediaPlayerControlExt 326 | */ 327 | private void disableUnsupportedButtons() { 328 | try { 329 | if (mPauseButton != null && !mPlayer.canPause()) { 330 | mPauseButton.setEnabled(false); 331 | } 332 | if (mRewButton != null && !mPlayer.canSeekBackward()) { 333 | mRewButton.setEnabled(false); 334 | } 335 | if (mFfwdButton != null && !mPlayer.canSeekForward()) { 336 | mFfwdButton.setEnabled(false); 337 | } 338 | } catch (IncompatibleClassChangeError ex) { 339 | // We were given an old version of the interface, that doesn't have 340 | // the canPause/canSeekXYZ methods. This is OK, it just means we 341 | // assume the media can be paused and seeked, and so we don't disable 342 | // the buttons. 343 | } 344 | } 345 | 346 | /** 347 | * Show the controller on screen. It will go away 348 | * automatically after 'timeout' milliseconds of inactivity. 349 | * @param timeout The timeout in milliseconds. Use 0 to show 350 | * the controller until hide() is called. 351 | */ 352 | public void show(int timeout) { 353 | if (!mShowing && mAnchor != null) { 354 | setProgress(); 355 | if (mPauseButton != null) { 356 | mPauseButton.requestFocus(); 357 | } 358 | disableUnsupportedButtons(); 359 | updateFloatingWindowLayout(); 360 | mWindowManager.addView(mDecor, mDecorLayoutParams); 361 | mShowing = true; 362 | } 363 | updatePausePlay(); 364 | 365 | // cause the progress bar to be updated even if mShowing 366 | // was already true. This happens, for example, if we're 367 | // paused with the progress bar showing the user hits play. 368 | mHandler.sendEmptyMessage(SHOW_PROGRESS); 369 | 370 | Message msg = mHandler.obtainMessage(FADE_OUT); 371 | if (timeout != 0) { 372 | mHandler.removeMessages(FADE_OUT); 373 | mHandler.sendMessageDelayed(msg, timeout); 374 | } 375 | } 376 | 377 | public boolean isShowing() { 378 | return mShowing; 379 | } 380 | 381 | /** 382 | * Remove the controller from the screen. 383 | */ 384 | public void hide() { 385 | if (mAnchor == null) 386 | return; 387 | 388 | if (mShowing) { 389 | try { 390 | mHandler.removeMessages(SHOW_PROGRESS); 391 | mWindowManager.removeView(mDecor); 392 | } catch (IllegalArgumentException ex) { 393 | Log.w("MediaController", "already removed"); 394 | } 395 | mShowing = false; 396 | } 397 | } 398 | 399 | @SuppressLint("HandlerLeak") 400 | private Handler mHandler = new Handler() { 401 | @Override 402 | public void handleMessage(Message msg) { 403 | int pos; 404 | switch (msg.what) { 405 | case FADE_OUT: 406 | hide(); 407 | break; 408 | case SHOW_PROGRESS: 409 | pos = setProgress(); 410 | if (!mDragging && mShowing && mPlayer.isPlaying()) { 411 | msg = obtainMessage(SHOW_PROGRESS); 412 | sendMessageDelayed(msg, 1000 - (pos % 1000)); 413 | } 414 | break; 415 | } 416 | } 417 | }; 418 | 419 | private String stringForTime(int timeMs) { 420 | int totalSeconds = timeMs / 1000; 421 | 422 | int seconds = totalSeconds % 60; 423 | int minutes = (totalSeconds / 60) % 60; 424 | int hours = totalSeconds / 3600; 425 | 426 | mFormatBuilder.setLength(0); 427 | if (hours > 0) { 428 | return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); 429 | } else { 430 | return mFormatter.format("%02d:%02d", minutes, seconds).toString(); 431 | } 432 | } 433 | 434 | private int setProgress() { 435 | if (mPlayer == null || mDragging) { 436 | return 0; 437 | } 438 | int position = mPlayer.getCurrentPosition(); 439 | int duration = mPlayer.getDuration(); 440 | if (mProgress != null) { 441 | if (duration > 0) { 442 | // use long to avoid overflow 443 | long pos = 1000L * position / duration; 444 | mProgress.setProgress( (int) pos); 445 | } 446 | int percent = mPlayer.getBufferPercentage(); 447 | mProgress.setSecondaryProgress(percent * 10); 448 | } 449 | 450 | if (mEndTime != null) 451 | mEndTime.setText(stringForTime(duration)); 452 | if (mCurrentTime != null) 453 | mCurrentTime.setText(stringForTime(position)); 454 | 455 | return position; 456 | } 457 | 458 | @Override 459 | public boolean onTouchEvent(MotionEvent event) { 460 | switch (event.getAction()) { 461 | case MotionEvent.ACTION_DOWN: 462 | show(0); // show until hide is called 463 | break; 464 | case MotionEvent.ACTION_UP: 465 | show(sDefaultTimeout); // start timeout 466 | break; 467 | case MotionEvent.ACTION_CANCEL: 468 | hide(); 469 | break; 470 | default: 471 | break; 472 | } 473 | return true; 474 | } 475 | 476 | @Override 477 | public boolean onTrackballEvent(MotionEvent ev) { 478 | show(sDefaultTimeout); 479 | return false; 480 | } 481 | 482 | @Override 483 | public boolean dispatchKeyEvent(KeyEvent event) { 484 | int keyCode = event.getKeyCode(); 485 | final boolean uniqueDown = event.getRepeatCount() == 0 486 | && event.getAction() == KeyEvent.ACTION_DOWN; 487 | if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK 488 | || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE 489 | || keyCode == KeyEvent.KEYCODE_SPACE) { 490 | if (uniqueDown) { 491 | doPauseResume(); 492 | show(sDefaultTimeout); 493 | if (mPauseButton != null) { 494 | mPauseButton.requestFocus(); 495 | } 496 | } 497 | return true; 498 | } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { 499 | if (uniqueDown && !mPlayer.isPlaying()) { 500 | mPlayer.start(); 501 | updatePausePlay(); 502 | show(sDefaultTimeout); 503 | } 504 | return true; 505 | } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 506 | || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { 507 | if (uniqueDown && mPlayer.isPlaying()) { 508 | mPlayer.pause(); 509 | updatePausePlay(); 510 | show(sDefaultTimeout); 511 | } 512 | return true; 513 | } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 514 | || keyCode == KeyEvent.KEYCODE_VOLUME_UP 515 | || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE 516 | || keyCode == KeyEvent.KEYCODE_CAMERA) { 517 | // don't show the controls for volume adjustment 518 | return super.dispatchKeyEvent(event); 519 | } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { 520 | if (uniqueDown) { 521 | hide(); 522 | } 523 | return true; 524 | } 525 | 526 | show(sDefaultTimeout); 527 | return super.dispatchKeyEvent(event); 528 | } 529 | 530 | private View.OnClickListener mPauseListener = new View.OnClickListener() { 531 | public void onClick(View v) { 532 | doPauseResume(); 533 | show(sDefaultTimeout); 534 | } 535 | }; 536 | 537 | private void updatePausePlay() { 538 | if (mRoot != null && mPauseButton != null) 539 | updatePausePlay(mPlayer.isPlaying(), mPauseButton); 540 | } 541 | 542 | protected void updatePausePlay(boolean isPlaying, ImageButton pauseButton) { 543 | if (isPlaying) { 544 | pauseButton.setImageResource(R.drawable.vvc_ic_media_pause); 545 | } else { 546 | pauseButton.setImageResource(R.drawable.vvc_ic_media_play); 547 | } 548 | } 549 | 550 | private void doPauseResume() { 551 | if (mPlayer.isPlaying()) { 552 | mPlayer.pause(); 553 | } else { 554 | mPlayer.start(); 555 | } 556 | updatePausePlay(); 557 | } 558 | 559 | // There are two scenarios that can trigger the seekbar listener to trigger: 560 | // 561 | // The first is the user using the touchpad to adjust the posititon of the 562 | // seekbar's thumb. In this case onStartTrackingTouch is called followed by 563 | // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. 564 | // We're setting the field "mDragging" to true for the duration of the dragging 565 | // session to avoid jumps in the position in case of ongoing playback. 566 | // 567 | // The second scenario involves the user operating the scroll ball, in this 568 | // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, 569 | // we will simply apply the updated position without suspending regular updates. 570 | private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { 571 | public void onStartTrackingTouch(SeekBar bar) { 572 | show(3600000); 573 | 574 | mDragging = true; 575 | 576 | // By removing these pending progress messages we make sure 577 | // that a) we won't update the progress while the user adjusts 578 | // the seekbar and b) once the user is done dragging the thumb 579 | // we will post one of these messages to the queue again and 580 | // this ensures that there will be exactly one message queued up. 581 | mHandler.removeMessages(SHOW_PROGRESS); 582 | } 583 | 584 | public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { 585 | if (!fromuser) { 586 | // We're not interested in programmatically generated changes to 587 | // the progress bar's position. 588 | return; 589 | } 590 | 591 | long duration = mPlayer.getDuration(); 592 | long newposition = (duration * progress) / 1000L; 593 | mPlayer.seekTo( (int) newposition); 594 | if (mCurrentTime != null) 595 | mCurrentTime.setText(stringForTime( (int) newposition)); 596 | } 597 | 598 | public void onStopTrackingTouch(SeekBar bar) { 599 | mDragging = false; 600 | setProgress(); 601 | updatePausePlay(); 602 | show(sDefaultTimeout); 603 | 604 | // Ensure that progress is properly updated in the future, 605 | // the call to show() does not guarantee this because it is a 606 | // no-op if we are already showing. 607 | mHandler.sendEmptyMessage(SHOW_PROGRESS); 608 | } 609 | }; 610 | 611 | @Override 612 | public void setEnabled(boolean enabled) { 613 | if (mPauseButton != null) { 614 | mPauseButton.setEnabled(enabled); 615 | } 616 | if (mFfwdButton != null) { 617 | mFfwdButton.setEnabled(enabled); 618 | } 619 | if (mRewButton != null) { 620 | mRewButton.setEnabled(enabled); 621 | } 622 | if (mNextButton != null) { 623 | mNextButton.setEnabled(enabled && mNextListener != null); 624 | } 625 | if (mPrevButton != null) { 626 | mPrevButton.setEnabled(enabled && mPrevListener != null); 627 | } 628 | if (mProgress != null) { 629 | mProgress.setEnabled(enabled); 630 | } 631 | disableUnsupportedButtons(); 632 | super.setEnabled(enabled); 633 | } 634 | 635 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 636 | @Override 637 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 638 | super.onInitializeAccessibilityEvent(event); 639 | event.setClassName(MediaController.class.getName()); 640 | } 641 | 642 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 643 | @Override 644 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 645 | super.onInitializeAccessibilityNodeInfo(info); 646 | info.setClassName(MediaController.class.getName()); 647 | } 648 | 649 | private View.OnClickListener mRewListener = new View.OnClickListener() { 650 | public void onClick(View v) { 651 | int pos = mPlayer.getCurrentPosition(); 652 | pos -= 5000; // milliseconds 653 | mPlayer.seekTo(pos); 654 | setProgress(); 655 | 656 | show(sDefaultTimeout); 657 | } 658 | }; 659 | 660 | private View.OnClickListener mFfwdListener = new View.OnClickListener() { 661 | public void onClick(View v) { 662 | int pos = mPlayer.getCurrentPosition(); 663 | pos += 15000; // milliseconds 664 | mPlayer.seekTo(pos); 665 | setProgress(); 666 | 667 | show(sDefaultTimeout); 668 | } 669 | }; 670 | 671 | private void installPrevNextListeners() { 672 | if (mNextButton != null) { 673 | mNextButton.setOnClickListener(mNextListener); 674 | mNextButton.setEnabled(mNextListener != null); 675 | } 676 | 677 | if (mPrevButton != null) { 678 | mPrevButton.setOnClickListener(mPrevListener); 679 | mPrevButton.setEnabled(mPrevListener != null); 680 | } 681 | } 682 | 683 | public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { 684 | mNextListener = next; 685 | mPrevListener = prev; 686 | mListenersSet = true; 687 | 688 | if (mRoot != null) { 689 | installPrevNextListeners(); 690 | 691 | if (mNextButton != null && !mFromXml) { 692 | mNextButton.setVisibility(View.VISIBLE); 693 | } 694 | if (mPrevButton != null && !mFromXml) { 695 | mPrevButton.setVisibility(View.VISIBLE); 696 | } 697 | } 698 | } 699 | 700 | public interface MediaPlayerControl { 701 | void start(); 702 | void pause(); 703 | int getDuration(); 704 | int getCurrentPosition(); 705 | void seekTo(int pos); 706 | boolean isPlaying(); 707 | int getBufferPercentage(); 708 | boolean canPause(); 709 | boolean canSeekBackward(); 710 | boolean canSeekForward(); 711 | 712 | /** 713 | * Get the audio session id for the player used by this VideoView. This can be used to 714 | * apply audio effects to the audio track of a video. 715 | * @return The audio session, or 0 if there was an error. 716 | */ 717 | int getAudioSessionId(); 718 | } 719 | } -------------------------------------------------------------------------------- /library/src/main/java/com/lowlevel/videoviewcompat/VideoView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2006 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.lowlevel.videoviewcompat; 18 | 19 | import java.io.IOException; 20 | import java.lang.reflect.Method; 21 | import java.util.Map; 22 | 23 | import android.annotation.TargetApi; 24 | import android.content.Context; 25 | import android.media.AudioManager; 26 | import android.media.MediaPlayer; 27 | import android.media.MediaPlayer.OnCompletionListener; 28 | import android.media.MediaPlayer.OnErrorListener; 29 | import android.media.MediaPlayer.OnInfoListener; 30 | import android.net.Uri; 31 | import android.os.Build; 32 | import android.util.AttributeSet; 33 | import android.util.Log; 34 | import android.view.KeyEvent; 35 | import android.view.MotionEvent; 36 | import android.view.SurfaceHolder; 37 | import android.view.SurfaceView; 38 | import android.view.View; 39 | import android.view.accessibility.AccessibilityEvent; 40 | import android.view.accessibility.AccessibilityNodeInfo; 41 | 42 | import com.lowlevel.videoviewcompat.MediaController.MediaPlayerControl; 43 | 44 | /** 45 | * Displays a video file. The VideoView class 46 | * can load images from various sources (such as resources or content 47 | * providers), takes care of computing its measurement from the video so that 48 | * it can be used in any layout manager, and provides various display options 49 | * such as scaling and tinting.

50 | * 51 | * Note: VideoView does not retain its full state when going into the 52 | * background. In particular, it does not restore the current play state, 53 | * play position, selected tracks, or any subtitle tracks added via 54 | * {@link #addSubtitleSource addSubtitleSource()}. Applications should 55 | * save and restore these on their own in 56 | * {@link android.app.Activity#onSaveInstanceState} and 57 | * {@link android.app.Activity#onRestoreInstanceState}.

58 | * Also note that the audio session id (from {@link #getAudioSessionId}) may 59 | * change from its previously returned value when the VideoView is restored. 60 | */ 61 | public class VideoView extends SurfaceView implements MediaPlayerControl { 62 | private String TAG = "VideoView"; 63 | // settable by the client 64 | private Uri mUri; 65 | private Map mHeaders; 66 | 67 | // all possible internal states 68 | private static final int STATE_ERROR = -1; 69 | private static final int STATE_IDLE = 0; 70 | private static final int STATE_PREPARING = 1; 71 | private static final int STATE_PREPARED = 2; 72 | private static final int STATE_PLAYING = 3; 73 | private static final int STATE_PAUSED = 4; 74 | private static final int STATE_PLAYBACK_COMPLETED = 5; 75 | 76 | // mCurrentState is a VideoView object's current state. 77 | // mTargetState is the state that a method caller intends to reach. 78 | // For instance, regardless the VideoView object's current state, 79 | // calling pause() intends to bring the object to a target state 80 | // of STATE_PAUSED. 81 | private int mCurrentState = STATE_IDLE; 82 | private int mTargetState = STATE_IDLE; 83 | 84 | // All the stuff we need for playing and showing a video 85 | private SurfaceHolder mSurfaceHolder = null; 86 | private MediaPlayer mMediaPlayer = null; 87 | private int mAudioSession; 88 | private int mVideoWidth; 89 | private int mVideoHeight; 90 | private int mSurfaceWidth; 91 | private int mSurfaceHeight; 92 | private MediaController mMediaController; 93 | private OnCompletionListener mOnCompletionListener; 94 | private MediaPlayer.OnPreparedListener mOnPreparedListener; 95 | private int mCurrentBufferPercentage; 96 | private OnErrorListener mOnErrorListener; 97 | private OnInfoListener mOnInfoListener; 98 | private int mSeekWhenPrepared; // recording the seek position while preparing 99 | private boolean mCanPause; 100 | private boolean mCanSeekBack; 101 | private boolean mCanSeekForward; 102 | 103 | public VideoView(Context context) { 104 | super(context); 105 | initVideoView(); 106 | } 107 | 108 | public VideoView(Context context, AttributeSet attrs) { 109 | super(context, attrs); 110 | initVideoView(); 111 | } 112 | 113 | public VideoView(Context context, AttributeSet attrs, int defStyleAttr) { 114 | super(context, attrs, defStyleAttr); 115 | initVideoView(); 116 | } 117 | 118 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 119 | public VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 120 | super(context, attrs, defStyleAttr, defStyleRes); 121 | initVideoView(); 122 | } 123 | 124 | @Override 125 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 126 | //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", " 127 | // + MeasureSpec.toString(heightMeasureSpec) + ")"); 128 | 129 | int width = getDefaultSize(mVideoWidth, widthMeasureSpec); 130 | int height = getDefaultSize(mVideoHeight, heightMeasureSpec); 131 | if (mVideoWidth > 0 && mVideoHeight > 0) { 132 | 133 | int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 134 | int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 135 | int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 136 | int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 137 | 138 | if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { 139 | // the size is fixed 140 | width = widthSpecSize; 141 | height = heightSpecSize; 142 | 143 | // for compatibility, we adjust size based on aspect ratio 144 | if ( mVideoWidth * height < width * mVideoHeight ) { 145 | //Log.i("@@@", "image too wide, correcting"); 146 | width = height * mVideoWidth / mVideoHeight; 147 | } else if ( mVideoWidth * height > width * mVideoHeight ) { 148 | //Log.i("@@@", "image too tall, correcting"); 149 | height = width * mVideoHeight / mVideoWidth; 150 | } 151 | } else if (widthSpecMode == MeasureSpec.EXACTLY) { 152 | // only the width is fixed, adjust the height to match aspect ratio if possible 153 | width = widthSpecSize; 154 | height = width * mVideoHeight / mVideoWidth; 155 | if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { 156 | // couldn't match aspect ratio within the constraints 157 | height = heightSpecSize; 158 | } 159 | } else if (heightSpecMode == MeasureSpec.EXACTLY) { 160 | // only the height is fixed, adjust the width to match aspect ratio if possible 161 | height = heightSpecSize; 162 | width = height * mVideoWidth / mVideoHeight; 163 | if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { 164 | // couldn't match aspect ratio within the constraints 165 | width = widthSpecSize; 166 | } 167 | } else { 168 | // neither the width nor the height are fixed, try to use actual video size 169 | width = mVideoWidth; 170 | height = mVideoHeight; 171 | if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { 172 | // too tall, decrease both width and height 173 | height = heightSpecSize; 174 | width = height * mVideoWidth / mVideoHeight; 175 | } 176 | if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { 177 | // too wide, decrease both width and height 178 | width = widthSpecSize; 179 | height = width * mVideoHeight / mVideoWidth; 180 | } 181 | } 182 | } else { 183 | // no size yet, just adopt the given spec sizes 184 | } 185 | setMeasuredDimension(width, height); 186 | } 187 | 188 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 189 | @Override 190 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 191 | super.onInitializeAccessibilityEvent(event); 192 | event.setClassName(VideoView.class.getName()); 193 | } 194 | 195 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 196 | @Override 197 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 198 | super.onInitializeAccessibilityNodeInfo(info); 199 | info.setClassName(VideoView.class.getName()); 200 | } 201 | 202 | public int resolveAdjustedSize(int desiredSize, int measureSpec) { 203 | return getDefaultSize(desiredSize, measureSpec); 204 | } 205 | 206 | @SuppressWarnings("deprecation") 207 | private void initVideoView() { 208 | mVideoWidth = 0; 209 | mVideoHeight = 0; 210 | getHolder().addCallback(mSHCallback); 211 | getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 212 | setFocusable(true); 213 | setFocusableInTouchMode(true); 214 | requestFocus(); 215 | mCurrentState = STATE_IDLE; 216 | mTargetState = STATE_IDLE; 217 | } 218 | 219 | /** 220 | * Sets video path. 221 | * 222 | * @param path the path of the video. 223 | */ 224 | public void setVideoPath(String path) { 225 | setVideoURI(Uri.parse(path)); 226 | } 227 | 228 | /** 229 | * Sets video URI. 230 | * 231 | * @param uri the URI of the video. 232 | */ 233 | public void setVideoURI(Uri uri) { 234 | setVideoURI(uri, null); 235 | } 236 | 237 | /** 238 | * Sets video URI using specific headers. 239 | * 240 | * @param uri the URI of the video. 241 | * @param headers the headers for the URI request. 242 | * Note that the cross domain redirection is allowed by default, but that can be 243 | * changed with key/value pairs through the headers parameter with 244 | * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value 245 | * to disallow or allow cross domain redirection. 246 | */ 247 | public void setVideoURI(Uri uri, Map headers) { 248 | mUri = uri; 249 | mHeaders = headers; 250 | mSeekWhenPrepared = 0; 251 | openVideo(); 252 | requestLayout(); 253 | invalidate(); 254 | } 255 | 256 | public void stopPlayback() { 257 | if (mMediaPlayer != null) { 258 | mMediaPlayer.stop(); 259 | mMediaPlayer.release(); 260 | mMediaPlayer = null; 261 | mCurrentState = STATE_IDLE; 262 | mTargetState = STATE_IDLE; 263 | } 264 | } 265 | 266 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 267 | private void openVideo() { 268 | if (mUri == null || mSurfaceHolder == null) { 269 | // not ready for playback just yet, will try again later 270 | return; 271 | } 272 | AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); 273 | am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 274 | 275 | // we shouldn't clear the target state, because somebody might have 276 | // called start() previously 277 | release(false); 278 | try { 279 | mMediaPlayer = new MediaPlayer(); 280 | mMediaPlayer.setOnPreparedListener(mPreparedListener); 281 | mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); 282 | mMediaPlayer.setOnCompletionListener(mCompletionListener); 283 | mMediaPlayer.setOnErrorListener(mErrorListener); 284 | mMediaPlayer.setOnInfoListener(mOnInfoListener); 285 | mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); 286 | mCurrentBufferPercentage = 0; 287 | try { 288 | Method m = MediaPlayer.class.getMethod("setDataSource", Context.class, Uri.class, Map.class); 289 | m.setAccessible(true); 290 | m.invoke(mMediaPlayer, getContext(), mUri, mHeaders); 291 | } catch (Exception e) { 292 | mMediaPlayer.setDataSource(getContext(), mUri); 293 | } 294 | mMediaPlayer.setDisplay(mSurfaceHolder); 295 | mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 296 | mMediaPlayer.setScreenOnWhilePlaying(true); 297 | mMediaPlayer.prepareAsync(); 298 | // we don't set the target state here either, but preserve the 299 | // target state that was there before. 300 | mCurrentState = STATE_PREPARING; 301 | attachMediaController(); 302 | } catch (IOException ex) { 303 | Log.w(TAG, "Unable to open content: " + mUri, ex); 304 | mCurrentState = STATE_ERROR; 305 | mTargetState = STATE_ERROR; 306 | mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); 307 | return; 308 | } catch (IllegalArgumentException ex) { 309 | Log.w(TAG, "Unable to open content: " + mUri, ex); 310 | mCurrentState = STATE_ERROR; 311 | mTargetState = STATE_ERROR; 312 | mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); 313 | return; 314 | } 315 | } 316 | 317 | public void setMediaController(MediaController controller) { 318 | if (mMediaController != null) { 319 | mMediaController.hide(); 320 | } 321 | mMediaController = controller; 322 | attachMediaController(); 323 | } 324 | 325 | private void attachMediaController() { 326 | if (mMediaPlayer != null && mMediaController != null) { 327 | mMediaController.setMediaPlayer(this); 328 | View anchorView = this.getParent() instanceof View ? 329 | (View)this.getParent() : this; 330 | mMediaController.setAnchorView(anchorView); 331 | mMediaController.setEnabled(isInPlaybackState()); 332 | } 333 | } 334 | 335 | MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = 336 | new MediaPlayer.OnVideoSizeChangedListener() { 337 | public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { 338 | mVideoWidth = mp.getVideoWidth(); 339 | mVideoHeight = mp.getVideoHeight(); 340 | if (mVideoWidth != 0 && mVideoHeight != 0) { 341 | getHolder().setFixedSize(mVideoWidth, mVideoHeight); 342 | requestLayout(); 343 | } 344 | } 345 | }; 346 | 347 | MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { 348 | public void onPrepared(MediaPlayer mp) { 349 | mCurrentState = STATE_PREPARED; 350 | 351 | mCanPause = mCanSeekBack = mCanSeekForward = true; 352 | 353 | if (mOnPreparedListener != null) { 354 | mOnPreparedListener.onPrepared(mMediaPlayer); 355 | } 356 | if (mMediaController != null) { 357 | mMediaController.setEnabled(true); 358 | } 359 | mVideoWidth = mp.getVideoWidth(); 360 | mVideoHeight = mp.getVideoHeight(); 361 | 362 | int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call 363 | if (seekToPosition != 0) { 364 | seekTo(seekToPosition); 365 | } 366 | if (mVideoWidth != 0 && mVideoHeight != 0) { 367 | //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight); 368 | getHolder().setFixedSize(mVideoWidth, mVideoHeight); 369 | if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { 370 | // We didn't actually change the size (it was already at the size 371 | // we need), so we won't get a "surface changed" callback, so 372 | // start the video here instead of in the callback. 373 | if (mTargetState == STATE_PLAYING) { 374 | start(); 375 | if (mMediaController != null) { 376 | mMediaController.show(); 377 | } 378 | } else if (!isPlaying() && 379 | (seekToPosition != 0 || getCurrentPosition() > 0)) { 380 | if (mMediaController != null) { 381 | // Show the media controls when we're paused into a video and make 'em stick. 382 | mMediaController.show(0); 383 | } 384 | } 385 | } 386 | } else { 387 | // We don't know the video size yet, but should start anyway. 388 | // The video size might be reported to us later. 389 | if (mTargetState == STATE_PLAYING) { 390 | start(); 391 | } 392 | } 393 | } 394 | }; 395 | 396 | private MediaPlayer.OnCompletionListener mCompletionListener = 397 | new MediaPlayer.OnCompletionListener() { 398 | public void onCompletion(MediaPlayer mp) { 399 | mCurrentState = STATE_PLAYBACK_COMPLETED; 400 | mTargetState = STATE_PLAYBACK_COMPLETED; 401 | if (mMediaController != null) { 402 | mMediaController.hide(); 403 | } 404 | if (mOnCompletionListener != null) { 405 | mOnCompletionListener.onCompletion(mMediaPlayer); 406 | } 407 | } 408 | }; 409 | 410 | private MediaPlayer.OnErrorListener mErrorListener = 411 | new MediaPlayer.OnErrorListener() { 412 | public boolean onError(MediaPlayer mp, int framework_err, int impl_err) { 413 | Log.d(TAG, "Error: " + framework_err + "," + impl_err); 414 | mCurrentState = STATE_ERROR; 415 | mTargetState = STATE_ERROR; 416 | if (mMediaController != null) { 417 | mMediaController.hide(); 418 | } 419 | 420 | /* If an error handler has been supplied, use it and finish. */ 421 | if (mOnErrorListener != null) { 422 | if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) { 423 | return true; 424 | } 425 | } 426 | 427 | return true; 428 | } 429 | }; 430 | 431 | private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener = 432 | new MediaPlayer.OnBufferingUpdateListener() { 433 | public void onBufferingUpdate(MediaPlayer mp, int percent) { 434 | mCurrentBufferPercentage = percent; 435 | } 436 | }; 437 | 438 | /** 439 | * Register a callback to be invoked when the media file 440 | * is loaded and ready to go. 441 | * 442 | * @param l The callback that will be run 443 | */ 444 | public void setOnPreparedListener(MediaPlayer.OnPreparedListener l) 445 | { 446 | mOnPreparedListener = l; 447 | } 448 | 449 | /** 450 | * Register a callback to be invoked when the end of a media file 451 | * has been reached during playback. 452 | * 453 | * @param l The callback that will be run 454 | */ 455 | public void setOnCompletionListener(OnCompletionListener l) 456 | { 457 | mOnCompletionListener = l; 458 | } 459 | 460 | /** 461 | * Register a callback to be invoked when an error occurs 462 | * during playback or setup. If no listener is specified, 463 | * or if the listener returned false, VideoView will inform 464 | * the user of any errors. 465 | * 466 | * @param l The callback that will be run 467 | */ 468 | public void setOnErrorListener(OnErrorListener l) 469 | { 470 | mOnErrorListener = l; 471 | } 472 | 473 | /** 474 | * Register a callback to be invoked when an informational event 475 | * occurs during playback or setup. 476 | * 477 | * @param l The callback that will be run 478 | */ 479 | public void setOnInfoListener(OnInfoListener l) { 480 | mOnInfoListener = l; 481 | } 482 | 483 | SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() 484 | { 485 | public void surfaceChanged(SurfaceHolder holder, int format, 486 | int w, int h) 487 | { 488 | mSurfaceWidth = w; 489 | mSurfaceHeight = h; 490 | boolean isValidState = (mTargetState == STATE_PLAYING); 491 | boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h); 492 | if (mMediaPlayer != null && isValidState && hasValidSize) { 493 | if (mSeekWhenPrepared != 0) { 494 | seekTo(mSeekWhenPrepared); 495 | } 496 | start(); 497 | } 498 | } 499 | 500 | public void surfaceCreated(SurfaceHolder holder) 501 | { 502 | mSurfaceHolder = holder; 503 | openVideo(); 504 | } 505 | 506 | public void surfaceDestroyed(SurfaceHolder holder) 507 | { 508 | // after we return from this we can't use the surface any more 509 | mSurfaceHolder = null; 510 | if (mMediaController != null) mMediaController.hide(); 511 | release(true); 512 | } 513 | }; 514 | 515 | /* 516 | * release the media player in any state 517 | */ 518 | private void release(boolean cleartargetstate) { 519 | if (mMediaPlayer != null) { 520 | mMediaPlayer.reset(); 521 | mMediaPlayer.release(); 522 | mMediaPlayer = null; 523 | mCurrentState = STATE_IDLE; 524 | if (cleartargetstate) { 525 | mTargetState = STATE_IDLE; 526 | } 527 | } 528 | } 529 | 530 | @Override 531 | public boolean onTouchEvent(MotionEvent ev) { 532 | if (isInPlaybackState() && mMediaController != null) { 533 | toggleMediaControlsVisiblity(); 534 | } 535 | return false; 536 | } 537 | 538 | @Override 539 | public boolean onTrackballEvent(MotionEvent ev) { 540 | if (isInPlaybackState() && mMediaController != null) { 541 | toggleMediaControlsVisiblity(); 542 | } 543 | return false; 544 | } 545 | 546 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 547 | @Override 548 | public boolean onKeyDown(int keyCode, KeyEvent event) 549 | { 550 | boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && 551 | keyCode != KeyEvent.KEYCODE_VOLUME_UP && 552 | keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && 553 | keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && 554 | keyCode != KeyEvent.KEYCODE_MENU && 555 | keyCode != KeyEvent.KEYCODE_CALL && 556 | keyCode != KeyEvent.KEYCODE_ENDCALL; 557 | if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { 558 | if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || 559 | keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { 560 | if (mMediaPlayer.isPlaying()) { 561 | pause(); 562 | mMediaController.show(); 563 | } else { 564 | start(); 565 | mMediaController.hide(); 566 | } 567 | return true; 568 | } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { 569 | if (!mMediaPlayer.isPlaying()) { 570 | start(); 571 | mMediaController.hide(); 572 | } 573 | return true; 574 | } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 575 | || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { 576 | if (mMediaPlayer.isPlaying()) { 577 | pause(); 578 | mMediaController.show(); 579 | } 580 | return true; 581 | } else { 582 | toggleMediaControlsVisiblity(); 583 | } 584 | } 585 | 586 | return super.onKeyDown(keyCode, event); 587 | } 588 | 589 | private void toggleMediaControlsVisiblity() { 590 | if (mMediaController.isShowing()) { 591 | mMediaController.hide(); 592 | } else { 593 | mMediaController.show(); 594 | } 595 | } 596 | 597 | @Override 598 | public void start() { 599 | if (isInPlaybackState()) { 600 | mMediaPlayer.start(); 601 | mCurrentState = STATE_PLAYING; 602 | } 603 | mTargetState = STATE_PLAYING; 604 | } 605 | 606 | @Override 607 | public void pause() { 608 | if (isInPlaybackState()) { 609 | if (mMediaPlayer.isPlaying()) { 610 | mMediaPlayer.pause(); 611 | mCurrentState = STATE_PAUSED; 612 | } 613 | } 614 | mTargetState = STATE_PAUSED; 615 | } 616 | 617 | public void suspend() { 618 | release(false); 619 | } 620 | 621 | public void resume() { 622 | openVideo(); 623 | } 624 | 625 | @Override 626 | public int getDuration() { 627 | if (isInPlaybackState()) { 628 | return mMediaPlayer.getDuration(); 629 | } 630 | 631 | return -1; 632 | } 633 | 634 | @Override 635 | public int getCurrentPosition() { 636 | if (isInPlaybackState()) { 637 | return mMediaPlayer.getCurrentPosition(); 638 | } 639 | return 0; 640 | } 641 | 642 | @Override 643 | public void seekTo(int msec) { 644 | if (isInPlaybackState()) { 645 | mMediaPlayer.seekTo(msec); 646 | mSeekWhenPrepared = 0; 647 | } else { 648 | mSeekWhenPrepared = msec; 649 | } 650 | } 651 | 652 | @Override 653 | public boolean isPlaying() { 654 | return isInPlaybackState() && mMediaPlayer.isPlaying(); 655 | } 656 | 657 | @Override 658 | public int getBufferPercentage() { 659 | if (mMediaPlayer != null) { 660 | return mCurrentBufferPercentage; 661 | } 662 | return 0; 663 | } 664 | 665 | private boolean isInPlaybackState() { 666 | return (mMediaPlayer != null && 667 | mCurrentState != STATE_ERROR && 668 | mCurrentState != STATE_IDLE && 669 | mCurrentState != STATE_PREPARING); 670 | } 671 | 672 | @Override 673 | public boolean canPause() { 674 | return mCanPause; 675 | } 676 | 677 | @Override 678 | public boolean canSeekBackward() { 679 | return mCanSeekBack; 680 | } 681 | 682 | @Override 683 | public boolean canSeekForward() { 684 | return mCanSeekForward; 685 | } 686 | 687 | @Override 688 | public int getAudioSessionId() { 689 | if (mAudioSession == 0) { 690 | MediaPlayer foo = new MediaPlayer(); 691 | mAudioSession = foo.getAudioSessionId(); 692 | foo.release(); 693 | } 694 | return mAudioSession; 695 | } 696 | } -------------------------------------------------------------------------------- /library/src/main/java/com/lowlevel/videoviewcompat/internal/PolicyCompat.java: -------------------------------------------------------------------------------- 1 | package com.lowlevel.videoviewcompat.internal; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Method; 5 | 6 | import android.content.Context; 7 | import android.os.Build; 8 | import android.view.Window; 9 | 10 | public class PolicyCompat { 11 | /* 12 | * Private constants 13 | */ 14 | private static final String PHONE_WINDOW_CLASS_NAME = "com.android.internal.policy.PhoneWindow"; 15 | private static final String POLICY_MANAGER_CLASS_NAME = "com.android.internal.policy.PolicyManager"; 16 | 17 | 18 | private PolicyCompat() { 19 | } 20 | 21 | 22 | /* 23 | * Private methods 24 | */ 25 | private static Window createPhoneWindow(Context context) { 26 | try { 27 | /* Find class */ 28 | Class cls = Class.forName(PHONE_WINDOW_CLASS_NAME); 29 | 30 | /* Get constructor */ 31 | Constructor c = cls.getConstructor(Context.class); 32 | 33 | /* Create instance */ 34 | return (Window)c.newInstance(context); 35 | } 36 | catch (ClassNotFoundException e) { 37 | throw new RuntimeException(PHONE_WINDOW_CLASS_NAME + " could not be loaded", e); 38 | } 39 | catch (Exception e) { 40 | throw new RuntimeException(PHONE_WINDOW_CLASS_NAME + " class could not be instantiated", e); 41 | } 42 | } 43 | 44 | private static Window makeNewWindow(Context context) { 45 | try { 46 | /* Find class */ 47 | Class cls = Class.forName(POLICY_MANAGER_CLASS_NAME); 48 | 49 | /* Find method */ 50 | Method m = cls.getMethod("makeNewWindow", Context.class); 51 | 52 | /* Invoke method */ 53 | return (Window)m.invoke(null, context); 54 | } 55 | catch (ClassNotFoundException e) { 56 | throw new RuntimeException(POLICY_MANAGER_CLASS_NAME + " could not be loaded", e); 57 | } 58 | catch (Exception e) { 59 | throw new RuntimeException(POLICY_MANAGER_CLASS_NAME + ".makeNewWindow could not be invoked", e); 60 | } 61 | } 62 | 63 | 64 | /* 65 | * Public methods 66 | */ 67 | public static Window createWindow(Context context) { 68 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 69 | return createPhoneWindow(context); 70 | else 71 | return makeNewWindow(context); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /library/src/main/res/drawable-hdpi/vvc_ic_media_ff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-hdpi/vvc_ic_media_ff.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-hdpi/vvc_ic_media_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-hdpi/vvc_ic_media_next.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-hdpi/vvc_ic_media_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-hdpi/vvc_ic_media_pause.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-hdpi/vvc_ic_media_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-hdpi/vvc_ic_media_play.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-hdpi/vvc_ic_media_previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-hdpi/vvc_ic_media_previous.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-hdpi/vvc_ic_media_rew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-hdpi/vvc_ic_media_rew.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-ldpi/vvc_ic_media_ff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-ldpi/vvc_ic_media_ff.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-ldpi/vvc_ic_media_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-ldpi/vvc_ic_media_next.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-ldpi/vvc_ic_media_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-ldpi/vvc_ic_media_pause.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-ldpi/vvc_ic_media_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-ldpi/vvc_ic_media_play.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-ldpi/vvc_ic_media_previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-ldpi/vvc_ic_media_previous.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-ldpi/vvc_ic_media_rew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-ldpi/vvc_ic_media_rew.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-mdpi/vvc_ic_media_ff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-mdpi/vvc_ic_media_ff.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-mdpi/vvc_ic_media_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-mdpi/vvc_ic_media_next.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-mdpi/vvc_ic_media_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-mdpi/vvc_ic_media_pause.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-mdpi/vvc_ic_media_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-mdpi/vvc_ic_media_play.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-mdpi/vvc_ic_media_previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-mdpi/vvc_ic_media_previous.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-mdpi/vvc_ic_media_rew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-mdpi/vvc_ic_media_rew.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/vvc_ic_media_ff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xhdpi/vvc_ic_media_ff.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/vvc_ic_media_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xhdpi/vvc_ic_media_next.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/vvc_ic_media_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xhdpi/vvc_ic_media_pause.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/vvc_ic_media_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xhdpi/vvc_ic_media_play.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/vvc_ic_media_previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xhdpi/vvc_ic_media_previous.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/vvc_ic_media_rew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xhdpi/vvc_ic_media_rew.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xxhdpi/vvc_ic_media_ff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xxhdpi/vvc_ic_media_ff.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xxhdpi/vvc_ic_media_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xxhdpi/vvc_ic_media_next.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xxhdpi/vvc_ic_media_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xxhdpi/vvc_ic_media_pause.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xxhdpi/vvc_ic_media_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xxhdpi/vvc_ic_media_play.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xxhdpi/vvc_ic_media_previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xxhdpi/vvc_ic_media_previous.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xxhdpi/vvc_ic_media_rew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowlevel-studios/VideoViewCompat/1f2239e5f744ddbca3960d5e5e87711af4e0a2be/library/src/main/res/drawable-xxhdpi/vvc_ic_media_rew.png -------------------------------------------------------------------------------- /library/src/main/res/layout/vvc_media_controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 57 | 58 | 64 | 65 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /library/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | #bebebe 22 | -------------------------------------------------------------------------------- /library/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 13 | 14 | 17 | 18 | 21 | 22 | 25 | 26 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library' --------------------------------------------------------------------------------