diff --git a/app/build.gradle b/app/build.gradle index 139657c0..7c9a95a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 28 - buildToolsVersion '28.0.3' + compileSdkVersion 29 + buildToolsVersion '29.0.2' defaultConfig { minSdkVersion 19 diff --git a/app/src/main/java/com/dkanada/gramophone/adapter/DirectplayCodecAdapter.java b/app/src/main/java/com/dkanada/gramophone/adapter/DirectplayCodecAdapter.java new file mode 100644 index 00000000..9806470a --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/adapter/DirectplayCodecAdapter.java @@ -0,0 +1,66 @@ +package com.dkanada.gramophone.adapter; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.dkanada.gramophone.R; +import com.dkanada.gramophone.model.DirectplayCodec; + +import java.util.List; + +public class DirectplayCodecAdapter extends RecyclerView.Adapter /*implements SwipeAndDragHelper.ActionCompletionContract*/ { + private List directplayCodecs; + + public DirectplayCodecAdapter(List directplayCodecs) { + this.directplayCodecs = directplayCodecs; + } + + @Override + @NonNull + public DirectplayCodecAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.preference_dialog_directplay_codecs_listitem, parent, false); + return new ViewHolder(view); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public void onBindViewHolder(@NonNull DirectplayCodecAdapter.ViewHolder holder, int position) { + DirectplayCodec directplayCodec = directplayCodecs.get(position); + + holder.checkBox.setChecked(directplayCodec.selected); + holder.title.setText(directplayCodec.title); + + holder.itemView.setOnClickListener(v -> { + directplayCodec.selected = !directplayCodec.selected; + holder.checkBox.setChecked(directplayCodec.selected); + }); + } + + @Override + public int getItemCount() { + return directplayCodecs.size(); + } + + public List getDirectplayCodecs() { + return directplayCodecs; + } + + static class ViewHolder extends RecyclerView.ViewHolder { + public CheckBox checkBox; + public TextView title; + + public ViewHolder(View view) { + super(view); + checkBox = view.findViewById(R.id.checkbox); + title = view.findViewById(R.id.title); + } + } +} + diff --git a/app/src/main/java/com/dkanada/gramophone/model/DirectplayCodec.java b/app/src/main/java/com/dkanada/gramophone/model/DirectplayCodec.java new file mode 100644 index 00000000..c0e43186 --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/model/DirectplayCodec.java @@ -0,0 +1,32 @@ +package com.dkanada.gramophone.model; + +public class DirectplayCodec { + public String codecName; + public String title; + public String value; + public boolean selected; + + public DirectplayCodec(String codecName, String title, String value, boolean selected) { + this.codecName = codecName; + this.title = title; + this.value = value; + this.selected = selected; + } + + public enum Codec { + // These are all non-translatable so just keep them here. + FLAC("FLAC","flac|flac"), + MP3("MP3", "mp3|mp3"), + AAC("AAC (.m4a)", "m4a|aac"), + OPUS("OPUS (.mka)", "mka|opus"), + VORBIS("VORBIS (.ogg)", "ogg|vorbis"); + + public final String title; + public final String value; + + Codec(String title, String value) { + this.value = value; + this.title = title; + } + } +} diff --git a/app/src/main/java/com/dkanada/gramophone/preferences/DirectplayPreference.java b/app/src/main/java/com/dkanada/gramophone/preferences/DirectplayPreference.java new file mode 100644 index 00000000..17ac990a --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/preferences/DirectplayPreference.java @@ -0,0 +1,24 @@ +package com.dkanada.gramophone.preferences; + +import android.content.Context; +import android.util.AttributeSet; + +import com.kabouzeid.appthemehelper.common.prefs.supportv7.ATEDialogPreference; + +public class DirectplayPreference extends ATEDialogPreference { + public DirectplayPreference(Context context) { + super(context); + } + + public DirectplayPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DirectplayPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public DirectplayPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } +} diff --git a/app/src/main/java/com/dkanada/gramophone/preferences/DirectplayPreferenceDialog.java b/app/src/main/java/com/dkanada/gramophone/preferences/DirectplayPreferenceDialog.java new file mode 100644 index 00000000..a82388ec --- /dev/null +++ b/app/src/main/java/com/dkanada/gramophone/preferences/DirectplayPreferenceDialog.java @@ -0,0 +1,48 @@ +package com.dkanada.gramophone.preferences; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.afollestad.materialdialogs.MaterialDialog; +import com.dkanada.gramophone.R; +import com.dkanada.gramophone.adapter.DirectplayCodecAdapter; +import com.dkanada.gramophone.util.PreferenceUtil; + +public class DirectplayPreferenceDialog extends DialogFragment { + public static DirectplayPreferenceDialog newInstance() { + return new DirectplayPreferenceDialog(); + } + + private DirectplayCodecAdapter adapter; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + View view = getActivity().getLayoutInflater().inflate(R.layout.preference_dialog_directplay_codecs, null); + + adapter = new DirectplayCodecAdapter(PreferenceUtil.getInstance(getContext()).getDirectplayCodecs()); + + RecyclerView recyclerView = view.findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + recyclerView.setAdapter(adapter); + + return new MaterialDialog.Builder(getContext()) + .title(R.string.directplay_codecs) + .customView(view, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .autoDismiss(false) + .onNegative((dialog, action) -> dismiss()) + .onPositive((dialog, action) -> { + PreferenceUtil.getInstance(getContext()).setDirectplayCodecs(adapter.getDirectplayCodecs()); + dismiss(); + }) + .build(); + } +} diff --git a/app/src/main/java/com/dkanada/gramophone/ui/activities/SettingsActivity.java b/app/src/main/java/com/dkanada/gramophone/ui/activities/SettingsActivity.java index f9c00b6f..a80502b5 100644 --- a/app/src/main/java/com/dkanada/gramophone/ui/activities/SettingsActivity.java +++ b/app/src/main/java/com/dkanada/gramophone/ui/activities/SettingsActivity.java @@ -16,6 +16,8 @@ import android.view.MenuItem; import android.view.View; import com.afollestad.materialdialogs.color.ColorChooserDialog; +import com.dkanada.gramophone.preferences.DirectplayPreference; +import com.dkanada.gramophone.preferences.DirectplayPreferenceDialog; import com.kabouzeid.appthemehelper.ThemeStore; import com.kabouzeid.appthemehelper.common.prefs.supportv7.ATEColorPreference; import com.kabouzeid.appthemehelper.common.prefs.supportv7.ATEPreferenceFragmentCompat; @@ -122,6 +124,7 @@ public class SettingsActivity extends AbsBaseActivity implements ColorChooserDia addPreferencesFromResource(R.xml.pref_lockscreen); addPreferencesFromResource(R.xml.pref_audio); addPreferencesFromResource(R.xml.pref_images); + addPreferencesFromResource(R.xml.pref_directplay); } @Nullable @@ -131,6 +134,8 @@ public class SettingsActivity extends AbsBaseActivity implements ColorChooserDia return NowPlayingScreenPreferenceDialog.newInstance(); } else if (preference instanceof LibraryPreference) { return LibraryPreferenceDialog.newInstance(); + } else if (preference instanceof DirectplayPreference) { + return DirectplayPreferenceDialog.newInstance(); } return super.onCreatePreferenceDialog(preference); diff --git a/app/src/main/java/com/dkanada/gramophone/util/MusicUtil.java b/app/src/main/java/com/dkanada/gramophone/util/MusicUtil.java index e5d0068b..6fbce92b 100644 --- a/app/src/main/java/com/dkanada/gramophone/util/MusicUtil.java +++ b/app/src/main/java/com/dkanada/gramophone/util/MusicUtil.java @@ -14,6 +14,7 @@ import com.dkanada.gramophone.App; import com.dkanada.gramophone.R; import com.dkanada.gramophone.model.Album; import com.dkanada.gramophone.model.Artist; +import com.dkanada.gramophone.model.DirectplayCodec; import com.dkanada.gramophone.model.Genre; import com.dkanada.gramophone.model.Song; @@ -26,27 +27,42 @@ import java.util.Locale; public class MusicUtil { public static Uri getSongFileUri(Song song) { - StringBuilder builder = new StringBuilder(); + PreferenceUtil preferenceUtil = PreferenceUtil.getInstance(App.getInstance()); + + StringBuilder builder = new StringBuilder(256); ApiClient apiClient = App.getApiClient(); builder.append(apiClient.getApiUrl()); builder.append("/Audio/"); builder.append(song.id); builder.append("/universal"); - builder.append("?UserId=" + apiClient.getCurrentUserId()); - builder.append("&DeviceId=" + apiClient.getDeviceId()); + builder.append("?UserId=").append(apiClient.getCurrentUserId()); + builder.append("&DeviceId=").append(apiClient.getDeviceId()); // web client maximum is 12444445 and 320kbps is 320000 - builder.append("&MaxStreamingBitrate=" + PreferenceUtil.getInstance(App.getInstance()).getMaximumBitrate()); - - builder.append("&Container=mka|opus,mp3|mp3,m4a|aac,ogg|vorbis,flac|flac"); + builder.append("&MaxStreamingBitrate=").append(preferenceUtil.getMaximumBitrate()); + + boolean containerAdded = false; + for (DirectplayCodec directplayCodec : preferenceUtil.getDirectplayCodecs()){ + if (directplayCodec.selected){ + if (!containerAdded){ + builder.append("&Container="); + containerAdded = true; + } + builder.append(directplayCodec.value).append(','); + } + } + if (containerAdded){ + // Remove last comma + builder.deleteCharAt(builder.length() - 1); + } builder.append("&TranscodingContainer=ts"); builder.append("&TranscodingProtocol=hls"); // preferred codec when transcoding - builder.append("&AudioCodec=" + PreferenceUtil.getInstance(App.getInstance()).getTranscodeCodec()); - builder.append("&api_key=" + apiClient.getAccessToken()); + builder.append("&AudioCodec=").append(preferenceUtil.getTranscodeCodec()); + builder.append("&api_key=").append(apiClient.getAccessToken()); Log.i(MusicUtil.class.getName(), "playing audio: " + builder); return Uri.parse(builder.toString()); diff --git a/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java b/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java index 9ba7d58b..a2576325 100644 --- a/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java +++ b/app/src/main/java/com/dkanada/gramophone/util/PreferenceUtil.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; + import androidx.annotation.StyleRes; import com.google.gson.Gson; @@ -14,14 +15,18 @@ import com.dkanada.gramophone.R; import com.dkanada.gramophone.helper.sort.SortMethod; import com.dkanada.gramophone.helper.sort.SortOrder; import com.dkanada.gramophone.model.CategoryInfo; +import com.dkanada.gramophone.model.DirectplayCodec; import com.dkanada.gramophone.ui.fragments.player.NowPlayingScreen; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; public final class PreferenceUtil { public static final String CATEGORIES = "library_categories"; + public static final String DIRECTPLAY_CODECS = "directplay_codecs"; public static final String MAXIMUM_LIST_SIZE = "maximum_list_size"; public static final String REMEMBER_LAST_TAB = "remember_last_tab"; public static final String LAST_TAB = "last_tab"; @@ -417,4 +422,38 @@ public final class PreferenceUtil { defaultCategories.add(new CategoryInfo(CategoryInfo.Category.PLAYLISTS, true)); return defaultCategories; } + + public List getDirectplayCodecs() { + DirectplayCodec.Codec[] codecs = DirectplayCodec.Codec.values(); + + Set selectedCodecNames = new HashSet<>(); + for (DirectplayCodec.Codec codec : codecs){ + selectedCodecNames.add(codec.name()); + } + + selectedCodecNames = mPreferences.getStringSet(DIRECTPLAY_CODECS, selectedCodecNames); + + ArrayList directplayCodecs = new ArrayList<>(); + for (DirectplayCodec.Codec codec : codecs){ + String name = codec.name(); + boolean selected = selectedCodecNames.contains(name); + + directplayCodecs.add(new DirectplayCodec(name, codec.title, codec.value, selected)); + } + + return directplayCodecs; + } + + public void setDirectplayCodecs(List directplayCodecs){ + Set codecNames = new HashSet<>(); + for (DirectplayCodec directplayCodec : directplayCodecs){ + if (directplayCodec.selected){ + codecNames.add(directplayCodec.codecName); + } + } + + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putStringSet(DIRECTPLAY_CODECS, codecNames); + editor.apply(); + } } diff --git a/app/src/main/res/layout/preference_dialog_directplay_codecs.xml b/app/src/main/res/layout/preference_dialog_directplay_codecs.xml new file mode 100644 index 00000000..e3f54417 --- /dev/null +++ b/app/src/main/res/layout/preference_dialog_directplay_codecs.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/layout/preference_dialog_directplay_codecs_listitem.xml b/app/src/main/res/layout/preference_dialog_directplay_codecs_listitem.xml new file mode 100644 index 00000000..436681e2 --- /dev/null +++ b/app/src/main/res/layout/preference_dialog_directplay_codecs_listitem.xml @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f0ffd314..3d827a34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,11 +93,13 @@ Colors Now Playing Audio + Directplay Images Library Lockscreen Notification + Directplay Codecs Categories Primary Color Accent Color @@ -135,6 +137,7 @@ Colors the app shortcuts in the primary color. Go to the last opened tab on launch. Configure visibility and order of display categories. + Disable directplay codecs to force transcoding. Delete Remove diff --git a/app/src/main/res/xml/pref_directplay.xml b/app/src/main/res/xml/pref_directplay.xml new file mode 100644 index 00000000..2ac7fa80 --- /dev/null +++ b/app/src/main/res/xml/pref_directplay.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index bdfedd59..1d4b35e0 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools:r8:1.6.84' - classpath 'com.android.tools.build:gradle:4.0.0' + classpath 'com.android.tools.build:gradle:4.0.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' } }