package f.f.freezer; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Service; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.util.Log; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.text.DecimalFormat; import java.util.ArrayList; import javax.net.ssl.HttpsURLConnection; public class DownloadService extends Service { //Message commands static final int SERVICE_LOAD_DOWNLOADS = 1; static final int SERVICE_START_DOWNLOAD = 2; static final int SERVICE_ON_PROGRESS = 3; static final int SERVICE_SETTINGS_UPDATE = 4; static final int SERVICE_STOP_DOWNLOADS = 5; static final int SERVICE_ON_STATE_CHANGE = 6; static final int SERVICE_REMOVE_DOWNLOAD = 7; static final int SERVICE_RETRY_DOWNLOADS = 8; static final int SERVICE_REMOVE_DOWNLOADS = 9; static final String NOTIFICATION_CHANNEL_ID = "freezerdownloads"; static final int NOTIFICATION_ID_START = 6969; boolean running = false; DownloadSettings settings; Context context; SQLiteDatabase db; Deezer deezer = new Deezer(); Messenger serviceMessenger; Messenger activityMessenger; NotificationManagerCompat notificationManager; ArrayList downloads = new ArrayList<>(); ArrayList threads = new ArrayList<>(); ArrayList updateRequests = new ArrayList<>(); boolean updating = false; Handler progressUpdateHandler = new Handler(); DownloadLog logger = new DownloadLog(); public DownloadService() { } @Override public void onCreate() { super.onCreate(); //Setup notifications context = this; notificationManager = NotificationManagerCompat.from(context); createNotificationChannel(); createProgressUpdateHandler(); //Setup logger, deezer api logger.open(context); deezer.init(logger, ""); //Get DB DownloadsDatabase dbHelper = new DownloadsDatabase(getApplicationContext()); db = dbHelper.getWritableDatabase(); } @Override public void onDestroy() { //Cancel notifications notificationManager.cancelAll(); //Logger logger.close(); super.onDestroy(); } @Override public IBinder onBind(Intent intent) { //Set messengers serviceMessenger = new Messenger(new IncomingHandler(this)); if (intent != null) activityMessenger = intent.getParcelableExtra("activityMessenger"); return serviceMessenger.getBinder(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { //Get messenger if (intent != null) { activityMessenger = intent.getParcelableExtra("activityMessenger"); } //return super.onStartCommand(intent, flags, startId); //Prevent battery savers I guess return START_STICKY; } //Android O+ Notifications private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "Downloads", NotificationManager.IMPORTANCE_MIN); NotificationManager nManager = getSystemService(NotificationManager.class); nManager.createNotificationChannel(channel); } } //Update download tasks private void updateQueue() { db.beginTransaction(); //Clear downloaded tracks for (int i=threads.size() - 1; i>=0; i--) { Download.DownloadState state = threads.get(i).download.state; if (state == Download.DownloadState.NONE || state == Download.DownloadState.DONE || state == Download.DownloadState.ERROR || state == Download.DownloadState.DEEZER_ERROR) { Download d = threads.get(i).download; //Update in queue for (int j=0; j 0) { updateQueue(); //Because threading if (updateRequests.size() > 0) updateRequests.remove(0); } } updating = false; } //Loads downloads from database private void loadDownloads() { Cursor cursor = db.query("Downloads", null, null, null, null, null, null); //Parse downloads while (cursor.moveToNext()) { //Duplicate check int downloadId = cursor.getInt(0); Download.DownloadState state = Download.DownloadState.values()[cursor.getInt(1)]; boolean skip = false; for (int i=0; i= 3) { downloads.set(i, Download.fromSQL(cursor)); } } skip = true; break; } } //Add to queue if (!skip) downloads.add(Download.fromSQL(cursor)); } cursor.close(); updateState(); } //Stop downloads private void stop() { running = false; for (int i=0; i { updateProgress(); createProgressUpdateHandler(); }, 500); } //Updates notification and UI private void updateProgress() { if (threads.size() > 0) { //Convert threads to bundles, send to activity; Bundle b = new Bundle(); ArrayList down = new ArrayList<>(); for (int i=0; i= 3) { notificationManager.cancel(NOTIFICATION_ID_START + download.id); return; } NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, DownloadService.NOTIFICATION_CHANNEL_ID) .setContentTitle(download.title) .setSmallIcon(R.drawable.ic_logo) .setPriority(NotificationCompat.PRIORITY_MIN); //Show progress when downloading if (download.state == Download.DownloadState.DOWNLOADING) { if (download.filesize <= 0) download.filesize = 1; notificationBuilder.setContentText(String.format("%s / %s", formatFilesize(download.received), formatFilesize(download.filesize))); notificationBuilder.setProgress(100, (int)((download.received / (float)download.filesize)*100), false); } //Indeterminate on PostProcess if (download.state == Download.DownloadState.POST) { //TODO: Use strings notificationBuilder.setContentText("Post processing..."); notificationBuilder.setProgress(1, 1, true); } notificationManager.notify(NOTIFICATION_ID_START + download.id, notificationBuilder.build()); } //https://stackoverflow.com/questions/3263892/format-file-size-as-mb-gb-etc public static String formatFilesize(long size) { if(size <= 0) return "0B"; final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" }; int digitGroups = (int) (Math.log10(size)/Math.log10(1024)); return new DecimalFormat("#,##0.##").format(size/Math.pow(1024, digitGroups)) + " " + units[digitGroups]; } //Handler for incoming messages class IncomingHandler extends Handler { IncomingHandler(Context context) { context.getApplicationContext(); } @Override public void handleMessage(Message msg) { switch (msg.what) { //Load downloads from DB case SERVICE_LOAD_DOWNLOADS: loadDownloads(); break; //Start/Resume case SERVICE_START_DOWNLOAD: running = true; if (downloads.size() == 0) loadDownloads(); updateQueue(); updateState(); break; //Load settings case SERVICE_SETTINGS_UPDATE: settings = DownloadSettings.fromBundle(msg.getData()); deezer.arl = settings.arl; break; //Stop downloads case SERVICE_STOP_DOWNLOADS: stop(); break; //Remove download case SERVICE_REMOVE_DOWNLOAD: int downloadId = msg.getData().getInt("id"); for (int i=0; i= 0) { Download d = downloads.get(i); if (d.state == state) { //Remove db.delete("Downloads", "id == ?", new String[]{Integer.toString(d.id)}); downloads.remove(i); } i--; } //Delete from DB, done downloads after app restart aren't in downloads array db.delete("Downloads", "state == ?", new String[]{Integer.toString(msg.getData().getInt("state"))}); //Save db.setTransactionSuccessful(); db.endTransaction(); updateState(); break; default: super.handleMessage(msg); } } } //Send message to MainActivity void sendMessage(int type, Bundle data) { if (serviceMessenger != null) { Message msg = Message.obtain(null, type); msg.setData(data); try { activityMessenger.send(msg); } catch (RemoteException e) { e.printStackTrace(); } } } static class DownloadSettings { int downloadThreads; boolean overwriteDownload; boolean downloadLyrics; boolean trackCover; String arl; boolean albumCover; boolean nomediaFiles; String artistSeparator; int albumArtResolution; SelectedTags tags; private DownloadSettings(int downloadThreads, boolean overwriteDownload, boolean downloadLyrics, boolean trackCover, String arl, boolean albumCover, boolean nomediaFiles, String artistSeparator, int albumArtResolution, SelectedTags tags) { this.downloadThreads = downloadThreads; this.overwriteDownload = overwriteDownload; this.downloadLyrics = downloadLyrics; this.trackCover = trackCover; this.arl = arl; this.albumCover = albumCover; this.nomediaFiles = nomediaFiles; this.artistSeparator = artistSeparator; this.albumArtResolution = albumArtResolution; this.tags = tags; } //Parse settings from bundle sent from UI static DownloadSettings fromBundle(Bundle b) { JSONObject json; try { json = new JSONObject(b.getString("json")); return new DownloadSettings( json.getInt("downloadThreads"), json.getBoolean("overwriteDownload"), json.getBoolean("downloadLyrics"), json.getBoolean("trackCover"), json.getString("arl"), json.getBoolean("albumCover"), json.getBoolean("nomediaFiles"), json.getString("artistSeparator"), json.getInt("albumArtResolution"), new SelectedTags(json.getJSONArray("tags")) ); } catch (Exception e) { //Shouldn't happen Log.e("ERR", "Error loading settings!"); return null; } } } static class SelectedTags { boolean title = false; boolean album = false; boolean artist = false; boolean track = false; boolean disc = false; boolean albumArtist = false; boolean date = false; boolean label = false; boolean isrc = false; boolean upc = false; boolean trackTotal = false; boolean bpm = false; boolean lyrics = false; boolean genre = false; boolean contributors = false; boolean albumArt = false; SelectedTags(JSONArray json) { //Array of tags, check if exist try { for (int i=0; i