├── .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 | *
72 | * - The "previous" and "next" buttons are hidden until setPrevNextListeners()
73 | * has been called
74 | *
- The "previous" and "next" buttons are visible but disabled if
75 | * setPrevNextListeners() was called with null listeners
76 | *
- The "rewind" and "fastforward" buttons are shown unless requested
77 | * otherwise by using the MediaController(Context, boolean) constructor
78 | * with the boolean set to false
79 | *
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'
--------------------------------------------------------------------------------