diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/10.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/10.json index e039aede..ac052d03 100644 --- a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/10.json +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/10.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 10, - "identityHash": "58cb958cdb09f054c27673d1de7f26d0", + "identityHash": "2b970b61706696dcd6c142bf22c89d37", "entities": [ { "tableName": "queue", @@ -230,7 +230,7 @@ }, { "tableName": "server", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, `custom_headers` TEXT, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "serverId", @@ -280,6 +280,12 @@ "affinity": "INTEGER", "notNull": true, "defaultValue": "false" + }, + { + "fieldPath": "customHeaders", + "columnName": "custom_headers", + "affinity": "TEXT", + "notNull": false } ], "primaryKey": { @@ -1059,7 +1065,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '58cb958cdb09f054c27673d1de7f26d0')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2b970b61706696dcd6c142bf22c89d37')" ] } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 986481bd..f432a391 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -75,4 +75,4 @@ android:value="true" /> - \ No newline at end of file + diff --git a/app/src/main/java/com/cappielloantonio/tempo/App.java b/app/src/main/java/com/cappielloantonio/tempo/App.java index 40105eea..39bace0e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/App.java +++ b/app/src/main/java/com/cappielloantonio/tempo/App.java @@ -13,6 +13,8 @@ import com.cappielloantonio.tempo.subsonic.SubsonicPreferences; import com.cappielloantonio.tempo.util.Preferences; +import java.util.Map; + public class App extends Application { private static App instance; private static Context context; @@ -98,11 +100,13 @@ private static SubsonicPreferences getSubsonicPreferences() { String token = Preferences.getToken(); String salt = Preferences.getSalt(); boolean isLowSecurity = Preferences.isLowScurity(); + Map customHeaders = Preferences.getCustomHeaders(); SubsonicPreferences preferences = new SubsonicPreferences(); preferences.setServerUrl(server); preferences.setUsername(username); preferences.setAuthentication(password, token, salt, isLowSecurity); + preferences.setCustomHeaders(customHeaders); return preferences; } diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java index ed37106b..15402d8a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java +++ b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java @@ -9,6 +9,7 @@ import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.database.converter.DateConverters; +import com.cappielloantonio.tempo.database.converter.MapTypeConverter; import com.cappielloantonio.tempo.database.dao.ChronologyDao; import com.cappielloantonio.tempo.database.dao.DownloadDao; import com.cappielloantonio.tempo.database.dao.FavoriteDao; @@ -32,7 +33,7 @@ entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class}, autoMigrations = {@AutoMigration(from = 9, to = 10)} ) -@TypeConverters({DateConverters.class}) +@TypeConverters({DateConverters.class, MapTypeConverter.class}) public abstract class AppDatabase extends RoomDatabase { private final static String DB_NAME = "tempo_db"; private static AppDatabase instance; diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/converter/MapTypeConverter.kt b/app/src/main/java/com/cappielloantonio/tempo/database/converter/MapTypeConverter.kt new file mode 100644 index 00000000..f9bf6b06 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/converter/MapTypeConverter.kt @@ -0,0 +1,31 @@ +package com.cappielloantonio.tempo.database.converter + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +object MapTypeConverter { + + @TypeConverter + @JvmStatic + fun stringToMap(value: String?): Map { + return if (value.isNullOrEmpty()) { + emptyMap() + } else { + try { + Gson().fromJson(value, object : TypeToken>() {}.type) + ?: emptyMap() + } catch (e: Exception) { + emptyMap() + } + } + } + + + @TypeConverter + @JvmStatic + fun mapToString(value: Map?): String { + return value?.let { Gson().toJson(it) } ?: "" + } +} + diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Server.kt b/app/src/main/java/com/cappielloantonio/tempo/model/Server.kt index 78bfa6ee..d67fcc4e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/model/Server.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/model/Server.kt @@ -35,5 +35,8 @@ data class Server( val timestamp: Long, @ColumnInfo(name = "low_security", defaultValue = "false") - val isLowSecurity: Boolean + val isLowSecurity: Boolean, + + @ColumnInfo(name = "custom_headers") + var customHeaders: Map? = null, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt index f05238ce..94ba1337 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt @@ -4,6 +4,7 @@ import com.cappielloantonio.tempo.App import com.cappielloantonio.tempo.subsonic.utils.CacheUtil import com.google.gson.GsonBuilder import okhttp3.Cache +import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit @@ -12,14 +13,15 @@ import java.util.concurrent.TimeUnit class RetrofitClient(subsonic: Subsonic) { var retrofit: Retrofit + var customHeaders: Map = subsonic.customHeaders; - init { - retrofit = Retrofit.Builder() - .baseUrl(subsonic.url) - .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create())) - .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create())) - .client(getOkHttpClient()) - .build() + init {; + retrofit = Retrofit.Builder().baseUrl(subsonic.url).addConverterFactory( + GsonConverterFactory.create( + GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create() + ) + ).addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create())) + .client(getOkHttpClient()).build() } private fun getOkHttpClient(): OkHttpClient { @@ -35,16 +37,18 @@ class RetrofitClient(subsonic: Subsonic) { // SystemClient 60 // AlbumSongListClient 60 - return OkHttpClient.Builder() - .callTimeout(2, TimeUnit.MINUTES) - .connectTimeout(20, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .addInterceptor(getHttpLoggingInterceptor()) + val builder = OkHttpClient.Builder().callTimeout(2, TimeUnit.MINUTES) + .connectTimeout(20, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS).addInterceptor(getHttpLoggingInterceptor()) .addInterceptor(cacheUtil.offlineInterceptor) // .addNetworkInterceptor(cacheUtil.onlineInterceptor) .cache(getCache()) - .build() + + if (customHeaders.isNotEmpty()) { + builder.addNetworkInterceptor(getCustomHeadersInterceptor()) + } + + return builder.build() } private fun getHttpLoggingInterceptor(): HttpLoggingInterceptor { @@ -53,6 +57,21 @@ class RetrofitClient(subsonic: Subsonic) { return loggingInterceptor } + private fun getCustomHeadersInterceptor(): Interceptor { + return Interceptor { chain -> + val original = chain.request() + val requestBuilder = original.newBuilder() + + customHeaders.forEach { (key, value) -> + requestBuilder.addHeader(key, value) + } + + val request = requestBuilder.build() + chain.proceed(request) + } + } + + private fun getCache(): Cache { val cacheSize = 10 * 1024 * 1024 return Cache(App.getContext().cacheDir, cacheSize.toLong()) diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java index de4b36b7..d8b73896 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java @@ -142,6 +142,10 @@ public String getUrl() { return url.replace("//rest", "/rest"); } + public Map getCustomHeaders() { + return preferences.getCustomHeaders(); + } + public Map getParams() { Map params = new HashMap<>(); params.put("u", preferences.getUsername()); diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/SubsonicPreferences.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/SubsonicPreferences.java index 06fac35d..9521b442 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/SubsonicPreferences.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/SubsonicPreferences.java @@ -2,6 +2,8 @@ import com.cappielloantonio.tempo.subsonic.utils.StringUtil; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; public class SubsonicPreferences { @@ -9,6 +11,7 @@ public class SubsonicPreferences { private String username; private String clientName = "Tempo"; private SubsonicAuthentication authentication; + private Map customHeaders; public String getServerUrl() { return serverUrl; @@ -48,6 +51,19 @@ public void setAuthentication(String password, String token, String salt, boolea } } + public void setCustomHeaders(Map headers) { + if (headers != null && !headers.isEmpty()) { + this.customHeaders = headers; + } + } + + public Map getCustomHeaders() { + if (this.customHeaders != null) { + return this.customHeaders; + } + return new HashMap<>(); + } + public static class SubsonicAuthentication { private String password; private String salt; diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CustomHeadersAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CustomHeadersAdapter.java new file mode 100644 index 00000000..0667fbdc --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CustomHeadersAdapter.java @@ -0,0 +1,82 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.annotation.SuppressLint; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CustomHeadersAdapter extends RecyclerView.Adapter { + private final List> localCustomHeaders; + private final OnDeleteListener onDeleteListener; + + public interface OnDeleteListener { + void onDelete(String key); + } + + // ViewHolder class + public static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView headerDetailsView; + private final Button deleteButton; + + public ViewHolder(View view) { + super(view); + headerDetailsView = view.findViewById(R.id.custom_header_value_text_view); + deleteButton = view.findViewById(R.id.delete_icon); + } + + public TextView getTextView() { + return headerDetailsView; + } + } + + public CustomHeadersAdapter(Map data, OnDeleteListener listener) { + this.localCustomHeaders = new ArrayList<>(data.entrySet()); + this.onDeleteListener = listener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_row_custom_header_value, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Map.Entry entry = localCustomHeaders.get(position); + String key = entry.getKey(); + String value = entry.getValue(); + + holder.getTextView().setText(String.format("%s: %s", key, value)); + + holder.deleteButton.setOnClickListener(v -> { + if (onDeleteListener != null) { + this.onDeleteListener.onDelete(key); + } + }); + } + + @Override + public int getItemCount() { + return localCustomHeaders.size(); + } + + @SuppressLint("NotifyDataSetChanged") + public void updateData(Map newData) { + localCustomHeaders.clear(); + localCustomHeaders.addAll(newData.entrySet()); + + notifyDataSetChanged(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerSignupDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerSignupDialog.java index 93381d33..d06776ee 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerSignupDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerSignupDialog.java @@ -1,22 +1,31 @@ package com.cappielloantonio.tempo.ui.dialog; +import android.annotation.SuppressLint; import android.app.Dialog; import android.os.Bundle; import android.text.TextUtils; +import android.view.LayoutInflater; import android.view.View; +import android.widget.Button; +import android.widget.EditText; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.databinding.DialogServerSignupBinding; import com.cappielloantonio.tempo.model.Server; +import com.cappielloantonio.tempo.ui.adapter.CustomHeadersAdapter; import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.viewmodel.LoginViewModel; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -32,19 +41,28 @@ public class ServerSignupDialog extends DialogFragment { private String server; private String localAddress; private boolean lowSecurity = false; + private boolean allowCustomHeaders = false; + + private Map customHeaders = new HashMap<>(); + + CustomHeadersAdapter adapter = new CustomHeadersAdapter(customHeaders, this::deleteCustomHeaders); @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { bind = DialogServerSignupBinding.inflate(getLayoutInflater()); - loginViewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class); - return new MaterialAlertDialogBuilder(getActivity()) + bind.customHeadersRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + bind.customHeadersRecyclerView.setAdapter(adapter); + + return new AlertDialog.Builder(getActivity(), R.style.FullScreenDialog) .setView(bind.getRoot()) .setTitle(R.string.server_signup_dialog_title) - .setNeutralButton(R.string.server_signup_dialog_neutral_button, (dialog, id) -> { }) - .setPositiveButton(R.string.server_signup_dialog_positive_button, (dialog, id) -> { }) + .setNeutralButton(R.string.server_signup_dialog_neutral_button, (dialog, id) -> { + }) + .setPositiveButton(R.string.server_signup_dialog_positive_button, (dialog, id) -> { + }) .setNegativeButton(R.string.server_signup_dialog_negative_button, (dialog, id) -> dialog.cancel()) .create(); } @@ -74,12 +92,24 @@ private void setServerInfo() { bind.serverTextView.setText(loginViewModel.getServerToEdit().getAddress()); bind.localAddressTextView.setText(loginViewModel.getServerToEdit().getLocalAddress()); bind.lowSecurityCheckbox.setChecked(loginViewModel.getServerToEdit().isLowSecurity()); + bind.allowCustomHeadersCheckbox.setChecked(!Objects.requireNonNull(loginViewModel.getServerToEdit().getCustomHeaders()).isEmpty()); + + customHeaders = loginViewModel.getServerToEdit().getCustomHeaders(); + adapter.updateData(customHeaders); + + if (customHeaders != null && !customHeaders.isEmpty()) { + bind.customHeadersRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + + bind.customHeadersRecyclerView.setVisibility(View.VISIBLE); + bind.customHeaderAddButton.setVisibility(View.VISIBLE); + } } } else { loginViewModel.setServerToEdit(null); } } + @SuppressLint("NotifyDataSetChanged") private void setButtonAction() { androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog()); @@ -92,11 +122,26 @@ private void setButtonAction() { alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> Toast.makeText(requireContext(), R.string.server_signup_dialog_action_delete_toast, Toast.LENGTH_SHORT).show()); + Button customActionButton = bind.customHeaderAddButton; + customActionButton.setOnClickListener(v -> { + showCustomHeaderInputDialog(); + }); + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnLongClickListener(v -> { loginViewModel.deleteServer(null); Objects.requireNonNull(getDialog()).dismiss(); return true; }); + + bind.allowCustomHeadersCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + bind.customHeadersRecyclerView.setVisibility(View.VISIBLE); + bind.customHeaderAddButton.setVisibility(View.VISIBLE); + } else { + bind.customHeadersRecyclerView.setVisibility(View.GONE); + bind.customHeaderAddButton.setVisibility(View.GONE); + } + }); } private boolean validateInput() { @@ -106,6 +151,7 @@ private boolean validateInput() { server = bind.serverTextView.getText() != null && !bind.serverTextView.getText().toString().trim().isBlank() ? bind.serverTextView.getText().toString().trim() : null; localAddress = bind.localAddressTextView.getText() != null && !bind.localAddressTextView.getText().toString().trim().isBlank() ? bind.localAddressTextView.getText().toString().trim() : null; lowSecurity = bind.lowSecurityCheckbox.isChecked(); + allowCustomHeaders = bind.allowCustomHeadersCheckbox.isChecked(); if (TextUtils.isEmpty(serverName)) { bind.serverNameTextView.setError(getString(R.string.error_required)); @@ -135,8 +181,47 @@ private boolean validateInput() { return true; } + private void showCustomHeaderInputDialog() { + // Create a dialog for user input + View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_custom_header_input, null); + EditText keyInput = dialogView.findViewById(R.id.server_header_key_view); + EditText valueInput = dialogView.findViewById(R.id.server_header_value_view); + + new MaterialAlertDialogBuilder(getContext()) + .setView(dialogView) + .setPositiveButton("Add", (dialog, which) -> { + String headerKey = keyInput.getText().toString().trim(); + String headerValue = valueInput.getText().toString().trim(); + + // Validate inputs + if (!headerKey.isEmpty() && !headerValue.isEmpty()) { + customHeaders.put(headerKey, headerValue); + adapter.updateData(customHeaders); + } else { + Toast.makeText(requireContext(), getString(R.string.server_signup_dialog_hint_custom_values_required), Toast.LENGTH_SHORT).show(); + } + }) + .setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss()).create().show(); + } + + private void deleteCustomHeaders(String key) { + customHeaders.remove(key); + adapter.updateData(customHeaders); + Toast.makeText(getContext(), getString(R.string.server_signup_dialog_custom_header_delete_success), Toast.LENGTH_SHORT).show(); + } + private void saveServerPreference() { String serverID = loginViewModel.getServerToEdit() != null ? loginViewModel.getServerToEdit().getServerId() : UUID.randomUUID().toString(); - loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, this.localAddress, System.currentTimeMillis(), this.lowSecurity)); + loginViewModel.addServer(new Server( + serverID, + this.serverName, + this.username, + this.password, + this.server, + this.localAddress, + System.currentTimeMillis(), + this.lowSecurity, + allowCustomHeaders ? customHeaders : new HashMap<>() + )); } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java index d5bb0fae..fb05b77e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java @@ -30,6 +30,8 @@ import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.LoginViewModel; +import java.util.Map; + @UnstableApi public class LoginFragment extends Fragment implements ClickCallback { private static final String TAG = "LoginFragment"; @@ -117,7 +119,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { @Override public void onServerClick(Bundle bundle) { Server server = bundle.getParcelable("server_object"); - saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity()); + saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity(), server.getCustomHeaders()); SystemRepository systemRepository = new SystemRepository(); systemRepository.checkUserCredential(new SystemCallback() { @@ -142,13 +144,14 @@ public void onServerLongClick(Bundle bundle) { dialog.show(activity.getSupportFragmentManager(), null); } - private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity) { + private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity, Map customHeaders) { Preferences.setServerId(serverId); Preferences.setServer(server); Preferences.setLocalAddress(localAddress); Preferences.setUser(user); Preferences.setPassword(password); Preferences.setLowSecurity(isLowSecurity); + Preferences.setCustomHeaders(customHeaders); App.getSubsonicClientInstance(true); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index 5cbe9035..8da44dda 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -1,10 +1,10 @@ package com.cappielloantonio.tempo.util -import android.util.Log import com.cappielloantonio.tempo.App import com.cappielloantonio.tempo.model.HomeSector import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension import com.google.gson.Gson +import com.google.gson.reflect.TypeToken object Preferences { @@ -15,6 +15,7 @@ object Preferences { private const val TOKEN = "token" private const val SALT = "salt" private const val LOW_SECURITY = "low_security" + private const val CUSTOM_HEADERS = "custom_headers" private const val BATTERY_OPTIMIZATION = "battery_optimization" private const val SERVER_ID = "server_id" private const val OPEN_SUBSONIC = "open_subsonic" @@ -126,6 +127,22 @@ object Preferences { App.getInstance().preferences.edit().putBoolean(LOW_SECURITY, isLowSecurity).apply() } + @JvmStatic + fun getCustomHeaders(): Map? { + val json = App.getInstance().preferences.getString(CUSTOM_HEADERS, null) + return if (json != null) { + Gson().fromJson(json, object : TypeToken>() {}.type) + } else { + null + } + } + + @JvmStatic + fun setCustomHeaders(customHeaders: Map) { + val json = Gson().toJson(customHeaders) + App.getInstance().preferences.edit().putString(CUSTOM_HEADERS, json).apply() + } + @JvmStatic fun getServerId(): String? { return App.getInstance().preferences.getString(SERVER_ID, null) @@ -153,7 +170,8 @@ object Preferences { @JvmStatic fun setOpenSubsonicExtensions(extension: List) { - App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply() + App.getInstance().preferences.edit() + .putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply() } @JvmStatic @@ -180,20 +198,22 @@ object Preferences { @JvmStatic fun switchInUseServerAddress() { - val inUseAddress = if (getInUseServerAddress() == getServer()) getLocalAddress() else getServer() + val inUseAddress = + if (getInUseServerAddress() == getServer()) getLocalAddress() else getServer() App.getInstance().preferences.edit().putString(IN_USE_SERVER_ADDRESS, inUseAddress).apply() } @JvmStatic fun isServerSwitchable(): Boolean { return App.getInstance().preferences.getLong( - NEXT_SERVER_SWITCH, 0 + NEXT_SERVER_SWITCH, 0 ) + 15000 < System.currentTimeMillis() && !getServer().isNullOrEmpty() && !getLocalAddress().isNullOrEmpty() } @JvmStatic fun setServerSwitchableTimer() { - App.getInstance().preferences.edit().putLong(NEXT_SERVER_SWITCH, System.currentTimeMillis()).apply() + App.getInstance().preferences.edit().putLong(NEXT_SERVER_SWITCH, System.currentTimeMillis()) + .apply() } @JvmStatic @@ -274,7 +294,7 @@ object Preferences { @JvmStatic fun setDataSavingMode(isDataSavingModeEnabled: Boolean) { App.getInstance().preferences.edit().putBoolean(DATA_SAVING_MODE, isDataSavingModeEnabled) - .apply() + .apply() } @JvmStatic @@ -285,20 +305,21 @@ object Preferences { @JvmStatic fun setStarredSyncEnabled(isStarredSyncEnabled: Boolean) { App.getInstance().preferences.edit().putBoolean( - SYNC_STARRED_TRACKS_FOR_OFFLINE_USE, isStarredSyncEnabled + SYNC_STARRED_TRACKS_FOR_OFFLINE_USE, isStarredSyncEnabled ).apply() } @JvmStatic fun showServerUnreachableDialog(): Boolean { return App.getInstance().preferences.getLong( - SERVER_UNREACHABLE, 0 + SERVER_UNREACHABLE, 0 ) + 86400000 < System.currentTimeMillis() } @JvmStatic fun setServerUnreachableDatetime() { - App.getInstance().preferences.edit().putLong(SERVER_UNREACHABLE, System.currentTimeMillis()).apply() + App.getInstance().preferences.edit().putLong(SERVER_UNREACHABLE, System.currentTimeMillis()) + .apply() } @JvmStatic @@ -364,8 +385,8 @@ object Preferences { @JvmStatic fun setStreamingCacheStoragePreference(streamingCachePreference: Int) { return App.getInstance().preferences.edit().putString( - STREAMING_CACHE_STORAGE, - streamingCachePreference.toString() + STREAMING_CACHE_STORAGE, + streamingCachePreference.toString() ).apply() } @@ -377,24 +398,24 @@ object Preferences { @JvmStatic fun setDownloadStoragePreference(storagePreference: Int) { return App.getInstance().preferences.edit().putString( - DOWNLOAD_STORAGE, - storagePreference.toString() + DOWNLOAD_STORAGE, + storagePreference.toString() ).apply() } @JvmStatic fun getDefaultDownloadViewType(): String { return App.getInstance().preferences.getString( - DEFAULT_DOWNLOAD_VIEW_TYPE, - Constants.DOWNLOAD_TYPE_TRACK + DEFAULT_DOWNLOAD_VIEW_TYPE, + Constants.DOWNLOAD_TYPE_TRACK )!! } @JvmStatic fun setDefaultDownloadViewType(viewType: String) { return App.getInstance().preferences.edit().putString( - DEFAULT_DOWNLOAD_VIEW_TYPE, - viewType + DEFAULT_DOWNLOAD_VIEW_TYPE, + viewType ).apply() } @@ -460,7 +481,8 @@ object Preferences { @JvmStatic fun setHomeSectorList(extension: List?) { - App.getInstance().preferences.edit().putString(HOME_SECTOR_LIST, Gson().toJson(extension)).apply() + App.getInstance().preferences.edit().putString(HOME_SECTOR_LIST, Gson().toJson(extension)) + .apply() } @JvmStatic @@ -471,13 +493,14 @@ object Preferences { @JvmStatic fun showTempoUpdateDialog(): Boolean { return App.getInstance().preferences.getLong( - NEXT_UPDATE_CHECK, 0 + NEXT_UPDATE_CHECK, 0 ) + 86400000 < System.currentTimeMillis() } @JvmStatic fun setTempoUpdateReminder() { - App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply() + App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()) + .apply() } @JvmStatic @@ -487,13 +510,14 @@ object Preferences { @JvmStatic fun setLastInstantMix() { - App.getInstance().preferences.edit().putLong(LAST_INSTANT_MIX, System.currentTimeMillis()).apply() + App.getInstance().preferences.edit().putLong(LAST_INSTANT_MIX, System.currentTimeMillis()) + .apply() } @JvmStatic fun isInstantMixUsable(): Boolean { return App.getInstance().preferences.getLong( - LAST_INSTANT_MIX, 0 + LAST_INSTANT_MIX, 0 ) + 5000 < System.currentTimeMillis() } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 00000000..e871093d --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_custom_header_input.xml b/app/src/main/res/layout/dialog_custom_header_input.xml new file mode 100644 index 00000000..b2521d5b --- /dev/null +++ b/app/src/main/res/layout/dialog_custom_header_input.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_server_signup.xml b/app/src/main/res/layout/dialog_server_signup.xml index 69597d3e..4b3667b8 100644 --- a/app/src/main/res/layout/dialog_server_signup.xml +++ b/app/src/main/res/layout/dialog_server_signup.xml @@ -1,6 +1,7 @@ @@ -129,6 +130,34 @@ android:layout_marginStart="24dp" android:layout_marginEnd="24dp" android:text="@string/server_signup_dialog_action_low_security" /> + + + + + +