File indexing completed on 2024-12-22 04:41:39
0001 /* 0002 * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 package org.kde.kdeconnect.Plugins.MprisPlugin; 0008 0009 import android.content.ActivityNotFoundException; 0010 import android.content.Intent; 0011 import android.content.SharedPreferences; 0012 import android.graphics.Bitmap; 0013 import android.graphics.drawable.Drawable; 0014 import android.net.Uri; 0015 import android.os.Bundle; 0016 import android.os.Handler; 0017 import android.preference.PreferenceManager; 0018 import android.view.LayoutInflater; 0019 import android.view.Menu; 0020 import android.view.MenuItem; 0021 import android.view.View; 0022 import android.view.ViewGroup; 0023 import android.widget.AdapterView; 0024 import android.widget.ArrayAdapter; 0025 import android.widget.SeekBar; 0026 import android.widget.Toast; 0027 0028 import androidx.annotation.NonNull; 0029 import androidx.annotation.Nullable; 0030 import androidx.core.content.ContextCompat; 0031 import androidx.core.graphics.drawable.DrawableCompat; 0032 import androidx.fragment.app.Fragment; 0033 0034 import org.apache.commons.lang3.ArrayUtils; 0035 import org.apache.commons.lang3.StringUtils; 0036 import org.kde.kdeconnect.Helpers.VideoUrlsHelper; 0037 import org.kde.kdeconnect.Helpers.VolumeHelperKt; 0038 import org.kde.kdeconnect.KdeConnect; 0039 import org.kde.kdeconnect_tp.R; 0040 import org.kde.kdeconnect_tp.databinding.MprisControlBinding; 0041 import org.kde.kdeconnect_tp.databinding.MprisNowPlayingBinding; 0042 0043 import java.net.MalformedURLException; 0044 import java.util.List; 0045 0046 public class MprisNowPlayingFragment extends Fragment implements VolumeKeyListener { 0047 0048 final static int MENU_OPEN_URL = Menu.FIRST; 0049 private final Handler positionSeekUpdateHandler = new Handler(); 0050 MprisControlBinding mprisControlBinding; 0051 private MprisNowPlayingBinding activityMprisBinding; 0052 private String deviceId; 0053 private Runnable positionSeekUpdateRunnable = null; 0054 0055 private String targetPlayerName = ""; 0056 private MprisPlugin.MprisPlayer targetPlayer = null; 0057 0058 public static MprisNowPlayingFragment newInstance(String deviceId) { 0059 MprisNowPlayingFragment mprisNowPlayingFragment = new MprisNowPlayingFragment(); 0060 0061 Bundle arguments = new Bundle(); 0062 arguments.putString(MprisPlugin.DEVICE_ID_KEY, deviceId); 0063 0064 mprisNowPlayingFragment.setArguments(arguments); 0065 0066 return mprisNowPlayingFragment; 0067 } 0068 0069 private static String milisToProgress(long milis) { 0070 int length = (int) (milis / 1000); //From milis to seconds 0071 StringBuilder text = new StringBuilder(); 0072 int minutes = length / 60; 0073 if (minutes > 60) { 0074 int hours = minutes / 60; 0075 minutes = minutes % 60; 0076 text.append(hours).append(':'); 0077 if (minutes < 10) text.append('0'); 0078 } 0079 text.append(minutes).append(':'); 0080 int seconds = (length % 60); 0081 if (seconds < 10) 0082 text.append('0'); // needed to show length properly (eg 4:05 instead of 4:5) 0083 text.append(seconds); 0084 return text.toString(); 0085 } 0086 0087 @Nullable 0088 @Override 0089 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 0090 activityMprisBinding = MprisNowPlayingBinding.inflate(inflater); 0091 mprisControlBinding = activityMprisBinding.mprisControl; 0092 0093 deviceId = requireArguments().getString(MprisPlugin.DEVICE_ID_KEY); 0094 0095 targetPlayerName = ""; 0096 Intent activityIntent = requireActivity().getIntent(); 0097 if (activityIntent.hasExtra("player")) { 0098 targetPlayerName = activityIntent.getStringExtra("player"); 0099 activityIntent.removeExtra("player"); 0100 } else if (savedInstanceState != null) { 0101 targetPlayerName = savedInstanceState.getString("targetPlayer"); 0102 } 0103 0104 connectToPlugin(); 0105 0106 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); 0107 String interval_time_str = prefs.getString(getString(R.string.mpris_time_key), 0108 getString(R.string.mpris_time_default)); 0109 final int interval_time = Integer.parseInt(interval_time_str); 0110 0111 performActionOnClick(mprisControlBinding.loopButton, p -> { 0112 switch (p.getLoopStatus()) { 0113 case "None": 0114 p.setLoopStatus("Track"); 0115 break; 0116 case "Track": 0117 p.setLoopStatus("Playlist"); 0118 break; 0119 case "Playlist": 0120 p.setLoopStatus("None"); 0121 break; 0122 } 0123 }); 0124 0125 performActionOnClick(mprisControlBinding.playButton, MprisPlugin.MprisPlayer::playPause); 0126 0127 performActionOnClick(mprisControlBinding.shuffleButton, p -> p.setShuffle(!p.getShuffle())); 0128 0129 performActionOnClick(mprisControlBinding.prevButton, MprisPlugin.MprisPlayer::previous); 0130 0131 performActionOnClick(mprisControlBinding.rewButton, p -> targetPlayer.seek(interval_time * -1)); 0132 0133 performActionOnClick(mprisControlBinding.ffButton, p -> p.seek(interval_time)); 0134 0135 performActionOnClick(mprisControlBinding.nextButton, MprisPlugin.MprisPlayer::next); 0136 0137 performActionOnClick(mprisControlBinding.stopButton, MprisPlugin.MprisPlayer::stop); 0138 0139 mprisControlBinding.volumeSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 0140 @Override 0141 public void onProgressChanged(SeekBar seekBar, int i, boolean b) { 0142 } 0143 0144 @Override 0145 public void onStartTrackingTouch(SeekBar seekBar) { 0146 } 0147 0148 @Override 0149 public void onStopTrackingTouch(final SeekBar seekBar) { 0150 if (targetPlayer == null) return; 0151 targetPlayer.setVolume(seekBar.getProgress()); 0152 } 0153 }); 0154 0155 positionSeekUpdateRunnable = () -> { 0156 if (!isAdded()) return; // Fragment was already detached 0157 if (targetPlayer != null) { 0158 mprisControlBinding.positionSeek.setProgress((int) (targetPlayer.getPosition())); 0159 } 0160 positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable); 0161 positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 1000); 0162 }; 0163 positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200); 0164 0165 mprisControlBinding.positionSeek.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 0166 @Override 0167 public void onProgressChanged(SeekBar seekBar, int progress, boolean byUser) { 0168 mprisControlBinding.progressTextview.setText(milisToProgress(progress)); 0169 } 0170 0171 @Override 0172 public void onStartTrackingTouch(SeekBar seekBar) { 0173 positionSeekUpdateHandler.removeCallbacks(positionSeekUpdateRunnable); 0174 } 0175 0176 @Override 0177 public void onStopTrackingTouch(final SeekBar seekBar) { 0178 if (targetPlayer != null) { 0179 targetPlayer.setPosition(seekBar.getProgress()); 0180 } 0181 positionSeekUpdateHandler.postDelayed(positionSeekUpdateRunnable, 200); 0182 } 0183 }); 0184 0185 mprisControlBinding.nowPlayingTextview.setSelected(true); 0186 0187 return activityMprisBinding.getRoot(); 0188 } 0189 0190 @Override 0191 public void onDestroyView() { 0192 disconnectFromPlugin(); 0193 super.onDestroyView(); 0194 } 0195 0196 private void disconnectFromPlugin() { 0197 MprisPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MprisPlugin.class); 0198 if (plugin != null) { 0199 plugin.removePlayerListUpdatedHandler("activity"); 0200 plugin.removePlayerStatusUpdatedHandler("activity"); 0201 } 0202 } 0203 0204 private void connectToPlugin() { 0205 MprisPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, MprisPlugin.class); 0206 if (plugin == null) { 0207 if (isAdded()) { 0208 requireActivity().finish(); 0209 } 0210 return; 0211 } 0212 targetPlayer = plugin.getPlayerStatus(targetPlayerName); 0213 0214 plugin.setPlayerStatusUpdatedHandler("activity", () -> requireActivity().runOnUiThread(() -> { 0215 updatePlayerStatus(plugin); 0216 })); 0217 plugin.setPlayerListUpdatedHandler("activity", () -> requireActivity().runOnUiThread(() -> { 0218 final List<String> playerList = plugin.getPlayerList(); 0219 final ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), 0220 android.R.layout.simple_spinner_item, 0221 playerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY) 0222 ); 0223 0224 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 0225 0226 mprisControlBinding.playerSpinner.setAdapter(adapter); 0227 0228 if (playerList.isEmpty()) { 0229 mprisControlBinding.noPlayers.setVisibility(View.VISIBLE); 0230 mprisControlBinding.playerSpinner.setVisibility(View.GONE); 0231 mprisControlBinding.nowPlayingTextview.setText(""); 0232 } else { 0233 mprisControlBinding.noPlayers.setVisibility(View.GONE); 0234 mprisControlBinding.playerSpinner.setVisibility(View.VISIBLE); 0235 } 0236 0237 mprisControlBinding.playerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 0238 @Override 0239 public void onItemSelected(AdapterView<?> arg0, View arg1, int pos, long id) { 0240 0241 if (pos >= playerList.size()) return; 0242 0243 String player = playerList.get(pos); 0244 if (targetPlayer != null && player.equals(targetPlayer.getPlayerName())) { 0245 return; //Player hasn't actually changed 0246 } 0247 targetPlayer = plugin.getPlayerStatus(player); 0248 if (targetPlayer != null) { 0249 targetPlayerName = targetPlayer.getPlayerName(); 0250 } 0251 0252 updatePlayerStatus(plugin); 0253 0254 if (targetPlayer != null && targetPlayer.isPlaying()) { 0255 MprisMediaSession.getInstance().playerSelected(targetPlayer); 0256 } 0257 } 0258 0259 @Override 0260 public void onNothingSelected(AdapterView<?> arg0) { 0261 targetPlayer = null; 0262 } 0263 }); 0264 0265 if (targetPlayer == null) { 0266 //If no player is selected, try to select a playing player 0267 targetPlayer = plugin.getPlayingPlayer(); 0268 } 0269 //Try to select the specified player 0270 if (targetPlayer != null) { 0271 int targetIndex = adapter.getPosition(targetPlayer.getPlayerName()); 0272 if (targetIndex >= 0) { 0273 mprisControlBinding.playerSpinner.setSelection(targetIndex); 0274 } else { 0275 targetPlayer = null; 0276 } 0277 } 0278 //If no player selected, select the first one (if any) 0279 if (targetPlayer == null && !playerList.isEmpty()) { 0280 targetPlayer = plugin.getPlayerStatus(playerList.get(0)); 0281 mprisControlBinding.playerSpinner.setSelection(0); 0282 } 0283 updatePlayerStatus(plugin); 0284 })); 0285 } 0286 0287 private void performActionOnClick(View v, MprisPlayerCallback l) { 0288 v.setOnClickListener(view -> { 0289 if (targetPlayer == null) return; 0290 l.performAction(targetPlayer); 0291 }); 0292 } 0293 0294 private void updatePlayerStatus(MprisPlugin plugin) { 0295 if (!isAdded()) { 0296 //Fragment is not attached to an activity. We will crash if we try to do anything here. 0297 return; 0298 } 0299 0300 MprisPlugin.MprisPlayer playerStatus = targetPlayer; 0301 if (playerStatus == null) { 0302 //No player with that name found, just display "empty" data 0303 playerStatus = plugin.getEmptyPlayer(); 0304 } 0305 0306 String song = playerStatus.getTitle(); 0307 if (!StringUtils.isEmpty(playerStatus.getArtist())) { 0308 song += " - " + playerStatus.getArtist(); 0309 } 0310 if (!mprisControlBinding.nowPlayingTextview.getText().toString().equals(song)) { 0311 mprisControlBinding.nowPlayingTextview.setText(song); 0312 } 0313 0314 Bitmap albumArt = playerStatus.getAlbumArt(); 0315 if (albumArt == null) { 0316 final Drawable drawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_album_art_placeholder); 0317 assert drawable != null; 0318 Drawable placeholder_art = DrawableCompat.wrap(drawable); 0319 DrawableCompat.setTint(placeholder_art, ContextCompat.getColor(requireContext(), R.color.primary)); 0320 activityMprisBinding.albumArt.setImageDrawable(placeholder_art); 0321 } else { 0322 activityMprisBinding.albumArt.setImageBitmap(albumArt); 0323 } 0324 0325 if (playerStatus.isSeekAllowed()) { 0326 mprisControlBinding.timeTextview.setText(milisToProgress(playerStatus.getLength())); 0327 mprisControlBinding.positionSeek.setMax((int) (playerStatus.getLength())); 0328 mprisControlBinding.positionSeek.setProgress((int) (playerStatus.getPosition())); 0329 mprisControlBinding.progressSlider.setVisibility(View.VISIBLE); 0330 } else { 0331 mprisControlBinding.progressSlider.setVisibility(View.GONE); 0332 } 0333 0334 int volume = playerStatus.getVolume(); 0335 mprisControlBinding.volumeSeek.setProgress(volume); 0336 if(!playerStatus.isSetVolumeAllowed()) { 0337 mprisControlBinding.volumeSeek.setEnabled(false); 0338 } 0339 boolean isPlaying = playerStatus.isPlaying(); 0340 if (isPlaying) { 0341 mprisControlBinding.playButton.setIconResource(R.drawable.ic_pause_black); 0342 mprisControlBinding.playButton.setEnabled(playerStatus.isPauseAllowed()); 0343 } else { 0344 mprisControlBinding.playButton.setIconResource(R.drawable.ic_play_black); 0345 mprisControlBinding.playButton.setEnabled(playerStatus.isPlayAllowed()); 0346 } 0347 0348 String loopStatus = playerStatus.getLoopStatus(); 0349 switch (loopStatus) { 0350 case "None": 0351 mprisControlBinding.loopButton.setIconResource(R.drawable.ic_loop_none_black); 0352 break; 0353 case "Track": 0354 mprisControlBinding.loopButton.setIconResource(R.drawable.ic_loop_track_black); 0355 break; 0356 case "Playlist": 0357 mprisControlBinding.loopButton.setIconResource(R.drawable.ic_loop_playlist_black); 0358 break; 0359 } 0360 0361 boolean shuffle = playerStatus.getShuffle(); 0362 if (shuffle) { 0363 mprisControlBinding.shuffleButton.setIconResource(R.drawable.ic_shuffle_on_black); 0364 } else { 0365 mprisControlBinding.shuffleButton.setIconResource(R.drawable.ic_shuffle_off_black); 0366 } 0367 0368 mprisControlBinding.loopButton.setVisibility(playerStatus.isLoopStatusAllowed() ? View.VISIBLE : View.GONE); 0369 mprisControlBinding.shuffleButton.setVisibility(playerStatus.isShuffleAllowed() ? View.VISIBLE : View.GONE); 0370 mprisControlBinding.volumeLayout.setVisibility(playerStatus.isSetVolumeAllowed() ? View.VISIBLE : View.GONE); 0371 mprisControlBinding.rewButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE); 0372 mprisControlBinding.ffButton.setVisibility(playerStatus.isSeekAllowed() ? View.VISIBLE : View.GONE); 0373 0374 requireActivity().invalidateOptionsMenu(); 0375 0376 //Show and hide previous/next buttons simultaneously 0377 if (playerStatus.isGoPreviousAllowed() || playerStatus.isGoNextAllowed()) { 0378 mprisControlBinding.prevButton.setVisibility(View.VISIBLE); 0379 mprisControlBinding.prevButton.setEnabled(playerStatus.isGoPreviousAllowed()); 0380 mprisControlBinding.nextButton.setVisibility(View.VISIBLE); 0381 mprisControlBinding.nextButton.setEnabled(playerStatus.isGoNextAllowed()); 0382 } else { 0383 mprisControlBinding.prevButton.setVisibility(View.GONE); 0384 mprisControlBinding.nextButton.setVisibility(View.GONE); 0385 } 0386 } 0387 0388 /** 0389 * Change current volume with provided step. 0390 * 0391 * @param step step size volume change 0392 */ 0393 private void updateVolume(int step) { 0394 if (targetPlayer == null) return; 0395 0396 int newVolume = VolumeHelperKt.calculateNewVolume(targetPlayer.getVolume(), VolumeHelperKt.DEFAULT_MAX_VOLUME, step); 0397 0398 if (targetPlayer.getVolume() != newVolume) { 0399 targetPlayer.setVolume(newVolume); 0400 } 0401 } 0402 0403 @Override 0404 public void onVolumeUp() { 0405 updateVolume(VolumeHelperKt.DEFAULT_VOLUME_STEP); 0406 } 0407 0408 @Override 0409 public void onVolumeDown() { 0410 updateVolume(-VolumeHelperKt.DEFAULT_VOLUME_STEP); 0411 } 0412 0413 @Override 0414 public void onPrepareOptionsMenu(@NonNull Menu menu) { 0415 menu.clear(); 0416 if (targetPlayer != null && !"".equals(targetPlayer.getUrl())) { 0417 menu.add(0, MENU_OPEN_URL, Menu.NONE, R.string.mpris_open_url); 0418 } 0419 } 0420 0421 @Override 0422 public boolean onOptionsItemSelected(@NonNull MenuItem item) { 0423 if (targetPlayer != null && item.getItemId() == MENU_OPEN_URL) { 0424 try { 0425 String url = VideoUrlsHelper.formatUriWithSeek(targetPlayer.getUrl(), targetPlayer.getPosition()).toString(); 0426 Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); 0427 startActivity(browserIntent); 0428 targetPlayer.pause(); 0429 return true; 0430 } catch (MalformedURLException | ActivityNotFoundException e) { 0431 e.printStackTrace(); 0432 Toast.makeText(requireContext(), getString(R.string.cant_open_url), Toast.LENGTH_LONG).show(); 0433 } 0434 } 0435 return super.onOptionsItemSelected(item); 0436 } 0437 0438 @Override 0439 public void onSaveInstanceState(@NonNull Bundle outState) { 0440 if (targetPlayer != null) { 0441 outState.putString("targetPlayer", targetPlayerName); 0442 } 0443 } 0444 0445 private interface MprisPlayerCallback { 0446 void performAction(MprisPlugin.MprisPlayer player); 0447 } 0448 }