25 Commits
3.4.4 ... 3.4.5

Author SHA1 Message Date
CappielloAntonio
dc13beca49 Update github_release.yml 2023-07-12 10:16:27 +02:00
antonio
6399488819 Merge remote-tracking branch 'origin/main' 2023-07-12 10:04:45 +02:00
antonio
69c1b93d28 gradle: bump up code version 2023-07-12 10:04:36 +02:00
antonio
50c240793a gradle: update dependencies 2023-07-12 10:03:52 +02:00
CappielloAntonio
80f7783b7d Update github_release.yml 2023-07-12 10:03:17 +02:00
antonio
259c8b3599 refactor: added search2 method as per Subsonic API specifications 2023-07-12 09:52:57 +02:00
antonio
ba1295440b feat: added ReplayGain functionality in main flavor 2023-07-12 09:27:58 +02:00
antonio
9e6178037c feat: added ReplayGain functionality in main flavor 2023-07-03 09:27:35 +02:00
antonio
47302815c1 clean: code cleanup 2023-07-03 09:26:31 +02:00
antonio
c81bfe1457 fix: deleted an irrelevant minus sign 2023-07-03 09:24:20 +02:00
antonio
f413b5d498 feat: added initial version ReplayGain functionality, still in development 2023-07-02 23:38:33 +02:00
antonio
feae1b8bdd feat: added settings to enable and disable ReplayGain 2023-07-02 23:36:52 +02:00
antonio
57bb51fb8d feat: version number is now taken dynamically from the VersionName parameter 2023-07-02 19:59:03 +02:00
antonio
0f1364502a fix: modified list scrolling behavior 2023-07-02 19:55:05 +02:00
antonio
a508ec0af6 feat: make music directory section optional 2023-07-02 19:36:39 +02:00
antonio
a9b264004c fix: fixed directory navigation 2023-07-02 19:23:08 +02:00
antonio
803b61430f build: removed unused dependencies 2023-07-02 12:32:08 +02:00
antonio
25367512c0 build: removed unused dependency 2023-07-02 12:13:50 +02:00
antonio
e3720f4a50 build: disabled jetifier 2023-07-02 12:11:36 +02:00
antonio
1168cc58f4 clean: removed unused repositories 2023-07-02 12:09:37 +02:00
antonio
c711b387bb refactor: extracted common toolbar to better handle two flavors and minimized file duplication 2023-07-01 18:11:44 +02:00
antonio
a3fe0de233 feat: initial groundwork for creating the flavor 2023-07-01 11:37:36 +02:00
antonio
922153837d fix: tap the player bottom sheet header to bring player up 2023-06-30 18:34:33 +02:00
antonio
960dd6c76a fix: shuffle all action 2023-06-30 18:23:12 +02:00
antonio
fcb7135c3d fix: changed the translation label 2023-06-25 22:12:45 +02:00
43 changed files with 863 additions and 356 deletions

View File

@@ -34,13 +34,13 @@ jobs:
- name: Build APK - name: Build APK
id: build id: build
run: bash ./gradlew assembleRelease run: bash ./gradlew assembleTempoRelease
- name: Sign APK - name: Sign APK
id: sign_apk id: sign_apk
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
with: with:
releaseDirectory: app/build/outputs/apk/release releaseDirectory: app/build/outputs/apk/tempo/release
signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }} signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
alias: ${{ secrets.KEY_ALIAS_GITHUB }} alias: ${{ secrets.KEY_ALIAS_GITHUB }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -98,7 +98,7 @@ jobs:
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{steps.sign_apk.outputs.signedReleaseFile}} asset_path: ${{steps.sign_apk.outputs.signedReleaseFile}}
asset_name: app-release.apk asset_name: app-tempo-release.apk
asset_content_type: application/zip asset_content_type: application/zip
# - name: Upload AAB # - name: Upload AAB

View File

@@ -7,11 +7,8 @@ android {
buildToolsVersion '33.0.0' buildToolsVersion '33.0.0'
defaultConfig { defaultConfig {
applicationId 'com.cappielloantonio.tempo'
minSdkVersion 24 minSdkVersion 24
targetSdkVersion 34 targetSdkVersion 34
versionCode 11
versionName '3.4.4'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -25,6 +22,24 @@ android {
} }
} }
flavorDimensions "default"
productFlavors {
tempo {
dimension "default"
applicationId 'com.cappielloantonio.tempo'
versionCode 12
versionName '3.4.5'
}
notquitemy {
dimension "default"
applicationId "com.cappielloantonio.notquitemy.tempo"
versionCode 1
versionName "1.0.0"
}
}
buildTypes { buildTypes {
release { release {
shrinkResources true shrinkResources true
@@ -54,8 +69,6 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
// AndroidX // AndroidX
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.0'
@@ -63,13 +76,8 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0' implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.room:room-runtime:2.5.2' implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.1'
// Google GMS
implementation 'com.google.android.gms:play-services-cast-framework:21.3.0'
// Android Material // Android Material
implementation 'com.google.android.material:material:1.9.0' implementation 'com.google.android.material:material:1.9.0'
@@ -78,23 +86,17 @@ dependencies {
implementation 'com.github.bumptech.glide:annotations:4.15.1' implementation 'com.github.bumptech.glide:annotations:4.15.1'
// Media3 // Media3
implementation 'androidx.media3:media3-session:1.0.2' implementation 'androidx.media3:media3-session:1.1.0'
implementation 'androidx.media3:media3-common:1.0.2' implementation 'androidx.media3:media3-common:1.1.0'
implementation 'androidx.media3:media3-exoplayer:1.0.2' implementation 'androidx.media3:media3-exoplayer:1.1.0'
implementation 'androidx.media3:media3-ui:1.0.2' implementation 'androidx.media3:media3-ui:1.1.0'
implementation 'androidx.media3:media3-cast:1.0.2' tempoImplementation 'androidx.media3:media3-cast:1.1.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1' annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1'
annotationProcessor 'androidx.room:room-compiler:2.5.2' annotationProcessor 'androidx.room:room-compiler:2.5.2'
// Retrofit // Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.6' implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Crash Report
// debugImplementation 'com.balsikandar.android:crashreporter:1.1.0'
// DB debug
// debugImplementation 'com.amitshekhar.android:debug-db:1.0.6'
} }

View File

@@ -0,0 +1,9 @@
package com.cappielloantonio.tempo.model
import androidx.annotation.Keep
@Keep
data class ReplayGain(
var trackGain: Float = 0f,
var albumGain: Float = 0f,
)

View File

@@ -11,6 +11,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
import com.cappielloantonio.tempo.subsonic.models.SearchResult3; import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
import java.util.ArrayList; import java.util.ArrayList;
@@ -24,7 +25,28 @@ import retrofit2.Response;
public class SearchingRepository { public class SearchingRepository {
private final RecentSearchDao recentSearchDao = AppDatabase.getInstance().recentSearchDao(); private final RecentSearchDao recentSearchDao = AppDatabase.getInstance().recentSearchDao();
public MutableLiveData<SearchResult3> search(String query) { public MutableLiveData<SearchResult2> search2(String query) {
MutableLiveData<SearchResult2> result = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getSearchingClient()
.search3(query, 20, 20, 20)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
result.setValue(response.body().getSubsonicResponse().getSearchResult2());
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return result;
}
public MutableLiveData<SearchResult3> search3(String query) {
MutableLiveData<SearchResult3> result = new MutableLiveData<>(); MutableLiveData<SearchResult3> result = new MutableLiveData<>();
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)

View File

@@ -141,6 +141,10 @@ public class MainActivity extends BaseActivity {
handler.postDelayed(runnable, 100); handler.postDelayed(runnable, 100);
} }
public void expandBottomSheet() {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
public void setBottomSheetDraggableState(Boolean isDraggable) { public void setBottomSheetDraggableState(Boolean isDraggable) {
bottomSheetBehavior.setDraggable(isDraggable); bottomSheetBehavior.setDraggable(isDraggable);
} }

View File

@@ -19,9 +19,8 @@ import androidx.media3.session.SessionToken;
import com.cappielloantonio.tempo.service.DownloaderService; import com.cappielloantonio.tempo.service.DownloaderService;
import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.ui.dialog.BatteryOptimizationDialog; import com.cappielloantonio.tempo.ui.dialog.BatteryOptimizationDialog;
import com.cappielloantonio.tempo.util.Flavors;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.UIUtil;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.material.elevation.SurfaceColors; import com.google.android.material.elevation.SurfaceColors;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@@ -34,7 +33,7 @@ public class BaseActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
initializeCastContext(); Flavors.initializeCastContext(this);
initializeDownloader(); initializeDownloader();
checkBatteryOptimization(); checkBatteryOptimization();
checkPermission(); checkPermission();
@@ -54,7 +53,7 @@ public class BaseActivity extends AppCompatActivity {
} }
private void checkBatteryOptimization() { private void checkBatteryOptimization() {
if (detectBatteryOptimization() && Preferences.askForOptimization()) { if (detectBatteryOptimization() && Boolean.TRUE.equals(Preferences.askForOptimization())) {
showBatteryOptimizationDialog(); showBatteryOptimizationDialog();
} }
} }
@@ -98,10 +97,6 @@ public class BaseActivity extends AppCompatActivity {
} }
} }
private void initializeCastContext() {
if (UIUtil.isCastApiAvailable(this)) CastContext.getSharedInstance(this);
}
private void setNavigationBarColor() { private void setNavigationBarColor() {
getWindow().setNavigationBarColor(SurfaceColors.getColorForElevation(this, 10)); getWindow().setNavigationBarColor(SurfaceColors.getColorForElevation(this, 10));
} }

View File

@@ -76,7 +76,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
if (children.get(getBindingAdapterPosition()).isDir()) { if (children.get(getBindingAdapterPosition()).isDir()) {
bundle.putParcelable(Constants.MUSIC_DIRECTORY_OBJECT, children.get(getBindingAdapterPosition())); bundle.putString(Constants.MUSIC_DIRECTORY_ID, children.get(getBindingAdapterPosition()).getId());
click.onMusicDirectoryClick(bundle); click.onMusicDirectoryClick(bundle);
} else { } else {
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(children)); bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(children));

View File

@@ -73,7 +73,7 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
public void onClick() { public void onClick() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelable(Constants.MUSIC_INDEX_OBJECT, artists.get(getBindingAdapterPosition())); bundle.putString(Constants.MUSIC_DIRECTORY_ID, artists.get(getBindingAdapterPosition()).getId());
click.onMusicIndexClick(bundle); click.onMusicIndexClick(bundle);
} }
} }

View File

@@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser; import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken; import androidx.media3.session.SessionToken;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.R;
@@ -20,17 +21,12 @@ import com.cappielloantonio.tempo.databinding.FragmentDirectoryBinding;
import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Artist;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter; import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel; import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
import java.util.Objects;
@UnstableApi @UnstableApi
public class DirectoryFragment extends Fragment implements ClickCallback { public class DirectoryFragment extends Fragment implements ClickCallback {
private static final String TAG = "DirectoryFragment"; private static final String TAG = "DirectoryFragment";
@@ -52,9 +48,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
directoryViewModel = new ViewModelProvider(requireActivity()).get(DirectoryViewModel.class); directoryViewModel = new ViewModelProvider(requireActivity()).get(DirectoryViewModel.class);
initAppBar(); initAppBar();
initButtons();
initDirectoryListView(); initDirectoryListView();
init();
return view; return view;
} }
@@ -77,17 +71,6 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
bind = null; bind = null;
} }
private void init() {
Artist artist = getArguments().getParcelable(Constants.MUSIC_INDEX_OBJECT);
if (artist != null) {
directoryViewModel.setMusicDirectoryId(artist.getId());
directoryViewModel.setMusicDirectoryName(artist.getName());
}
directoryViewModel.loadMusicDirectory(getViewLifecycleOwner());
}
private void initAppBar() { private void initAppBar() {
activity.setSupportActionBar(bind.toolbar); activity.setSupportActionBar(bind.toolbar);
@@ -96,39 +79,10 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
activity.getSupportActionBar().setDisplayShowHomeEnabled(true); activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
} }
if (bind != null) if (bind != null) {
bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.directoryBackImageView.setOnClickListener(v -> activity.navController.navigateUp());
if (bind != null) }
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
if ((bind.directoryInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) {
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null) {
bind.toolbar.setTitle(directory.getName());
}
});
} else {
bind.toolbar.setTitle(R.string.empty_string);
}
});
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null) {
bind.directoryTitleLabel.setText(directory.getName());
}
});
}
private void initButtons() {
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null && directory.getParentId() != null && !Objects.equals(directory.getParentId(), "-1")) {
bind.directoryBackImageView.setVisibility(View.VISIBLE);
} else {
bind.directoryBackImageView.setVisibility(View.GONE);
}
});
bind.directoryBackImageView.setOnClickListener(v -> directoryViewModel.goBack());
} }
private void initDirectoryListView() { private void initDirectoryListView() {
@@ -137,13 +91,18 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
musicDirectoryAdapter = new MusicDirectoryAdapter(this); musicDirectoryAdapter = new MusicDirectoryAdapter(this);
bind.directoryRecyclerView.setAdapter(musicDirectoryAdapter); bind.directoryRecyclerView.setAdapter(musicDirectoryAdapter);
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
if ((bind.directoryInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) {
bind.toolbar.setTitle(directory.getName());
} else {
bind.toolbar.setTitle(R.string.empty_string);
}
});
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> { bind.directoryTitleLabel.setText(directory.getName());
if (directory != null) {
musicDirectoryAdapter.setItems(directory.getChildren()); musicDirectoryAdapter.setItems(directory.getChildren());
} else {
musicDirectoryAdapter.setItems(Collections.emptyList());
}
}); });
} }
@@ -162,11 +121,6 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
@Override @Override
public void onMusicDirectoryClick(Bundle bundle) { public void onMusicDirectoryClick(Bundle bundle) {
Child child = bundle.getParcelable(Constants.MUSIC_DIRECTORY_OBJECT); Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle);
if (child != null) {
directoryViewModel.setMusicDirectoryId(child.getId());
directoryViewModel.setMusicDirectoryName(child.getTitle());
}
} }
} }

View File

@@ -3,9 +3,6 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName; import android.content.ComponentName;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -28,7 +25,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter; import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel; import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.material.appbar.MaterialToolbar;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.Objects; import java.util.Objects;
@@ -43,18 +40,7 @@ public class DownloadFragment extends Fragment implements ClickCallback {
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture; private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Override private MaterialToolbar materialToolbar;
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu);
CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item);
}
@Nullable @Nullable
@Override @Override
@@ -97,22 +83,11 @@ public class DownloadFragment extends Fragment implements ClickCallback {
bind = null; bind = null;
} }
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_search) {
activity.navController.navigate(R.id.action_downloadFragment_to_searchFragment);
return true;
} else if (item.getItemId() == R.id.action_settings) {
activity.navController.navigate(R.id.action_downloadFragment_to_settingsFragment);
return true;
}
return false;
}
private void initAppBar() { private void initAppBar() {
activity.setSupportActionBar(bind.toolbar); materialToolbar = bind.getRoot().findViewById(R.id.toolbar);
Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
activity.setSupportActionBar(materialToolbar);
Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
} }
private void initDownloadedSongView() { private void initDownloadedSongView() {

View File

@@ -2,9 +2,6 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -18,7 +15,9 @@ import com.cappielloantonio.tempo.databinding.FragmentHomeBinding;
import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.fragment.pager.HomePager; import com.cappielloantonio.tempo.ui.fragment.pager.HomePager;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator; import com.google.android.material.tabs.TabLayoutMediator;
import java.util.Objects; import java.util.Objects;
@@ -30,18 +29,9 @@ public class HomeFragment extends Fragment {
private FragmentHomeBinding bind; private FragmentHomeBinding bind;
private MainActivity activity; private MainActivity activity;
@Override private MaterialToolbar materialToolbar;
public void onCreate(@Nullable Bundle savedInstanceState) { private AppBarLayout appBarLayout;
super.onCreate(savedInstanceState); private TabLayout tabLayout;
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu);
CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item);
}
@Nullable @Nullable
@Override @Override
@@ -73,22 +63,18 @@ public class HomeFragment extends Fragment {
bind = null; bind = null;
} }
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_search) {
activity.navController.navigate(R.id.action_homeFragment_to_searchFragment);
return true;
} else if (item.getItemId() == R.id.action_settings) {
activity.navController.navigate(R.id.action_homeFragment_to_settingsFragment);
return true;
}
return false;
}
private void initAppBar() { private void initAppBar() {
activity.setSupportActionBar(bind.toolbar); appBarLayout = bind.getRoot().findViewById(R.id.toolbar_fragment);
Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); materialToolbar = bind.getRoot().findViewById(R.id.toolbar);
activity.setSupportActionBar(materialToolbar);
Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
tabLayout = new TabLayout(requireContext());
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
tabLayout.setTabMode(TabLayout.MODE_FIXED);
appBarLayout.addView(tabLayout);
} }
private void initHomePager() { private void initHomePager() {
@@ -106,13 +92,13 @@ public class HomeFragment extends Fragment {
bind.homeViewPager.setOffscreenPageLimit(3); bind.homeViewPager.setOffscreenPageLimit(3);
bind.homeViewPager.setUserInputEnabled(false); bind.homeViewPager.setUserInputEnabled(false);
new TabLayoutMediator(bind.homeTabLayout, bind.homeViewPager, new TabLayoutMediator(tabLayout, bind.homeViewPager,
(tab, position) -> { (tab, position) -> {
tab.setText(pager.getPageTitle(position)); tab.setText(pager.getPageTitle(position));
// tab.setIcon(pager.getPageIcon(position)); // tab.setIcon(pager.getPageIcon(position));
} }
).attach(); ).attach();
bind.homeTabLayout.setVisibility(Preferences.isPodcastSectionVisible() || Preferences.isRadioSectionVisible() ? View.VISIBLE : View.GONE); tabLayout.setVisibility(Preferences.isPodcastSectionVisible() || Preferences.isRadioSectionVisible() ? View.VISIBLE : View.GONE);
} }
} }

View File

@@ -5,6 +5,7 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -138,6 +139,15 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
return true; return true;
}); });
bind.discoveryTextViewClickable.setOnClickListener(v -> {
homeViewModel.getRandomShuffleSample().observe(getViewLifecycleOwner(), songs -> {
if (songs.size() > 0) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);
}
});
});
bind.similarTracksTextViewRefreshable.setOnLongClickListener(v -> { bind.similarTracksTextViewRefreshable.setOnLongClickListener(v -> {
homeViewModel.refreshSimilarSongSample(getViewLifecycleOwner()); homeViewModel.refreshSimilarSongSample(getViewLifecycleOwner());
return true; return true;

View File

@@ -2,9 +2,6 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -29,8 +26,9 @@ import com.cappielloantonio.tempo.ui.adapter.MusicFolderAdapter;
import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter; import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter;
import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog; import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.LibraryViewModel; import com.cappielloantonio.tempo.viewmodel.LibraryViewModel;
import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.material.appbar.MaterialToolbar;
import java.util.Objects; import java.util.Objects;
@@ -46,21 +44,9 @@ public class LibraryFragment extends Fragment implements ClickCallback {
private AlbumAdapter albumAdapter; private AlbumAdapter albumAdapter;
private ArtistAdapter artistAdapter; private ArtistAdapter artistAdapter;
private GenreAdapter genreAdapter; private GenreAdapter genreAdapter;
private PlaylistHorizontalAdapter playlistHorizontalAdapter; private PlaylistHorizontalAdapter playlistHorizontalAdapter;
@Override private MaterialToolbar materialToolbar;
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu);
CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item);
}
@Nullable @Nullable
@Override @Override
@@ -100,19 +86,6 @@ public class LibraryFragment extends Fragment implements ClickCallback {
bind = null; bind = null;
} }
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_search) {
activity.navController.navigate(R.id.action_libraryFragment_to_searchFragment);
return true;
} else if (item.getItemId() == R.id.action_settings) {
activity.navController.navigate(R.id.action_libraryFragment_to_settingsFragment);
return true;
}
return false;
}
private void init() { private void init() {
bind.albumCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_albumCatalogueFragment)); bind.albumCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_albumCatalogueFragment));
bind.artistCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_artistCatalogueFragment)); bind.artistCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_artistCatalogueFragment));
@@ -142,11 +115,18 @@ public class LibraryFragment extends Fragment implements ClickCallback {
} }
private void initAppBar() { private void initAppBar() {
activity.setSupportActionBar(bind.toolbar); materialToolbar = bind.getRoot().findViewById(R.id.toolbar);
Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
activity.setSupportActionBar(materialToolbar);
Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
} }
private void initMusicFolderView() { private void initMusicFolderView() {
if (!Preferences.isMusicDirectorySectionVisible()) {
bind.libraryMusicFolderSector.setVisibility(View.GONE);
return;
}
bind.musicFolderRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); bind.musicFolderRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.musicFolderRecyclerView.setHasFixedSize(true); bind.musicFolderRecyclerView.setHasFixedSize(true);

View File

@@ -27,6 +27,7 @@ import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.PlayQueue; import com.cappielloantonio.tempo.subsonic.models.PlayQueue;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerVerticalPager; import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerVerticalPager;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
@@ -57,6 +58,7 @@ public class PlayerBottomSheetFragment extends Fragment {
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
customizeBottomSheetBackground(); customizeBottomSheetBackground();
customizeBottomSheetAction();
initViewPager(); initViewPager();
setHeaderBookmarksButton(); setHeaderBookmarksButton();
@@ -87,6 +89,10 @@ public class PlayerBottomSheetFragment extends Fragment {
bind.playerHeaderLayout.getRoot().setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 2)); bind.playerHeaderLayout.getRoot().setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 2));
} }
private void customizeBottomSheetAction() {
bind.playerHeaderLayout.getRoot().setOnClickListener(view -> ((MainActivity) requireActivity()).expandBottomSheet());
}
private void initViewPager() { private void initViewPager() {
bind.playerBodyLayout.playerBodyBottomSheetViewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL); bind.playerBodyLayout.playerBodyBottomSheetViewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
bind.playerBodyLayout.playerBodyBottomSheetViewPager.setAdapter(new PlayerControllerVerticalPager(this)); bind.playerBodyLayout.playerBodyBottomSheetViewPager.setAdapter(new PlayerControllerVerticalPager(this));

View File

@@ -215,7 +215,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
} }
private void performSearch(String query) { private void performSearch(String query) {
searchViewModel.search(query).observe(getViewLifecycleOwner(), result -> { searchViewModel.search3(query).observe(getViewLifecycleOwner(), result -> {
if (bind != null) { if (bind != null) {
if (result.getArtists() != null) { if (result.getArtists() != null) {
bind.searchArtistSector.setVisibility(!result.getArtists().isEmpty() ? View.VISIBLE : View.GONE); bind.searchArtistSector.setVisibility(!result.getArtists().isEmpty() ? View.VISIBLE : View.GONE);

View File

@@ -17,6 +17,7 @@ import androidx.media3.common.util.UnstableApi;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import com.cappielloantonio.tempo.BuildConfig;
import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.helper.ThemeHelper; import com.cappielloantonio.tempo.helper.ThemeHelper;
import com.cappielloantonio.tempo.interfaces.ScanCallback; import com.cappielloantonio.tempo.interfaces.ScanCallback;
@@ -69,6 +70,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
findPreference("version").setSummary(BuildConfig.VERSION_NAME);
findPreference("logout").setOnPreferenceClickListener(preference -> { findPreference("logout").setOnPreferenceClickListener(preference -> {
activity.quit(); activity.quit();
return true; return true;

View File

@@ -17,6 +17,7 @@ object Constants {
const val MUSIC_FOLDER_OBJECT = "MUSIC_FOLDER_OBJECT" const val MUSIC_FOLDER_OBJECT = "MUSIC_FOLDER_OBJECT"
const val MUSIC_DIRECTORY_OBJECT = "MUSIC_DIRECTORY_OBJECT" const val MUSIC_DIRECTORY_OBJECT = "MUSIC_DIRECTORY_OBJECT"
const val MUSIC_INDEX_OBJECT = "MUSIC_DIRECTORY_OBJECT" const val MUSIC_INDEX_OBJECT = "MUSIC_DIRECTORY_OBJECT"
const val MUSIC_DIRECTORY_ID = "MUSIC_DIRECTORY_ID"
const val ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED" const val ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED"
const val ALBUM_MOST_PLAYED = "ALBUM_MOST_PLAYED" const val ALBUM_MOST_PLAYED = "ALBUM_MOST_PLAYED"

View File

@@ -30,6 +30,8 @@ object Preferences {
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size" private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility" private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility"
private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility" private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility"
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
@JvmStatic @JvmStatic
fun getServer(): String? { fun getServer(): String? {
@@ -240,4 +242,14 @@ object Preferences {
fun setRadioSectionHidden() { fun setRadioSectionHidden() {
App.getInstance().preferences.edit().putBoolean(RADIO_SECTION_VISIBILITY, false).apply() App.getInstance().preferences.edit().putBoolean(RADIO_SECTION_VISIBILITY, false).apply()
} }
@JvmStatic
fun isMusicDirectorySectionVisible(): Boolean {
return App.getInstance().preferences.getBoolean(MUSIC_DIRECTORY_SECTION_VISIBILITY, true)
}
@JvmStatic
fun getReplayGainMode(): String? {
return App.getInstance().preferences.getString(REPLAY_GAIN_MODE, "disabled")
}
} }

View File

@@ -0,0 +1,107 @@
package com.cappielloantonio.tempo.util;
import androidx.media3.common.Metadata;
import androidx.media3.common.Tracks;
import androidx.media3.exoplayer.ExoPlayer;
import com.cappielloantonio.tempo.model.ReplayGain;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class ReplayGainUtil {
private static final String[] tags = {"REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "R128_TRACK_GAIN", "R128_ALBUM_GAIN"};
public static void setReplayGain(ExoPlayer player, Tracks tracks) {
List<Metadata> metadata = getMetadata(tracks);
List<ReplayGain> gains = getReplayGains(metadata);
applyReplayGain(player, gains);
}
private static List<Metadata> getMetadata(Tracks tracks) {
List<Metadata> metadata = new ArrayList<>();
for (int i = 0; i < tracks.getGroups().size(); i++) {
Tracks.Group group = tracks.getGroups().get(i);
for (int j = 0; j < group.getMediaTrackGroup().length; j++) {
metadata.add(group.getTrackFormat(j).metadata);
}
}
return metadata;
}
private static List<ReplayGain> getReplayGains(List<Metadata> metadata) {
List<ReplayGain> gains = new ArrayList<>();
for (int i = 0; i < metadata.size(); i++) {
for (int j = 0; j < metadata.get(i).length(); j++) {
Metadata.Entry entry = metadata.get(i).get(j);
if (checkReplayGain(entry)) {
ReplayGain replayGain = setReplayGains(entry);
gains.add(replayGain);
}
}
}
return gains;
}
private static boolean checkReplayGain(Metadata.Entry entry) {
for (String tag : tags) {
if (entry.toString().contains(tag)) {
return true;
}
}
return false;
}
private static ReplayGain setReplayGains(Metadata.Entry entry) {
ReplayGain replayGain = new ReplayGain();
if (entry.toString().contains(tags[0])) {
replayGain.setTrackGain(parseReplayGainTag(entry));
}
if (entry.toString().contains(tags[1])) {
replayGain.setAlbumGain(parseReplayGainTag(entry));
}
if (entry.toString().contains(tags[2])) {
replayGain.setTrackGain(parseReplayGainTag(entry) / 256f);
}
if (entry.toString().contains(tags[3])) {
replayGain.setAlbumGain(parseReplayGainTag(entry) / 256f);
}
return replayGain;
}
private static Float parseReplayGainTag(Metadata.Entry entry) {
try {
return Float.parseFloat(entry.toString().replaceAll("[^\\d.]", ""));
} catch (NumberFormatException exception) {
return 0f;
}
}
private static void applyReplayGain(ExoPlayer player, List<ReplayGain> gains) {
if (Objects.equals(Preferences.getReplayGainMode(), "disabled") || gains.size() == 0) {
setReplayGain(player, 0f);
} else if (Objects.equals(Preferences.getReplayGainMode(), "track")) {
setReplayGain(player, gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(0).getAlbumGain());
} else if (Objects.equals(Preferences.getReplayGainMode(), "album")) {
setReplayGain(player, gains.get(0).getAlbumGain() != 0f ? gains.get(0).getAlbumGain() : gains.get(0).getTrackGain());
}
}
private static void setReplayGain(ExoPlayer player, float gain) {
player.setVolume((float) Math.pow(10f, gain / 20f));
}
}

View File

@@ -7,9 +7,6 @@ import android.graphics.drawable.InsetDrawable;
import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.DividerItemDecoration;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
public class UIUtil { public class UIUtil {
public static int getSpanCount(int itemCount, int maxSpan) { public static int getSpanCount(int itemCount, int maxSpan) {
int itemSize = itemCount == 0 ? 1 : itemCount; int itemSize = itemCount == 0 ? 1 : itemCount;
@@ -21,10 +18,6 @@ public class UIUtil {
} }
} }
public static boolean isCastApiAvailable(Context context) {
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS;
}
public static DividerItemDecoration getDividerItemDecoration(Context context) { public static DividerItemDecoration getDividerItemDecoration(Context context) {
int[] ATTRS = new int[]{android.R.attr.listDivider}; int[] ATTRS = new int[]{android.R.attr.listDivider};

View File

@@ -4,9 +4,7 @@ import android.app.Application;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.repository.DirectoryRepository; import com.cappielloantonio.tempo.repository.DirectoryRepository;
import com.cappielloantonio.tempo.subsonic.models.Directory; import com.cappielloantonio.tempo.subsonic.models.Directory;
@@ -14,34 +12,13 @@ import com.cappielloantonio.tempo.subsonic.models.Directory;
public class DirectoryViewModel extends AndroidViewModel { public class DirectoryViewModel extends AndroidViewModel {
private final DirectoryRepository directoryRepository; private final DirectoryRepository directoryRepository;
private MutableLiveData<String> id = new MutableLiveData<>(null);
private MutableLiveData<String> name = new MutableLiveData<>(null);
private MutableLiveData<Directory> directory = new MutableLiveData<>(null);
public DirectoryViewModel(@NonNull Application application) { public DirectoryViewModel(@NonNull Application application) {
super(application); super(application);
directoryRepository = new DirectoryRepository(); directoryRepository = new DirectoryRepository();
} }
public LiveData<Directory> getDirectory() { public LiveData<Directory> loadMusicDirectory(String id) {
return directory; return directoryRepository.getMusicDirectory(id);
}
public void setMusicDirectoryId(String id) {
this.id.setValue(id);
}
public void setMusicDirectoryName(String name) {
this.name.setValue(name);
}
public void loadMusicDirectory(LifecycleOwner owner) {
this.id.observe(owner, id -> directoryRepository.getMusicDirectory(id).observe(owner, directory -> this.directory.setValue(directory)));
}
public void goBack() {
this.id.setValue(this.directory.getValue().getParentId());
} }
} }

View File

@@ -67,6 +67,10 @@ public class HomeViewModel extends AndroidViewModel {
return dicoverSongSample; return dicoverSongSample;
} }
public LiveData<List<Child>> getRandomShuffleSample() {
return songRepository.getRandomSample(100, null, null);
}
public LiveData<List<Chronology>> getGridSongSample(LifecycleOwner owner) { public LiveData<List<Chronology>> getGridSongSample(LifecycleOwner owner) {
String server = Preferences.getServerId(); String server = Preferences.getServerId();
chronologyRepository.getLastWeek(server).observe(owner, thisGridTopSong::postValue); chronologyRepository.getLastWeek(server).observe(owner, thisGridTopSong::postValue);

View File

@@ -8,6 +8,7 @@ import androidx.lifecycle.LiveData;
import com.cappielloantonio.tempo.model.RecentSearch; import com.cappielloantonio.tempo.model.RecentSearch;
import com.cappielloantonio.tempo.repository.SearchingRepository; import com.cappielloantonio.tempo.repository.SearchingRepository;
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
import com.cappielloantonio.tempo.subsonic.models.SearchResult3; import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
import java.util.ArrayList; import java.util.ArrayList;
@@ -38,8 +39,12 @@ public class SearchViewModel extends AndroidViewModel {
} }
} }
public LiveData<SearchResult3> search(String title) { public LiveData<SearchResult2> search2(String title) {
return searchingRepository.search(title); return searchingRepository.search2(title);
}
public LiveData<SearchResult3> search3(String title) {
return searchingRepository.search3(title);
} }
public void insertNewSearch(String search) { public void insertNewSearch(String search) {

View File

@@ -188,6 +188,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:nestedScrollingEnabled="false"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingTop="8dp" /> android:paddingTop="8dp" />
</LinearLayout> </LinearLayout>

View File

@@ -4,40 +4,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout <fragment
android:id="@+id/appbar" android:id="@+id/toolbar_fragment"
android:name="com.cappielloantonio.tempo.ui.fragment.ToolbarFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<ProgressBar <ProgressBar
android:id="@+id/loading_progress_bar" android:id="@+id/loading_progress_bar"
@@ -121,6 +92,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingBottom="8dp" /> android:paddingBottom="8dp" />
</LinearLayout> </LinearLayout>

View File

@@ -6,47 +6,11 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.fragment.HomeFragment"> tools:context=".ui.fragment.HomeFragment">
<com.google.android.material.appbar.AppBarLayout <fragment
android:id="@+id/appbar" android:id="@+id/toolbar_fragment"
android:name="com.cappielloantonio.tempo.ui.fragment.ToolbarFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/homeTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabMode="fixed" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:id="@+id/homeViewPager" android:id="@+id/homeViewPager"

View File

@@ -113,16 +113,35 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <!-- Label and button -->
android:id="@+id/discovery_text_view_refreshable" <LinearLayout
style="@style/TitleLarge"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:paddingStart="16dp" android:paddingStart="8dp"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingEnd="16dp" android:paddingEnd="8dp">
android:text="@string/home_title_discovery" />
<TextView
android:id="@+id/discovery_text_view_refreshable"
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/home_title_discovery" />
<TextView
android:id="@+id/discovery_text_view_clickable"
style="@style/TitleMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/home_title_discovery_shuffle_all_button" />
</LinearLayout>
<!-- slideview --> <!-- slideview -->
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2

View File

@@ -4,40 +4,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout <fragment
android:id="@+id/appbar" android:id="@+id/toolbar_fragment"
android:name="com.cappielloantonio.tempo.ui.fragment.ToolbarFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/fragment_library_nested_scroll_view" android:id="@+id/fragment_library_nested_scroll_view"
@@ -75,6 +46,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:nestedScrollingEnabled="false"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingTop="8dp" android:paddingTop="8dp"
@@ -308,6 +280,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:nestedScrollingEnabled="false"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="8dp" /> android:paddingBottom="8dp" />

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="@drawable/ic_toolbar_tempo" />
<TextView
style="@style/HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/app_name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -286,13 +286,16 @@
android:id="@+id/directoryFragment" android:id="@+id/directoryFragment"
android:name="com.cappielloantonio.tempo.ui.fragment.DirectoryFragment" android:name="com.cappielloantonio.tempo.ui.fragment.DirectoryFragment"
android:label="DirectoryFragment" android:label="DirectoryFragment"
tools:layout="@layout/fragment_directory" /> tools:layout="@layout/fragment_directory">
<action
android:id="@+id/action_directoryFragment_to_directoryFragment"
app:destination="@id/directoryFragment" />
</fragment>
<fragment <fragment
android:id="@+id/indexFragment" android:id="@+id/indexFragment"
android:name="com.cappielloantonio.tempo.ui.fragment.IndexFragment" android:name="com.cappielloantonio.tempo.ui.fragment.IndexFragment"
android:label="IndexFragment" android:label="IndexFragment"
tools:layout="@layout/fragment_index"> tools:layout="@layout/fragment_index">
<action <action
android:id="@+id/action_indexFragment_to_directoryFragment" android:id="@+id/action_indexFragment_to_directoryFragment"
app:destination="@id/directoryFragment" /> app:destination="@id/directoryFragment" />

View File

@@ -141,4 +141,15 @@
<item>12</item> <item>12</item>
<item>6</item> <item>6</item>
</string-array> </string-array>
<string-array name="replay_gain_titles">
<item>Disabled</item>
<item>Track preferred</item>
<item>Album preferred</item>
</string-array>
<string-array name="replay_gain_values">
<item>disabled</item>
<item>track</item>
<item>album</item>
</string-array>
</resources> </resources>

View File

@@ -77,6 +77,7 @@
<string name="home_title_discovery">Discovery</string> <string name="home_title_discovery">Discovery</string>
<string name="home_title_recently_added">Recently added</string> <string name="home_title_recently_added">Recently added</string>
<string name="home_title_recently_added_see_all_button">See all</string> <string name="home_title_recently_added_see_all_button">See all</string>
<string name="home_title_discovery_shuffle_all_button">Shuffle all</string>
<string name="home_title_starred_albums">★ Starred albums</string> <string name="home_title_starred_albums">★ Starred albums</string>
<string name="home_title_starred_albums_see_all_button">See all</string> <string name="home_title_starred_albums_see_all_button">See all</string>
<string name="home_title_starred_artists">★ Starred artists</string> <string name="home_title_starred_artists">★ Starred artists</string>
@@ -184,6 +185,8 @@
<string name="settings_max_bitrate_wifi">Bitrate in Wi-Fi</string> <string name="settings_max_bitrate_wifi">Bitrate in Wi-Fi</string>
<string name="settings_max_bitrate_mobile">Bitrate in mobile</string> <string name="settings_max_bitrate_mobile">Bitrate in mobile</string>
<string name="settings_media_cache">Size of media file cache</string> <string name="settings_media_cache">Size of media file cache</string>
<string name="settings_music_directory">Show music directories</string>
<string name="settings_music_directory_summary">If enabled, show the music directory section. Please note that for folder navigation to work properly, the server must support this feature.</string>
<string name="settings_queue_syncing_title">Sync play queue for this user</string> <string name="settings_queue_syncing_title">Sync play queue for this user</string>
<string name="settings_queue_syncing_countdown">Sync timer</string> <string name="settings_queue_syncing_countdown">Sync timer</string>
<string name="settings_queue_syncing_summary">If enabled, the user will have the ability to save their play queue and will have the ability to load state when opening the application.</string> <string name="settings_queue_syncing_summary">If enabled, the user will have the ability to save their play queue and will have the ability to load state when opening the application.</string>
@@ -191,11 +194,13 @@
<string name="settings_podcast_summary">If enabled, show the podcast section.</string> <string name="settings_podcast_summary">If enabled, show the podcast section.</string>
<string name="settings_radio">Show radio</string> <string name="settings_radio">Show radio</string>
<string name="settings_radio_summary">If enabled, show the radio section.</string> <string name="settings_radio_summary">If enabled, show the radio section.</string>
<string name="settings_replay_gain">Set replay gain mode</string>
<string name="settings_rounded_corner">Rounded corners</string> <string name="settings_rounded_corner">Rounded corners</string>
<string name="settings_rounded_corner_summary">If enabled, sets a curvature angle for all rendered covers. The changes will take effect on restart.</string> <string name="settings_rounded_corner_summary">If enabled, sets a curvature angle for all rendered covers. The changes will take effect on restart.</string>
<string name="settings_rounded_corner_size">Corners size</string> <string name="settings_rounded_corner_size">Corners size</string>
<string name="settings_rounded_corner_size_summary">Sets the magnitude of the curvature angle.</string> <string name="settings_rounded_corner_size_summary">Sets the magnitude of the curvature angle.</string>
<string name="settings_scan_title">Scan library</string> <string name="settings_scan_title">Scan library</string>
<string name="settings_summary_replay_gain">Replay gain is a feature that allows you to adjust the volume level of audio tracks for a consistent listening experience. This setting is only effective if the track contains the necessary metadata.</string>
<string name="settings_summary_syncing">Returns the state of the play queue for this user. This includes the tracks in the play queue, the currently playing track, and the position within this track. The server must support this feature.</string> <string name="settings_summary_syncing">Returns the state of the play queue for this user. This includes the tracks in the play queue, the currently playing track, and the position within this track. The server must support this feature.</string>
<string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string> <string name="settings_summary_transcoding">Priority given to the transcoding mode. If set to \"Direct play\" the bitrate of the file will not be changed.</string>
<string name="settings_sync_starred_tracks_for_offline_use_summary">If enabled, starred tracks will be downloaded for offline use.</string> <string name="settings_sync_starred_tracks_for_offline_use_summary">If enabled, starred tracks will be downloaded for offline use.</string>
@@ -203,6 +208,7 @@
<string name="settings_theme">Theme</string> <string name="settings_theme">Theme</string>
<string name="settings_title_data">Data</string> <string name="settings_title_data">Data</string>
<string name="settings_title_general">General</string> <string name="settings_title_general">General</string>
<string name="settings_title_replay_gain">Replay Gain</string>
<string name="settings_title_syncing">Syncing</string> <string name="settings_title_syncing">Syncing</string>
<string name="settings_title_transcoding">Transcoding</string> <string name="settings_title_transcoding">Transcoding</string>
<string name="settings_title_ui">UI</string> <string name="settings_title_ui">UI</string>
@@ -255,4 +261,6 @@
<string name="menu_sort_name">Name</string> <string name="menu_sort_name">Name</string>
<string name="menu_sort_random">Random</string> <string name="menu_sort_random">Random</string>
<string name="description_empty_title">No description available</string> <string name="description_empty_title">No description available</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources> </resources>

View File

@@ -51,6 +51,12 @@
android:defaultValue="true" android:defaultValue="true"
android:summary="@string/settings_radio_summary" android:summary="@string/settings_radio_summary"
android:key="radio_section_visibility" /> android:key="radio_section_visibility" />
<SwitchPreference
android:title="@string/settings_music_directory"
android:defaultValue="true"
android:summary="@string/settings_music_directory_summary"
android:key="music_directory_section_visibility" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_data"> <PreferenceCategory app:title="@string/settings_title_data">
@@ -133,6 +139,22 @@
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_replay_gain">
<Preference
app:selectable="false"
app:summary="@string/settings_summary_replay_gain" />
<ListPreference
app:defaultValue="disabled"
app:dialogTitle="@string/settings_replay_gain"
app:entries="@array/replay_gain_titles"
app:entryValues="@array/replay_gain_values"
app:key="replay_gain_mode"
app:title="@string/settings_replay_gain"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_syncing"> <PreferenceCategory app:title="@string/settings_title_syncing">
<Preference <Preference
app:selectable="false" app:selectable="false"
@@ -141,7 +163,7 @@
<SwitchPreference <SwitchPreference
android:title="@string/settings_queue_syncing_title" android:title="@string/settings_queue_syncing_title"
android:defaultValue="false" android:defaultValue="false"
android:summary="@string/settings_sync_starred_tracks_for_offline_use_summary" android:summary="@string/settings_queue_syncing_summary"
android:key="queue_syncing" /> android:key="queue_syncing" />
<ListPreference <ListPreference
@@ -160,6 +182,7 @@
app:summary="@string/settings_about_summary" /> app:summary="@string/settings_about_summary" />
<Preference <Preference
android:key="version"
app:summary="@string/settings_version_summary" app:summary="@string/settings_version_summary"
app:title="@string/settings_version_title" /> app:title="@string/settings_version_title" />

View File

@@ -0,0 +1,242 @@
package com.cappielloantonio.tempo.service
import android.annotation.SuppressLint
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder
import android.content.Intent
import android.os.Bundle
import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.TrackGroupArray
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.ui.activity.MainActivity
import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil
import com.cappielloantonio.tempo.util.ReplayGainUtil
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
@UnstableApi
class MediaService : MediaLibraryService() {
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
private lateinit var player: ExoPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var customCommands: List<CommandButton>
private var customLayout = ImmutableList.of<CommandButton>()
companion object {
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_OFF"
}
override fun onCreate() {
super.onCreate()
initializeCustomCommands()
initializePlayer()
initializeMediaLibrarySession()
initializePlayerListener()
setPlayer(player)
}
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
override fun onDestroy() {
releasePlayer()
super.onDestroy()
}
private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
override fun onConnect(
session: MediaSession,
controller: ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
customCommands.forEach { commandButton ->
// TODO: Aggiungere i comandi personalizzati
// commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
}
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout))
}
}
override fun onCustomCommand(
session: MediaSession,
controller: ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
player.shuffleModeEnabled = true
customLayout = ImmutableList.of(customCommands[1])
session.setCustomLayout(customLayout)
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
player.shuffleModeEnabled = false
customLayout = ImmutableList.of(customCommands[0])
session.setCustomLayout(customLayout)
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
val updatedMediaItems = mediaItems.map {
it.buildUpon()
.setUri(it.requestMetadata.mediaUri)
.setMediaMetadata(it.mediaMetadata)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.build()
}
return Futures.immediateFuture(updatedMediaItems)
}
}
private fun initializeCustomCommands() {
customCommands =
listOf(
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)
),
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY)
)
)
customLayout = ImmutableList.of(customCommands[0])
}
private fun initializePlayer() {
player = ExoPlayer.Builder(this)
.setRenderersFactory(getRenderersFactory())
.setMediaSourceFactory(getMediaSourceFactory())
.setAudioAttributes(AudioAttributes.DEFAULT, true)
.setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_NETWORK)
.build()
}
private fun initializeMediaLibrarySession() {
val sessionActivityPendingIntent =
TaskStackBuilder.create(this).run {
addNextIntent(Intent(this@MediaService, MainActivity::class.java))
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
}
mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setSessionActivity(sessionActivityPendingIntent)
.build()
if (!customLayout.isEmpty()) {
mediaLibrarySession.setCustomLayout(customLayout)
}
}
private fun initializePlayerListener() {
player.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (mediaItem == null) return
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
MediaManager.setLastPlayedTimestamp(mediaItem)
}
}
override fun onTracksChanged(tracks: Tracks) {
ReplayGainUtil.setReplayGain(player, tracks)
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (!isPlaying) {
MediaManager.setPlayingPausedTimestamp(
player.currentMediaItem,
player.currentPosition
)
}
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
MediaManager.scrobble(oldPosition.mediaItem)
MediaManager.saveChronology(oldPosition.mediaItem)
}
if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
MediaManager.setLastPlayedTimestamp(newPosition.mediaItem)
}
}
}
})
}
private fun setPlayer(player: Player) {
mediaLibrarySession.player = player
}
private fun releasePlayer() {
player.release()
mediaLibrarySession.release()
}
@SuppressLint("PrivateResource")
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
return CommandButton.Builder()
.setDisplayName(
getString(
if (isOn) R.string.exo_controls_shuffle_on_description
else R.string.exo_controls_shuffle_off_description
)
)
.setSessionCommand(sessionCommand)
.setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on)
.build()
}
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
private fun getMediaSourceFactory() =
DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this))
}

View File

@@ -0,0 +1,65 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentToolbarBinding;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
@UnstableApi
public class ToolbarFragment extends Fragment {
private static final String TAG = "ToolbarFragment";
private FragmentToolbarBinding bind;
private MainActivity activity;
public ToolbarFragment() {
// Required empty public constructor
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
bind = FragmentToolbarBinding.inflate(inflater, container, false);
View view = bind.getRoot();
return view;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_search) {
activity.navController.navigate(R.id.searchFragment);
return true;
} else if (item.getItemId() == R.id.action_settings) {
activity.navController.navigate(R.id.settingsFragment);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,9 @@
package com.cappielloantonio.tempo.util;
import android.content.Context;
public class Flavors {
public static void initializeCastContext(Context context) {
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:title="@string/menu_search_button"
app:showAsAction="always" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings"
android:title="@string/menu_settings_button"
app:showAsAction="never" />
</menu>

View File

@@ -18,8 +18,10 @@ import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.ui.activity.MainActivity
import com.cappielloantonio.tempo.util.Constants import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil import com.cappielloantonio.tempo.util.DownloadUtil
import com.cappielloantonio.tempo.util.UIUtil import com.cappielloantonio.tempo.util.ReplayGainUtil
import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
@@ -117,6 +119,9 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
browser: ControllerInfo, browser: ControllerInfo,
params: LibraryParams? params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> { ): ListenableFuture<LibraryResult<MediaItem>> {
if (params != null && params.isRecent) {
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED))
}
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params)) return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
} }
@@ -206,7 +211,9 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
} }
private fun initializeCastPlayer() { private fun initializeCastPlayer() {
if (UIUtil.isCastApiAvailable(this)) { if (GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
) {
castPlayer = CastPlayer(CastContext.getSharedInstance(this)) castPlayer = CastPlayer(CastContext.getSharedInstance(this))
castPlayer.setSessionAvailabilityListener(this) castPlayer.setSessionAvailabilityListener(this)
} }
@@ -234,11 +241,15 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (mediaItem == null) return if (mediaItem == null) return
if(reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
MediaManager.setLastPlayedTimestamp(mediaItem) MediaManager.setLastPlayedTimestamp(mediaItem)
} }
} }
override fun onTracksChanged(tracks: Tracks) {
ReplayGainUtil.setReplayGain(player, tracks)
}
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
if (!isPlaying) { if (!isPlaying) {
MediaManager.setPlayingPausedTimestamp( MediaManager.setPlayingPausedTimestamp(
@@ -255,7 +266,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
) { ) {
super.onPositionDiscontinuity(oldPosition, newPosition, reason) super.onPositionDiscontinuity(oldPosition, newPosition, reason)
if(reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
MediaManager.scrobble(oldPosition.mediaItem) MediaManager.scrobble(oldPosition.mediaItem)
MediaManager.saveChronology(oldPosition.mediaItem) MediaManager.saveChronology(oldPosition.mediaItem)

View File

@@ -0,0 +1,67 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentToolbarBinding;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.google.android.gms.cast.framework.CastButtonFactory;
@UnstableApi
public class ToolbarFragment extends Fragment {
private static final String TAG = "ToolbarFragment";
private FragmentToolbarBinding bind;
private MainActivity activity;
public ToolbarFragment() {
// Required empty public constructor
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.main_page_menu, menu);
CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
bind = FragmentToolbarBinding.inflate(inflater, container, false);
View view = bind.getRoot();
return view;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_search) {
activity.navController.navigate(R.id.searchFragment);
return true;
} else if (item.getItemId() == R.id.action_settings) {
activity.navController.navigate(R.id.settingsFragment);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,14 @@
package com.cappielloantonio.tempo.util;
import android.content.Context;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
public class Flavors {
public static void initializeCastContext(Context context) {
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS)
CastContext.getSharedInstance(context);
}
}

View File

@@ -13,13 +13,6 @@ allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven {
url 'https://jitpack.io'
}
maven {
url 'https://oss.sonatype.org/content/repositories/snapshots'
}
jcenter()
} }
} }

View File

@@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx2048m
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=false
android.defaults.buildfeatures.buildconfig=true android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
android.nonFinalResIds=false android.nonFinalResIds=false