1 /* 2 * Copyright (C) 2009 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 android.webkit; 18 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.BitmapFactory; 22 import android.graphics.SurfaceTexture; 23 import android.media.MediaPlayer; 24 import android.net.http.EventHandler; 25 import android.net.http.Headers; 26 import android.net.http.RequestHandle; 27 import android.net.http.RequestQueue; 28 import android.net.http.SslCertificate; 29 import android.net.http.SslError; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.os.Message; 33 import android.util.Log; 34 35 import java.io.ByteArrayOutputStream; 36 import java.io.IOException; 37 import java.net.MalformedURLException; 38 import java.net.URL; 39 import java.util.HashMap; 40 import java.util.Map; 41 42 /** 43 * <p>Proxy for HTML5 video views. 44 */ 45 class HTML5VideoViewProxy extends Handler 46 implements MediaPlayer.OnPreparedListener, 47 MediaPlayer.OnCompletionListener, 48 MediaPlayer.OnErrorListener, 49 MediaPlayer.OnInfoListener, 50 SurfaceTexture.OnFrameAvailableListener { 51 // Logging tag. 52 private static final String LOGTAG = "HTML5VideoViewProxy"; 53 54 // Message Ids for WebCore thread -> UI thread communication. 55 private static final int PLAY = 100; 56 private static final int SEEK = 101; 57 private static final int PAUSE = 102; 58 private static final int ERROR = 103; 59 private static final int LOAD_DEFAULT_POSTER = 104; 60 private static final int BUFFERING_START = 105; 61 private static final int BUFFERING_END = 106; 62 63 // Message Ids to be handled on the WebCore thread 64 private static final int PREPARED = 200; 65 private static final int ENDED = 201; 66 private static final int POSTER_FETCHED = 202; 67 private static final int PAUSED = 203; 68 private static final int STOPFULLSCREEN = 204; 69 70 // Timer thread -> UI thread 71 private static final int TIMEUPDATE = 300; 72 73 // The C++ MediaPlayerPrivateAndroid object. 74 int mNativePointer; 75 // The handler for WebCore thread messages; 76 private Handler mWebCoreHandler; 77 // The WebView instance that created this view. 78 private WebView mWebView; 79 // The poster image to be shown when the video is not playing. 80 // This ref prevents the bitmap from being GC'ed. 81 private Bitmap mPoster; 82 // The poster downloader. 83 private PosterDownloader mPosterDownloader; 84 // The seek position. 85 private int mSeekPosition; 86 // A helper class to control the playback. This executes on the UI thread! 87 private static final class VideoPlayer { 88 // The proxy that is currently playing (if any). 89 private static HTML5VideoViewProxy mCurrentProxy; 90 // The VideoView instance. This is a singleton for now, at least until 91 // http://b/issue?id=1973663 is fixed. 92 private static HTML5VideoView mHTML5VideoView; 93 94 private static boolean isVideoSelfEnded = false; 95 // By using the baseLayer and the current video Layer ID, we can 96 // identify the exact layer on the UI thread to use the SurfaceTexture. 97 private static int mBaseLayer = 0; 98 99 private static void setPlayerBuffering(boolean playerBuffering) { 100 mHTML5VideoView.setPlayerBuffering(playerBuffering); 101 } 102 103 // Every time webView setBaseLayer, this will be called. 104 // When we found the Video layer, then we set the Surface Texture to it. 105 // Otherwise, we may want to delete the Surface Texture to save memory. 106 public static void setBaseLayer(int layer) { 107 // Don't do this for full screen mode. 108 if (mHTML5VideoView != null 109 && !mHTML5VideoView.isFullScreenMode() 110 && !mHTML5VideoView.surfaceTextureDeleted()) { 111 mBaseLayer = layer; 112 113 int currentVideoLayerId = mHTML5VideoView.getVideoLayerId(); 114 SurfaceTexture surfTexture = mHTML5VideoView.getSurfaceTexture(currentVideoLayerId); 115 int textureName = mHTML5VideoView.getTextureName(); 116 117 if (layer != 0 && surfTexture != null && currentVideoLayerId != -1) { 118 int playerState = mHTML5VideoView.getCurrentState(); 119 if (mHTML5VideoView.getPlayerBuffering()) 120 playerState = HTML5VideoView.STATE_NOTPREPARED; 121 boolean foundInTree = nativeSendSurfaceTexture(surfTexture, 122 layer, currentVideoLayerId, textureName, 123 playerState); 124 if (playerState >= HTML5VideoView.STATE_PREPARED 125 && !foundInTree) { 126 mHTML5VideoView.pauseAndDispatch(mCurrentProxy); 127 mHTML5VideoView.deleteSurfaceTexture(); 128 } 129 } 130 } 131 } 132 133 // When a WebView is paused, we also want to pause the video in it. 134 public static void pauseAndDispatch() { 135 if (mHTML5VideoView != null) { 136 mHTML5VideoView.pauseAndDispatch(mCurrentProxy); 137 // When switching out, clean the video content on the old page 138 // by telling the layer not readyToUseSurfTex. 139 setBaseLayer(mBaseLayer); 140 } 141 } 142 143 public static void enterFullScreenVideo(int layerId, String url, 144 HTML5VideoViewProxy proxy, WebView webView) { 145 // Save the inline video info and inherit it in the full screen 146 int savePosition = 0; 147 boolean savedIsPlaying = false; 148 if (mHTML5VideoView != null) { 149 // If we are playing the same video, then it is better to 150 // save the current position. 151 if (layerId == mHTML5VideoView.getVideoLayerId()) { 152 savePosition = mHTML5VideoView.getCurrentPosition(); 153 savedIsPlaying = mHTML5VideoView.isPlaying(); 154 } 155 mHTML5VideoView.pauseAndDispatch(mCurrentProxy); 156 mHTML5VideoView.release(); 157 } 158 mHTML5VideoView = new HTML5VideoFullScreen(proxy.getContext(), 159 layerId, savePosition, savedIsPlaying); 160 mCurrentProxy = proxy; 161 162 mHTML5VideoView.setVideoURI(url, mCurrentProxy); 163 164 mHTML5VideoView.enterFullScreenVideoState(layerId, proxy, webView); 165 } 166 167 // This is on the UI thread. 168 // When native tell Java to play, we need to check whether or not it is 169 // still the same video by using videoLayerId and treat it differently. 170 public static void play(String url, int time, HTML5VideoViewProxy proxy, 171 WebChromeClient client, int videoLayerId) { 172 int currentVideoLayerId = -1; 173 boolean backFromFullScreenMode = false; 174 if (mHTML5VideoView != null) { 175 currentVideoLayerId = mHTML5VideoView.getVideoLayerId(); 176 backFromFullScreenMode = mHTML5VideoView.fullScreenExited(); 177 } 178 179 if (backFromFullScreenMode 180 || currentVideoLayerId != videoLayerId 181 || mHTML5VideoView.surfaceTextureDeleted()) { 182 // Here, we handle the case when switching to a new video, 183 // either inside a WebView or across WebViews 184 // For switching videos within a WebView or across the WebView, 185 // we need to pause the old one and re-create a new media player 186 // inside the HTML5VideoView. 187 if (mHTML5VideoView != null) { 188 if (!backFromFullScreenMode) { 189 mHTML5VideoView.pauseAndDispatch(mCurrentProxy); 190 } 191 // release the media player to avoid finalize error 192 mHTML5VideoView.release(); 193 } 194 mCurrentProxy = proxy; 195 mHTML5VideoView = new HTML5VideoInline(videoLayerId, time, false); 196 197 mHTML5VideoView.setVideoURI(url, mCurrentProxy); 198 mHTML5VideoView.prepareDataAndDisplayMode(proxy); 199 } else if (mCurrentProxy == proxy) { 200 // Here, we handle the case when we keep playing with one video 201 if (!mHTML5VideoView.isPlaying()) { 202 mHTML5VideoView.seekTo(time); 203 mHTML5VideoView.start(); 204 } 205 } else if (mCurrentProxy != null) { 206 // Some other video is already playing. Notify the caller that 207 // its playback ended. 208 proxy.dispatchOnEnded(); 209 } 210 } 211 212 public static boolean isPlaying(HTML5VideoViewProxy proxy) { 213 return (mCurrentProxy == proxy && mHTML5VideoView != null 214 && mHTML5VideoView.isPlaying()); 215 } 216 217 public static int getCurrentPosition() { 218 int currentPosMs = 0; 219 if (mHTML5VideoView != null) { 220 currentPosMs = mHTML5VideoView.getCurrentPosition(); 221 } 222 return currentPosMs; 223 } 224 225 public static void seek(int time, HTML5VideoViewProxy proxy) { 226 if (mCurrentProxy == proxy && time >= 0 && mHTML5VideoView != null) { 227 mHTML5VideoView.seekTo(time); 228 } 229 } 230 231 public static void pause(HTML5VideoViewProxy proxy) { 232 if (mCurrentProxy == proxy && mHTML5VideoView != null) { 233 mHTML5VideoView.pause(); 234 } 235 } 236 237 public static void onPrepared() { 238 if (!mHTML5VideoView.isFullScreenMode() || mHTML5VideoView.getAutostart()) { 239 mHTML5VideoView.start(); 240 } 241 if (mBaseLayer != 0) { 242 setBaseLayer(mBaseLayer); 243 } 244 } 245 246 public static void end() { 247 if (mCurrentProxy != null) { 248 if (isVideoSelfEnded) 249 mCurrentProxy.dispatchOnEnded(); 250 else 251 mCurrentProxy.dispatchOnPaused(); 252 } 253 isVideoSelfEnded = false; 254 } 255 } 256 257 // A bunch event listeners for our VideoView 258 // MediaPlayer.OnPreparedListener 259 public void onPrepared(MediaPlayer mp) { 260 VideoPlayer.onPrepared(); 261 Message msg = Message.obtain(mWebCoreHandler, PREPARED); 262 Map<String, Object> map = new HashMap<String, Object>(); 263 map.put("dur", new Integer(mp.getDuration())); 264 map.put("width", new Integer(mp.getVideoWidth())); 265 map.put("height", new Integer(mp.getVideoHeight())); 266 msg.obj = map; 267 mWebCoreHandler.sendMessage(msg); 268 } 269 270 // MediaPlayer.OnCompletionListener; 271 public void onCompletion(MediaPlayer mp) { 272 // The video ended by itself, so we need to 273 // send a message to the UI thread to dismiss 274 // the video view and to return to the WebView. 275 // arg1 == 1 means the video ends by itself. 276 sendMessage(obtainMessage(ENDED, 1, 0)); 277 } 278 279 // MediaPlayer.OnErrorListener 280 public boolean onError(MediaPlayer mp, int what, int extra) { 281 sendMessage(obtainMessage(ERROR)); 282 return false; 283 } 284 285 public void dispatchOnEnded() { 286 Message msg = Message.obtain(mWebCoreHandler, ENDED); 287 mWebCoreHandler.sendMessage(msg); 288 } 289 290 public void dispatchOnPaused() { 291 Message msg = Message.obtain(mWebCoreHandler, PAUSED); 292 mWebCoreHandler.sendMessage(msg); 293 } 294 295 public void dispatchOnStopFullScreen() { 296 Message msg = Message.obtain(mWebCoreHandler, STOPFULLSCREEN); 297 mWebCoreHandler.sendMessage(msg); 298 } 299 300 public void onTimeupdate() { 301 sendMessage(obtainMessage(TIMEUPDATE)); 302 } 303 304 // When there is a frame ready from surface texture, we should tell WebView 305 // to refresh. 306 @Override 307 public void onFrameAvailable(SurfaceTexture surfaceTexture) { 308 // TODO: This should support partial invalidation too. 309 mWebView.invalidate(); 310 } 311 312 // Handler for the messages from WebCore or Timer thread to the UI thread. 313 @Override 314 public void handleMessage(Message msg) { 315 // This executes on the UI thread. 316 switch (msg.what) { 317 case PLAY: { 318 String url = (String) msg.obj; 319 WebChromeClient client = mWebView.getWebChromeClient(); 320 int videoLayerID = msg.arg1; 321 if (client != null) { 322 VideoPlayer.play(url, mSeekPosition, this, client, videoLayerID); 323 } 324 break; 325 } 326 case SEEK: { 327 Integer time = (Integer) msg.obj; 328 mSeekPosition = time; 329 VideoPlayer.seek(mSeekPosition, this); 330 break; 331 } 332 case PAUSE: { 333 VideoPlayer.pause(this); 334 break; 335 } 336 case ENDED: 337 if (msg.arg1 == 1) 338 VideoPlayer.isVideoSelfEnded = true; 339 VideoPlayer.end(); 340 break; 341 case ERROR: { 342 WebChromeClient client = mWebView.getWebChromeClient(); 343 if (client != null) { 344 client.onHideCustomView(); 345 } 346 break; 347 } 348 case LOAD_DEFAULT_POSTER: { 349 WebChromeClient client = mWebView.getWebChromeClient(); 350 if (client != null) { 351 doSetPoster(client.getDefaultVideoPoster()); 352 } 353 break; 354 } 355 case TIMEUPDATE: { 356 if (VideoPlayer.isPlaying(this)) { 357 sendTimeupdate(); 358 } 359 break; 360 } 361 case BUFFERING_START: { 362 VideoPlayer.setPlayerBuffering(true); 363 break; 364 } 365 case BUFFERING_END: { 366 VideoPlayer.setPlayerBuffering(false); 367 break; 368 } 369 } 370 } 371 372 // Everything below this comment executes on the WebCore thread, except for 373 // the EventHandler methods, which are called on the network thread. 374 375 // A helper class that knows how to download posters 376 private static final class PosterDownloader implements EventHandler { 377 // The request queue. This is static as we have one queue for all posters. 378 private static RequestQueue mRequestQueue; 379 private static int mQueueRefCount = 0; 380 // The poster URL 381 private URL mUrl; 382 // The proxy we're doing this for. 383 private final HTML5VideoViewProxy mProxy; 384 // The poster bytes. We only touch this on the network thread. 385 private ByteArrayOutputStream mPosterBytes; 386 // The request handle. We only touch this on the WebCore thread. 387 private RequestHandle mRequestHandle; 388 // The response status code. 389 private int mStatusCode; 390 // The response headers. 391 private Headers mHeaders; 392 // The handler to handle messages on the WebCore thread. 393 private Handler mHandler; 394 395 public PosterDownloader(String url, HTML5VideoViewProxy proxy) { 396 try { 397 mUrl = new URL(url); 398 } catch (MalformedURLException e) { 399 mUrl = null; 400 } 401 mProxy = proxy; 402 mHandler = new Handler(); 403 } 404 // Start the download. Called on WebCore thread. 405 public void start() { 406 retainQueue(); 407 408 if (mUrl == null) { 409 return; 410 } 411 412 // Only support downloading posters over http/https. 413 // FIXME: Add support for other schemes. WebKit seems able to load 414 // posters over other schemes e.g. file://, but gets the dimensions wrong. 415 String protocol = mUrl.getProtocol(); 416 if ("http".equals(protocol) || "https".equals(protocol)) { 417 mRequestHandle = mRequestQueue.queueRequest(mUrl.toString(), "GET", null, 418 this, null, 0); 419 } 420 } 421 // Cancel the download if active and release the queue. Called on WebCore thread. 422 public void cancelAndReleaseQueue() { 423 if (mRequestHandle != null) { 424 mRequestHandle.cancel(); 425 mRequestHandle = null; 426 } 427 releaseQueue(); 428 } 429 // EventHandler methods. Executed on the network thread. 430 public void status(int major_version, 431 int minor_version, 432 int code, 433 String reason_phrase) { 434 mStatusCode = code; 435 } 436 437 public void headers(Headers headers) { 438 mHeaders = headers; 439 } 440 441 public void data(byte[] data, int len) { 442 if (mPosterBytes == null) { 443 mPosterBytes = new ByteArrayOutputStream(); 444 } 445 mPosterBytes.write(data, 0, len); 446 } 447 448 public void endData() { 449 if (mStatusCode == 200) { 450 if (mPosterBytes.size() > 0) { 451 Bitmap poster = BitmapFactory.decodeByteArray( 452 mPosterBytes.toByteArray(), 0, mPosterBytes.size()); 453 mProxy.doSetPoster(poster); 454 } 455 cleanup(); 456 } else if (mStatusCode >= 300 && mStatusCode < 400) { 457 // We have a redirect. 458 try { 459 mUrl = new URL(mHeaders.getLocation()); 460 } catch (MalformedURLException e) { 461 mUrl = null; 462 } 463 if (mUrl != null) { 464 mHandler.post(new Runnable() { 465 public void run() { 466 if (mRequestHandle != null) { 467 mRequestHandle.setupRedirect(mUrl.toString(), mStatusCode, 468 new HashMap<String, String>()); 469 } 470 } 471 }); 472 } 473 } 474 } 475 476 public void certificate(SslCertificate certificate) { 477 // Don't care. 478 } 479 480 public void error(int id, String description) { 481 cleanup(); 482 } 483 484 public boolean handleSslErrorRequest(SslError error) { 485 // Don't care. If this happens, data() will never be called so 486 // mPosterBytes will never be created, so no need to call cleanup. 487 return false; 488 } 489 // Tears down the poster bytes stream. Called on network thread. 490 private void cleanup() { 491 if (mPosterBytes != null) { 492 try { 493 mPosterBytes.close(); 494 } catch (IOException ignored) { 495 // Ignored. 496 } finally { 497 mPosterBytes = null; 498 } 499 } 500 } 501 502 // Queue management methods. Called on WebCore thread. 503 private void retainQueue() { 504 if (mRequestQueue == null) { 505 mRequestQueue = new RequestQueue(mProxy.getContext()); 506 } 507 mQueueRefCount++; 508 } 509 510 private void releaseQueue() { 511 if (mQueueRefCount == 0) { 512 return; 513 } 514 if (--mQueueRefCount == 0) { 515 mRequestQueue.shutdown(); 516 mRequestQueue = null; 517 } 518 } 519 } 520 521 /** 522 * Private constructor. 523 * @param webView is the WebView that hosts the video. 524 * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. 525 */ 526 private HTML5VideoViewProxy(WebView webView, int nativePtr) { 527 // This handler is for the main (UI) thread. 528 super(Looper.getMainLooper()); 529 // Save the WebView object. 530 mWebView = webView; 531 // Pass Proxy into webview, such that every time we have a setBaseLayer 532 // call, we tell this Proxy to call the native to update the layer tree 533 // for the Video Layer's surface texture info 534 mWebView.setHTML5VideoViewProxy(this); 535 // Save the native ptr 536 mNativePointer = nativePtr; 537 // create the message handler for this thread 538 createWebCoreHandler(); 539 } 540 541 private void createWebCoreHandler() { 542 mWebCoreHandler = new Handler() { 543 @Override 544 public void handleMessage(Message msg) { 545 switch (msg.what) { 546 case PREPARED: { 547 Map<String, Object> map = (Map<String, Object>) msg.obj; 548 Integer duration = (Integer) map.get("dur"); 549 Integer width = (Integer) map.get("width"); 550 Integer height = (Integer) map.get("height"); 551 nativeOnPrepared(duration.intValue(), width.intValue(), 552 height.intValue(), mNativePointer); 553 break; 554 } 555 case ENDED: 556 mSeekPosition = 0; 557 nativeOnEnded(mNativePointer); 558 break; 559 case PAUSED: 560 nativeOnPaused(mNativePointer); 561 break; 562 case POSTER_FETCHED: 563 Bitmap poster = (Bitmap) msg.obj; 564 nativeOnPosterFetched(poster, mNativePointer); 565 break; 566 case TIMEUPDATE: 567 nativeOnTimeupdate(msg.arg1, mNativePointer); 568 break; 569 case STOPFULLSCREEN: 570 nativeOnStopFullscreen(mNativePointer); 571 break; 572 } 573 } 574 }; 575 } 576 577 private void doSetPoster(Bitmap poster) { 578 if (poster == null) { 579 return; 580 } 581 // Save a ref to the bitmap and send it over to the WebCore thread. 582 mPoster = poster; 583 Message msg = Message.obtain(mWebCoreHandler, POSTER_FETCHED); 584 msg.obj = poster; 585 mWebCoreHandler.sendMessage(msg); 586 } 587 588 private void sendTimeupdate() { 589 Message msg = Message.obtain(mWebCoreHandler, TIMEUPDATE); 590 msg.arg1 = VideoPlayer.getCurrentPosition(); 591 mWebCoreHandler.sendMessage(msg); 592 } 593 594 public Context getContext() { 595 return mWebView.getContext(); 596 } 597 598 // The public methods below are all called from WebKit only. 599 /** 600 * Play a video stream. 601 * @param url is the URL of the video stream. 602 */ 603 public void play(String url, int position, int videoLayerID) { 604 if (url == null) { 605 return; 606 } 607 608 if (position > 0) { 609 seek(position); 610 } 611 Message message = obtainMessage(PLAY); 612 message.arg1 = videoLayerID; 613 message.obj = url; 614 sendMessage(message); 615 } 616 617 /** 618 * Seek into the video stream. 619 * @param time is the position in the video stream. 620 */ 621 public void seek(int time) { 622 Message message = obtainMessage(SEEK); 623 message.obj = new Integer(time); 624 sendMessage(message); 625 } 626 627 /** 628 * Pause the playback. 629 */ 630 public void pause() { 631 Message message = obtainMessage(PAUSE); 632 sendMessage(message); 633 } 634 635 /** 636 * Tear down this proxy object. 637 */ 638 public void teardown() { 639 // This is called by the C++ MediaPlayerPrivate dtor. 640 // Cancel any active poster download. 641 if (mPosterDownloader != null) { 642 mPosterDownloader.cancelAndReleaseQueue(); 643 } 644 mNativePointer = 0; 645 } 646 647 /** 648 * Load the poster image. 649 * @param url is the URL of the poster image. 650 */ 651 public void loadPoster(String url) { 652 if (url == null) { 653 Message message = obtainMessage(LOAD_DEFAULT_POSTER); 654 sendMessage(message); 655 return; 656 } 657 // Cancel any active poster download. 658 if (mPosterDownloader != null) { 659 mPosterDownloader.cancelAndReleaseQueue(); 660 } 661 // Load the poster asynchronously 662 mPosterDownloader = new PosterDownloader(url, this); 663 mPosterDownloader.start(); 664 } 665 666 // These three function are called from UI thread only by WebView. 667 public void setBaseLayer(int layer) { 668 VideoPlayer.setBaseLayer(layer); 669 } 670 671 public void pauseAndDispatch() { 672 VideoPlayer.pauseAndDispatch(); 673 } 674 675 public void enterFullScreenVideo(int layerId, String url) { 676 VideoPlayer.enterFullScreenVideo(layerId, url, this, mWebView); 677 } 678 679 /** 680 * The factory for HTML5VideoViewProxy instances. 681 * @param webViewCore is the WebViewCore that is requesting the proxy. 682 * 683 * @return a new HTML5VideoViewProxy object. 684 */ 685 public static HTML5VideoViewProxy getInstance(WebViewCore webViewCore, int nativePtr) { 686 return new HTML5VideoViewProxy(webViewCore.getWebView(), nativePtr); 687 } 688 689 /* package */ WebView getWebView() { 690 return mWebView; 691 } 692 693 private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); 694 private native void nativeOnEnded(int nativePointer); 695 private native void nativeOnPaused(int nativePointer); 696 private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); 697 private native void nativeOnTimeupdate(int position, int nativePointer); 698 private native void nativeOnStopFullscreen(int nativePointer); 699 private native static boolean nativeSendSurfaceTexture(SurfaceTexture texture, 700 int baseLayer, int videoLayerId, int textureName, 701 int playerState); 702 703 @Override 704 public boolean onInfo(MediaPlayer mp, int what, int extra) { 705 if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) { 706 sendMessage(obtainMessage(BUFFERING_START, what, extra)); 707 } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) { 708 sendMessage(obtainMessage(BUFFERING_END, what, extra)); 709 } 710 return false; 711 } 712 } 713