Compare commits

..

No commits in common. "master" and "v1.4.0" have entirely different histories.

63 changed files with 285 additions and 1125 deletions

View file

@ -23,17 +23,12 @@ This is a native music player for Android devices that connects to Jellyfin medi
* Playback history reporting
* Filter content by library
# Requisites
- A Jellfin server. See how to setup one [here](https://jellyfin.org/docs/general/quick-start/).
- Android 4.4 or later
# Issues
Since this was a small project intended mainly for myself, there are some things I haven't resolved yet. I would appreciate pull requests to fix any of these issues!
* Artist sorting isn't available through the API
* Playlists and favorites will not update automatically when changed ([#5](https://github.com/adrianvic/jamfish/issues/5))
* App may crash on really low end devices due exceeding the maximum bitmap memory ([#4](https://github.com/adrianvic/jamfish/issues/4))
* Playlists and favorites will not update automatically when changed
# Future Plans

View file

@ -10,12 +10,10 @@ android {
minSdk 19
targetSdk 33
versionCode 2
versionName '1.4.1'
versionCode 1
versionName '1.4.0'
// for SDK < 19
multiDexEnabled true
vectorDrawables {
useSupportLibrary true
}
@ -107,9 +105,8 @@ dependencies {
implementation 'com.android.support:multidex:1.0.3'
implementation 'com.mlegy.redscreenofdeath:red-screen-of-death:0.1.3'
// old version of retrofit to work with api < 21
implementation 'com.squareup.retrofit2:retrofit:2.6.4'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
@ -120,10 +117,4 @@ dependencies {
implementation 'com.github.bumptech.glide:annotations:4.12.0'
implementation 'com.github.bumptech.glide:glide:4.12.0'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.12.0'
// for supporting legacy android versions:
implementation('com.squareup.okhttp3:okhttp:3.12.13')
implementation "androidx.multidex:multidex:2.0.1"
implementation 'org.conscrypt:conscrypt-android:2.5.2'
}

View file

@ -2,9 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
@ -51,7 +49,6 @@
<activity
android:name="org.adrianvictor.geleia.activities.SettingsActivity"
android:label="@string/action_settings" />
<activity android:name="org.adrianvictor.geleia.activities.DirectoryPickerActivity"/>
<activity
android:name="org.adrianvictor.geleia.activities.AboutActivity"
android:label="@string/action_about" />
@ -67,7 +64,7 @@
android:name="org.adrianvictor.geleia.views.shortcuts.AppShortcutLauncherActivity"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity android:name="org.adrianvictor.geleia.activities.UnreachableActivity" />
<service android:name="org.adrianvictor.geleia.service.DownloadService" />
<service android:name="org.adrianvictor.geleia.service.LoginService" />
<service android:name="org.adrianvictor.geleia.service.MusicService" />
@ -139,12 +136,6 @@
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="remove" />
</application>
</manifest>

View file

@ -6,9 +6,6 @@ import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.multidex.MultiDexApplication;
import androidx.room.Room;
import org.adrianvictor.geleia.database.JellyDatabase;
@ -17,7 +14,6 @@ import org.adrianvictor.geleia.util.PreferenceUtil;
import org.adrianvictor.geleia.views.shortcuts.DynamicShortcutManager;
import com.melegy.redscreenofdeath.RedScreenOfDeath;
import org.conscrypt.Conscrypt;
import org.jellyfin.apiclient.interaction.AndroidDevice;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.VolleyHttpClient;
@ -26,16 +22,7 @@ import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient;
import org.jellyfin.apiclient.logging.AndroidLogger;
import org.jellyfin.apiclient.logging.ILogger;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
import com.bumptech.glide.load.model.GlideUrl;
import java.io.InputStream;
import okhttp3.OkHttpClient;
import java.security.Security;
public class App extends MultiDexApplication {
public class App extends Application {
private static App app;
private static JellyDatabase database;
@ -43,14 +30,8 @@ public class App extends MultiDexApplication {
@Override
public void onCreate() {
// Initializing stuff for older Android APIs compatibility
Security.insertProviderAt(Conscrypt.newProvider(), 1); // To have SSL 1.2 on API < 19
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); // To load vectors on API < 19
super.onCreate();
initializeGlide(this);
if (BuildConfig.DEBUG) {
RedScreenOfDeath.init(this);
}
@ -59,7 +40,7 @@ public class App extends MultiDexApplication {
database = createDatabase(this);
apiClient = createApiClient(this);
if (database.userDao().getUsers().isEmpty()) {
if (database.userDao().getUsers().size() == 0) {
PreferenceUtil.getInstance(this).setServer(null);
PreferenceUtil.getInstance(this).setUser(null);
}
@ -95,7 +76,6 @@ public class App extends MultiDexApplication {
IDevice device = new AndroidDevice(deviceId, deviceName);
EventListener eventListener = new EventListener();
return new ApiClient(httpClient, logger, server, appName, appVersion, device, eventListener);
}
@ -110,15 +90,4 @@ public class App extends MultiDexApplication {
public static App getInstance() {
return app;
}
private void initializeGlide(@NonNull Context context) {
// This OkHttpClient is now created with Conscrypt as the SSL provider.
OkHttpClient client = new OkHttpClient.Builder().build();
// Manually create and register Glide's OkHttp component.
OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(client);
// Ensure Glide is initialized and then register the component.
Glide.get(context).getRegistry().replace(GlideUrl.class, InputStream.class, factory);
}
}

View file

@ -7,7 +7,6 @@ import android.view.View;
import androidx.annotation.NonNull;
import org.adrianvictor.geleia.service.notifications.ErrorNotification;
import org.adrianvictor.geleia.util.NavigationUtil;
import org.adrianvictor.geleia.util.PreferenceUtil;
import org.adrianvictor.geleia.databinding.ActivityAboutBinding;
@ -103,7 +102,7 @@ public class AboutActivity extends AbsBaseActivity implements View.OnClickListen
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
ErrorNotification.show(context, e.getMessage());
e.printStackTrace();
}
return "Unknown";

View file

@ -1,127 +0,0 @@
package org.adrianvictor.geleia.activities;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
public class DirectoryPickerActivity extends AppCompatActivity {
public static final String EXTRA_RESULT_PATH = "result_path";
private File rootDir;
private File currentDir;
private ArrayAdapter<File> adapter;
private TextView pathView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
rootDir = Environment.getExternalStorageDirectory();
currentDir = rootDir;
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
pathView = new TextView(this);
pathView.setPadding(24, 24, 24, 24);
ListView listView = new ListView(this);
Button selectButton = new Button(this);
selectButton.setText("Select this folder");
layout.addView(pathView);
layout.addView(listView, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
layout.addView(selectButton);
setContentView(layout);
adapter = new ArrayAdapter<File>(this,
android.R.layout.simple_list_item_1, new ArrayList<>()) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView tv = (TextView) super.getView(position, convertView, parent);
File f = getItem(position);
File parentFile = currentDir.getParentFile();
if (parentFile != null && parentFile.equals(f)) {
tv.setText("..");
} else {
tv.setText(f.getName());
}
return tv;
}
};
listView.setAdapter(adapter);
listView.setOnItemClickListener((p, v, pos, id) -> {
File dir = adapter.getItem(pos);
if (dir != null) openDir(dir);
});
selectButton.setOnClickListener(v -> {
Intent result = new Intent();
result.putExtra(EXTRA_RESULT_PATH, currentDir.getAbsolutePath());
setResult(RESULT_OK, result);
finish();
});
openDir(rootDir);
}
private void openDir(File dir) {
if (!dir.exists() || !dir.canRead()) return;
File[] dirs = dir.listFiles(f ->
f.isDirectory() && f.canRead()
);
if (dirs == null) return;
adapter.clear();
File parent = dir.getParentFile();
if (parent != null && parent.getAbsolutePath().startsWith(rootDir.getAbsolutePath())) {
adapter.add(parent);
}
Arrays.sort(dirs);
for (File f : dirs) {
adapter.add(f);
}
currentDir = dir;
pathView.setText(dir.getAbsolutePath());
}
@Override
public void onBackPressed() {
File parent = currentDir.getParentFile();
if (parent != null &&
parent.getAbsolutePath().startsWith(rootDir.getAbsolutePath())) {
openDir(parent);
} else {
super.onBackPressed();
}
}
}

View file

@ -13,14 +13,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.lifecycle.Lifecycle;
import com.afollestad.materialcab.attached.AttachedCab;
import com.afollestad.materialcab.attached.AttachedCabKt;
import org.adrianvictor.geleia.activities.base.AbsMusicContentActivity;
import org.adrianvictor.geleia.fragments.OfflineFragment;
import org.adrianvictor.geleia.interfaces.CabHolder;
import org.adrianvictor.geleia.util.NavigationUtil;
import org.adrianvictor.geleia.util.PreferenceUtil;
import org.adrianvictor.geleia.util.ThemeUtil;
import org.adrianvictor.geleia.databinding.ActivityMainContentBinding;
@ -45,7 +42,6 @@ public class MainActivity extends AbsMusicContentActivity implements CabHolder {
private ActivityMainContentBinding contentBinding;
private NavigationDrawerHeaderBinding navigationBinding;
private boolean onLogout;
private boolean pendingShowOffline = false;
@Nullable
private AttachedCab cab;
@ -101,15 +97,6 @@ public class MainActivity extends AbsMusicContentActivity implements CabHolder {
});
}
@Override
public void onStateOffline() {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
NavigationUtil.startUnreachable(this);
} else {
pendingShowOffline = true;
}
}
@Override
public void onPause() {
super.onPause();
@ -121,16 +108,6 @@ public class MainActivity extends AbsMusicContentActivity implements CabHolder {
}
}
@Override
protected void onResume() {
super.onResume();
if (pendingShowOffline) {
setCurrentFragment(OfflineFragment.newInstance());
pendingShowOffline = false;
}
}
private void setCurrentFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment, null).commit();
}

View file

@ -93,9 +93,6 @@ public class SearchActivity extends AbsMusicContentActivity implements SearchVie
public void onStateOnline() {
}
@Override
public void onStateOffline() {}
private void setUpToolBar() {
binding.toolbar.setBackgroundColor(PreferenceUtil.getInstance(this).getPrimaryColor());
setSupportActionBar(binding.toolbar);

View file

@ -51,7 +51,6 @@ public class SettingsActivity extends AbsBaseActivity {
public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
private ActivityResultLauncher<Intent> dirPickerLauncher;
private ActivityResultLauncher<Intent> legacyDirPickerLauncher;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -72,25 +71,6 @@ public class SettingsActivity extends AbsBaseActivity {
}
}
});
legacyDirPickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
String path = result.getData().getStringExtra(
DirectoryPickerActivity.EXTRA_RESULT_PATH
);
if (path != null) {
SharedPreferences.Editor editor =
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit();
editor.putString(PreferenceUtil.LOCATION_DOWNLOAD, path);
editor.apply();
invalidateSettings();
}
}
}
);
}
@Override
@ -181,13 +161,8 @@ public class SettingsActivity extends AbsBaseActivity {
}
private void openDirectoryPicker() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
dirPickerLauncher.launch(intent);
} else {
Intent intent = new Intent(requireContext(), DirectoryPickerActivity.class);
legacyDirPickerLauncher.launch(intent);
}
}
@Override

View file

@ -1,13 +1,8 @@
package org.adrianvictor.geleia.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import android.os.Handler;
import org.adrianvictor.geleia.App;
import org.adrianvictor.geleia.R;
@ -20,41 +15,10 @@ import org.adrianvictor.geleia.util.PreferenceUtil;
import java.util.List;
public class SplashActivity extends AbsBaseActivity {
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, @NonNull Intent intent) {
if (intent.getAction() == null) {
return;
}
switch (intent.getAction()) {
case LoginService.STATE_ONLINE:
NavigationUtil.startMain(context);
finish();
break;
case LoginService.STATE_OFFLINE:
NavigationUtil.startUnreachable(context);
finish();
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final IntentFilter filter = new IntentFilter();
filter.addAction(LoginService.STATE_ONLINE);
filter.addAction(LoginService.STATE_OFFLINE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(receiver, filter);
}
setContentView(R.layout.activity_splash);
}
@ -71,20 +35,13 @@ public class SplashActivity extends AbsBaseActivity {
User user = App.getDatabase().userDao().getUser(PreferenceUtil.getInstance(this).getUser());
List<User> available = App.getDatabase().userDao().getUsers();
if (user == null && !available.isEmpty()) {
if (user == null && available.size() != 0) {
NavigationUtil.startSelect(this);
finish();
} else if (user == null) {
NavigationUtil.startLogin(this);
finish();
} else {
startService(new Intent(this, LoginService.class));
new Handler().postDelayed(() -> NavigationUtil.startMain(this), 1000);
}
}
@Override
protected void onDestroy() {
unregisterReceiver(receiver);
super.onDestroy();
}
}

View file

@ -1,21 +0,0 @@
package org.adrianvictor.geleia.activities;
import android.os.Bundle;
import android.view.View;
import org.adrianvictor.geleia.R;
import org.adrianvictor.geleia.activities.base.AbsThemeActivity;
import org.adrianvictor.geleia.util.NavigationUtil;
public class UnreachableActivity extends AbsThemeActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_unreachable);
}
public void onSelectClick(View view) {
NavigationUtil.startSelect(this);
}
}

View file

@ -59,7 +59,7 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
.setPositiveButton(R.string.disable, (dialog, id) -> requestBatteryOptimization());
new Handler().postDelayed(builder::show, 2000);
} else if (!permissions.isEmpty() && ActivityCompat.shouldShowRequestPermissionRationale(this, permissions.get(0))) {
} else if (permissions.size() != 0 && ActivityCompat.shouldShowRequestPermissionRationale(this, permissions.get(0))) {
builder.setMessage(getPermissionMessage())
.setTitle(R.string.permissions_denied)
.setPositiveButton(R.string.action_grant, (dialog, id) -> requestPermissions());
@ -127,11 +127,8 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
private boolean checkBatteryOptimization() {
String packageName = getPackageName();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return pm.isIgnoringBatteryOptimizations(packageName);
} else {
return true;
}
}
@RequiresApi(api = Build.VERSION_CODES.M)
@ -141,13 +138,11 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
@RequiresApi(api = Build.VERSION_CODES.M)
private boolean checkPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
for (String permission : permissions) {
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
}
return true;
}

View file

@ -1,19 +1,14 @@
package org.adrianvictor.geleia.activities.base;
import static org.adrianvictor.geleia.adapter.CustomFragmentStatePagerAdapter.TAG;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import org.adrianvictor.geleia.App;
import org.adrianvictor.geleia.fragments.OfflineFragment;
import org.adrianvictor.geleia.interfaces.StateListener;
import org.adrianvictor.geleia.service.LoginService;
import org.adrianvictor.geleia.util.NavigationUtil;
@ -29,7 +24,7 @@ public abstract class AbsMusicContentActivity extends AbsMusicPanelActivity impl
onStateOnline();
break;
case LoginService.STATE_OFFLINE:
onStateOffline();
NavigationUtil.startLogin(context);
break;
}
}
@ -44,11 +39,7 @@ public abstract class AbsMusicContentActivity extends AbsMusicPanelActivity impl
filter.addAction(LoginService.STATE_ONLINE);
filter.addAction(LoginService.STATE_OFFLINE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(receiver, filter);
}
if (App.getApiClient() == null) {
startService(new Intent(this, LoginService.class));
@ -60,14 +51,24 @@ public abstract class AbsMusicContentActivity extends AbsMusicPanelActivity impl
@Override
protected void onResume() {
super.onResume();
if (App.getApiClient() == null) {
startService(new Intent(this, LoginService.class));
}
}
@Override
protected void onDestroy() {
unregisterReceiver(receiver);
super.onDestroy();
}
@Override
public void onStatePolling() {}
public void onStatePolling() {
}
@Override
public void onStateOffline() {
}
}

View file

@ -74,11 +74,6 @@ public class AlbumDetailActivity extends AbsMusicContentActivity implements Pale
});
}
@Override
public void onStateOffline() {
}
@Override
public void onOffsetChanged (AppBarLayout appBarLayout, int verticalOffset) {
float headerAlpha = Math.max(0, Math.min(1, 1 + (2 * (float) verticalOffset / headerViewHeight)));
@ -236,6 +231,6 @@ public class AlbumDetailActivity extends AbsMusicContentActivity implements Pale
binding.durationText.setText(MusicUtil.getReadableDurationString(MusicUtil.getTotalDuration(this, album.songs)));
binding.albumYearText.setText(MusicUtil.getYearString(album.year));
if (!album.songs.isEmpty()) adapter.swapDataSet(album.songs);
if (album.songs.size() != 0) adapter.swapDataSet(album.songs);
}
}

View file

@ -87,11 +87,6 @@ public class ArtistDetailActivity extends AbsMusicContentActivity implements Pal
});
}
@Override
public void onStateOffline() {
}
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
float headerAlpha = Math.max(0, Math.min(1, 1 + (2 * (float) verticalOffset / headerViewHeight)));
@ -263,7 +258,7 @@ public class ArtistDetailActivity extends AbsMusicContentActivity implements Pal
binding.albumCountText.setText(MusicUtil.getAlbumCountString(this, artist.albums.size()));
binding.durationText.setText(MusicUtil.getReadableDurationString(MusicUtil.getTotalDuration(this, artist.songs)));
if (!artist.songs.isEmpty()) songAdapter.swapDataSet(artist.songs);
if (!artist.albums.isEmpty()) albumAdapter.swapDataSet(artist.albums);
if (artist.songs.size() != 0) songAdapter.swapDataSet(artist.songs);
if (artist.albums.size() != 0) albumAdapter.swapDataSet(artist.albums);
}
}

View file

@ -59,11 +59,6 @@ public class GenreDetailActivity extends AbsMusicContentActivity implements CabH
});
}
@Override
public void onStateOffline() {
}
@Override
protected View createContentView() {
binding = ActivityGenreDetailBinding.inflate(getLayoutInflater());

View file

@ -71,11 +71,6 @@ public class PlaylistDetailActivity extends AbsMusicContentActivity implements C
});
}
@Override
public void onStateOffline() {
}
@Override
protected View createContentView() {
binding = ActivityPlaylistDetailBinding.inflate(getLayoutInflater());

View file

@ -180,7 +180,7 @@ public abstract class CustomFragmentStatePagerAdapter extends PagerAdapter {
@Override
public Parcelable saveState() {
Bundle state = null;
if (!mSavedState.isEmpty()) {
if (mSavedState.size() > 0) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);

View file

@ -1,69 +0,0 @@
package org.adrianvictor.geleia.adapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.adrianvictor.geleia.R;
import org.adrianvictor.geleia.model.Song;
import java.util.ArrayList;
import java.util.List;
public class DownloadsAdapter extends RecyclerView.Adapter<DownloadsAdapter.ViewHolder> {
private final List<Song> mSongs;
private final int mLayoutId;
public DownloadsAdapter(int layoutId) {
mLayoutId = layoutId;
this.mSongs = new ArrayList<>();
}
public void swapDataSet(List<Song> newSongs) {
mSongs.clear();
if (newSongs != null) {
mSongs.addAll(newSongs);
}
notifyDataSetChanged();
}
@NonNull
@Override
public DownloadsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull DownloadsAdapter.ViewHolder holder, int position) {
final Song song = mSongs.get(position);
holder.title.setText(song.title);
holder.artist.setText(song.artistName);
// TODO: Load album cover into holder.cover using Glide
}
@Override
public int getItemCount() {
return mSongs.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView title;
public final TextView artist;
public final ImageView cover;
public ViewHolder(View itemView) {
super(itemView);
title = itemView.findViewById(R.id.title);
artist = itemView.findViewById(R.id.text);
cover = itemView.findViewById(R.id.image);
}
}
}

View file

@ -10,9 +10,6 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import org.adrianvictor.geleia.activities.UnreachableActivity;
import org.adrianvictor.geleia.fragments.OfflineFragment;
import org.adrianvictor.geleia.fragments.library.DownloadsFragment;
import org.adrianvictor.geleia.fragments.library.FavoritesFragment;
import org.adrianvictor.geleia.model.Category;
import org.adrianvictor.geleia.fragments.library.AlbumsFragment;
@ -160,8 +157,7 @@ public class MusicLibraryPagerAdapter extends FragmentPagerAdapter {
ARTISTS(ArtistsFragment.class),
GENRES(GenresFragment.class),
PLAYLISTS(PlaylistsFragment.class),
FAVORITES(FavoritesFragment.class),
DOWNLOADS(DownloadsFragment.class);
FAVORITES(FavoritesFragment.class);
private final Class<? extends Fragment> mFragmentClass;

View file

@ -14,9 +14,6 @@ public interface CacheDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertCache(Cache cache);
@Query("SELECT * FROM cache")
List<Cache> getAll();
@Query("SELECT * FROM songs LEFT JOIN cache USING(id) WHERE songs.id IN (:ids)")
List<Song> getSongs(List<String> ids);

View file

@ -31,7 +31,7 @@ public class SongShareDialog extends DialogFragment {
final String currentlyListening = getString(R.string.currently_listening_to_x_by_x, song.title, song.artistName);
return new MaterialDialog.Builder(requireActivity())
.title(R.string.what_do_you_want_to_share)
.items(getString(R.string.the_audio_file), "" + currentlyListening + "")
.items(getString(R.string.the_audio_file), "\u201C" + currentlyListening + "\u201D")
.itemsCallback((materialDialog, view, i, charSequence) -> {
switch (i) {
case 0:

View file

@ -1,25 +0,0 @@
package org.adrianvictor.geleia.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.adrianvictor.geleia.R;
public class OfflineFragment extends Fragment {
public static OfflineFragment newInstance() {
return new OfflineFragment();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_offline, container, false);
}
}

View file

@ -1,125 +0,0 @@
package org.adrianvictor.geleia.fragments.library;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import org.adrianvictor.geleia.App;
import org.adrianvictor.geleia.adapter.DownloadsAdapter;
import org.adrianvictor.geleia.database.Cache;
import org.adrianvictor.geleia.model.Song;
import org.adrianvictor.geleia.model.SortMethod;
import org.adrianvictor.geleia.model.SortOrder;
import java.util.ArrayList;
import java.util.List;
public class DownloadsFragment extends AbsLibraryPagerRecyclerViewCustomGridSizeFragment<DownloadsAdapter, GridLayoutManager, Void> {
@NonNull
@Override
protected DownloadsAdapter createAdapter() {
return new DownloadsAdapter(getItemLayoutRes());
}
@NonNull
@Override
protected GridLayoutManager createLayoutManager() {
return new GridLayoutManager(getActivity(), getGridSize());
}
@NonNull
@Override
protected Void createQuery() {
return null;
}
@Override
protected void loadItems(int index) {
new Thread(() -> {
List<Cache> cachedEntries = App.getDatabase().cacheDao().getAll();
List<String> songIds = new ArrayList<>();
for (Cache entry : cachedEntries) {
songIds.add(entry.id);
}
final List<Song> downloadedSongs = App.getDatabase().cacheDao().getSongs(songIds);
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
getAdapter().swapDataSet(downloadedSongs);
});
}
}).start();
}
@Override
protected int loadGridSize() {
return 1;
}
@Override
protected void saveGridSize(int gridColumns) {
}
@Override
protected int loadGridSizeLand() {
return 1;
}
@Override
protected void saveGridSizeLand(int gridColumns) {
}
@Override
protected void saveUsePalette(boolean usePalette) {
}
@Override
protected boolean loadUsePalette() {
return false;
}
@Override
protected void setUsePalette(boolean usePalette) {
}
@Override
protected void setGridSize(int gridSize) {
}
@Override
protected SortMethod loadSortMethod() {
return SortMethod.ADDED;
}
@Override
protected void saveSortMethod(SortMethod sortMethod) {
}
@Override
protected void setSortMethod(SortMethod sortMethod) {
}
@Override
protected SortOrder loadSortOrder() {
return SortOrder.ASCENDING;
}
@Override
protected void saveSortOrder(SortOrder sortOrder) {
}
@Override
protected void setSortOrder(SortOrder sortOrder) {
}
}

View file

@ -273,7 +273,7 @@ public class MusicPlayerRemote {
public static boolean playNext(Song song) {
if (musicService != null && musicService.queueManager != null) {
if (!getPlayingQueue().isEmpty()) {
if (getPlayingQueue().size() > 0) {
musicService.queueManager.addSong(getPosition() + 1, song);
} else {
List<Song> queue = new ArrayList<>();
@ -290,7 +290,7 @@ public class MusicPlayerRemote {
public static boolean playNext(@NonNull List<Song> songs) {
if (musicService != null && musicService.queueManager != null) {
if (!getPlayingQueue().isEmpty()) {
if (getPlayingQueue().size() > 0) {
musicService.queueManager.addSongs(getPosition() + 1, songs);
} else {
openQueue(songs, 0, false);
@ -306,7 +306,7 @@ public class MusicPlayerRemote {
public static boolean enqueue(Song song) {
if (musicService != null && musicService.queueManager != null) {
if (!getPlayingQueue().isEmpty()) {
if (getPlayingQueue().size() > 0) {
musicService.queueManager.addSong(song);
} else {
List<Song> queue = new ArrayList<>();
@ -323,7 +323,7 @@ public class MusicPlayerRemote {
public static boolean enqueue(@NonNull List<Song> songs) {
if (musicService != null && musicService.queueManager != null) {
if (!getPlayingQueue().isEmpty()) {
if (getPlayingQueue().size() > 0) {
musicService.queueManager.addSongs(songs);
} else {
openQueue(songs, 0, false);

View file

@ -29,10 +29,10 @@ public class Album implements Parcelable {
this.title = itemDto.getName();
this.year = itemDto.getProductionYear() != null ? itemDto.getProductionYear() : 0;
if (!itemDto.getAlbumArtists().isEmpty()) {
if (itemDto.getAlbumArtists().size() != 0) {
this.artistId = itemDto.getAlbumArtists().get(0).getId();
this.artistName = itemDto.getAlbumArtists().get(0).getName();
} else if (!itemDto.getArtistItems().isEmpty()) {
} else if (itemDto.getArtistItems().size() != 0) {
this.artistId = itemDto.getArtistItems().get(0).getId();
this.artistName = itemDto.getArtistItems().get(0).getName();
}

View file

@ -10,8 +10,7 @@ public enum Category {
ARTISTS(R.string.artists),
GENRES(R.string.genres),
PLAYLISTS(R.string.playlists),
FAVORITES(R.string.favorites),
DOWNLOADS(R.string.downloads);
FAVORITES(R.string.favorites);
@StringRes
public final int title;

View file

@ -67,10 +67,10 @@ public class Song implements Parcelable {
this.albumId = itemDto.getAlbumId();
this.albumName = itemDto.getAlbum();
if (!itemDto.getArtistItems().isEmpty()) {
if (itemDto.getArtistItems().size() != 0) {
this.artistId = itemDto.getArtistItems().get(0).getId();
this.artistName = itemDto.getArtistItems().get(0).getName();
} else if (!itemDto.getAlbumArtists().isEmpty()) {
} else if (itemDto.getAlbumArtists().size() != 0) {
this.artistId = itemDto.getAlbumArtists().get(0).getId();
this.artistName = itemDto.getAlbumArtists().get(0).getName();
}
@ -93,7 +93,7 @@ public class Song implements Parcelable {
this.supportsTranscoding = source.getSupportsTranscoding();
if (source.getMediaStreams() != null && !source.getMediaStreams().isEmpty()) {
if (source.getMediaStreams() != null && source.getMediaStreams().size() != 0) {
MediaStream stream = source.getMediaStreams().get(0);
this.codec = stream.getCodec();

View file

@ -4,8 +4,6 @@ import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import android.util.Log;
import androidx.documentfile.provider.DocumentFile;
import org.adrianvictor.geleia.App;
@ -13,7 +11,6 @@ import org.adrianvictor.geleia.BuildConfig;
import org.adrianvictor.geleia.database.Cache;
import org.adrianvictor.geleia.model.Song;
import org.adrianvictor.geleia.service.notifications.DownloadNotification;
import org.adrianvictor.geleia.service.notifications.ErrorNotification;
import org.adrianvictor.geleia.util.MusicUtil;
import org.adrianvictor.geleia.util.PreferenceUtil;
@ -26,8 +23,8 @@ import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@SuppressWarnings("ResultOfMethodCallIgnored")
public class DownloadService extends Service {
public static final String TAG = DownloadService.class.getSimpleName();
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
public static final String ACTION_START = PACKAGE_NAME + ".action.start";
public static final String ACTION_CANCEL = PACKAGE_NAME + ".action.cancel";
@ -36,8 +33,6 @@ public class DownloadService extends Service {
private ExecutorService executor;
private DownloadNotification notification;
private static final Object lock = new Object();
@Override
public void onCreate() {
super.onCreate();
@ -60,7 +55,6 @@ public class DownloadService extends Service {
break;
case DownloadService.ACTION_START:
List<Song> songs = intent.getParcelableArrayListExtra(EXTRA_SONGS);
assert songs != null;
for (Song song : songs) {
download(song);
notification.start(song);
@ -84,25 +78,20 @@ public class DownloadService extends Service {
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
DocumentFile root;
if (location.startsWith("content://")) {
root = DocumentFile.fromTreeUri(this, Uri.parse(location));
} else {
if (location.equals(getApplicationContext().getCacheDir().toString())) {
root = DocumentFile.fromFile(new File(location));
} else {
root = DocumentFile.fromTreeUri(this, Uri.parse(location));
}
DocumentFile artist;
DocumentFile album;
synchronized (lock) {
artist = root.findFile(MusicUtil.ascii(song.artistName));
DocumentFile artist = root.findFile(MusicUtil.ascii(song.artistName));
if (artist == null) {
artist = root.createDirectory(MusicUtil.ascii(song.artistName));
}
album = artist.findFile(MusicUtil.ascii(song.albumName));
DocumentFile album = artist.findFile(MusicUtil.ascii(song.albumName));
if (album == null) {
album = artist.createDirectory(MusicUtil.ascii(song.albumName));
}
}
String fileName = song.discNumber + "." + song.trackNumber + " - " + MusicUtil.ascii(song.title) + "." + song.container;
DocumentFile audio = album.createFile("audio/" + song.container, fileName);
@ -127,8 +116,7 @@ public class DownloadService extends Service {
App.getDatabase().cacheDao().insertCache(new Cache(song));
notification.stop(song);
} catch (Exception e) {
Log.e(TAG, "Failed to download song: " + song.title, e);
ErrorNotification.show(this, e.getMessage());
e.printStackTrace();
}
});
}

View file

@ -46,19 +46,9 @@ public class LoginService extends Service {
if (user == null) {
Toast.makeText(this, context.getResources().getString(R.string.error_unexpected), Toast.LENGTH_SHORT).show();
sendBroadcast(new Intent(STATE_OFFLINE));
return;
}
if (App.getApiClient() == null) {
try {
App.createApiClient(context);
} catch (Exception e) {
sendBroadcast(new Intent(STATE_OFFLINE));
return;
}
}
App.getApiClient().ChangeServerLocation(user.server);
App.getApiClient().SetAuthenticationInfo(user.token, user.id);
App.getApiClient().GetSystemInfoAsync(new Response<SystemInfo>() {

View file

@ -1,134 +1,114 @@
package org.adrianvictor.geleia.service.notifications;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.widget.RemoteViews;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import org.adrianvictor.geleia.R;
import org.adrianvictor.geleia.activities.MainActivity;
import org.adrianvictor.geleia.model.Song;
import org.adrianvictor.geleia.service.DownloadService;
import java.util.Collections;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static android.content.Context.NOTIFICATION_SERVICE;
public class DownloadNotification {
private static final String CHANNEL_ID = "download_channel";
private final int ID = 2;
private static final String CHANNEL_ID = DownloadNotification.class.getSimpleName();
private static final int NOTIFICATION_ID = 2;
private final Context context;
private final NotificationManager notificationManager;
private final List<Song> queue = Collections.synchronizedList(new LinkedList<>());
private long totalSize;
private long downloadedSize;
private int lastPercentage = -1;
private final List<Song> songs;
private int current;
private int maximum;
public DownloadNotification(Context context) {
this.notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
this.context = context;
this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
this.songs = new ArrayList<>();
}
public synchronized void start(Song song) {
this.songs.add(song);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel();
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
context.getString(R.string.download_channel_name),
NotificationManager.IMPORTANCE_LOW
);
notificationManager.createNotificationChannel(channel);
}
}
public void start(Song song) {
queue.add(song);
if (queue.size() == 1) { // This is the first song of a new batch
totalSize = 0;
downloadedSize = 0;
lastPercentage = -1;
notificationManager.notify(ID, getNotification());
} else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
// For KitKat, update for every new song to show correct count.
notificationManager.notify(ID, getNotification());
}
}
public synchronized void update(int current, int maximum) {
this.current += current;
this.maximum += maximum;
public void update(long downloaded, long total) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
return; // No progress updates for KitKat
}
Intent action = new Intent(context, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent clickIntent = PendingIntent.getActivity(context, 0, action, PendingIntent.FLAG_IMMUTABLE);
synchronized (this) {
totalSize += total;
downloadedSize += downloaded;
Intent cancel = new Intent(context, DownloadService.class).setAction(DownloadService.ACTION_CANCEL);
PendingIntent pendingCancel = PendingIntent.getService(context, 0, cancel, PendingIntent.FLAG_IMMUTABLE);
int percentage = 0;
if (totalSize > 0) {
percentage = (int) ((downloadedSize * 100) / totalSize);
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
for (Song item : songs.stream().limit(5).collect(Collectors.toList())) {
style.addLine(item.title);
}
if (percentage > lastPercentage) {
lastPercentage = percentage;
notificationManager.notify(ID, getNotification());
}
}
}
public void stop(Song song) {
queue.remove(song);
if (queue.isEmpty()) {
notificationManager.cancel(ID);
} else {
// Update notification to show new queue size.
// On KitKat, this is the only update after a download finishes.
notificationManager.notify(ID, getNotification());
}
}
public void cancelAll() {
queue.clear();
notificationManager.cancel(ID);
}
private Notification getNotification() {
Intent intent = new Intent(context, DownloadService.class);
intent.setAction(DownloadService.ACTION_CANCEL);
int flags = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, flags);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(context.getString(R.string.downloading_songs))
.setOngoing(true)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, context.getString(android.R.string.cancel), pendingIntent);
.setContentIntent(clickIntent)
.setContentTitle(String.format(context.getString(R.string.downloading_x_songs), songs.size()))
.setProgress(this.maximum, this.current, false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.addAction(R.drawable.ic_close_white_24dp, context.getString(R.string.action_cancel), pendingCancel)
.setStyle(style)
.setShowWhen(false);
String contentText = context.getResources().getQuantityString(R.plurals.downloading_s_songs, queue.size(), queue.size());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.notification_download);
remoteViews.setTextViewText(R.id.notification_download_title, contentText);
int progress = lastPercentage > 0 ? lastPercentage : 0;
remoteViews.setProgressBar(R.id.notification_download_progress, 100, progress, totalSize == 0 && downloadedSize == 0);
builder.setCustomContentView(remoteViews);
} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
int progress = lastPercentage > 0 ? lastPercentage : 0;
builder.setProgress(100, progress, totalSize == 0 && downloadedSize == 0);
builder.setContentText(contentText);
} else { // KitKat
builder.setContentText(contentText);
notificationManager.notify(NOTIFICATION_ID, builder.build());
}
return builder.build();
public synchronized void stop(Song song) {
if (song != null) {
songs.remove(song);
} else {
songs.clear();
}
if (songs.size() != 0) {
return;
}
current = 0;
maximum = 0;
notificationManager.cancel(NOTIFICATION_ID);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.deleteNotificationChannel(CHANNEL_ID);
}
}
@RequiresApi(Build.VERSION_CODES.O)
private void createNotificationChannel() {
NotificationChannel notificationChannel = notificationManager.getNotificationChannel(CHANNEL_ID);
if (notificationChannel == null) {
notificationChannel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.action_download), NotificationManager.IMPORTANCE_LOW);
notificationChannel.setDescription(context.getString(R.string.playing_notification_description));
notificationChannel.enableLights(false);
notificationChannel.enableVibration(false);
notificationManager.createNotificationChannel(notificationChannel);
}
}
}

View file

@ -1,49 +0,0 @@
package org.adrianvictor.geleia.service.notifications;
import static android.content.Context.NOTIFICATION_SERVICE;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import org.adrianvictor.geleia.R;
public class ErrorNotification {
private static final String CHANNEL_ID = ErrorNotification.class.getSimpleName();
private static final int NOTIFICATION_ID = 3;
private ErrorNotification() {}
public static void show(Context context, String error) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
createNotificationChannel(notificationManager);
NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle();
style.setBigContentTitle("Error:").bigText(error);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_bug_report_white_24dp)
.setContentTitle(context.getString(R.string.error_notification_title))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setStyle(style)
.setContentText("Expand the notification for details.");
}
public static void createNotificationChannel(NotificationManager notificationManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = notificationManager.getNotificationChannel(CHANNEL_ID);
if (channel == null) {
channel = new NotificationChannel(CHANNEL_ID, "Errors", NotificationManager.IMPORTANCE_LOW);
channel.setDescription("Displays all sorts of errors.");
channel.enableLights(false);
channel.enableVibration(true);
notificationManager.createNotificationChannel(channel);
}
}
}
}

View file

@ -2,7 +2,6 @@ package org.adrianvictor.geleia.service.playback;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
@ -142,18 +141,12 @@ public class LocalPlayer implements Playback {
private List<MediaItem> createMediaItems(List<Song> queue) {
return queue.stream().map(song -> {
String fileUri = MusicUtil.getFileUri(song);
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && fileUri.startsWith("content://")) {
uri = Uri.parse(fileUri);
} else {
File audio = new File(fileUri);
uri = Uri.fromFile(audio);
File audio = new File(MusicUtil.getFileUri(song));
Uri uri = Uri.fromFile(audio);
if (!audio.exists()) {
uri = Uri.parse(MusicUtil.getTranscodeUri(song));
}
}
List<String> containers = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream()
.map(codec -> codec.container.toLowerCase(Locale.ROOT))

View file

@ -14,7 +14,6 @@ import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.content.res.ResourcesCompat;
@ -92,22 +91,18 @@ public class ImageUtil {
}
public static Drawable getTintedVectorDrawable(@NonNull Context context, @DrawableRes int resId, @ColorInt int color) {
final Drawable drawable = AppCompatResources.getDrawable(context, resId);
Drawable drawable = getVectorDrawable(context.getResources(), resId, context.getTheme());
if (drawable != null) {
Drawable wrappedDrawable = DrawableCompat.wrap(drawable).mutate();
DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_IN);
DrawableCompat.setTint(drawable, color);
DrawableCompat.setTintMode(wrappedDrawable, PorterDuff.Mode.SRC_IN);
DrawableCompat.setTint(wrappedDrawable, color);
return wrappedDrawable;
return drawable;
}
return null;
}
public static Drawable getVectorDrawable(@NonNull Context context, @DrawableRes int resId) {
return AppCompatResources.getDrawable(context, resId);
return getVectorDrawable(context.getResources(), resId, context.getTheme());
}
public static Drawable resolveDrawable(@NonNull Context context, @AttrRes int drawableAttr) {
TypedArray a = context.obtainStyledAttributes(new int[]{drawableAttr});
Drawable drawable = a.getDrawable(0);

View file

@ -3,7 +3,6 @@ package org.adrianvictor.geleia.util;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
@ -19,7 +18,6 @@ import org.adrianvictor.geleia.model.Codec;
import org.adrianvictor.geleia.model.Genre;
import org.adrianvictor.geleia.model.Song;
import org.adrianvictor.geleia.service.notifications.ErrorNotification;
import org.jellyfin.apiclient.interaction.ApiClient;
import org.jellyfin.apiclient.interaction.Response;
import org.jellyfin.apiclient.model.dto.UserItemDataDto;
@ -51,7 +49,7 @@ public class MusicUtil {
List<Codec> codecs = preferenceUtil.getDirectPlayCodecs();
Stream<String> values = codecs.stream().map(codec -> codec.value);
if (!codecs.isEmpty()) {
if (codecs.size() != 0) {
builder.append("&Container=").append(values.collect(Collectors.joining(",")));
}
@ -83,8 +81,7 @@ public class MusicUtil {
public static String getFileUri(Song song) {
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
File root = new File(location, "music");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !location.equals(App.getInstance().getCacheDir().toString())) {
if (!location.equals(App.getInstance().getCacheDir().toString())) {
Uri uri = Uri.parse(location);
return new File(uri.getPath()).getAbsolutePath();
}
@ -105,7 +102,7 @@ public class MusicUtil {
try {
return new Intent();
} catch (IllegalArgumentException e) {
ErrorNotification.show(context, e.getMessage());
e.printStackTrace();
Toast.makeText(context, R.string.error_share_file, Toast.LENGTH_SHORT).show();
return new Intent();
}
@ -113,7 +110,7 @@ public class MusicUtil {
@NonNull
public static String getArtistInfoString(@NonNull final Context context, @NonNull final Artist artist) {
return !artist.genres.isEmpty() ? artist.genres.get(0).name : "";
return artist.genres.size() != 0 ? artist.genres.get(0).name : "";
}
@NonNull

View file

@ -12,7 +12,6 @@ import androidx.core.util.Pair;
import org.adrianvictor.geleia.activities.LoginActivity;
import org.adrianvictor.geleia.activities.MainActivity;
import org.adrianvictor.geleia.activities.SelectActivity;
import org.adrianvictor.geleia.activities.UnreachableActivity;
import org.adrianvictor.geleia.model.Album;
import org.adrianvictor.geleia.model.Artist;
import org.adrianvictor.geleia.model.Genre;
@ -62,13 +61,6 @@ public class NavigationUtil {
context.startActivity(intent);
}
public static void startUnreachable(Context context) {
final Intent intent = new Intent(context, UnreachableActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
context.startActivity(intent);
}
public static void startSelect(Context context) {
final Intent intent = new Intent(context, SelectActivity.class);

View file

@ -48,7 +48,7 @@ public class PlaylistUtil {
PlaylistCreationRequest request = new PlaylistCreationRequest();
request.setUserId(App.getApiClient().getCurrentUserId());
request.setName(name);
if (!ids.isEmpty()) request.setItemIdList(ids);
if (ids.size() != 0) request.setItemIdList(ids);
App.getApiClient().CreatePlaylist(request, new Response<>());
}

View file

@ -448,6 +448,7 @@ public final class PreferenceUtil {
}).collect(Collectors.toList());
}
@SuppressWarnings("SimplifyStreamApiCallChains")
public void setCategories(List<Category> categories) {
List<String> values = categories.stream().map(category -> {
return category.select ? category.toString() : category.toString().toLowerCase();

View file

@ -10,27 +10,23 @@ import org.adrianvictor.geleia.util.ThemeUtil;
import org.adrianvictor.geleia.R;
public class IconImageView extends AppCompatImageView {
public IconImageView(Context context) {
super(context);
init(context);
}
public IconImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public IconImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
init();
}
private void init() {
if (getContext() == null) return;
setColorFilter(ThemeUtil.getColorResource(getContext(), R.attr.iconColor), PorterDuff.Mode.SRC_IN);
private void init(Context context) {
if (context == null) return;
setColorFilter(ThemeUtil.getColorResource(context, R.attr.iconColor), PorterDuff.Mode.SRC_IN);
}
}

View file

@ -36,7 +36,7 @@ public class DynamicShortcutManager {
}
public void initDynamicShortcuts() {
if (shortcutManager.getDynamicShortcuts().isEmpty()) {
if (shortcutManager.getDynamicShortcuts().size() == 0) {
shortcutManager.setDynamicShortcuts(getDefaultShortcuts());
}
}

View file

@ -1,4 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
</selector>
<vector android:height="192dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="192dp"
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:pathData="m12,9.8425c-1.1866,0 -2.1575,0.9709 -2.1575,2.1575s0.9709,2.1575 2.1575,2.1575 2.1575,-0.9709 2.1575,-2.1575 -0.9709,-2.1575 -2.1575,-2.1575zM12,1c-6.072,0 -11,4.928 -11,11s4.928,11 11,11 11,-4.928 11,-11 -4.928,-11 -11,-11zM12,7.05c2.739,0 4.95,2.211 4.95,4.95s-2.211,4.95 -4.95,4.95 -4.95,-2.211 -4.95,-4.95 2.211,-4.95 4.95,-4.95z" android:strokeWidth="2.1575">
<aapt:attr name="android:fillColor">
<gradient android:endX="25.2" android:endY="25.1996"
android:startX="1" android:startY="0.99960005" android:type="linear">
<item android:color="#FFAA5CC3" android:offset="0"/>
<item android:color="#FF00A4DC" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
</vector>

View file

@ -19,22 +19,21 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ScrollView
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="40dp"
android:layout_marginBottom="40dp"
app:srcCompat="@drawable/ic_launcher_nodpi"
tools:ignore="ContentDescription"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/username_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/username_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:id="@+id/username_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
@ -44,24 +43,27 @@
app:boxCornerRadiusBottomStart="24dp"
app:boxCornerRadiusTopEnd="24dp"
app:boxCornerRadiusTopStart="24dp"
app:boxStrokeColor="?android:textColorSecondary"
app:errorEnabled="true"
app:endIconMode="clear_text"
app:endIconTint="?android:textColorSecondary"
app:errorEnabled="true"
app:hintTextColor="?android:textColorSecondary">
app:boxStrokeColor="?android:textColorSecondary"
app:hintTextColor="?android:textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/password_layout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/username"
android:inputType="textNoSuggestions" />
android:inputType="textNoSuggestions"
android:hint="@string/username" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:id="@+id/password_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
@ -71,24 +73,27 @@
app:boxCornerRadiusBottomStart="24dp"
app:boxCornerRadiusTopEnd="24dp"
app:boxCornerRadiusTopStart="24dp"
app:boxStrokeColor="?android:textColorSecondary"
app:errorEnabled="true"
app:endIconMode="password_toggle"
app:endIconTint="?android:textColorSecondary"
app:errorEnabled="true"
app:hintTextColor="?android:textColorSecondary">
app:boxStrokeColor="?android:textColorSecondary"
app:hintTextColor="?android:textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/server_layout">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:inputType="textPassword" />
android:inputType="textPassword"
android:hint="@string/password" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/server_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:id="@+id/server_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
@ -98,18 +103,21 @@
app:boxCornerRadiusBottomStart="24dp"
app:boxCornerRadiusTopEnd="24dp"
app:boxCornerRadiusTopStart="24dp"
app:boxStrokeColor="?android:textColorSecondary"
app:errorEnabled="true"
app:endIconMode="clear_text"
app:endIconTint="?android:textColorSecondary"
app:errorEnabled="true"
app:hintTextColor="?android:textColorSecondary">
app:boxStrokeColor="?android:textColorSecondary"
app:hintTextColor="?android:textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/login">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/server"
android:inputType="textUri" />
android:inputType="textUri"
android:hint="@string/server" />
</com.google.android.material.textfield.TextInputLayout>
@ -121,7 +129,10 @@
android:layout_marginTop="32dp"
android:layout_marginBottom="16dp"
android:text="@string/login"
app:cornerRadius="24dp" />
app:cornerRadius="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/select" />
<com.google.android.material.button.MaterialButton
android:id="@+id/select"
@ -130,11 +141,12 @@
android:layout_marginHorizontal="32dp"
android:layout_marginTop="0dp"
android:layout_marginBottom="32dp"
android:backgroundTint="?cardBackgroundColor"
android:text="@string/select"
android:backgroundTint="?cardBackgroundColor"
app:cornerRadius="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="UnusedAttribute" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="org.adrianvictor.geleia.activities.UnreachableActivity">
<include layout="@layout/status_bar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="16dp"
android:clipToPadding="false"
android:isScrollContainer="true">
<include layout="@layout/fragment_offline" />
</ScrollView>
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_marginBottom="16dp"
android:onClick="onSelectClick"
android:text="@string/change_server" />
</LinearLayout>

View file

@ -82,50 +82,6 @@
</LinearLayout>
<!-- <LinearLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:background="?attr/rectSelector"-->
<!-- android:clickable="true"-->
<!-- android:focusable="true"-->
<!-- android:gravity="center_vertical"-->
<!-- android:minHeight="@dimen/item_height"-->
<!-- android:orientation="horizontal"-->
<!-- android:paddingLeft="16dp"-->
<!-- android:paddingRight="16dp">-->
<!-- <org.adrianvictor.geleia.views.IconImageView-->
<!-- android:id="@+id/icon_license"-->
<!-- android:layout_width="24dp"-->
<!-- android:layout_height="24dp"-->
<!-- app:srcCompat="@android:drawable/ic_lock_idle_lock"-->
<!-- tools:ignore="ContentDescription" />-->
<!-- <LinearLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="32dp"-->
<!-- android:orientation="vertical"-->
<!-- android:paddingBottom="8dp"-->
<!-- android:paddingTop="8dp">-->
<!-- <TextView-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="@string/license"-->
<!-- android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />-->
<!-- <TextView-->
<!-- android:id="@+id/app_license"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:textAppearance="@style/TextAppearance.AppCompat.Caption"-->
<!-- tools:text="@string/application_license" />-->
<!-- </LinearLayout>-->
<!-- </LinearLayout>-->
<LinearLayout
android:id="@+id/app_source"
android:layout_width="match_parent"
@ -157,14 +113,6 @@
</LinearLayout>
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAlignment="center"
android:text="@string/application_license" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View file

@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="@drawable/card_server"
android:elevation="4dp"
tools:ignore="UnusedAttribute">

View file

@ -27,7 +27,6 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="10dp"
android:background="?attr/dividerColor" />
<LinearLayout
@ -77,12 +76,6 @@
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="10dp"
android:background="?attr/dividerColor" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -141,7 +134,6 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="10dp"
android:background="?attr/dividerColor" />
<LinearLayout
@ -194,7 +186,6 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="10dp"
android:background="?attr/dividerColor" />
<LinearLayout
@ -247,7 +238,6 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="10dp"
android:background="?attr/dividerColor" />
<LinearLayout
@ -308,7 +298,6 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="10dp"
android:background="?attr/dividerColor" />
<LinearLayout

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.adrianvictor.geleia.fragments.library.DownloadsFragment">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="409dp"
android:layout_height="729dp"
tools:layout_editor_absoluteX="1dp"
tools:layout_editor_absoluteY="1dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,40 +0,0 @@
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_margin="32dp"
tools:context=".fragments.OfflineFragment">
<TextView
android:id="@+id/offline_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:text="@string/sad_face"
android:textAppearance="@style/TextAppearance.AppCompat.Display3" />
<TextView
android:id="@+id/error_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/offline_icon"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:text="@string/oops"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/error_title"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:text="@string/server_is_unreachable"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
</RelativeLayout>

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/notification_download_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
tools:text="Downloading 2 songs" />
<ProgressBar
android:id="@+id/notification_download_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
</LinearLayout>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

@ -235,22 +235,5 @@
<string name="error_empty_username">Please fill in your username.</string>
<string name="dkanada_summary">Forking Phonograph and making Gelli</string>
<string name="adrianvictor">Adrian Victor</string>
<string name="error_notification_title">An error occurred in Jamfish.</string>
<string name="offline">You are offline.</string>
<string name="change_server">Change server</string>
<string name="oops">Oops! I did it again...</string>
<string name="sad_face">;(</string>
<string name="server_is_unreachable">Sorry, but we couldn\'t reach this server right now.</string>
<string name="license">License</string>
<string name="app_license">GNU General Public License v3.0</string>
<string name="application_license">GNU General Public License v3.0</string>
<string name="download_channel_name">Downloads</string>
<string name="downloading_songs">Downloading songs</string>
<string name="downloads">Downloads</string>
<plurals name="downloading_s_songs">
<item quantity="one">Downloading %d song</item>
<item quantity="other">Downloading %d songs</item>
</plurals>
</resources>

View file

@ -1,16 +1 @@
This is a native music player for Android devices that connects to Jellyfin media servers. The code is based on Gelli's archived repository, which is based on an old version of Phonograph. Jamfish is made for personal use, but contributions are welcome! Please open an issue to discuss larger changes before submitting a pull request.
Features
- Basic library navigation
- Download songs to internal storage individually or through batch actions
- Gapless playback
- Sort albums and songs by different fields
- Search media for partial matches
- Media service integration with notification
- Favorites and playlists
- Playback history reporting
- Filter content by library
Requisites:
- A Jellfin server
- Android 4.4 or later

View file

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 650 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 340 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Before After
Before After