Fix compatibility with Android 4.4.
The app was pretty broken on API 19, fixes: - Remove SelectActivity card background because API 19 does not support XML SVG as bg - Add simple folder browser for APIs without SAF - Add simplified version of download notification - Downgrade and replace incompatible libraries to enable SSL 1.2 on older Android versions
|
|
@ -13,7 +13,9 @@ android {
|
||||||
versionCode 2
|
versionCode 2
|
||||||
versionName '1.4.1'
|
versionName '1.4.1'
|
||||||
|
|
||||||
|
// for SDK < 19
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary true
|
useSupportLibrary true
|
||||||
}
|
}
|
||||||
|
|
@ -105,8 +107,9 @@ dependencies {
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
implementation 'com.mlegy.redscreenofdeath:red-screen-of-death:0.1.3'
|
implementation 'com.mlegy.redscreenofdeath:red-screen-of-death:0.1.3'
|
||||||
|
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
// old version of retrofit to work with api < 21
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
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'
|
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:annotations:4.12.0'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||||
implementation 'com.github.bumptech.glide:okhttp3-integration: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'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="org.adrianvictor.geleia.activities.SettingsActivity"
|
android:name="org.adrianvictor.geleia.activities.SettingsActivity"
|
||||||
android:label="@string/action_settings" />
|
android:label="@string/action_settings" />
|
||||||
|
<activity android:name="org.adrianvictor.geleia.activities.DirectoryPickerActivity"/>
|
||||||
<activity
|
<activity
|
||||||
android:name="org.adrianvictor.geleia.activities.AboutActivity"
|
android:name="org.adrianvictor.geleia.activities.AboutActivity"
|
||||||
android:label="@string/action_about" />
|
android:label="@string/action_about" />
|
||||||
|
|
@ -138,6 +139,12 @@
|
||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.multidex.MultiDexApplication;
|
||||||
import androidx.room.Room;
|
import androidx.room.Room;
|
||||||
|
|
||||||
import org.adrianvictor.geleia.database.JellyDatabase;
|
import org.adrianvictor.geleia.database.JellyDatabase;
|
||||||
|
|
@ -14,6 +17,7 @@ import org.adrianvictor.geleia.util.PreferenceUtil;
|
||||||
import org.adrianvictor.geleia.views.shortcuts.DynamicShortcutManager;
|
import org.adrianvictor.geleia.views.shortcuts.DynamicShortcutManager;
|
||||||
import com.melegy.redscreenofdeath.RedScreenOfDeath;
|
import com.melegy.redscreenofdeath.RedScreenOfDeath;
|
||||||
|
|
||||||
|
import org.conscrypt.Conscrypt;
|
||||||
import org.jellyfin.apiclient.interaction.AndroidDevice;
|
import org.jellyfin.apiclient.interaction.AndroidDevice;
|
||||||
import org.jellyfin.apiclient.interaction.ApiClient;
|
import org.jellyfin.apiclient.interaction.ApiClient;
|
||||||
import org.jellyfin.apiclient.interaction.VolleyHttpClient;
|
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.AndroidLogger;
|
||||||
import org.jellyfin.apiclient.logging.ILogger;
|
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 App app;
|
||||||
|
|
||||||
private static JellyDatabase database;
|
private static JellyDatabase database;
|
||||||
|
|
@ -30,8 +43,14 @@ public class App extends Application {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
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();
|
super.onCreate();
|
||||||
|
|
||||||
|
initializeGlide(this);
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
RedScreenOfDeath.init(this);
|
RedScreenOfDeath.init(this);
|
||||||
}
|
}
|
||||||
|
|
@ -91,4 +110,15 @@ public class App extends Application {
|
||||||
public static App getInstance() {
|
public static App getInstance() {
|
||||||
return app;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<File> adapter;
|
||||||
|
private TextView pathView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
rootDir = Environment.getExternalStorageDirectory();
|
||||||
|
currentDir = rootDir;
|
||||||
|
|
||||||
|
LinearLayout layout = new LinearLayout(this);
|
||||||
|
layout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
pathView = new TextView(this);
|
||||||
|
pathView.setPadding(24, 24, 24, 24);
|
||||||
|
|
||||||
|
ListView listView = new ListView(this);
|
||||||
|
|
||||||
|
Button selectButton = new Button(this);
|
||||||
|
selectButton.setText("Select this folder");
|
||||||
|
|
||||||
|
layout.addView(pathView);
|
||||||
|
layout.addView(listView, new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||||
|
layout.addView(selectButton);
|
||||||
|
|
||||||
|
setContentView(layout);
|
||||||
|
|
||||||
|
adapter = new ArrayAdapter<File>(this,
|
||||||
|
android.R.layout.simple_list_item_1, new ArrayList<>()) {
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, ViewGroup parent) {
|
||||||
|
TextView tv = (TextView) super.getView(position, convertView, parent);
|
||||||
|
File f = getItem(position);
|
||||||
|
|
||||||
|
File parentFile = currentDir.getParentFile();
|
||||||
|
|
||||||
|
if (parentFile != null && parentFile.equals(f)) {
|
||||||
|
tv.setText("..");
|
||||||
|
} else {
|
||||||
|
tv.setText(f.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return tv;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
listView.setAdapter(adapter);
|
||||||
|
|
||||||
|
listView.setOnItemClickListener((p, v, pos, id) -> {
|
||||||
|
File dir = adapter.getItem(pos);
|
||||||
|
if (dir != null) openDir(dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
selectButton.setOnClickListener(v -> {
|
||||||
|
Intent result = new Intent();
|
||||||
|
result.putExtra(EXTRA_RESULT_PATH, currentDir.getAbsolutePath());
|
||||||
|
setResult(RESULT_OK, result);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
openDir(rootDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openDir(File dir) {
|
||||||
|
if (!dir.exists() || !dir.canRead()) return;
|
||||||
|
|
||||||
|
File[] dirs = dir.listFiles(f ->
|
||||||
|
f.isDirectory() && f.canRead()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dirs == null) return;
|
||||||
|
|
||||||
|
adapter.clear();
|
||||||
|
|
||||||
|
File parent = dir.getParentFile();
|
||||||
|
if (parent != null && parent.getAbsolutePath().startsWith(rootDir.getAbsolutePath())) {
|
||||||
|
adapter.add(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Arrays.sort(dirs);
|
||||||
|
|
||||||
|
for (File f : dirs) {
|
||||||
|
adapter.add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDir = dir;
|
||||||
|
pathView.setText(dir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
File parent = currentDir.getParentFile();
|
||||||
|
if (parent != null &&
|
||||||
|
parent.getAbsolutePath().startsWith(rootDir.getAbsolutePath())) {
|
||||||
|
openDir(parent);
|
||||||
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,7 @@ public class SettingsActivity extends AbsBaseActivity {
|
||||||
public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
|
public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private ActivityResultLauncher<Intent> dirPickerLauncher;
|
private ActivityResultLauncher<Intent> dirPickerLauncher;
|
||||||
|
private ActivityResultLauncher<Intent> legacyDirPickerLauncher;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
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
|
@Override
|
||||||
|
|
@ -161,8 +181,13 @@ public class SettingsActivity extends AbsBaseActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDirectoryPicker() {
|
private void openDirectoryPicker() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
dirPickerLauncher.launch(intent);
|
dirPickerLauncher.launch(intent);
|
||||||
|
} else {
|
||||||
|
Intent intent = new Intent(requireContext(), DirectoryPickerActivity.class);
|
||||||
|
legacyDirPickerLauncher.launch(intent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -127,8 +127,11 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
|
||||||
private boolean checkBatteryOptimization() {
|
private boolean checkBatteryOptimization() {
|
||||||
String packageName = getPackageName();
|
String packageName = getPackageName();
|
||||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
return pm.isIgnoringBatteryOptimizations(packageName);
|
return pm.isIgnoringBatteryOptimizations(packageName);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
|
@ -138,11 +141,13 @@ public abstract class AbsBaseActivity extends AbsThemeActivity {
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
private boolean checkPermissions() {
|
private boolean checkPermissions() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
for (String permission : permissions) {
|
for (String permission : permissions) {
|
||||||
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import android.app.Service;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
import org.adrianvictor.geleia.App;
|
import org.adrianvictor.geleia.App;
|
||||||
|
|
@ -25,6 +27,7 @@ import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public class DownloadService extends Service {
|
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 PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
||||||
public static final String ACTION_START = PACKAGE_NAME + ".action.start";
|
public static final String ACTION_START = PACKAGE_NAME + ".action.start";
|
||||||
public static final String ACTION_CANCEL = PACKAGE_NAME + ".action.cancel";
|
public static final String ACTION_CANCEL = PACKAGE_NAME + ".action.cancel";
|
||||||
|
|
@ -79,10 +82,10 @@ public class DownloadService extends Service {
|
||||||
|
|
||||||
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
|
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
|
||||||
DocumentFile root;
|
DocumentFile root;
|
||||||
if (location.equals(getApplicationContext().getCacheDir().toString())) {
|
if (location.startsWith("content://")) {
|
||||||
root = DocumentFile.fromFile(new File(location));
|
|
||||||
} else {
|
|
||||||
root = DocumentFile.fromTreeUri(this, Uri.parse(location));
|
root = DocumentFile.fromTreeUri(this, Uri.parse(location));
|
||||||
|
} else {
|
||||||
|
root = DocumentFile.fromFile(new File(location));
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentFile artist = root.findFile(MusicUtil.ascii(song.artistName));
|
DocumentFile artist = root.findFile(MusicUtil.ascii(song.artistName));
|
||||||
|
|
@ -117,6 +120,7 @@ public class DownloadService extends Service {
|
||||||
App.getDatabase().cacheDao().insertCache(new Cache(song));
|
App.getDatabase().cacheDao().insertCache(new Cache(song));
|
||||||
notification.stop(song);
|
notification.stop(song);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Failed to download song: " + song.title, e);
|
||||||
ErrorNotification.show(this, e.getMessage());
|
ErrorNotification.show(this, e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,114 +1,134 @@
|
||||||
package org.adrianvictor.geleia.service.notifications;
|
package org.adrianvictor.geleia.service.notifications;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.widget.RemoteViews;
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
import org.adrianvictor.geleia.R;
|
import org.adrianvictor.geleia.R;
|
||||||
import org.adrianvictor.geleia.activities.MainActivity;
|
|
||||||
import org.adrianvictor.geleia.model.Song;
|
import org.adrianvictor.geleia.model.Song;
|
||||||
import org.adrianvictor.geleia.service.DownloadService;
|
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.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static android.content.Context.NOTIFICATION_SERVICE;
|
|
||||||
|
|
||||||
public class DownloadNotification {
|
public class DownloadNotification {
|
||||||
private static final String CHANNEL_ID = DownloadNotification.class.getSimpleName();
|
private static final String CHANNEL_ID = "download_channel";
|
||||||
private static final int NOTIFICATION_ID = 2;
|
private final int ID = 2;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final NotificationManager notificationManager;
|
private final NotificationManager notificationManager;
|
||||||
|
private final List<Song> queue = Collections.synchronizedList(new LinkedList<>());
|
||||||
|
|
||||||
private final List<Song> songs;
|
private long totalSize;
|
||||||
|
private long downloadedSize;
|
||||||
private int current;
|
private int lastPercentage = -1;
|
||||||
private int maximum;
|
|
||||||
|
|
||||||
public DownloadNotification(Context context) {
|
public DownloadNotification(Context context) {
|
||||||
this.notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
|
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
this.songs = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void start(Song song) {
|
|
||||||
this.songs.add(song);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
createNotificationChannel();
|
createNotificationChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
context.getString(R.string.download_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
);
|
||||||
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void update(int current, int maximum) {
|
public void start(Song song) {
|
||||||
this.current += current;
|
queue.add(song);
|
||||||
this.maximum += maximum;
|
if (queue.size() == 1) { // This is the first song of a new batch
|
||||||
|
totalSize = 0;
|
||||||
Intent action = new Intent(context, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
downloadedSize = 0;
|
||||||
PendingIntent clickIntent = PendingIntent.getActivity(context, 0, action, PendingIntent.FLAG_IMMUTABLE);
|
lastPercentage = -1;
|
||||||
|
notificationManager.notify(ID, getNotification());
|
||||||
Intent cancel = new Intent(context, DownloadService.class).setAction(DownloadService.ACTION_CANCEL);
|
} else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
|
||||||
PendingIntent pendingCancel = PendingIntent.getService(context, 0, cancel, PendingIntent.FLAG_IMMUTABLE);
|
// For KitKat, update for every new song to show correct count.
|
||||||
|
notificationManager.notify(ID, getNotification());
|
||||||
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
|
|
||||||
for (Song item : songs.stream().limit(5).collect(Collectors.toList())) {
|
|
||||||
style.addLine(item.title);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentIntent(clickIntent)
|
.setContentTitle(context.getString(R.string.downloading_songs))
|
||||||
.setContentTitle(String.format(context.getString(R.string.downloading_x_songs), songs.size()))
|
.setOngoing(true)
|
||||||
.setProgress(this.maximum, this.current, false)
|
.addAction(android.R.drawable.ic_menu_close_clear_cancel, context.getString(android.R.string.cancel), pendingIntent);
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.addAction(R.drawable.ic_close_white_24dp, context.getString(R.string.action_cancel), pendingCancel)
|
|
||||||
.setStyle(style)
|
|
||||||
.setShowWhen(false);
|
|
||||||
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
String contentText = context.getResources().getQuantityString(R.plurals.downloading_s_songs, queue.size(), queue.size());
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.notification_download);
|
||||||
|
remoteViews.setTextViewText(R.id.notification_download_title, contentText);
|
||||||
|
int progress = lastPercentage > 0 ? lastPercentage : 0;
|
||||||
|
remoteViews.setProgressBar(R.id.notification_download_progress, 100, progress, totalSize == 0 && downloadedSize == 0);
|
||||||
|
builder.setCustomContentView(remoteViews);
|
||||||
|
} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
|
||||||
|
int progress = lastPercentage > 0 ? lastPercentage : 0;
|
||||||
|
builder.setProgress(100, progress, totalSize == 0 && downloadedSize == 0);
|
||||||
|
builder.setContentText(contentText);
|
||||||
|
} else { // KitKat
|
||||||
|
builder.setContentText(contentText);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void stop(Song song) {
|
return builder.build();
|
||||||
if (song != null) {
|
|
||||||
songs.remove(song);
|
|
||||||
} else {
|
|
||||||
songs.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!songs.isEmpty()) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package org.adrianvictor.geleia.service.playback;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
|
@ -141,12 +142,18 @@ public class LocalPlayer implements Playback {
|
||||||
|
|
||||||
private List<MediaItem> createMediaItems(List<Song> queue) {
|
private List<MediaItem> createMediaItems(List<Song> queue) {
|
||||||
return queue.stream().map(song -> {
|
return queue.stream().map(song -> {
|
||||||
File audio = new File(MusicUtil.getFileUri(song));
|
String fileUri = MusicUtil.getFileUri(song);
|
||||||
Uri uri = Uri.fromFile(audio);
|
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()) {
|
if (!audio.exists()) {
|
||||||
uri = Uri.parse(MusicUtil.getTranscodeUri(song));
|
uri = Uri.parse(MusicUtil.getTranscodeUri(song));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<String> containers = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream()
|
List<String> containers = PreferenceUtil.getInstance(context).getDirectPlayCodecs().stream()
|
||||||
.map(codec -> codec.container.toLowerCase(Locale.ROOT))
|
.map(codec -> codec.container.toLowerCase(Locale.ROOT))
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.core.graphics.drawable.DrawableCompat;
|
import androidx.core.graphics.drawable.DrawableCompat;
|
||||||
import androidx.core.content.res.ResourcesCompat;
|
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) {
|
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);
|
if (drawable != null) {
|
||||||
DrawableCompat.setTint(drawable, color);
|
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) {
|
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) {
|
public static Drawable resolveDrawable(@NonNull Context context, @AttrRes int drawableAttr) {
|
||||||
TypedArray a = context.obtainStyledAttributes(new int[]{drawableAttr});
|
TypedArray a = context.obtainStyledAttributes(new int[]{drawableAttr});
|
||||||
Drawable drawable = a.getDrawable(0);
|
Drawable drawable = a.getDrawable(0);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package org.adrianvictor.geleia.util;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
@ -82,7 +83,8 @@ public class MusicUtil {
|
||||||
public static String getFileUri(Song song) {
|
public static String getFileUri(Song song) {
|
||||||
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
|
String location = PreferenceUtil.getInstance(App.getInstance()).getLocationDownload();
|
||||||
File root = new File(location, "music");
|
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);
|
Uri uri = Uri.parse(location);
|
||||||
return new File(uri.getPath()).getAbsolutePath();
|
return new File(uri.getPath()).getAbsolutePath();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,27 @@ import org.adrianvictor.geleia.util.ThemeUtil;
|
||||||
import org.adrianvictor.geleia.R;
|
import org.adrianvictor.geleia.R;
|
||||||
|
|
||||||
public class IconImageView extends AppCompatImageView {
|
public class IconImageView extends AppCompatImageView {
|
||||||
|
|
||||||
public IconImageView(Context context) {
|
public IconImageView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
init(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IconImageView(Context context, AttributeSet attrs) {
|
public IconImageView(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
init(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IconImageView(Context context, AttributeSet attrs, int defStyleAttr) {
|
public IconImageView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
init(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init(Context context) {
|
@Override
|
||||||
if (context == null) return;
|
protected void onAttachedToWindow() {
|
||||||
setColorFilter(ThemeUtil.getColorResource(context, R.attr.iconColor), PorterDuff.Mode.SRC_IN);
|
super.onAttachedToWindow();
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
if (getContext() == null) return;
|
||||||
|
setColorFilter(ThemeUtil.getColorResource(getContext(), R.attr.iconColor), PorterDuff.Mode.SRC_IN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,4 @@
|
||||||
<vector android:height="192dp" android:viewportHeight="24"
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
android:viewportWidth="24" android:width="192dp"
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path
|
</selector>
|
||||||
android:pathData="m12,9.8425c-1.1866,0 -2.1575,0.9709 -2.1575,2.1575s0.9709,2.1575 2.1575,2.1575 2.1575,-0.9709 2.1575,-2.1575 -0.9709,-2.1575 -2.1575,-2.1575zM12,1c-6.072,0 -11,4.928 -11,11s4.928,11 11,11 11,-4.928 11,-11 -4.928,-11 -11,-11zM12,7.05c2.739,0 4.95,2.211 4.95,4.95s-2.211,4.95 -4.95,4.95 -4.95,-2.211 -4.95,-4.95 2.211,-4.95 4.95,-4.95z" android:strokeWidth="2.1575">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient android:endX="25.2" android:endY="25.1996"
|
|
||||||
android:startX="1" android:startY="0.99960005" android:type="linear">
|
|
||||||
<item android:color="#FFAA5CC3" android:offset="0"/>
|
|
||||||
<item android:color="#FF00A4DC" android:offset="1"/>
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -19,21 +19,22 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
<ImageView
|
<ScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="40dp"
|
|
||||||
android:layout_marginBottom="40dp"
|
|
||||||
app:srcCompat="@drawable/ic_launcher_nodpi"
|
|
||||||
tools:ignore="ContentDescription"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
app:layout_constraintBottom_toTopOf="@id/username_layout" />
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical" >
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:id="@+id/username_layout"
|
android:id="@+id/username_layout"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="32dp"
|
android:layout_marginStart="32dp"
|
||||||
|
|
@ -43,27 +44,24 @@
|
||||||
app:boxCornerRadiusBottomStart="24dp"
|
app:boxCornerRadiusBottomStart="24dp"
|
||||||
app:boxCornerRadiusTopEnd="24dp"
|
app:boxCornerRadiusTopEnd="24dp"
|
||||||
app:boxCornerRadiusTopStart="24dp"
|
app:boxCornerRadiusTopStart="24dp"
|
||||||
app:errorEnabled="true"
|
app:boxStrokeColor="?android:textColorSecondary"
|
||||||
app:endIconMode="clear_text"
|
app:endIconMode="clear_text"
|
||||||
app:endIconTint="?android:textColorSecondary"
|
app:endIconTint="?android:textColorSecondary"
|
||||||
app:boxStrokeColor="?android:textColorSecondary"
|
app:errorEnabled="true"
|
||||||
app:hintTextColor="?android:textColorSecondary"
|
app:hintTextColor="?android:textColorSecondary">
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/password_layout">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/username"
|
android:id="@+id/username"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textNoSuggestions"
|
android:hint="@string/username"
|
||||||
android:hint="@string/username" />
|
android:inputType="textNoSuggestions" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:id="@+id/password_layout"
|
android:id="@+id/password_layout"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="32dp"
|
android:layout_marginStart="32dp"
|
||||||
|
|
@ -73,27 +71,24 @@
|
||||||
app:boxCornerRadiusBottomStart="24dp"
|
app:boxCornerRadiusBottomStart="24dp"
|
||||||
app:boxCornerRadiusTopEnd="24dp"
|
app:boxCornerRadiusTopEnd="24dp"
|
||||||
app:boxCornerRadiusTopStart="24dp"
|
app:boxCornerRadiusTopStart="24dp"
|
||||||
app:errorEnabled="true"
|
app:boxStrokeColor="?android:textColorSecondary"
|
||||||
app:endIconMode="password_toggle"
|
app:endIconMode="password_toggle"
|
||||||
app:endIconTint="?android:textColorSecondary"
|
app:endIconTint="?android:textColorSecondary"
|
||||||
app:boxStrokeColor="?android:textColorSecondary"
|
app:errorEnabled="true"
|
||||||
app:hintTextColor="?android:textColorSecondary"
|
app:hintTextColor="?android:textColorSecondary">
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/server_layout">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/password"
|
android:id="@+id/password"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textPassword"
|
android:hint="@string/password"
|
||||||
android:hint="@string/password" />
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
|
||||||
android:id="@+id/server_layout"
|
android:id="@+id/server_layout"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="32dp"
|
android:layout_marginStart="32dp"
|
||||||
|
|
@ -103,21 +98,18 @@
|
||||||
app:boxCornerRadiusBottomStart="24dp"
|
app:boxCornerRadiusBottomStart="24dp"
|
||||||
app:boxCornerRadiusTopEnd="24dp"
|
app:boxCornerRadiusTopEnd="24dp"
|
||||||
app:boxCornerRadiusTopStart="24dp"
|
app:boxCornerRadiusTopStart="24dp"
|
||||||
app:errorEnabled="true"
|
app:boxStrokeColor="?android:textColorSecondary"
|
||||||
app:endIconMode="clear_text"
|
app:endIconMode="clear_text"
|
||||||
app:endIconTint="?android:textColorSecondary"
|
app:endIconTint="?android:textColorSecondary"
|
||||||
app:boxStrokeColor="?android:textColorSecondary"
|
app:errorEnabled="true"
|
||||||
app:hintTextColor="?android:textColorSecondary"
|
app:hintTextColor="?android:textColorSecondary">
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/login">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/server"
|
android:id="@+id/server"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textUri"
|
android:hint="@string/server"
|
||||||
android:hint="@string/server" />
|
android:inputType="textUri" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|
@ -129,10 +121,7 @@
|
||||||
android:layout_marginTop="32dp"
|
android:layout_marginTop="32dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:text="@string/login"
|
android:text="@string/login"
|
||||||
app:cornerRadius="24dp"
|
app:cornerRadius="24dp" />
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/select" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/select"
|
android:id="@+id/select"
|
||||||
|
|
@ -141,12 +130,11 @@
|
||||||
android:layout_marginHorizontal="32dp"
|
android:layout_marginHorizontal="32dp"
|
||||||
android:layout_marginTop="0dp"
|
android:layout_marginTop="0dp"
|
||||||
android:layout_marginBottom="32dp"
|
android:layout_marginBottom="32dp"
|
||||||
android:text="@string/select"
|
|
||||||
android:backgroundTint="?cardBackgroundColor"
|
android:backgroundTint="?cardBackgroundColor"
|
||||||
|
android:text="@string/select"
|
||||||
app:cornerRadius="24dp"
|
app:cornerRadius="24dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
tools:ignore="UnusedAttribute" />
|
tools:ignore="UnusedAttribute" />
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="100dp"
|
android:layout_height="100dp"
|
||||||
android:layout_margin="20dp"
|
android:layout_margin="20dp"
|
||||||
android:background="@drawable/card_server"
|
|
||||||
android:elevation="4dp"
|
android:elevation="4dp"
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
|
|
|
||||||
23
app/src/main/res/layout/notification_download.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_download_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
tools:text="Downloading 2 songs" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/notification_download_progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 40 KiB |
|
|
@ -245,4 +245,11 @@
|
||||||
<string name="app_license">GNU General Public License v3.0</string>
|
<string name="app_license">GNU General Public License v3.0</string>
|
||||||
<string name="application_license">GNU General Public License v3.0</string>
|
<string name="application_license">GNU General Public License v3.0</string>
|
||||||
|
|
||||||
|
<string name="download_channel_name">Downloads</string>
|
||||||
|
<string name="downloading_songs">Downloading songs</string>
|
||||||
|
<plurals name="downloading_s_songs">
|
||||||
|
<item quantity="one">Downloading %d song</item>
|
||||||
|
<item quantity="other">Downloading %d songs</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||