diff --git a/app/src/main/java/max/music_cyclon/service/BeetsFetcher.java b/app/src/main/java/max/music_cyclon/service/BeetsFetcher.java new file mode 100644 index 0000000..557908c --- /dev/null +++ b/app/src/main/java/max/music_cyclon/service/BeetsFetcher.java @@ -0,0 +1,161 @@ +package max.music_cyclon.service; + +import android.content.res.Resources; +import android.util.JsonReader; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +import max.music_cyclon.SynchronizeConfig; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class BeetsFetcher { + public static final Random RANDOM = new Random(); + + private final String address; + private final Resources resources; + + public BeetsFetcher(String address, Resources resources) { + this.address = address; + this.resources = resources; + } + + public List fetch(SynchronizeConfig config) throws IOException { + StringBuilder get; + + if (config.isAlbum(resources)) { + get = new StringBuilder("/album"); + } else { + get = new StringBuilder("/item"); + } + + String query = config.getQuery(resources); + if (!query.isEmpty()) { + get.append("/query/").append(query); + } + + get.append("?expand"); + + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(address + get) + .build(); + + Response response = client.newCall(request).execute(); + + if (response.code() != 200) { + Log.e("ERROR", "Server returned HTTP " + response.message()); + return Collections.emptyList(); + } + + + InputStream stream = response.body().byteStream(); + List items = parseJson(stream, config.getSize(resources), config.isAlbum(resources)); + stream.close(); + + return items; + } + + private List parseJson(InputStream stream, int size, boolean isAlbums) throws IOException { + JsonReader reader = new JsonReader(new BufferedReader(new InputStreamReader(stream, "UTF-8"))); + + List items = new ArrayList<>(); + List> albums = new ArrayList<>(); + + reader.beginObject(); + String root = reader.nextName(); +// boolean isAlbums = root.equals("albums"); + reader.beginArray(); + while (reader.hasNext()) { + if (isAlbums) { + albums.add(parseAlbum(reader)); + } else { + items.add(parseItem(reader)); + } + } + reader.endArray(); + reader.endObject(); + + // Select random + if (isAlbums) { + List> randomAlbums = selectRandom(albums, size); + + for (ArrayList album : randomAlbums) { + items.addAll(album); + } + return Collections.unmodifiableList(items); + } else { + return selectRandom(items, size); + } + } + + public List selectRandom(List list, int n) { + if (list.isEmpty()) { + return Collections.emptyList(); + } + + ArrayList out = new ArrayList<>(); + for (int i = 0; i < n; i++) { + out.add(list.get(RANDOM.nextInt(list.size() - 1))); + } + + return Collections.unmodifiableList(out); + } + + private ArrayList parseAlbum(JsonReader reader) throws IOException { + reader.beginObject(); + + ArrayList items = new ArrayList<>(); + + while (reader.hasNext()) { + String tag = reader.nextName(); + if (tag.equals("items")) { + reader.beginArray(); + while (reader.hasNext()) { + items.add(parseItem(reader)); + } + reader.endArray(); + } else { + reader.skipValue(); + } + } + + reader.endObject(); + + return items; + } + + private Item parseItem(JsonReader reader) throws IOException { + reader.beginObject(); + Item item = new Item(); + + while (reader.hasNext()) { + String tag = reader.nextName(); + switch (tag) { + case "id": + item.setId(reader.nextInt()); + break; + case "path": + item.setPath(reader.nextString()); + break; + default: + reader.skipValue(); + break; + } + } + + reader.endObject(); + + return item; + } + +} diff --git a/app/src/main/java/max/music_cyclon/service/DownloadTask.java b/app/src/main/java/max/music_cyclon/service/DownloadTask.java index f999e83..308c8fe 100644 --- a/app/src/main/java/max/music_cyclon/service/DownloadTask.java +++ b/app/src/main/java/max/music_cyclon/service/DownloadTask.java @@ -10,18 +10,18 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.zip.Adler32; -import cz.msebera.android.httpclient.client.methods.CloseableHttpResponse; -import cz.msebera.android.httpclient.client.methods.HttpGet; -import cz.msebera.android.httpclient.impl.client.CloseableHttpClient; -import cz.msebera.android.httpclient.impl.client.HttpClients; -import max.music_cyclon.service.db.FileTracker; +import max.music_cyclon.SynchronizeConfig; +import max.music_cyclon.tracker.FileTracker; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; public class DownloadTask implements Runnable { + private final SynchronizeConfig config; private final URI uri; private final String itemPath; @@ -29,30 +29,36 @@ public class DownloadTask implements Runnable { private final ProgressUpdater progressUpdater; private CountDownLatch itemsLeftLatch; - private static final CloseableHttpClient httpclient = HttpClients.createDefault(); - public DownloadTask(URI uri, String itemPath, - FileTracker tracker, ProgressUpdater progressUpdater, - CountDownLatch itemsLeftLatch) { + public DownloadTask(SynchronizeConfig config, URI uri, String itemPath, + FileTracker tracker, ProgressUpdater progressUpdater) { + this.config = config; this.uri = uri; this.itemPath = itemPath; this.tracker = tracker; this.progressUpdater = progressUpdater; - this.itemsLeftLatch = itemsLeftLatch; } private InputStream prepareConnection() throws IOException { - HttpGet httpGet = new HttpGet(uri); + OkHttpClient client = new OkHttpClient(); - CloseableHttpResponse response = httpclient.execute(httpGet); + Request request = new Request.Builder() + .url(uri.toURL()) + .build(); - if (response.getStatusLine().getStatusCode() != 200) { - Log.e("ERROR", "Server returned HTTP " + response.getStatusLine().getStatusCode()); + Response response = client.newCall(request).execute(); + + if (response.code() != 200) { + Log.e("ERROR", "Server returned HTTP " + response.message()); return null; } - return response.getEntity().getContent(); + return response.body().byteStream(); + } + + public void setItemsLeftLatch(CountDownLatch itemsLeftLatch) { + this.itemsLeftLatch = itemsLeftLatch; } @Override @@ -81,9 +87,9 @@ public class DownloadTask implements Runnable { input.close(); } - tracker.track(target, checksum.getValue()); + tracker.track(config, target, checksum.getValue()); } catch (IOException e) { - Log.wtf("WTF", e); + Log.e("DOWNLOAD", "Failed to download", e); } progressUpdater.increment(); diff --git a/app/src/main/java/max/music_cyclon/service/Item.java b/app/src/main/java/max/music_cyclon/service/Item.java index f408955..99ad735 100644 --- a/app/src/main/java/max/music_cyclon/service/Item.java +++ b/app/src/main/java/max/music_cyclon/service/Item.java @@ -3,9 +3,7 @@ package max.music_cyclon.service; public class Item { private int id; - private String name; - private String artist; - private String album; + private String path; public int getId() { return id; @@ -15,27 +13,11 @@ public class Item { this.id = id; } - public String getAlbum() { - return album; + public String getPath() { + return path; } - public void setAlbum(String album) { - this.album = album; - } - - public String getArtist() { - return artist; - } - - public void setArtist(String artist) { - this.artist = artist; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; + public void setPath(String path) { + this.path = path; } } diff --git a/app/src/main/java/max/music_cyclon/service/LibraryService.java b/app/src/main/java/max/music_cyclon/service/LibraryService.java index 3d1d308..ba7559a 100644 --- a/app/src/main/java/max/music_cyclon/service/LibraryService.java +++ b/app/src/main/java/max/music_cyclon/service/LibraryService.java @@ -1,9 +1,10 @@ package max.music_cyclon.service; +import android.Manifest; import android.app.IntentService; import android.content.Intent; import android.content.SharedPreferences; -import android.content.res.Resources; +import android.content.pm.PackageManager; import android.os.Environment; import android.os.Handler; import android.os.IBinder; @@ -12,57 +13,40 @@ import android.os.Messenger; import android.os.Parcelable; import android.os.RemoteException; import android.preference.PreferenceManager; -import android.util.JsonReader; +import android.support.v4.content.ContextCompat; import android.util.Log; import com.maxmpz.poweramp.player.PowerampAPI; -import java.io.BufferedReader; import java.io.File; -import java.io.FilenameFilter; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.MalformedURLException; +import java.lang.ref.WeakReference; +import java.net.URI; import java.net.URISyntaxException; -import java.net.URL; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import cz.msebera.android.httpclient.client.methods.CloseableHttpResponse; -import cz.msebera.android.httpclient.client.methods.HttpGet; -import cz.msebera.android.httpclient.impl.client.CloseableHttpClient; -import cz.msebera.android.httpclient.impl.client.HttpClients; +import max.music_cyclon.R; import max.music_cyclon.SynchronizeConfig; -import max.music_cyclon.service.db.FileTracker; +import max.music_cyclon.tracker.FileTracker; public class LibraryService extends IntentService { - - public static final FilenameFilter NOMEDIA_FILTER = new FilenameFilter() { - @Override - public boolean accept(File file, String s) { - return !s.equals(".nomedia"); - } - }; - /** - * Command to the service to register a client, receiving callbacks - * from the service. The Message's replyTo field must be a Messenger of + * Command to the serviceReference to register a client, receiving callbacks + * from the serviceReference. The Message's replyTo field must be a Messenger of * the client where callbacks should be sent. */ public static final int MSG_REGISTER_CLIENT = 1; /** - * Command to the service to unregister a client, ot stop receiving callbacks - * from the service. The Message's replyTo field must be a Messenger of + * Command to the serviceReference to unregister a client, ot stop receiving callbacks + * from the serviceReference. The Message's replyTo field must be a Messenger of * the client as previously given with MSG_REGISTER_CLIENT. */ public static final int MSG_UNREGISTER_CLIENT = 2; @@ -71,7 +55,6 @@ public class LibraryService extends IntentService { public static final int MSG_CANCEL = 3; public static final int MSG_STARTED = 4; public static final int MSG_FINISHED = 5; - public static final Random RANDOM = new Random(); /** * Keeps track of all current registered clients. @@ -81,151 +64,42 @@ public class LibraryService extends IntentService { /** * Target we publish for clients to send messages to IncomingHandler. */ - private final Messenger mMessenger = new Messenger(new IncomingHandler()); + private final Messenger mMessenger = new Messenger( + new IncomingHandler(new WeakReference<>(this)) + ); public LibraryService() { - super("max.music_cyclon.service.LibraryService"); + super(LibraryService.class.getName()); } - public List fetchRandom(String address, SynchronizeConfig config, Resources resources) throws IOException { - StringBuilder get; - - if (config.isAlbum(resources)) { - get = new StringBuilder("/album"); - } else { - get = new StringBuilder("/item"); - } - - String query = config.getQuery(resources); - if (!query.isEmpty()) { - get.append("/query/").append(query); - } - - get.append("?expand"); - - CloseableHttpClient httpclient = HttpClients.createDefault(); - HttpGet httpGet = new HttpGet(address + get); - CloseableHttpResponse response = httpclient.execute(httpGet); - - if (response.getStatusLine().getStatusCode() != 200) { - Log.e("ERROR", "Server returned HTTP " + response.getStatusLine().getStatusCode()); - return Collections.emptyList(); - } - - - InputStream stream = response.getEntity().getContent(); - ArrayList items = parseJson(stream, config.getSize(resources)); - stream.close(); - - return items; - } - - private ArrayList parseJson(InputStream stream, int size) throws IOException { - JsonReader reader = new JsonReader(new BufferedReader(new InputStreamReader(stream, "UTF-8"))); - - ArrayList items = new ArrayList<>(); - ArrayList> albums = new ArrayList<>(); - - reader.beginObject(); - boolean isAlbums = reader.nextName().equals("albums"); - reader.beginArray(); - while (reader.hasNext()) { - if (isAlbums) { - albums.add(parseAlbum(reader)); - } else { - items.add(parseItem(reader)); - } - } - reader.endArray(); - reader.endObject(); - - items = selectRandom(items, size); - - ArrayList> randomAlbums = selectRandom(albums, size); - - for (ArrayList album : randomAlbums) { - items.addAll(album); - } - - return items; - } - - public ArrayList selectRandom(ArrayList list, int n) { - ArrayList out = new ArrayList<>(); - for (int i = 0; i < n; i++) { - out.add(list.get(RANDOM.nextInt(list.size() - 1))); - } - - return out; - } - - private ArrayList parseAlbum(JsonReader reader) throws IOException { - reader.beginObject(); - - ArrayList items = new ArrayList<>(); - - while (reader.hasNext()) { - String tag = reader.nextName(); - if (tag.equals("items")) { - reader.beginArray(); - while (reader.hasNext()) { - items.add(parseItem(reader)); - } - reader.endArray(); - } else { - reader.skipValue(); - } - } - - reader.endObject(); - - return items; - } - - private Item parseItem(JsonReader reader) throws IOException { - reader.beginObject(); - Item item = new Item(); - - while (reader.hasNext()) { - String tag = reader.nextName(); - switch (tag) { - case "id": - item.setId(reader.nextInt()); - break; - case "title": - item.setName(reader.nextString()); - break; - case "album": - item.setAlbum(reader.nextString()); - break; - case "artist": - item.setArtist(reader.nextString()); - break; - default: - reader.skipValue(); - break; - } - } - - reader.endObject(); - - return item; - } @Override protected void onHandleIntent(Intent intent) { - broadcast(Message.obtain(null, MSG_STARTED)); - Parcelable[] configs = intent.getParcelableArrayExtra("configs"); - SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(this); - ExecutorService executor = Executors.newFixedThreadPool(Integer.parseInt(globalSettings.getString("threads", "2"))); - String address = globalSettings.getString("address", "127.0.0.1"); - File root = new File(Environment.getExternalStorageDirectory(), "library"); ProgressUpdater updater = new ProgressUpdater(this); + broadcast(Message.obtain(null, MSG_STARTED)); + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + updater.showMessage("No permission to write!", false); + finished(); + return; + } + + SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(this); + int threads = Integer.parseInt(globalSettings.getString("threads", Integer.toString(getResources().getInteger(R.integer.threads)))); + String address = globalSettings.getString("address", getResources().getString(R.string.address)); + + ExecutorService executor = Executors.newFixedThreadPool(threads); + File root = new File(Environment.getExternalStorageDirectory(), "library"); + + BeetsFetcher fetcher = new BeetsFetcher(address, getResources()); + if (root.exists() && !root.isDirectory()) { - updater.showMessage("Library is no dictionary! Fix manually"); + updater.showMessage("Library is no dictionary! Fix manually", false); + finished(); return; } @@ -234,54 +108,62 @@ public class LibraryService extends IntentService { FileTracker tracker = new FileTracker(getApplicationContext()); try { - updater.showMessage("Cleaning library"); + updater.showMessage("Cleaning library", true); tracker.delete(); } catch (IOException e) { e.printStackTrace(); } - if (root.exists() && root.list(NOMEDIA_FILTER).length != 0) { - updater.showMessage("Library not empty! Clean in manually"); + if (root.exists() && root.list().length != 0) { + updater.showMessage("Library not empty! Clean in manually", false); + finished(); return; } + ArrayList tasks = new ArrayList<>(); + for (Parcelable parcelable : configs) { SynchronizeConfig config = (SynchronizeConfig) parcelable; List items; try { - updater.showMessage("Fetching music information for %s", config.getName()); - items = fetchRandom(address, config, getResources()); + updater.showMessage("Fetching music information for %s", true, config.getName()); + items = fetcher.fetch(config); } catch (IOException e) { Log.wtf("WTF", e); - updater.showMessage("Remote not available"); + updater.showMessage("Remote not available", false); + finished(); return; } - updater.showMessage("Mixing new music for %s!", config.getName()); + updater.showMessage("Mixing new music for %s!", true, config.getName()); updater.setMaximumProgress(items.size()); - CountDownLatch itemsLeftLatch = new CountDownLatch(items.size()); - for (Item item : items) { try { - executor.submit(new DownloadTask(new URL(address + "/item/" + item.getId() + "/file").toURI(), item.getArtist() + "/" + item.getAlbum() + "/" + item.getName() + ".mp3", tracker, updater, itemsLeftLatch)); + URI uri = new URI(address + "/item/" + item.getId() + "/file"); + tasks.add(new DownloadTask(config, uri, item.getPath(), tracker, updater)); } catch (URISyntaxException e) { e.printStackTrace(); - } catch (MalformedURLException e) { - e.printStackTrace(); } } - - try { - itemsLeftLatch.await(); - executor.shutdown(); - executor.awaitTermination(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } } - updater.showMessage("Musik aktualisiert"); + CountDownLatch itemsLeftLatch = new CountDownLatch(tasks.size()); + + for (DownloadTask task : tasks) { + task.setItemsLeftLatch(itemsLeftLatch); + executor.submit(task); + } + + try { + itemsLeftLatch.await(); + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + updater.showMessage("Musik aktualisiert", false); // Update last_updated info SharedPreferences preferences = getSharedPreferences("info", MODE_PRIVATE); @@ -293,21 +175,38 @@ public class LibraryService extends IntentService { poweramp.putExtra(PowerampAPI.Scanner.EXTRA_FULL_RESCAN, true); startService(poweramp); + finished(); + } + + public void finished() { broadcast(Message.obtain(null, MSG_FINISHED)); } /** * Handler of incoming messages from clients. */ - private class IncomingHandler extends Handler { + private static class IncomingHandler extends Handler { + + private final WeakReference serviceReference; + + private IncomingHandler(WeakReference serviceReference) { + this.serviceReference = serviceReference; + } + @Override public void handleMessage(Message msg) { + LibraryService service = serviceReference.get(); + + if (service == null) { + return; + } + switch (msg.what) { case MSG_REGISTER_CLIENT: - mClients.add(msg.replyTo); + service.mClients.add(msg.replyTo); break; case MSG_UNREGISTER_CLIENT: - mClients.remove(msg.replyTo); + service.mClients.remove(msg.replyTo); break; case MSG_CANCEL: //todo diff --git a/app/src/main/java/max/music_cyclon/service/ProgressUpdater.java b/app/src/main/java/max/music_cyclon/service/ProgressUpdater.java index 74579e0..03b2e07 100644 --- a/app/src/main/java/max/music_cyclon/service/ProgressUpdater.java +++ b/app/src/main/java/max/music_cyclon/service/ProgressUpdater.java @@ -26,16 +26,15 @@ public class ProgressUpdater { this.notificationManager = NotificationManagerCompat.from(context); } - public void showMessage(String message, Object... args) { - showMessage(String.format(message, args)); + public void showMessage(String message, boolean persist, Object... args) { + showMessage(String.format(message, args), persist); } - public void showMessage(String message) { + public void showMessage(String message, boolean persist) { NotificationCompat.Builder builder = notificationBuilder(); - builder.setContentTitle(message); builder.setContentText(""); - builder.setProgress(0, 0, false); + builder.setProgress(0, 0, !persist); updateNotification(builder); } diff --git a/app/src/main/res/layout/activity_main_preference.xml b/app/src/main/res/layout/activity_main_preference.xml index cd42929..3c51e70 100644 --- a/app/src/main/res/layout/activity_main_preference.xml +++ b/app/src/main/res/layout/activity_main_preference.xml @@ -14,7 +14,7 @@