From 0ec9c1cda5415ec15eeefbb0f21ab80ac4459ee6 Mon Sep 17 00:00:00 2001 From: Max Ammann Date: Wed, 19 Aug 2015 17:48:05 +0200 Subject: [PATCH] Initial commit --- app/.gitignore | 1 + app/app.iml | 98 ++++++++++++ app/build.gradle | 30 ++++ app/proguard-rules.pro | 17 +++ .../max/music_cyclon/ApplicationTest.java | 13 ++ app/src/main/AndroidManifest.xml | 45 ++++++ .../java/max/music_cyclon/ControlWidget.java | 37 +++++ .../java/max/music_cyclon/FileTracker.java | 85 +++++++++++ .../java/max/music_cyclon/LibraryService.java | 144 ++++++++++++++++++ .../music_cyclon/PowerConnectionReceiver.java | 30 ++++ .../java/max/music_cyclon/ProcessTask.java | 111 ++++++++++++++ .../max/music_cyclon/SettingsActivity.java | 24 +++ .../example_appwidget_preview.png | Bin 0 -> 3522 bytes app/src/main/res/layout/activity_settings.xml | 17 +++ app/src/main/res/layout/control_widget.xml | 21 +++ app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes app/src/main/res/values-v14/dimens.xml | 10 ++ app/src/main/res/values-v21/styles.xml | 5 + app/src/main/res/values-w820dp/dimens.xml | 6 + app/src/main/res/values/dimens.xml | 11 ++ app/src/main/res/values/strings.xml | 6 + app/src/main/res/values/styles.xml | 8 + app/src/main/res/xml/new_app_widget_info.xml | 10 ++ build.gradle | 19 +++ gradle.properties | 18 +++ gradle/wrapper/gradle-wrapper.properties | 6 + settings.gradle | 1 + 30 files changed, 773 insertions(+) create mode 100644 app/.gitignore create mode 100644 app/app.iml create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/max/music_cyclon/ApplicationTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/max/music_cyclon/ControlWidget.java create mode 100644 app/src/main/java/max/music_cyclon/FileTracker.java create mode 100644 app/src/main/java/max/music_cyclon/LibraryService.java create mode 100644 app/src/main/java/max/music_cyclon/PowerConnectionReceiver.java create mode 100644 app/src/main/java/max/music_cyclon/ProcessTask.java create mode 100644 app/src/main/java/max/music_cyclon/SettingsActivity.java create mode 100644 app/src/main/res/drawable-nodpi/example_appwidget_preview.png create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 app/src/main/res/layout/control_widget.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/values-v14/dimens.xml create mode 100644 app/src/main/res/values-v21/styles.xml create mode 100644 app/src/main/res/values-w820dp/dimens.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/new_app_widget_info.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 settings.gradle diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..eaf5b40 --- /dev/null +++ b/app/app.iml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..28a66fe --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + applicationId "max.music_cyclon" + minSdkVersion 21 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.android.support:appcompat-v7:22.0.0' + compile 'commons-io:commons-io:2.4' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..d253434 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/max/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/max/music_cyclon/ApplicationTest.java b/app/src/androidTest/java/max/music_cyclon/ApplicationTest.java new file mode 100644 index 0000000..004baae --- /dev/null +++ b/app/src/androidTest/java/max/music_cyclon/ApplicationTest.java @@ -0,0 +1,13 @@ +package max.music_cyclon; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9e82dc8 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/max/music_cyclon/ControlWidget.java b/app/src/main/java/max/music_cyclon/ControlWidget.java new file mode 100644 index 0000000..8b07785 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/ControlWidget.java @@ -0,0 +1,37 @@ +package max.music_cyclon; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.widget.RemoteViews; + + + +public class ControlWidget extends AppWidgetProvider { + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + + static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, + int appWidgetId) { + + CharSequence widgetText = context.getString(R.string.control_button); + + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.control_widget); + views.setTextViewText(R.id.control_button, widgetText); + + Intent intent = new Intent(context, LibraryService.class); + PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); + views.setOnClickPendingIntent(R.id.control_button, pendingIntent); + + + appWidgetManager.updateAppWidget(appWidgetId, views); + } +} + diff --git a/app/src/main/java/max/music_cyclon/FileTracker.java b/app/src/main/java/max/music_cyclon/FileTracker.java new file mode 100644 index 0000000..9acca9b --- /dev/null +++ b/app/src/main/java/max/music_cyclon/FileTracker.java @@ -0,0 +1,85 @@ +package max.music_cyclon; + + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Map; +import java.util.zip.Adler32; + +public class FileTracker { + + private SharedPreferences.Editor editor; + private SharedPreferences preferences; + + @SuppressLint("CommitPrefEdits") + public FileTracker(SharedPreferences preferences) { + this.preferences = preferences; + this.editor = preferences.edit(); + } + + public void track(File file, long checksum) { + editor.putLong(file.getAbsolutePath(), checksum); + } + + public void delete() throws IOException { + Map preferences = this.preferences.getAll(); + + for (Map.Entry entry : preferences.entrySet()) { + if (!(entry.getValue() instanceof Long)) { + continue; + } + + File file = new File(entry.getKey()); + + if (((Long) entry.getValue()) != checksum(file)) { + continue; + } + + removeFile(file); + } + } + + private long checksum(File file) throws IOException { + if (!file.exists()) { + return 0; + } + + Adler32 checksum = new Adler32(); + byte[] buffer = new byte[4 * 1024]; + int n; + + FileInputStream input = FileUtils.openInputStream(file); + + while (-1 != (n = input.read(buffer))) { + checksum.update(buffer, 0, n); + } + + input.close(); + + return checksum.getValue(); + } + + public void removeFile(File path) throws IOException { + if (path == null) return; + + if (path.isFile()) { + FileUtils.deleteQuietly(path); + } else if (path.isDirectory()) { + if (!path.delete()) { + return; + } + } + + removeFile(path.getParentFile()); + } + + public void commit() { + editor.apply(); + } +} diff --git a/app/src/main/java/max/music_cyclon/LibraryService.java b/app/src/main/java/max/music_cyclon/LibraryService.java new file mode 100644 index 0000000..226ec51 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/LibraryService.java @@ -0,0 +1,144 @@ +package max.music_cyclon; + +import android.app.IntentService; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Environment; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.util.JsonReader; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +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 java.util.concurrent.atomic.AtomicInteger; + +public class LibraryService extends IntentService { + + private final String host; + private final int port; + + public static int NOTIFICATION_ID = new Random().nextInt(); + + public LibraryService() { + this("max-arch", 5000); + } + + public LibraryService(String host, int port) { + super("max.music_cyclon.LibraryService"); + this.host = host; + this.port = port; + } + + public List fetchRandom(int amount) throws IOException { + URL url = new URL("http", host, port, "/random/" + amount); + InputStream in = url.openStream(); + + JsonReader reader = new JsonReader(new BufferedReader(new InputStreamReader(in, "UTF-8"))); + + ArrayList items = new ArrayList<>(); + + reader.beginArray(); + while (reader.hasNext()) { + items.add(reader.nextString()); + } + reader.endArray(); + + return items; + } + + private NotificationCompat.Builder notificationBuilder() { + return new NotificationCompat.Builder(this) + .setSmallIcon(R.mipmap.ic_launcher); + } + + + private NotificationCompat.Builder progressNotificationBuilder() { + return notificationBuilder().setUsesChronometer(true) + .setOngoing(true) + .setProgress(0, 0, true); + } + + + @Override + protected void onHandleIntent(Intent intent) { + ExecutorService executor = Executors.newFixedThreadPool(4); + + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + NotificationCompat.Builder builder = progressNotificationBuilder().setContentTitle("Aktualisiere Musik"); + File root = new File(Environment.getExternalStorageDirectory(), "library"); + + if (root.exists() && !root.isDirectory()) { + notificationManager.notify(NOTIFICATION_ID, notificationBuilder().setContentTitle("Library is no dictionary! Fix manually").build()); + return; + } + + AtomicInteger current = new AtomicInteger(); + + FileTracker tracker = new FileTracker(getSharedPreferences("library", MODE_PRIVATE)); + + + List items; + try { + builder.setContentTitle("Fetching music information"); + notificationManager.notify(NOTIFICATION_ID, builder.build()); + items = fetchRandom(1000); + + builder.setContentTitle("Cleaning library"); + notificationManager.notify(NOTIFICATION_ID, builder.build()); + tracker.delete(); + } catch (IOException e) { + Log.wtf("WTF", e); + notificationManager.cancel(NOTIFICATION_ID); + return; + } + + + if (root.exists() && root.list().length != 0) { + notificationManager.notify(NOTIFICATION_ID, notificationBuilder().setContentTitle("Library not empty! Clean in manually").build()); + return; + } + + builder.setContentTitle("Mixing new music!"); +// builder.setProgress(1, 0, false); + notificationManager.notify(NOTIFICATION_ID, builder.build()); + + final CountDownLatch latch = new CountDownLatch(items.size()); + + + for (int i = 0, size = items.size(); i < size; i++) { + executor.submit(new ProcessTask(latch, items.get(i), tracker, + current, items.size(), builder, notificationManager)); + } + + try { + latch.await(); + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + tracker.commit(); + + notificationManager.cancel(NOTIFICATION_ID); + + SharedPreferences preferences = getSharedPreferences("info", MODE_PRIVATE); + preferences.edit().putLong("last_updated", System.currentTimeMillis()).apply(); + + + notificationManager.notify(NOTIFICATION_ID, notificationBuilder().setContentTitle("Musik aktualisiert").build()); + } +} diff --git a/app/src/main/java/max/music_cyclon/PowerConnectionReceiver.java b/app/src/main/java/max/music_cyclon/PowerConnectionReceiver.java new file mode 100644 index 0000000..4a94691 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/PowerConnectionReceiver.java @@ -0,0 +1,30 @@ +package max.music_cyclon; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; + +public class PowerConnectionReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + Intent batteryStatus = context.registerReceiver(null, ifilter); + + + int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + + boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL; + boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC; + + if (acCharge && isCharging) { +// Intent serviceIntend = new Intent(context, LibraryService.class); +// +// context.startService(serviceIntend); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/max/music_cyclon/ProcessTask.java b/app/src/main/java/max/music_cyclon/ProcessTask.java new file mode 100644 index 0000000..52b0825 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/ProcessTask.java @@ -0,0 +1,111 @@ +package max.music_cyclon; + +import android.os.Environment; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.util.Log; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.Adler32; + +public class ProcessTask implements Runnable { + + private final CountDownLatch latch; + + private final String item; + private final FileTracker tracker; + + private final AtomicInteger current; + private final int maximum; + private final NotificationCompat.Builder builder; + private final NotificationManagerCompat notificationManager; + + public ProcessTask(CountDownLatch latch, String item, FileTracker tracker, + AtomicInteger current, int maximum,NotificationCompat.Builder builder, NotificationManagerCompat notificationCompat) { + this.latch = latch; + this.item = item; + + this.tracker = tracker; + this.current = current; + this.maximum = maximum; + this.builder = builder; + this.notificationManager = notificationCompat; + } + + @Override + public void run() { + File root = new File(Environment.getExternalStorageDirectory(), "library"); + + try { + File target = new File(root, item); + Adler32 checksum = new Adler32(); + +// builder.setContentText(item); +// notificationManager.notify(NOTIFICATION_ID, builder.build()); + + + HttpURLConnection connection = prepareConnection(item); + + if (connection == null) { + return; + } + + + InputStream input = connection.getInputStream(); + FileOutputStream output = FileUtils.openOutputStream(target); + + byte[] buffer = new byte[4 * 1024]; + int n; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + checksum.update(buffer, 0, n); + } + + output.flush(); + output.close(); + input.close(); + + tracker.track(target, checksum.getValue()); + + latch.countDown(); + + int current = this.current.incrementAndGet(); + builder.setProgress(maximum, current, false); + builder.setContentText(current + "/" + maximum); + notificationManager.notify(LibraryService.NOTIFICATION_ID, builder.build()); + } catch (IOException e) { + Log.wtf("WTF", e); + } + } + + public HttpURLConnection prepareConnection(String item) throws IOException { + + URL url = new URL("http", "max-arch", 5000, "/get"); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + + PrintWriter output = new PrintWriter(connection.getOutputStream()); + output.write(item); + output.flush(); + + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + Log.e("ERROR", "Server returned HTTP " + connection.getResponseCode() + + " " + connection.getResponseMessage()); + return null; + } + + return connection; + } +} diff --git a/app/src/main/java/max/music_cyclon/SettingsActivity.java b/app/src/main/java/max/music_cyclon/SettingsActivity.java new file mode 100644 index 0000000..26581c1 --- /dev/null +++ b/app/src/main/java/max/music_cyclon/SettingsActivity.java @@ -0,0 +1,24 @@ +package max.music_cyclon; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + + +public class SettingsActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + + findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(SettingsActivity.this, LibraryService.class); + startService(intent); + } + }); + } +} diff --git a/app/src/main/res/drawable-nodpi/example_appwidget_preview.png b/app/src/main/res/drawable-nodpi/example_appwidget_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..894b069a4907d258f60b1b2406b90f5a0fe1c35b GIT binary patch literal 3522 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 + +