isundil 6 年之前
父节点
当前提交
c491edb519

+ 3 - 0
.idea/gradle.xml

@@ -3,6 +3,9 @@
   <component name="GradleSettings">
     <option name="linkedExternalProjectsSettings">
       <GradleProjectSettings>
+        <compositeConfiguration>
+          <compositeBuild compositeDefinitionSource="SCRIPT" />
+        </compositeConfiguration>
         <option name="distributionType" value="DEFAULT_WRAPPED" />
         <option name="externalProjectPath" value="$PROJECT_DIR$" />
         <option name="modules">

+ 29 - 0
.idea/misc.xml

@@ -1,5 +1,34 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
+  <component name="NullableNotNullManager">
+    <option name="myDefaultNullable" value="android.support.annotation.Nullable" />
+    <option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
+    <option name="myNullables">
+      <value>
+        <list size="7">
+          <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
+          <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
+          <item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
+          <item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
+          <item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
+          <item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
+          <item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
+        </list>
+      </value>
+    </option>
+    <option name="myNotNulls">
+      <value>
+        <list size="6">
+          <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
+          <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
+          <item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
+          <item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
+          <item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
+          <item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
+        </list>
+      </value>
+    </option>
+  </component>
   <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 6 - 2
app/build.gradle

@@ -4,7 +4,7 @@ android {
     compileSdkVersion 28
     defaultConfig {
         applicationId "homespeakers.knacki.info.homespeakersandroid"
-        minSdkVersion 14
+        minSdkVersion 16
         targetSdkVersion 28
         versionCode 1
         versionName "1.0"
@@ -16,10 +16,14 @@ android {
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
     }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
 }
 
 dependencies {
-    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
     implementation 'com.android.support:appcompat-v7:28.0.0'
     implementation 'com.android.support:support-v4:28.0.0'
     implementation 'com.android.support:support-vector-drawable:28.0.0'

+ 6 - 0
app/src/main/AndroidManifest.xml

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="homespeakers.knacki.info.homespeakersandroid">
+    <uses-permission android:name="android.permission.INTERNET"/>
 
     <application
         android:allowBackup="true"
@@ -18,6 +19,11 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <service
+            android:name=".Service"
+            android:label="@string/app_name"
+            android:enabled="true">
+        </service>
     </application>
 
 </manifest>

+ 2 - 0
app/src/main/java/homespeakers/knacki/info/homespeakersandroid/AppCompatPreferenceActivity.java

@@ -1,5 +1,6 @@
 package homespeakers.knacki.info.homespeakersandroid;
 
+import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.preference.PreferenceActivity;
@@ -22,6 +23,7 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        startService(new Intent(this, Service.class));
         getDelegate().installViewFactory();
         getDelegate().onCreate(savedInstanceState);
         super.onCreate(savedInstanceState);

+ 153 - 0
app/src/main/java/homespeakers/knacki/info/homespeakersandroid/Service.java

@@ -0,0 +1,153 @@
+package homespeakers.knacki.info.homespeakersandroid;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.v4.app.NotificationCompat;
+import android.util.JsonReader;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import homespeakers.knacki.info.homespeakersandroid.radio.Input;
+import homespeakers.knacki.info.homespeakersandroid.radio.InputPlayer;
+import homespeakers.knacki.info.homespeakersandroid.radio.InputReader;
+
+public class Service extends android.app.Service {
+    public final static String NOTIFICATION_CHANNEL_ID = "info.knacki.homespeakers.service";
+    public final static int NOTIFICATION_ID = NOTIFICATION_CHANNEL_ID.hashCode();
+    public final static long RECONNECT_DELAY = 60000; // 1 minute
+    public final static String ADDRESS = "192.168.0.5";
+    public final static short PORT = 9001;
+
+    private final static Logger log = Logger.getLogger(Service.class.getName());
+    private Collection<InputPlayer> playingInputs = new ArrayDeque<>();
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        Intent notificationIntent = new Intent(this, Service.class);
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(this, 0, notificationIntent, 0);
+
+        Notification notification =
+                new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
+                        .setContentTitle(getText(R.string.app_name))
+                        .setContentText(getText(R.string.app_name))
+                        .setSmallIcon(R.drawable.ic_launcher_background)
+                        .setContentIntent(pendingIntent)
+                        .setTicker(getText(R.string.app_name))
+                        .build();
+
+        Logger.getAnonymousLogger().severe("Service started !");
+        startForeground(NOTIFICATION_ID, notification);
+
+        new Thread() {
+            @Override
+            public void run() {
+                createSocket();
+            }
+        }.start();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    public String getName() {
+        final String value = PreferenceManager.getDefaultSharedPreferences(this).getString("name", null);
+        return value == null || value.isEmpty() ? null : value;
+    }
+
+    public void setInputs(Collection<Input> inputs) {
+        oldLoop: for (InputPlayer i : playingInputs) {
+            for (Input j : inputs) {
+                if (i.fName.equals(j.fName)) {
+                    if (!j.isActive()) {
+                        // No longer playing source, kill it
+                        i.stop();
+                        playingInputs.remove(i);
+                    }
+                    continue oldLoop;
+                }
+            }
+            // Source removed from server, kill it
+            i.stop();
+            playingInputs.remove(i);
+        }
+        newLoop: for (Input i : inputs) {
+            if (i.isActive()) {
+                for (InputPlayer j : playingInputs)
+                    if (i.fName.equals(j.fName))
+                        continue newLoop;
+                // New source, add it
+                InputPlayer player = new InputPlayer(i);
+                player.start();
+                playingInputs.add(player);
+            }
+        }
+    }
+
+    public void manageSocket(BufferedReader stream) throws IOException {
+        while (true) {
+            String config = stream.readLine();
+            if (config == null)
+                return;
+            Collection<Input> inputs = InputReader.Read(new JsonReader(new StringReader(config)));
+            setInputs(inputs);
+        }
+    }
+
+    private static Socket fSock;
+
+    private static void sendName(String name) {
+        if (fSock == null) return;
+        try {
+            fSock.getOutputStream().write(("HELO " +name + "\n").getBytes("utf-8"));
+        }
+        catch (IOException e) {}
+    }
+
+    public void createSocket() {
+        fSock = null;
+        while (true) {
+            try {
+                fSock = new Socket();
+                log.info("Connecting to " +ADDRESS +" port " +PORT);
+                fSock.connect(new InetSocketAddress(InetAddress.getByName(ADDRESS), PORT));
+                String name = getName();
+                if (name != null)
+                    sendName(name);
+                manageSocket(new BufferedReader(new InputStreamReader(fSock.getInputStream())));
+            } catch (IOException e) {
+                fSock = null;
+                log.log(Level.SEVERE, "Cannot connect to server", e);
+                try {
+                    Thread.sleep(RECONNECT_DELAY);
+                }
+                catch (InterruptedException e2) {}
+            }
+        }
+    }
+
+    public static void setName(String value) {
+        new Thread() {
+            @Override
+            public void run() {
+                sendName(value);
+            }
+        }.start();
+    }
+}

+ 6 - 69
app/src/main/java/homespeakers/knacki/info/homespeakersandroid/SettingsActivity.java

@@ -82,6 +82,9 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 // simple string representation.
                 preference.setSummary(stringValue);
             }
+            if (preference.hasKey() && preference.getKey().equals("name")) {
+                Service.setName(stringValue);
+            }
             return true;
         }
     };
@@ -141,9 +144,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
         return isXLargeTablet(this);
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     @TargetApi(Build.VERSION_CODES.HONEYCOMB)
     public void onBuildHeaders(List<Header> target) {
@@ -155,10 +155,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
      * Make sure to deny any unknown fragments here.
      */
     protected boolean isValidFragment(String fragmentName) {
-        return PreferenceFragment.class.getName().equals(fragmentName)
-                || GeneralPreferenceFragment.class.getName().equals(fragmentName)
-                || DataSyncPreferenceFragment.class.getName().equals(fragmentName)
-                || NotificationPreferenceFragment.class.getName().equals(fragmentName);
+        return PreferenceFragment.class.getName().equals(fragmentName) ||
+            GeneralPreferenceFragment.class.getName().equals(fragmentName);
     }
 
     /**
@@ -177,68 +175,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
             // to their values. When their values change, their summaries are
             // updated to reflect the new value, per the Android Design
             // guidelines.
-            bindPreferenceSummaryToValue(findPreference("example_text"));
-            bindPreferenceSummaryToValue(findPreference("example_list"));
-        }
-
-        @Override
-        public boolean onOptionsItemSelected(MenuItem item) {
-            int id = item.getItemId();
-            if (id == android.R.id.home) {
-                startActivity(new Intent(getActivity(), SettingsActivity.class));
-                return true;
-            }
-            return super.onOptionsItemSelected(item);
-        }
-    }
-
-    /**
-     * This fragment shows notification preferences only. It is used when the
-     * activity is showing a two-pane settings UI.
-     */
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
-    public static class NotificationPreferenceFragment extends PreferenceFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            addPreferencesFromResource(R.xml.pref_notification);
-            setHasOptionsMenu(true);
-
-            // Bind the summaries of EditText/List/Dialog/Ringtone preferences
-            // to their values. When their values change, their summaries are
-            // updated to reflect the new value, per the Android Design
-            // guidelines.
-            bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
-        }
-
-        @Override
-        public boolean onOptionsItemSelected(MenuItem item) {
-            int id = item.getItemId();
-            if (id == android.R.id.home) {
-                startActivity(new Intent(getActivity(), SettingsActivity.class));
-                return true;
-            }
-            return super.onOptionsItemSelected(item);
-        }
-    }
-
-    /**
-     * This fragment shows data and sync preferences only. It is used when the
-     * activity is showing a two-pane settings UI.
-     */
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
-    public static class DataSyncPreferenceFragment extends PreferenceFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            addPreferencesFromResource(R.xml.pref_data_sync);
-            setHasOptionsMenu(true);
-
-            // Bind the summaries of EditText/List/Dialog/Ringtone preferences
-            // to their values. When their values change, their summaries are
-            // updated to reflect the new value, per the Android Design
-            // guidelines.
-            bindPreferenceSummaryToValue(findPreference("sync_frequency"));
+            bindPreferenceSummaryToValue(findPreference("name"));
         }
 
         @Override

+ 17 - 0
app/src/main/java/homespeakers/knacki/info/homespeakersandroid/radio/Input.java

@@ -0,0 +1,17 @@
+package homespeakers.knacki.info.homespeakersandroid.radio;
+
+public class Input {
+    public final String fName;
+    String fAddress;
+    boolean fActive;
+
+    Input(String name) {
+        fName = name;
+        fAddress = null;
+        fActive = false;
+    }
+
+    public boolean isActive() {
+        return fActive;
+    }
+}

+ 75 - 0
app/src/main/java/homespeakers/knacki/info/homespeakersandroid/radio/InputPlayer.java

@@ -0,0 +1,75 @@
+package homespeakers.knacki.info.homespeakersandroid.radio;
+
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class InputPlayer implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnCompletionListener {
+    private final static Logger log = Logger.getLogger(Input.class.getName());
+
+    public final String fName;
+    private final String fUrl;
+    private MediaPlayer fPlayer;
+
+    public InputPlayer(Input in) {
+        fName = in.fName;
+        fUrl = in.fAddress;
+    }
+
+    public void start() {
+        log.warning("Start source " + this.fName +" on resource " +this.fUrl);
+        fPlayer = null;
+        try {
+            fPlayer = new MediaPlayer();
+            fPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+            fPlayer.setDataSource(fUrl);
+            fPlayer.setOnPreparedListener(this);
+            fPlayer.setOnErrorListener(this);
+            fPlayer.setOnBufferingUpdateListener(this);
+            fPlayer.setOnCompletionListener(this);
+            fPlayer.prepareAsync();
+        } catch (IOException e) {
+            log.log(Level.SEVERE, "Cannot read source", e);
+            try {
+                Thread.sleep(30 * 1000);
+            } catch (InterruptedException e1) {
+                if (fPlayer != null)
+                    fPlayer.release();
+            }
+        }
+        fPlayer.start();
+    }
+
+    public void stop() {
+        if (fPlayer != null) {
+            fPlayer.stop();
+            fPlayer.release();
+            fPlayer = null;
+        }
+    }
+
+    @Override
+    public void onPrepared(MediaPlayer mp) {
+        if (fPlayer != null)
+            fPlayer.start();
+    }
+
+    @Override
+    public boolean onError(MediaPlayer mp, int what, int extra) {
+        log.severe("Error " +what +" " +extra);
+        return true;
+    }
+
+    @Override
+    public void onBufferingUpdate(MediaPlayer mp, int percent) {
+        log.warning("on buffering update: " +percent);
+    }
+
+    @Override
+    public void onCompletion(MediaPlayer mp) {
+        log.warning("on Completion called");
+    }
+}

+ 38 - 0
app/src/main/java/homespeakers/knacki/info/homespeakersandroid/radio/InputReader.java

@@ -0,0 +1,38 @@
+package homespeakers.knacki.info.homespeakersandroid.radio;
+
+import android.util.JsonReader;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Collection;
+
+public class InputReader {
+    private InputReader(){}
+
+    private static Input ReadNext(JsonReader reader) throws IOException {
+        Input result = new Input(reader.nextName());
+        reader.beginObject();
+        while (reader.hasNext()) {
+            switch (reader.nextName()) {
+                case "address":
+                    result.fAddress = reader.nextString();
+                    break;
+                case "state":
+                    result.fActive = reader.nextBoolean();
+                    break;
+            }
+        }
+        reader.endObject();
+        return result;
+    }
+
+    public static Collection<Input> Read(JsonReader reader) throws IOException {
+        Collection<Input> result = new ArrayDeque<>();
+        reader.beginObject();
+        while (reader.hasNext()) {
+            result.add(ReadNext(reader));
+        }
+        reader.endObject();
+        return result;
+    }
+}

+ 0 - 21
app/src/main/res/xml/pref_data_sync.xml

@@ -1,21 +0,0 @@
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <!-- NOTE: Hide buttons to simplify the UI. Users can touch outside the dialog to
-         dismiss it. -->
-    <!-- NOTE: ListPreference's summary should be set to its value by the activity code. -->
-    <ListPreference
-        android:defaultValue="180"
-        android:entries="@array/pref_sync_frequency_titles"
-        android:entryValues="@array/pref_sync_frequency_values"
-        android:key="sync_frequency"
-        android:negativeButtonText="@null"
-        android:positiveButtonText="@null"
-        android:title="@string/pref_title_sync_frequency" />
-
-    <!-- This preference simply launches an intent when selected. Use this UI sparingly, per
-         design guidelines. -->
-    <Preference android:title="@string/pref_title_system_sync_settings">
-        <intent android:action="android.settings.SYNC_SETTINGS" />
-    </Preference>
-
-</PreferenceScreen>

+ 1 - 24
app/src/main/res/xml/pref_general.xml

@@ -1,33 +1,10 @@
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <SwitchPreference
-        android:defaultValue="true"
-        android:key="example_switch"
-        android:summary="@string/pref_description_social_recommendations"
-        android:title="@string/pref_title_social_recommendations" />
-
-    <!-- NOTE: EditTextPreference accepts EditText attributes. -->
-    <!-- NOTE: EditTextPreference's summary should be set to its value by the activity code. -->
     <EditTextPreference
         android:capitalize="words"
-        android:defaultValue="@string/pref_default_display_name"
         android:inputType="textCapWords"
-        android:key="example_text"
+        android:key="name"
         android:maxLines="1"
         android:selectAllOnFocus="true"
         android:singleLine="true"
         android:title="@string/pref_title_display_name" />
-
-    <!-- NOTE: Hide buttons to simplify the UI. Users can touch outside the dialog to
-         dismiss it. -->
-    <!-- NOTE: ListPreference's summary should be set to its value by the activity code. -->
-    <ListPreference
-        android:defaultValue="-1"
-        android:entries="@array/pref_example_list_titles"
-        android:entryValues="@array/pref_example_list_values"
-        android:key="example_list"
-        android:negativeButtonText="@null"
-        android:positiveButtonText="@null"
-        android:title="@string/pref_title_add_friends_to_messages" />
-
 </PreferenceScreen>

+ 0 - 11
app/src/main/res/xml/pref_headers.xml

@@ -6,15 +6,4 @@
         android:fragment="homespeakers.knacki.info.homespeakersandroid.SettingsActivity$GeneralPreferenceFragment"
         android:icon="@drawable/ic_info_black_24dp"
         android:title="@string/pref_header_general" />
-
-    <header
-        android:fragment="homespeakers.knacki.info.homespeakersandroid.SettingsActivity$NotificationPreferenceFragment"
-        android:icon="@drawable/ic_notifications_black_24dp"
-        android:title="@string/pref_header_notifications" />
-
-    <header
-        android:fragment="homespeakers.knacki.info.homespeakersandroid.SettingsActivity$DataSyncPreferenceFragment"
-        android:icon="@drawable/ic_sync_black_24dp"
-        android:title="@string/pref_header_data_sync" />
-
 </preference-headers>

+ 0 - 27
app/src/main/res/xml/pref_notification.xml

@@ -1,27 +0,0 @@
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <!-- A 'parent' preference, which enables/disables child preferences (below)
-         when checked/unchecked. -->
-    <SwitchPreference
-        android:defaultValue="true"
-        android:key="notifications_new_message"
-        android:title="@string/pref_title_new_message_notifications" />
-
-    <!-- Allows the user to choose a ringtone in the 'notification' category. -->
-    <!-- NOTE: This preference will be enabled only when the checkbox above is checked. -->
-    <!-- NOTE: RingtonePreference's summary should be set to its value by the activity code. -->
-    <RingtonePreference
-        android:defaultValue="content://settings/system/notification_sound"
-        android:dependency="notifications_new_message"
-        android:key="notifications_new_message_ringtone"
-        android:ringtoneType="notification"
-        android:title="@string/pref_title_ringtone" />
-
-    <!-- NOTE: This preference will be enabled only when the checkbox above is checked. -->
-    <SwitchPreference
-        android:defaultValue="true"
-        android:dependency="notifications_new_message"
-        android:key="notifications_new_message_vibrate"
-        android:title="@string/pref_title_vibrate" />
-
-</PreferenceScreen>

+ 1 - 1
build.gradle

@@ -7,7 +7,7 @@ buildscript {
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.2.1'
+        classpath 'com.android.tools.build:gradle:3.3.0'
         
 
         // NOTE: Do not place your application dependencies here; they belong

+ 2 - 1
gradle/wrapper/gradle-wrapper.properties

@@ -1,5 +1,6 @@
+#Sun Jan 20 15:19:52 CET 2019
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip