Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2a2fcca4 authored by Robert Snoeberger's avatar Robert Snoeberger Committed by Automerger Merge Worker
Browse files

Merge "Add seek bar to QS Media Player" into rvc-dev am: b131e1b7

Change-Id: Ic0611ef4c41fdebbef0803cbf08bad6a77bb67e7
parents be47f931 b131e1b7
Loading
Loading
Loading
Loading
+41 −0
Original line number Diff line number Diff line
@@ -136,6 +136,47 @@
            </LinearLayout>
        </LinearLayout>

        <!-- Seek Bar -->
        <SeekBar
            android:id="@+id/media_progress_bar"
            android:clickable="true"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:maxHeight="3dp"
            android:paddingTop="24dp"
            android:paddingBottom="24dp"
            android:layout_marginBottom="-24dp"
            android:layout_marginTop="-24dp"
            android:splitTrack="false"
        />

        <FrameLayout
            android:id="@+id/notification_media_progress_time"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            >
            <!-- width is set to "match_parent" to avoid extra layout calls -->
            <TextView
                android:id="@+id/media_elapsed_time"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:fontFamily="@*android:string/config_bodyFontFamily"
                android:textSize="14sp"
                android:gravity="left"
            />
            <TextView
                android:id="@+id/media_total_time"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:fontFamily="@*android:string/config_bodyFontFamily"
                android:layout_alignParentRight="true"
                android:textSize="14sp"
                android:gravity="right"
            />
        </FrameLayout>

        <!-- Controls -->
        <LinearLayout
            android:id="@+id/media_actions"
+86 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.media

import android.content.res.ColorStateList
import android.text.format.DateUtils
import android.view.View
import android.widget.SeekBar
import android.widget.TextView
import androidx.annotation.UiThread
import androidx.lifecycle.Observer

import com.android.systemui.R

/**
 * Observer for changes from SeekBarViewModel.
 *
 * <p>Updates the seek bar views in response to changes to the model.
 */
class SeekBarObserver(view: View) : Observer<SeekBarViewModel.Progress> {

    private val seekBarView: SeekBar
    private val elapsedTimeView: TextView
    private val totalTimeView: TextView

    init {
        seekBarView = view.findViewById(R.id.media_progress_bar)
        elapsedTimeView = view.findViewById(R.id.media_elapsed_time)
        totalTimeView = view.findViewById(R.id.media_total_time)
    }

    /** Updates seek bar views when the data model changes. */
    @UiThread
    override fun onChanged(data: SeekBarViewModel.Progress) {
        if (data.enabled && seekBarView.visibility == View.GONE) {
            seekBarView.visibility = View.VISIBLE
            elapsedTimeView.visibility = View.VISIBLE
            totalTimeView.visibility = View.VISIBLE
        } else if (!data.enabled && seekBarView.visibility == View.VISIBLE) {
            seekBarView.visibility = View.GONE
            elapsedTimeView.visibility = View.GONE
            totalTimeView.visibility = View.GONE
            return
        }

        // TODO: update the style of the disabled progress bar
        seekBarView.setEnabled(data.seekAvailable)

        data.color?.let {
            var tintList = ColorStateList.valueOf(it)
            seekBarView.setThumbTintList(tintList)
            tintList = tintList.withAlpha(192) // 75%
            seekBarView.setProgressTintList(tintList)
            tintList = tintList.withAlpha(128) // 50%
            seekBarView.setProgressBackgroundTintList(tintList)
            elapsedTimeView.setTextColor(it)
            totalTimeView.setTextColor(it)
        }

        data.elapsedTime?.let {
            seekBarView.setProgress(it)
            elapsedTimeView.setText(DateUtils.formatElapsedTime(
                    it / DateUtils.SECOND_IN_MILLIS))
        }

        data.duration?.let {
            seekBarView.setMax(it)
            totalTimeView.setText(DateUtils.formatElapsedTime(
                    it / DateUtils.SECOND_IN_MILLIS))
        }
    }
}
+152 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.media

import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.PlaybackState
import android.view.MotionEvent
import android.view.View
import android.widget.SeekBar
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData

import com.android.systemui.util.concurrency.DelayableExecutor

private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L

/** ViewModel for seek bar in QS media player. */
class SeekBarViewModel(val bgExecutor: DelayableExecutor) {

    private val _progress = MutableLiveData<Progress>().apply {
        postValue(Progress(false, false, null, null, null))
    }
    val progress: LiveData<Progress>
        get() = _progress
    private var controller: MediaController? = null
    private var playbackState: PlaybackState? = null

    /** Listening state (QS open or closed) is used to control polling of progress. */
    var listening = true
        set(value) {
            if (value) {
                checkPlaybackPosition()
            }
        }

    /**
     * Handle request to change the current position in the media track.
     * @param position Place to seek to in the track.
     */
    @WorkerThread
    fun onSeek(position: Long) {
        controller?.transportControls?.seekTo(position)
    }

    /**
     * Updates media information.
     * @param mediaController controller for media session
     * @param color foreground color for UI elements
     */
    @WorkerThread
    fun updateController(mediaController: MediaController?, color: Int) {
        controller = mediaController
        playbackState = controller?.playbackState
        val mediaMetadata = controller?.metadata
        val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L
        val position = playbackState?.position?.toInt()
        val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt()
        val enabled = if (duration != null && duration <= 0) false else true
        _progress.postValue(Progress(enabled, seekAvailable, position, duration, color))
        if (shouldPollPlaybackPosition()) {
            checkPlaybackPosition()
        }
    }

    @AnyThread
    private fun checkPlaybackPosition(): Runnable = bgExecutor.executeDelayed({
        val currentPosition = controller?.playbackState?.position?.toInt()
        if (currentPosition != null && _progress.value!!.elapsedTime != currentPosition) {
            _progress.postValue(_progress.value!!.copy(elapsedTime = currentPosition))
        }
        if (shouldPollPlaybackPosition()) {
            checkPlaybackPosition()
        }
    }, POSITION_UPDATE_INTERVAL_MILLIS)

    @WorkerThread
    private fun shouldPollPlaybackPosition(): Boolean {
        val state = playbackState?.state
        val moving = if (state == null) false else
                state == PlaybackState.STATE_PLAYING ||
                state == PlaybackState.STATE_BUFFERING ||
                state == PlaybackState.STATE_FAST_FORWARDING ||
                state == PlaybackState.STATE_REWINDING
        return moving && listening
    }

    /** Gets a listener to attach to the seek bar to handle seeking. */
    val seekBarListener: SeekBar.OnSeekBarChangeListener
        get() {
            return SeekBarChangeListener(this, bgExecutor)
        }

    /** Gets a listener to attach to the seek bar to disable touch intercepting. */
    val seekBarTouchListener: View.OnTouchListener
        get() {
            return SeekBarTouchListener()
        }

    private class SeekBarChangeListener(
        val viewModel: SeekBarViewModel,
        val bgExecutor: DelayableExecutor
    ) : SeekBar.OnSeekBarChangeListener {
        override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) {
            if (fromUser) {
                bgExecutor.execute {
                    viewModel.onSeek(progress.toLong())
                }
            }
        }
        override fun onStartTrackingTouch(bar: SeekBar) {
        }
        override fun onStopTrackingTouch(bar: SeekBar) {
            val pos = bar.progress.toLong()
            bgExecutor.execute {
                viewModel.onSeek(pos)
            }
        }
    }

    private class SeekBarTouchListener : View.OnTouchListener {
        override fun onTouch(view: View, event: MotionEvent): Boolean {
            view.parent.requestDisallowInterceptTouchEvent(true)
            return view.onTouchEvent(event)
        }
    }

    /** State seen by seek bar UI. */
    data class Progress(
        val enabled: Boolean,
        val seekAvailable: Boolean,
        val elapsedTime: Int?,
        val duration: Int?,
        val color: Int?
    )
}
+38 −1
Original line number Diff line number Diff line
@@ -16,11 +16,14 @@

package com.android.systemui.qs;

import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;

import android.app.Notification;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.util.Log;
import android.view.View;
@@ -28,12 +31,16 @@ import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;

import com.android.settingslib.media.MediaDevice;
import com.android.systemui.R;
import com.android.systemui.media.MediaControlPanel;
import com.android.systemui.media.SeekBarObserver;
import com.android.systemui.media.SeekBarViewModel;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.util.concurrency.DelayableExecutor;

import java.util.concurrent.Executor;

@@ -54,6 +61,9 @@ public class QSMediaPlayer extends MediaControlPanel {
    };

    private final QSPanel mParent;
    private final DelayableExecutor mBackgroundExecutor;
    private final SeekBarViewModel mSeekBarViewModel;
    private final SeekBarObserver mSeekBarObserver;

    /**
     * Initialize quick shade version of player
@@ -64,10 +74,20 @@ public class QSMediaPlayer extends MediaControlPanel {
     * @param backgroundExecutor
     */
    public QSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager,
            Executor foregroundExecutor, Executor backgroundExecutor) {
            Executor foregroundExecutor, DelayableExecutor backgroundExecutor) {
        super(context, parent, manager, R.layout.qs_media_panel, QS_ACTION_IDS, foregroundExecutor,
                backgroundExecutor);
        mParent = (QSPanel) parent;
        mBackgroundExecutor = backgroundExecutor;
        mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
        mSeekBarObserver = new SeekBarObserver(getView());
        // Can't use the viewAttachLifecycle of media player because remove/add is used to adjust
        // priority of players. As soon as it is removed, the lifecycle will end and the seek bar
        // will stop updating. So, use the lifecycle of the parent instead.
        mSeekBarViewModel.getProgress().observe(viewAttachLifecycle(parent), mSeekBarObserver);
        SeekBar bar = getView().findViewById(R.id.media_progress_bar);
        bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
        bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
    }

    /**
@@ -115,6 +135,11 @@ public class QSMediaPlayer extends MediaControlPanel {
            thisBtn.setVisibility(View.GONE);
        }

        // Seek Bar
        final MediaController controller = new MediaController(getContext(), token);
        mBackgroundExecutor.execute(
                () -> mSeekBarViewModel.updateController(controller, iconColor));

        // Set up long press menu
        View guts = mMediaNotifView.findViewById(R.id.media_guts);
        View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
@@ -155,4 +180,16 @@ public class QSMediaPlayer extends MediaControlPanel {
            return true; // consumed click
        });
    }

    /**
     * Sets the listening state of the player.
     *
     * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
     * unnecessary work when the QS panel is closed.
     *
     * @param listening True when player should be active. Otherwise, false.
     */
    public void setListening(boolean listening) {
        mSeekBarViewModel.setListening(listening);
    }
}
+7 −3
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
import com.android.systemui.util.concurrency.DelayableExecutor;

import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -103,7 +104,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
    private final NotificationMediaManager mNotificationMediaManager;
    private final LocalBluetoothManager mLocalBluetoothManager;
    private final Executor mForegroundExecutor;
    private final Executor mBackgroundExecutor;
    private final DelayableExecutor mBackgroundExecutor;
    private LocalMediaManager mLocalMediaManager;
    private MediaDevice mDevice;
    private boolean mUpdateCarousel = false;
@@ -166,7 +167,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
            QSLogger qsLogger,
            NotificationMediaManager notificationMediaManager,
            @Main Executor foregroundExecutor,
            @Background Executor backgroundExecutor,
            @Background DelayableExecutor backgroundExecutor,
            @Nullable LocalBluetoothManager localBluetoothManager
    ) {
        super(context, attrs);
@@ -278,7 +279,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
            Log.d(TAG, "creating new player");
            player = new QSMediaPlayer(mContext, this, mNotificationMediaManager,
                    mForegroundExecutor, mBackgroundExecutor);

            player.setListening(mListening);
            if (player.isPlaying()) {
                mMediaCarousel.addView(player.getView(), 0, lp); // add in front
            } else {
@@ -584,6 +585,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
        if (mListening) {
            refreshAllTiles();
        }
        for (QSMediaPlayer player : mMediaPlayers) {
            player.setListening(mListening);
        }
    }

    private String getTilesSpecs() {
Loading