diff --git a/README.md b/README.md
index ae7eb67d..52420d24 100644
--- a/README.md
+++ b/README.md
@@ -23,12 +23,17 @@ 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
+* 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))
# Future Plans
diff --git a/app/build.gradle b/app/build.gradle
index 30aa7bdf..534ad6d3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,10 +10,12 @@ android {
minSdk 19
targetSdk 33
- versionCode 1
- versionName '1.4.0'
+ versionCode 2
+ versionName '1.4.1'
+ // for SDK < 19
multiDexEnabled true
+
vectorDrawables {
useSupportLibrary true
}
@@ -105,8 +107,9 @@ dependencies {
implementation 'com.android.support:multidex:1.0.3'
implementation 'com.mlegy.redscreenofdeath:red-screen-of-death:0.1.3'
- implementation 'com.squareup.retrofit2:retrofit:2.9.0'
- implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
+ // 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'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
@@ -117,4 +120,10 @@ 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'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2ad3febf..e5ab2f2f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,9 @@
+
+
@@ -49,6 +51,7 @@
+
@@ -64,7 +67,7 @@
android:name="org.adrianvictor.geleia.views.shortcuts.AppShortcutLauncherActivity"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
-
+
@@ -136,6 +139,12 @@
android:resource="@xml/provider_paths" />
+
+
diff --git a/app/src/main/java/org/adrianvictor/geleia/App.java b/app/src/main/java/org/adrianvictor/geleia/App.java
index 3145450b..231cee4c 100644
--- a/app/src/main/java/org/adrianvictor/geleia/App.java
+++ b/app/src/main/java/org/adrianvictor/geleia/App.java
@@ -6,6 +6,9 @@ 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;
@@ -14,6 +17,7 @@ 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;
@@ -22,7 +26,16 @@ import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient;
import org.jellyfin.apiclient.logging.AndroidLogger;
import org.jellyfin.apiclient.logging.ILogger;
-public class App extends Application {
+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 {
private static App app;
private static JellyDatabase database;
@@ -30,8 +43,14 @@ public class App extends Application {
@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);
}
@@ -40,7 +59,7 @@ public class App extends Application {
database = createDatabase(this);
apiClient = createApiClient(this);
- if (database.userDao().getUsers().size() == 0) {
+ if (database.userDao().getUsers().isEmpty()) {
PreferenceUtil.getInstance(this).setServer(null);
PreferenceUtil.getInstance(this).setUser(null);
}
@@ -76,6 +95,7 @@ public class App extends Application {
IDevice device = new AndroidDevice(deviceId, deviceName);
EventListener eventListener = new EventListener();
+
return new ApiClient(httpClient, logger, server, appName, appVersion, device, eventListener);
}
@@ -90,4 +110,15 @@ public class App extends Application {
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);
+ }
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/AboutActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/AboutActivity.java
index 50601fac..56ffa5cf 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/AboutActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/AboutActivity.java
@@ -7,6 +7,7 @@ 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;
@@ -102,7 +103,7 @@ public class AboutActivity extends AbsBaseActivity implements View.OnClickListen
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
+ ErrorNotification.show(context, e.getMessage());
}
return "Unknown";
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/DirectoryPickerActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/DirectoryPickerActivity.java
new file mode 100644
index 00000000..4452596d
--- /dev/null
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/DirectoryPickerActivity.java
@@ -0,0 +1,127 @@
+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 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(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();
+ }
+ }
+}
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/MainActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/MainActivity.java
index 0f1df8a7..41a46462 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/MainActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/MainActivity.java
@@ -13,11 +13,14 @@ 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;
@@ -42,6 +45,7 @@ public class MainActivity extends AbsMusicContentActivity implements CabHolder {
private ActivityMainContentBinding contentBinding;
private NavigationDrawerHeaderBinding navigationBinding;
private boolean onLogout;
+ private boolean pendingShowOffline = false;
@Nullable
private AttachedCab cab;
@@ -97,6 +101,15 @@ 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();
@@ -108,6 +121,16 @@ 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();
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/SearchActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/SearchActivity.java
index 2958e587..b483b51d 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/SearchActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/SearchActivity.java
@@ -93,6 +93,9 @@ 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);
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/SettingsActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/SettingsActivity.java
index b1406474..ab860b08 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/SettingsActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/SettingsActivity.java
@@ -51,6 +51,7 @@ public class SettingsActivity extends AbsBaseActivity {
public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
private ActivityResultLauncher dirPickerLauncher;
+ private ActivityResultLauncher legacyDirPickerLauncher;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -71,6 +72,25 @@ 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
@@ -161,8 +181,13 @@ public class SettingsActivity extends AbsBaseActivity {
}
private void openDirectoryPicker() {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
- dirPickerLauncher.launch(intent);
+ 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
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/SplashActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/SplashActivity.java
index 88a3b8af..d3460d22 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/SplashActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/SplashActivity.java
@@ -1,8 +1,13 @@
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 android.os.Handler;
+
+import androidx.annotation.NonNull;
import org.adrianvictor.geleia.App;
import org.adrianvictor.geleia.R;
@@ -15,10 +20,41 @@ 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);
}
@@ -35,13 +71,20 @@ public class SplashActivity extends AbsBaseActivity {
User user = App.getDatabase().userDao().getUser(PreferenceUtil.getInstance(this).getUser());
List available = App.getDatabase().userDao().getUsers();
- if (user == null && available.size() != 0) {
+ if (user == null && !available.isEmpty()) {
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();
+ }
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/UnreachableActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/UnreachableActivity.java
new file mode 100644
index 00000000..94afba90
--- /dev/null
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/UnreachableActivity.java
@@ -0,0 +1,21 @@
+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);
+ }
+}
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/base/AbsBaseActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/base/AbsBaseActivity.java
index 406f6237..d64b6bc2 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/base/AbsBaseActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/base/AbsBaseActivity.java
@@ -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.size() != 0 && ActivityCompat.shouldShowRequestPermissionRationale(this, permissions.get(0))) {
+ } else if (!permissions.isEmpty() && ActivityCompat.shouldShowRequestPermissionRationale(this, permissions.get(0))) {
builder.setMessage(getPermissionMessage())
.setTitle(R.string.permissions_denied)
.setPositiveButton(R.string.action_grant, (dialog, id) -> requestPermissions());
@@ -127,8 +127,11 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
private boolean checkBatteryOptimization() {
String packageName = getPackageName();
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
-
- return pm.isIgnoringBatteryOptimizations(packageName);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return pm.isIgnoringBatteryOptimizations(packageName);
+ } else {
+ return true;
+ }
}
@RequiresApi(api = Build.VERSION_CODES.M)
@@ -138,9 +141,11 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
@RequiresApi(api = Build.VERSION_CODES.M)
private boolean checkPermissions() {
- for (String permission : permissions) {
- if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
- return false;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ for (String permission : permissions) {
+ if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
}
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/base/AbsMusicContentActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/base/AbsMusicContentActivity.java
index 904b928f..90843227 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/base/AbsMusicContentActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/base/AbsMusicContentActivity.java
@@ -1,14 +1,19 @@
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;
@@ -24,7 +29,7 @@ public abstract class AbsMusicContentActivity extends AbsMusicPanelActivity impl
onStateOnline();
break;
case LoginService.STATE_OFFLINE:
- NavigationUtil.startLogin(context);
+ onStateOffline();
break;
}
}
@@ -39,7 +44,11 @@ public abstract class AbsMusicContentActivity extends AbsMusicPanelActivity impl
filter.addAction(LoginService.STATE_ONLINE);
filter.addAction(LoginService.STATE_OFFLINE);
- registerReceiver(receiver, filter);
+ 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));
@@ -51,24 +60,14 @@ 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() {
- }
-
- @Override
- public void onStateOffline() {
- }
+ public void onStatePolling() {}
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/details/AlbumDetailActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/details/AlbumDetailActivity.java
index 9c7ca01b..ad920baa 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/details/AlbumDetailActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/details/AlbumDetailActivity.java
@@ -74,6 +74,11 @@ 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)));
@@ -231,6 +236,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.size() != 0) adapter.swapDataSet(album.songs);
+ if (!album.songs.isEmpty()) adapter.swapDataSet(album.songs);
}
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/details/ArtistDetailActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/details/ArtistDetailActivity.java
index d7cde428..2f98a935 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/details/ArtistDetailActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/details/ArtistDetailActivity.java
@@ -87,6 +87,11 @@ 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)));
@@ -258,7 +263,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.size() != 0) songAdapter.swapDataSet(artist.songs);
- if (artist.albums.size() != 0) albumAdapter.swapDataSet(artist.albums);
+ if (!artist.songs.isEmpty()) songAdapter.swapDataSet(artist.songs);
+ if (!artist.albums.isEmpty()) albumAdapter.swapDataSet(artist.albums);
}
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/details/GenreDetailActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/details/GenreDetailActivity.java
index 9449daa5..aacc7db8 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/details/GenreDetailActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/details/GenreDetailActivity.java
@@ -59,6 +59,11 @@ public class GenreDetailActivity extends AbsMusicContentActivity implements CabH
});
}
+ @Override
+ public void onStateOffline() {
+
+ }
+
@Override
protected View createContentView() {
binding = ActivityGenreDetailBinding.inflate(getLayoutInflater());
diff --git a/app/src/main/java/org/adrianvictor/geleia/activities/details/PlaylistDetailActivity.java b/app/src/main/java/org/adrianvictor/geleia/activities/details/PlaylistDetailActivity.java
index f111dc14..bb3d1d57 100644
--- a/app/src/main/java/org/adrianvictor/geleia/activities/details/PlaylistDetailActivity.java
+++ b/app/src/main/java/org/adrianvictor/geleia/activities/details/PlaylistDetailActivity.java
@@ -71,6 +71,11 @@ public class PlaylistDetailActivity extends AbsMusicContentActivity implements C
});
}
+ @Override
+ public void onStateOffline() {
+
+ }
+
@Override
protected View createContentView() {
binding = ActivityPlaylistDetailBinding.inflate(getLayoutInflater());
diff --git a/app/src/main/java/org/adrianvictor/geleia/adapter/CustomFragmentStatePagerAdapter.java b/app/src/main/java/org/adrianvictor/geleia/adapter/CustomFragmentStatePagerAdapter.java
index d19a6ffe..f6c1c4f5 100644
--- a/app/src/main/java/org/adrianvictor/geleia/adapter/CustomFragmentStatePagerAdapter.java
+++ b/app/src/main/java/org/adrianvictor/geleia/adapter/CustomFragmentStatePagerAdapter.java
@@ -180,7 +180,7 @@ public abstract class CustomFragmentStatePagerAdapter extends PagerAdapter {
@Override
public Parcelable saveState() {
Bundle state = null;
- if (mSavedState.size() > 0) {
+ if (!mSavedState.isEmpty()) {
state = new Bundle();
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
mSavedState.toArray(fss);
diff --git a/app/src/main/java/org/adrianvictor/geleia/adapter/DownloadsAdapter.java b/app/src/main/java/org/adrianvictor/geleia/adapter/DownloadsAdapter.java
new file mode 100644
index 00000000..25cf4c79
--- /dev/null
+++ b/app/src/main/java/org/adrianvictor/geleia/adapter/DownloadsAdapter.java
@@ -0,0 +1,69 @@
+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 {
+ private final List mSongs;
+ private final int mLayoutId;
+
+ public DownloadsAdapter(int layoutId) {
+ mLayoutId = layoutId;
+ this.mSongs = new ArrayList<>();
+ }
+
+ public void swapDataSet(List 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);
+ }
+ }
+}
diff --git a/app/src/main/java/org/adrianvictor/geleia/adapter/MusicLibraryPagerAdapter.java b/app/src/main/java/org/adrianvictor/geleia/adapter/MusicLibraryPagerAdapter.java
index 2979ee5c..23c1162d 100644
--- a/app/src/main/java/org/adrianvictor/geleia/adapter/MusicLibraryPagerAdapter.java
+++ b/app/src/main/java/org/adrianvictor/geleia/adapter/MusicLibraryPagerAdapter.java
@@ -10,6 +10,9 @@ 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;
@@ -157,7 +160,8 @@ public class MusicLibraryPagerAdapter extends FragmentPagerAdapter {
ARTISTS(ArtistsFragment.class),
GENRES(GenresFragment.class),
PLAYLISTS(PlaylistsFragment.class),
- FAVORITES(FavoritesFragment.class);
+ FAVORITES(FavoritesFragment.class),
+ DOWNLOADS(DownloadsFragment.class);
private final Class extends Fragment> mFragmentClass;
diff --git a/app/src/main/java/org/adrianvictor/geleia/database/CacheDao.java b/app/src/main/java/org/adrianvictor/geleia/database/CacheDao.java
index 8df0ded5..54ff3733 100644
--- a/app/src/main/java/org/adrianvictor/geleia/database/CacheDao.java
+++ b/app/src/main/java/org/adrianvictor/geleia/database/CacheDao.java
@@ -14,6 +14,9 @@ public interface CacheDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertCache(Cache cache);
+ @Query("SELECT * FROM cache")
+ List getAll();
+
@Query("SELECT * FROM songs LEFT JOIN cache USING(id) WHERE songs.id IN (:ids)")
List getSongs(List ids);
diff --git a/app/src/main/java/org/adrianvictor/geleia/dialogs/SongShareDialog.java b/app/src/main/java/org/adrianvictor/geleia/dialogs/SongShareDialog.java
index 101c9377..d2becf6a 100644
--- a/app/src/main/java/org/adrianvictor/geleia/dialogs/SongShareDialog.java
+++ b/app/src/main/java/org/adrianvictor/geleia/dialogs/SongShareDialog.java
@@ -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), "\u201C" + currentlyListening + "\u201D")
+ .items(getString(R.string.the_audio_file), "“" + currentlyListening + "”")
.itemsCallback((materialDialog, view, i, charSequence) -> {
switch (i) {
case 0:
diff --git a/app/src/main/java/org/adrianvictor/geleia/fragments/OfflineFragment.java b/app/src/main/java/org/adrianvictor/geleia/fragments/OfflineFragment.java
new file mode 100644
index 00000000..ed1d40a0
--- /dev/null
+++ b/app/src/main/java/org/adrianvictor/geleia/fragments/OfflineFragment.java
@@ -0,0 +1,25 @@
+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);
+ }
+}
diff --git a/app/src/main/java/org/adrianvictor/geleia/fragments/library/DownloadsFragment.java b/app/src/main/java/org/adrianvictor/geleia/fragments/library/DownloadsFragment.java
new file mode 100644
index 00000000..f6f259f5
--- /dev/null
+++ b/app/src/main/java/org/adrianvictor/geleia/fragments/library/DownloadsFragment.java
@@ -0,0 +1,125 @@
+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 {
+ @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 cachedEntries = App.getDatabase().cacheDao().getAll();
+
+ List songIds = new ArrayList<>();
+
+ for (Cache entry : cachedEntries) {
+ songIds.add(entry.id);
+ }
+
+ final List 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) {
+
+ }
+}
diff --git a/app/src/main/java/org/adrianvictor/geleia/helper/MusicPlayerRemote.java b/app/src/main/java/org/adrianvictor/geleia/helper/MusicPlayerRemote.java
index 4f658348..f409842c 100644
--- a/app/src/main/java/org/adrianvictor/geleia/helper/MusicPlayerRemote.java
+++ b/app/src/main/java/org/adrianvictor/geleia/helper/MusicPlayerRemote.java
@@ -273,7 +273,7 @@ public class MusicPlayerRemote {
public static boolean playNext(Song song) {
if (musicService != null && musicService.queueManager != null) {
- if (getPlayingQueue().size() > 0) {
+ if (!getPlayingQueue().isEmpty()) {
musicService.queueManager.addSong(getPosition() + 1, song);
} else {
List queue = new ArrayList<>();
@@ -290,7 +290,7 @@ public class MusicPlayerRemote {
public static boolean playNext(@NonNull List songs) {
if (musicService != null && musicService.queueManager != null) {
- if (getPlayingQueue().size() > 0) {
+ if (!getPlayingQueue().isEmpty()) {
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().size() > 0) {
+ if (!getPlayingQueue().isEmpty()) {
musicService.queueManager.addSong(song);
} else {
List queue = new ArrayList<>();
@@ -323,7 +323,7 @@ public class MusicPlayerRemote {
public static boolean enqueue(@NonNull List songs) {
if (musicService != null && musicService.queueManager != null) {
- if (getPlayingQueue().size() > 0) {
+ if (!getPlayingQueue().isEmpty()) {
musicService.queueManager.addSongs(songs);
} else {
openQueue(songs, 0, false);
diff --git a/app/src/main/java/org/adrianvictor/geleia/model/Album.java b/app/src/main/java/org/adrianvictor/geleia/model/Album.java
index 36ff5554..81e4e481 100644
--- a/app/src/main/java/org/adrianvictor/geleia/model/Album.java
+++ b/app/src/main/java/org/adrianvictor/geleia/model/Album.java
@@ -29,10 +29,10 @@ public class Album implements Parcelable {
this.title = itemDto.getName();
this.year = itemDto.getProductionYear() != null ? itemDto.getProductionYear() : 0;
- if (itemDto.getAlbumArtists().size() != 0) {
+ if (!itemDto.getAlbumArtists().isEmpty()) {
this.artistId = itemDto.getAlbumArtists().get(0).getId();
this.artistName = itemDto.getAlbumArtists().get(0).getName();
- } else if (itemDto.getArtistItems().size() != 0) {
+ } else if (!itemDto.getArtistItems().isEmpty()) {
this.artistId = itemDto.getArtistItems().get(0).getId();
this.artistName = itemDto.getArtistItems().get(0).getName();
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/model/Category.java b/app/src/main/java/org/adrianvictor/geleia/model/Category.java
index 91e54717..3c897e81 100644
--- a/app/src/main/java/org/adrianvictor/geleia/model/Category.java
+++ b/app/src/main/java/org/adrianvictor/geleia/model/Category.java
@@ -10,7 +10,8 @@ public enum Category {
ARTISTS(R.string.artists),
GENRES(R.string.genres),
PLAYLISTS(R.string.playlists),
- FAVORITES(R.string.favorites);
+ FAVORITES(R.string.favorites),
+ DOWNLOADS(R.string.downloads);
@StringRes
public final int title;
diff --git a/app/src/main/java/org/adrianvictor/geleia/model/Song.java b/app/src/main/java/org/adrianvictor/geleia/model/Song.java
index 277a5942..b9218cb3 100644
--- a/app/src/main/java/org/adrianvictor/geleia/model/Song.java
+++ b/app/src/main/java/org/adrianvictor/geleia/model/Song.java
@@ -67,10 +67,10 @@ public class Song implements Parcelable {
this.albumId = itemDto.getAlbumId();
this.albumName = itemDto.getAlbum();
- if (itemDto.getArtistItems().size() != 0) {
+ if (!itemDto.getArtistItems().isEmpty()) {
this.artistId = itemDto.getArtistItems().get(0).getId();
this.artistName = itemDto.getArtistItems().get(0).getName();
- } else if (itemDto.getAlbumArtists().size() != 0) {
+ } else if (!itemDto.getAlbumArtists().isEmpty()) {
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().size() != 0) {
+ if (source.getMediaStreams() != null && !source.getMediaStreams().isEmpty()) {
MediaStream stream = source.getMediaStreams().get(0);
this.codec = stream.getCodec();
diff --git a/app/src/main/java/org/adrianvictor/geleia/service/DownloadService.java b/app/src/main/java/org/adrianvictor/geleia/service/DownloadService.java
index b3a61b5f..7cb72766 100644
--- a/app/src/main/java/org/adrianvictor/geleia/service/DownloadService.java
+++ b/app/src/main/java/org/adrianvictor/geleia/service/DownloadService.java
@@ -4,6 +4,8 @@ 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;
@@ -11,6 +13,7 @@ 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;
@@ -23,8 +26,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";
@@ -33,6 +36,8 @@ public class DownloadService extends Service {
private ExecutorService executor;
private DownloadNotification notification;
+ private static final Object lock = new Object();
+
@Override
public void onCreate() {
super.onCreate();
@@ -55,6 +60,7 @@ public class DownloadService extends Service {
break;
case DownloadService.ACTION_START:
List songs = intent.getParcelableArrayListExtra(EXTRA_SONGS);
+ assert songs != null;
for (Song song : songs) {
download(song);
notification.start(song);
@@ -78,19 +84,24 @@ public class DownloadService extends Service {
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
DocumentFile root;
- if (location.equals(getApplicationContext().getCacheDir().toString())) {
- root = DocumentFile.fromFile(new File(location));
- } else {
+ if (location.startsWith("content://")) {
root = DocumentFile.fromTreeUri(this, Uri.parse(location));
+ } else {
+ root = DocumentFile.fromFile(new File(location));
}
- DocumentFile artist = root.findFile(MusicUtil.ascii(song.artistName));
- if (artist == null) {
- artist = root.createDirectory(MusicUtil.ascii(song.artistName));
- }
- DocumentFile album = artist.findFile(MusicUtil.ascii(song.albumName));
- if (album == null) {
- album = artist.createDirectory(MusicUtil.ascii(song.albumName));
+ DocumentFile artist;
+ DocumentFile album;
+
+ synchronized (lock) {
+ artist = root.findFile(MusicUtil.ascii(song.artistName));
+ if (artist == null) {
+ artist = root.createDirectory(MusicUtil.ascii(song.artistName));
+ }
+ 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;
@@ -116,7 +127,8 @@ public class DownloadService extends Service {
App.getDatabase().cacheDao().insertCache(new Cache(song));
notification.stop(song);
} catch (Exception e) {
- e.printStackTrace();
+ Log.e(TAG, "Failed to download song: " + song.title, e);
+ ErrorNotification.show(this, e.getMessage());
}
});
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/service/LoginService.java b/app/src/main/java/org/adrianvictor/geleia/service/LoginService.java
index 82f7a2a7..55db8664 100644
--- a/app/src/main/java/org/adrianvictor/geleia/service/LoginService.java
+++ b/app/src/main/java/org/adrianvictor/geleia/service/LoginService.java
@@ -46,9 +46,19 @@ 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() {
diff --git a/app/src/main/java/org/adrianvictor/geleia/service/notifications/DownloadNotification.java b/app/src/main/java/org/adrianvictor/geleia/service/notifications/DownloadNotification.java
index fe6c0efa..014eb570 100644
--- a/app/src/main/java/org/adrianvictor/geleia/service/notifications/DownloadNotification.java
+++ b/app/src/main/java/org/adrianvictor/geleia/service/notifications/DownloadNotification.java
@@ -1,114 +1,134 @@
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.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
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 = DownloadNotification.class.getSimpleName();
- private static final int NOTIFICATION_ID = 2;
-
+ private static final String CHANNEL_ID = "download_channel";
+ private final int ID = 2;
private final Context context;
private final NotificationManager notificationManager;
+ private final List queue = Collections.synchronizedList(new LinkedList<>());
- private final List songs;
-
- private int current;
- private int maximum;
+ private long totalSize;
+ private long downloadedSize;
+ private int lastPercentage = -1;
public DownloadNotification(Context context) {
- this.notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
this.context = context;
-
- this.songs = new ArrayList<>();
+ this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ createNotificationChannel();
}
- public synchronized void start(Song song) {
- this.songs.add(song);
-
+ private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- createNotificationChannel();
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ context.getString(R.string.download_channel_name),
+ NotificationManager.IMPORTANCE_LOW
+ );
+ notificationManager.createNotificationChannel(channel);
}
}
- public synchronized void update(int current, int maximum) {
- this.current += current;
- this.maximum += maximum;
-
- 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);
-
- Intent cancel = new Intent(context, DownloadService.class).setAction(DownloadService.ACTION_CANCEL);
- PendingIntent pendingCancel = PendingIntent.getService(context, 0, cancel, PendingIntent.FLAG_IMMUTABLE);
-
- NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
- for (Song item : songs.stream().limit(5).collect(Collectors.toList())) {
- style.addLine(item.title);
+ 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 void update(long downloaded, long total) {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
+ return; // No progress updates for KitKat
+ }
+
+ synchronized (this) {
+ totalSize += total;
+ downloadedSize += downloaded;
+
+ int percentage = 0;
+ if (totalSize > 0) {
+ percentage = (int) ((downloadedSize * 100) / totalSize);
+ }
+
+ 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)
- .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);
+ .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);
- notificationManager.notify(NOTIFICATION_ID, builder.build());
- }
+ String contentText = context.getResources().getQuantityString(R.plurals.downloading_s_songs, queue.size(), queue.size());
- public synchronized void stop(Song song) {
- if (song != null) {
- songs.remove(song);
- } else {
- songs.clear();
+ 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);
}
- 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);
- }
+ return builder.build();
}
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/service/notifications/ErrorNotification.java b/app/src/main/java/org/adrianvictor/geleia/service/notifications/ErrorNotification.java
new file mode 100644
index 00000000..793a9db7
--- /dev/null
+++ b/app/src/main/java/org/adrianvictor/geleia/service/notifications/ErrorNotification.java
@@ -0,0 +1,49 @@
+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);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/adrianvictor/geleia/service/playback/LocalPlayer.java b/app/src/main/java/org/adrianvictor/geleia/service/playback/LocalPlayer.java
index 0a63184d..2832d553 100644
--- a/app/src/main/java/org/adrianvictor/geleia/service/playback/LocalPlayer.java
+++ b/app/src/main/java/org/adrianvictor/geleia/service/playback/LocalPlayer.java
@@ -2,6 +2,7 @@ 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;
@@ -141,11 +142,17 @@ public class LocalPlayer implements Playback {
private List createMediaItems(List queue) {
return queue.stream().map(song -> {
- File audio = new File(MusicUtil.getFileUri(song));
- Uri uri = Uri.fromFile(audio);
+ 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);
- if (!audio.exists()) {
- uri = Uri.parse(MusicUtil.getTranscodeUri(song));
+ if (!audio.exists()) {
+ uri = Uri.parse(MusicUtil.getTranscodeUri(song));
+ }
}
List containers = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream()
diff --git a/app/src/main/java/org/adrianvictor/geleia/util/ImageUtil.java b/app/src/main/java/org/adrianvictor/geleia/util/ImageUtil.java
index 1388f345..de326d91 100644
--- a/app/src/main/java/org/adrianvictor/geleia/util/ImageUtil.java
+++ b/app/src/main/java/org/adrianvictor/geleia/util/ImageUtil.java
@@ -14,6 +14,7 @@ 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;
@@ -91,18 +92,22 @@ public class ImageUtil {
}
public static Drawable getTintedVectorDrawable(@NonNull Context context, @DrawableRes int resId, @ColorInt int color) {
- Drawable drawable = getVectorDrawable(context.getResources(), resId, context.getTheme());
+ final Drawable drawable = AppCompatResources.getDrawable(context, resId);
- DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_IN);
- DrawableCompat.setTint(drawable, color);
+ if (drawable != null) {
+ Drawable wrappedDrawable = DrawableCompat.wrap(drawable).mutate();
- return drawable;
+ DrawableCompat.setTintMode(wrappedDrawable, PorterDuff.Mode.SRC_IN);
+ DrawableCompat.setTint(wrappedDrawable, color);
+ return wrappedDrawable;
+ }
+ return null;
}
+
public static Drawable getVectorDrawable(@NonNull Context context, @DrawableRes int resId) {
- return getVectorDrawable(context.getResources(), resId, context.getTheme());
+ return AppCompatResources.getDrawable(context, resId);
}
-
public static Drawable resolveDrawable(@NonNull Context context, @AttrRes int drawableAttr) {
TypedArray a = context.obtainStyledAttributes(new int[]{drawableAttr});
Drawable drawable = a.getDrawable(0);
diff --git a/app/src/main/java/org/adrianvictor/geleia/util/MusicUtil.java b/app/src/main/java/org/adrianvictor/geleia/util/MusicUtil.java
index 8831fe38..8d62cbbf 100644
--- a/app/src/main/java/org/adrianvictor/geleia/util/MusicUtil.java
+++ b/app/src/main/java/org/adrianvictor/geleia/util/MusicUtil.java
@@ -3,6 +3,7 @@ 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;
@@ -18,6 +19,7 @@ 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;
@@ -49,7 +51,7 @@ public class MusicUtil {
List codecs = preferenceUtil.getDirectPlayCodecs();
Stream values = codecs.stream().map(codec -> codec.value);
- if (codecs.size() != 0) {
+ if (!codecs.isEmpty()) {
builder.append("&Container=").append(values.collect(Collectors.joining(",")));
}
@@ -81,7 +83,8 @@ public class MusicUtil {
public static String getFileUri(Song song) {
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
File root = new File(location, "music");
- if (!location.equals(App.getInstance().getCacheDir().toString())) {
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !location.equals(App.getInstance().getCacheDir().toString())) {
Uri uri = Uri.parse(location);
return new File(uri.getPath()).getAbsolutePath();
}
@@ -102,7 +105,7 @@ public class MusicUtil {
try {
return new Intent();
} catch (IllegalArgumentException e) {
- e.printStackTrace();
+ ErrorNotification.show(context, e.getMessage());
Toast.makeText(context, R.string.error_share_file, Toast.LENGTH_SHORT).show();
return new Intent();
}
@@ -110,7 +113,7 @@ public class MusicUtil {
@NonNull
public static String getArtistInfoString(@NonNull final Context context, @NonNull final Artist artist) {
- return artist.genres.size() != 0 ? artist.genres.get(0).name : "";
+ return !artist.genres.isEmpty() ? artist.genres.get(0).name : "";
}
@NonNull
diff --git a/app/src/main/java/org/adrianvictor/geleia/util/NavigationUtil.java b/app/src/main/java/org/adrianvictor/geleia/util/NavigationUtil.java
index 924c47ba..30460c35 100644
--- a/app/src/main/java/org/adrianvictor/geleia/util/NavigationUtil.java
+++ b/app/src/main/java/org/adrianvictor/geleia/util/NavigationUtil.java
@@ -12,6 +12,7 @@ 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;
@@ -61,6 +62,13 @@ 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);
diff --git a/app/src/main/java/org/adrianvictor/geleia/util/PlaylistUtil.java b/app/src/main/java/org/adrianvictor/geleia/util/PlaylistUtil.java
index 9144b16a..731724ea 100644
--- a/app/src/main/java/org/adrianvictor/geleia/util/PlaylistUtil.java
+++ b/app/src/main/java/org/adrianvictor/geleia/util/PlaylistUtil.java
@@ -48,7 +48,7 @@ public class PlaylistUtil {
PlaylistCreationRequest request = new PlaylistCreationRequest();
request.setUserId(App.getApiClient().getCurrentUserId());
request.setName(name);
- if (ids.size() != 0) request.setItemIdList(ids);
+ if (!ids.isEmpty()) request.setItemIdList(ids);
App.getApiClient().CreatePlaylist(request, new Response<>());
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/util/PreferenceUtil.java b/app/src/main/java/org/adrianvictor/geleia/util/PreferenceUtil.java
index 1becdcb9..71ff53d5 100644
--- a/app/src/main/java/org/adrianvictor/geleia/util/PreferenceUtil.java
+++ b/app/src/main/java/org/adrianvictor/geleia/util/PreferenceUtil.java
@@ -448,7 +448,6 @@ public final class PreferenceUtil {
}).collect(Collectors.toList());
}
- @SuppressWarnings("SimplifyStreamApiCallChains")
public void setCategories(List categories) {
List values = categories.stream().map(category -> {
return category.select ? category.toString() : category.toString().toLowerCase();
diff --git a/app/src/main/java/org/adrianvictor/geleia/views/IconImageView.java b/app/src/main/java/org/adrianvictor/geleia/views/IconImageView.java
index 557797cf..6af3adc6 100644
--- a/app/src/main/java/org/adrianvictor/geleia/views/IconImageView.java
+++ b/app/src/main/java/org/adrianvictor/geleia/views/IconImageView.java
@@ -10,23 +10,27 @@ 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);
}
- private void init(Context context) {
- if (context == null) return;
- setColorFilter(ThemeUtil.getColorResource(context, R.attr.iconColor), PorterDuff.Mode.SRC_IN);
+ @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);
}
}
diff --git a/app/src/main/java/org/adrianvictor/geleia/views/shortcuts/DynamicShortcutManager.java b/app/src/main/java/org/adrianvictor/geleia/views/shortcuts/DynamicShortcutManager.java
index ee889cd8..c45ab175 100644
--- a/app/src/main/java/org/adrianvictor/geleia/views/shortcuts/DynamicShortcutManager.java
+++ b/app/src/main/java/org/adrianvictor/geleia/views/shortcuts/DynamicShortcutManager.java
@@ -36,7 +36,7 @@ public class DynamicShortcutManager {
}
public void initDynamicShortcuts() {
- if (shortcutManager.getDynamicShortcuts().size() == 0) {
+ if (shortcutManager.getDynamicShortcuts().isEmpty()) {
shortcutManager.setDynamicShortcuts(getDefaultShortcuts());
}
}
diff --git a/app/src/main/res/drawable/ic_launcher_nodpi.xml b/app/src/main/res/drawable/ic_launcher_nodpi.xml
index 1361edfa..a8b409b1 100644
--- a/app/src/main/res/drawable/ic_launcher_nodpi.xml
+++ b/app/src/main/res/drawable/ic_launcher_nodpi.xml
@@ -1,14 +1,4 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml
index a807a132..61ff6cc1 100644
--- a/app/src/main/res/layout/activity_login.xml
+++ b/app/src/main/res/layout/activity_login.xml
@@ -19,134 +19,122 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_unreachable.xml b/app/src/main/res/layout/activity_unreachable.xml
new file mode 100644
index 00000000..ddc6f028
--- /dev/null
+++ b/app/src/main/res/layout/activity_unreachable.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/card_about_app.xml b/app/src/main/res/layout/card_about_app.xml
index 9bc04637..59e36ce1 100644
--- a/app/src/main/res/layout/card_about_app.xml
+++ b/app/src/main/res/layout/card_about_app.xml
@@ -82,6 +82,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/card_server.xml b/app/src/main/res/layout/card_server.xml
index 09819218..bb7fcde2 100644
--- a/app/src/main/res/layout/card_server.xml
+++ b/app/src/main/res/layout/card_server.xml
@@ -5,7 +5,6 @@
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="20dp"
- android:background="@drawable/card_server"
android:elevation="4dp"
tools:ignore="UnusedAttribute">
diff --git a/app/src/main/res/layout/card_special_thanks.xml b/app/src/main/res/layout/card_special_thanks.xml
index d99e26e6..2d21f38a 100644
--- a/app/src/main/res/layout/card_special_thanks.xml
+++ b/app/src/main/res/layout/card_special_thanks.xml
@@ -27,7 +27,8 @@
+ android:layout_marginBottom="10dp"
+ android:background="?attr/dividerColor"/>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_offline.xml b/app/src/main/res/layout/fragment_offline.xml
new file mode 100644
index 00000000..51d88627
--- /dev/null
+++ b/app/src/main/res/layout/fragment_offline.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/notification_download.xml b/app/src/main/res/layout/notification_download.xml
new file mode 100644
index 00000000..3b2740bf
--- /dev/null
+++ b/app/src/main/res/layout/notification_download.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 036d09bc..00000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index c455e356..2db960bf 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 1d5f0a3b..f995d681 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index e8c79820..81b597fe 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index b00f5a28..e258b0af 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index f9b1b953..fc01141f 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6b3353bf..235eb77f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -235,5 +235,22 @@
Please fill in your username.
Forking Phonograph and making Gelli
Adrian Victor
+ An error occurred in Jamfish.
+ You are offline.
+ Change server
+ Oops! I did it again...
+ ;(
+ Sorry, but we couldn\'t reach this server right now.
+ License
+ GNU General Public License v3.0
+ GNU General Public License v3.0
+
+ Downloads
+ Downloading songs
+ Downloads
+
+ - Downloading %d song
+ - Downloading %d songs
+
diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt
index 62c58c1e..2a3d80a1 100644
--- a/metadata/en-US/full_description.txt
+++ b/metadata/en-US/full_description.txt
@@ -1 +1,16 @@
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
\ No newline at end of file
diff --git a/metadata/en-US/icon.png b/metadata/en-US/images/icon.png
similarity index 100%
rename from metadata/en-US/icon.png
rename to metadata/en-US/images/icon.png
diff --git a/metadata/en-US/phoneScreenshots/1.png b/metadata/en-US/images/phoneScreenshots/1.png
similarity index 100%
rename from metadata/en-US/phoneScreenshots/1.png
rename to metadata/en-US/images/phoneScreenshots/1.png
diff --git a/metadata/en-US/phoneScreenshots/2.png b/metadata/en-US/images/phoneScreenshots/2.png
similarity index 100%
rename from metadata/en-US/phoneScreenshots/2.png
rename to metadata/en-US/images/phoneScreenshots/2.png
diff --git a/metadata/en-US/phoneScreenshots/3.png b/metadata/en-US/images/phoneScreenshots/3.png
similarity index 100%
rename from metadata/en-US/phoneScreenshots/3.png
rename to metadata/en-US/images/phoneScreenshots/3.png
diff --git a/metadata/en-US/phoneScreenshots/4.png b/metadata/en-US/images/phoneScreenshots/4.png
similarity index 100%
rename from metadata/en-US/phoneScreenshots/4.png
rename to metadata/en-US/images/phoneScreenshots/4.png
diff --git a/metadata/en-US/phoneScreenshots/5.png b/metadata/en-US/images/phoneScreenshots/5.png
similarity index 100%
rename from metadata/en-US/phoneScreenshots/5.png
rename to metadata/en-US/images/phoneScreenshots/5.png