yixia / VitamioBundleStudio

Other
326 stars 134 forks source link

MediaController (seekbar) does not show on SDK 24 (previous and successive SDKs are OK) #24

Closed moster67 closed 6 years ago

moster67 commented 6 years ago

Trying the VideoView demo, the MediaController works fine on 4.4.2 (API 19) but on my Samsung S8 running 7.0 (SDK 24), the mediacontroller and seekbar does not show up. Does this happen to someone else? How can it be resolved?

moster67 commented 6 years ago

If I use an emulator running SDK 25, it works again! It seems there is a problem but only with SDK 24.

I modified the MediaController-class to use callbacks instead of Handlers (as used in original Android MediaController) but with the same result i.e. OK on SDK19, SDK 25 but not on SDK 24. It's weird. The error from logcat seems to be:

01-04 18:07:54.198 1626-1969/system_process I/WindowManager: Destroying surface Surface(name=PopupWindow:7a167d2) called by com.android.server.wm.WindowStateAnimator.destroySurface:2014 com.android.server.wm.WindowStateAnimator.destroySurfaceLocked:881 com.android.server.wm.WindowState.removeLocked:1449 com.android.server.wm.WindowManagerService.removeWindowInnerLocked:2478 com.android.server.wm.WindowManagerService.removeWindowLocked:2436 com.android.server.wm.WindowManagerService.removeWindowLocked:2305 com.android.server.wm.WindowManagerService.removeWindow:2300 com.android.server.wm.Session.remove:193

Anyone who can try this on a device or emulator running SDK 24 and let me know if you have the same problem. I tested using the Demo project attached in the bundle.

Here below is my modified MediaController-class if you're interested.

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

package io.vov.vitamio.widget;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.media.AudioManager;
import android.os.Build;
//import android.os.Message;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.PopupWindow;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;

import java.lang.reflect.Method;

import io.vov.vitamio.utils.Log;
import io.vov.vitamio.utils.StringUtils;

//import android.os.Message;

//import android.os.Handler;
//import android.os.Message;

/**
 * A view containing controls for a MediaPlayer. Typically contains the buttons
 * like "Play/Pause" and a progress slider. It takes care of synchronizing the
 * controls with the state of the MediaPlayer.
 * <p/>
 * The way to use this class is to a) instantiate it programatically or b)
 * create it in your xml layout.
 * <p/>
 * a) The MediaController will create a default set of controls and put them in
 * a window floating above your application. Specifically, the controls will
 * float above the view specified with setAnchorView(). By default, the window
 * will disappear if left idle for three seconds and reappear when the user
 * touches the anchor view. To customize the MediaController's style, layout and
 * controls you should extend MediaController and override the {#link
 * {@link #makeControllerView()} method.
 * <p/>
 * b) The MediaController is a FrameLayout, you can put it in your layout xml
 * and get it through {@link #findViewById(int)}.
 * <p/>
 * NOTES: In each way, if you want customize the MediaController, the SeekBar's
 * id must be mediacontroller_progress, the Play/Pause's must be
 * mediacontroller_pause, current time's must be mediacontroller_time_current,
 * total time's must be mediacontroller_time_total, file name's must be
 * mediacontroller_file_name. And your resources must have a pause_button
 * drawable and a play_button drawable.
 * <p/>
 * Functions like show() and hide() have no effect when MediaController is
 * created in an xml layout.
 */
public class MediaController extends FrameLayout {
  private static final int sDefaultTimeout = 3000;
  private MediaPlayerControl mPlayer;
  private Context mContext;
  private PopupWindow mWindow;
  private int mAnimStyle;
  private View mAnchor;
  private View mRoot;
  private SeekBar mProgress;
  private TextView mEndTime, mCurrentTime;
  private TextView mFileName;
  private OutlineTextView mInfoView;
  private String mTitle;
  private long mDuration;
  private boolean mShowing;
  private boolean mDragging;
  private boolean mInstantSeeking = false;
  private boolean mFromXml = false;
  private ImageButton mPauseButton;
  private AudioManager mAM;
  private OnShownListener mShownListener;
  private OnHiddenListener mHiddenListener;
  @SuppressLint("HandlerLeak")

  private View.OnClickListener mPauseListener = new View.OnClickListener() {
    public void onClick(View v) {
      doPauseResume();
      show(sDefaultTimeout);
    }
  };
  private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
    public void onStartTrackingTouch(SeekBar bar) {
      mDragging = true;
      show(3600000);
      removeCallbacks(mShowProgress);
      if (mInstantSeeking)
        mAM.setStreamMute(AudioManager.STREAM_MUSIC, true);
      if (mInfoView != null) {
        mInfoView.setText("");
        mInfoView.setVisibility(View.VISIBLE);
      }
    }

    public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
      if (!fromuser)
        return;

      long newposition = (mDuration * progress) / 1000;
      String time = StringUtils.generateTime(newposition);
      if (mInstantSeeking)
        mPlayer.seekTo(newposition);
      if (mInfoView != null)
        mInfoView.setText(time);
      if (mCurrentTime != null)
        mCurrentTime.setText(time);
    }

    public void onStopTrackingTouch(SeekBar bar) {
      if (!mInstantSeeking)
        mPlayer.seekTo((mDuration * bar.getProgress()) / 1000);
      if (mInfoView != null) {
        mInfoView.setText("");
        mInfoView.setVisibility(View.GONE);
      }
      show(sDefaultTimeout);
//      mHandler.removeMessages(SHOW_PROGRESS);
      removeCallbacks(mShowProgress);
      mAM.setStreamMute(AudioManager.STREAM_MUSIC, false);
      mDragging = false;
//      mHandler.sendEmptyMessageDelayed(SHOW_PROGRESS, 1000);
      post(mShowProgress);
    }
  };

  public MediaController(Context context, AttributeSet attrs) {
    super(context, attrs);
    mRoot = this;
    mFromXml = true;
    initController(context);
  }

  public MediaController(Context context) {
    super(context);
    if (!mFromXml && initController(context))
      initFloatingWindow();
  }

  private boolean initController(Context context) {
    mContext = context;
    mAM = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
    return true;
  }

  @Override
  public void onFinishInflate() {
    if (mRoot != null)
      initControllerView(mRoot);
  }

  private void initFloatingWindow() {
    mWindow = new PopupWindow(mContext);
    mWindow.setFocusable(false);
    mWindow.setBackgroundDrawable(null);
    mWindow.setOutsideTouchable(true);
    mAnimStyle = android.R.style.Animation;
  }

  @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    public void setWindowLayoutType() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            try {
                mAnchor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
                Method setWindowLayoutType = PopupWindow.class.getMethod("setWindowLayoutType", new Class[] { int.class });
                setWindowLayoutType.invoke(mWindow, WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG);
            } catch (Exception e) {
                Log.e("setWindowLayoutType", e);
            }
        }
    }

  /**
   * Set the view that acts as the anchor for the control view. This can for
   * example be a VideoView, or your Activity's main view.
   *
   * @param view The view to which to anchor the controller when it is visible.
   */
  public void setAnchorView(View view) {
    mAnchor = view;
    if (!mFromXml) {
      removeAllViews();
      mRoot = makeControllerView();
      mWindow.setContentView(mRoot);
      mWindow.setWidth(LayoutParams.MATCH_PARENT);
      mWindow.setHeight(LayoutParams.WRAP_CONTENT);
    }
    initControllerView(mRoot);
  }

  /**
   * Create the view that holds the widgets that control playback. Derived
   * classes can override this to create their own.
   *
   * @return The controller view.
   */
  protected View makeControllerView() {
    return ((LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(getResources().getIdentifier("mediacontroller", "layout", mContext.getPackageName()), this);
  }

  private void initControllerView(View v) {
    mPauseButton = (ImageButton) v.findViewById(getResources().getIdentifier("mediacontroller_play_pause", "id", mContext.getPackageName()));
    if (mPauseButton != null) {
      mPauseButton.requestFocus();
      mPauseButton.setOnClickListener(mPauseListener);
    }

    mProgress = (SeekBar) v.findViewById(getResources().getIdentifier("mediacontroller_seekbar", "id", mContext.getPackageName()));
    if (mProgress != null) {
      if (mProgress instanceof SeekBar) {
        SeekBar seeker = (SeekBar) mProgress;
        seeker.setOnSeekBarChangeListener(mSeekListener);
      }
      mProgress.setMax(1000);
    }

    mEndTime = (TextView) v.findViewById(getResources().getIdentifier("mediacontroller_time_total", "id", mContext.getPackageName()));
    mCurrentTime = (TextView) v.findViewById(getResources().getIdentifier("mediacontroller_time_current", "id", mContext.getPackageName()));
    mFileName = (TextView) v.findViewById(getResources().getIdentifier("mediacontroller_file_name", "id", mContext.getPackageName()));
    if (mFileName != null)
      mFileName.setText(mTitle);
  }

  public void setMediaPlayer(MediaPlayerControl player) {
    mPlayer = player;
    updatePausePlay();
  }

  /**
   * Control the action when the seekbar dragged by user
   *
   * @param seekWhenDragging True the media will seek periodically
   */
  public void setInstantSeeking(boolean seekWhenDragging) {
    mInstantSeeking = seekWhenDragging;
  }

  public void show() {
    show(sDefaultTimeout);
  }

  /**
   * Set the content of the file_name TextView
   *
   * @param name
   */
  public void setFileName(String name) {
    mTitle = name;
    if (mFileName != null)
      mFileName.setText(mTitle);
  }

  /**
   * Set the View to hold some information when interact with the
   * MediaController
   *
   * @param v
   */
  public void setInfoView(OutlineTextView v) {
    mInfoView = v;
  }

  /**
   * <p>
   * Change the animation style resource for this controller.
   * </p>
   * <p/>
   * <p>
   * If the controller is showing, calling this method will take effect only the
   * next time the controller is shown.
   * </p>
   *
   * @param animationStyle animation style to use when the controller appears
   *                       and disappears. Set to -1 for the default animation, 0 for no animation, or
   *                       a resource identifier for an explicit animation.
   */
  public void setAnimationStyle(int animationStyle) {
    mAnimStyle = animationStyle;
  }

  /**
   * Show the controller on screen. It will go away automatically after
   * 'timeout' milliseconds of inactivity.
   *
   * @param timeout The timeout in milliseconds. Use 0 to show the controller
   *                until hide() is called.
   */
  public void show(int timeout) {
    if (!mShowing && mAnchor != null && mAnchor.getWindowToken() != null) {
      if (mPauseButton != null)
        mPauseButton.requestFocus();

      if (mFromXml) {
        setVisibility(View.VISIBLE);
      } else {
        int[] location = new int[2];

        mAnchor.getLocationOnScreen(location);
        Rect anchorRect = new Rect(location[0], location[1], location[0] + mAnchor.getWidth(), location[1] + mAnchor.getHeight());

        mWindow.setAnimationStyle(mAnimStyle);
        setWindowLayoutType();
        mWindow.showAtLocation(mAnchor, Gravity.NO_GRAVITY, anchorRect.left, anchorRect.bottom);
      }
      mShowing = true;
      if (mShownListener != null)
        mShownListener.onShown();
    }
    updatePausePlay();
//    mHandler.sendEmptyMessage(SHOW_PROGRESS);
    post(mShowProgress); // DA VEDERE

    if (timeout != 0) {
//      mHandler.removeMessages(FADE_OUT);
//      mHandler.sendMessageDelayed(mHandler.obtainMessage(FADE_OUT), timeout);
      removeCallbacks(mFadeOut); //DA VEDERE
      postDelayed(mFadeOut, timeout); //DA VEDERE
    }
  }

  public boolean isShowing() {
    return mShowing;
  }

  public void hide() {
    if (mAnchor == null)
      return;

    if (mShowing) {
      try {
//        mHandler.removeMessages(SHOW_PROGRESS);
        removeCallbacks(mShowProgress); //DA VEDERE
        if (mFromXml)
          setVisibility(View.GONE);
        else
          mWindow.dismiss();
      } catch (IllegalArgumentException ex) {
        Log.d("MediaController already removed");
      }
      mShowing = false;
      if (mHiddenListener != null)
        mHiddenListener.onHidden();
    }
  }

  private final Runnable mFadeOut = new Runnable() {
    @Override
    public void run() {
      hide();
    }
  };

  private final Runnable mShowProgress = new Runnable() {
    @Override
    public void run() {
      long pos = setProgress();
      if (!mDragging && mShowing && mPlayer.isPlaying()) {
        postDelayed(mShowProgress, 1000 - (pos % 1000));
      }
    }
  };

  public void setOnShownListener(OnShownListener l) {
    mShownListener = l;
  }

  public void setOnHiddenListener(OnHiddenListener l) {
    mHiddenListener = l;
  }

  private long setProgress() {
    if (mPlayer == null || mDragging)
      return 0;

    long position = mPlayer.getCurrentPosition();
    long duration = mPlayer.getDuration();
    if (mProgress != null) {
      if (duration > 0) {
        long pos = 1000L * position / duration;
        mProgress.setProgress((int) pos);
      }
      int percent = mPlayer.getBufferPercentage();
      mProgress.setSecondaryProgress(percent * 10);
    }

    mDuration = duration;

    if (mEndTime != null)
      mEndTime.setText(StringUtils.generateTime(mDuration));
    if (mCurrentTime != null)
      mCurrentTime.setText(StringUtils.generateTime(position));

    return position;
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    show(sDefaultTimeout);
    return true;
  }

  @Override
  public boolean onTrackballEvent(MotionEvent ev) {
    show(sDefaultTimeout);
    return false;
  }

  @Override
  public boolean dispatchKeyEvent(KeyEvent event) {
    int keyCode = event.getKeyCode();
    if (event.getRepeatCount() == 0 && (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_SPACE)) {
      doPauseResume();
      show(sDefaultTimeout);
      if (mPauseButton != null)
        mPauseButton.requestFocus();
      return true;
    } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP) {
      if (mPlayer.isPlaying()) {
        mPlayer.pause();
        updatePausePlay();
      }
      return true;
    } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
      hide();
      return true;
    } else {
      show(sDefaultTimeout);
    }
    return super.dispatchKeyEvent(event);
  }

  private void updatePausePlay() {
    if (mRoot == null || mPauseButton == null)
      return;

    if (mPlayer.isPlaying())
      mPauseButton.setImageResource(getResources().getIdentifier("mediacontroller_pause", "drawable", mContext.getPackageName()));
    else
      mPauseButton.setImageResource(getResources().getIdentifier("mediacontroller_play", "drawable", mContext.getPackageName()));
  }

  private void doPauseResume() {
    if (mPlayer.isPlaying())
      mPlayer.pause();
    else
      mPlayer.start();
    updatePausePlay();
  }

  @Override
  public void setEnabled(boolean enabled) {
    if (mPauseButton != null)
      mPauseButton.setEnabled(enabled);
    if (mProgress != null)
      mProgress.setEnabled(enabled);
    super.setEnabled(enabled);
  }

  public interface OnShownListener {
    public void onShown();
  }

  public interface OnHiddenListener {
    public void onHidden();
  }

  public interface MediaPlayerControl {
    void start();

    void pause();

    long getDuration();

    long getCurrentPosition();

    void seekTo(long pos);

    boolean isPlaying();

    int getBufferPercentage();
  }

}
moster67 commented 6 years ago

Turns out it had nothing to do with SurfaceView, Callbacks, Handlers etc. It was related to Android's PopupWindow which was not working in the same way as in previous versions. Some say it's a bug - anyway now it's working. To find out how I resolved it, check my SO-post here.