├── .gitignore ├── AndroidManifest.xml ├── LICENSE.txt ├── Makefile ├── Makefile.test ├── README.md ├── RELEASE.txt ├── TODO.txt ├── build.xml ├── google-play-store ├── .gitignore ├── AndroidManifest.xml ├── Makefile ├── build.xml ├── proguard-project.txt ├── project.properties ├── res │ └── values │ │ └── strings.xml └── src │ └── .nothing ├── icons ├── intent_radio_large_1024x1024.png └── intent_radio_large_1024x500-0.png ├── ir_library ├── .gitignore ├── AndroidManifest.xml ├── Makefile ├── ant.properties ├── build.xml ├── misc │ ├── Metadata.java │ ├── Now.java │ ├── PlaylistM3u.java │ ├── PlaylistPls.java │ ├── Radio.prj.xml │ └── old-README.md ├── proguard-project.txt ├── project.properties ├── res │ ├── drawable-hdpi │ │ └── intent_radio.png │ ├── drawable-ldpi │ │ └── intent_radio.png │ ├── drawable-mdpi │ │ └── intent_radio.png │ ├── drawable-xhdpi │ │ └── intent_radio.png │ ├── drawable-xxhdpi │ │ └── intent_radio.png │ ├── drawable-xxxhdpi │ │ └── intent_radio.png │ ├── layout │ │ ├── buttons.xml │ │ ├── main.xml │ │ └── prefs.xml │ ├── menu │ │ └── prefs.xml │ ├── raw │ │ ├── message.html │ │ ├── playing.html │ │ └── tasker.prj │ └── values │ │ ├── strings.xml │ │ └── version.xml └── src │ └── org │ └── smblott │ └── intentradio │ ├── .gitignore │ ├── Build.java │ ├── ClipButtons.java │ ├── Clipper.java │ ├── Connectivity.java │ ├── CopyResource.java │ ├── Counter.java │ ├── HttpGetter.java │ ├── IntentPlayer.java │ ├── IntentRadio.java │ ├── Intents.java │ ├── Later.java │ ├── Logger.java │ ├── Makefile │ ├── Notify.java │ ├── Playlist.java │ ├── PreferenceActivity.java │ ├── Prefs.java │ ├── ReadRawTextFile.java │ ├── State.java │ └── WifiLocker.java ├── proguard-project.txt ├── project.properties ├── res └── values │ └── strings.xml ├── script └── version.sh ├── src └── .nothing ├── tasker_scripts ├── .gitignore ├── Makefile └── playlist └── web ├── .gitignore ├── IR_Playlist.prj.xml ├── Makefile ├── debug.ascii ├── favicon.ico ├── index.ascii ├── intent_radio.png ├── playlist.ascii ├── privacy.ascii ├── release.ascii └── tasker.ascii /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | gen/ 3 | local.properties 4 | player 5 | releases 6 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Stephen Blott 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | srv = smblott.org 3 | www = /home/www/smblott.org/intent_radio 4 | 5 | versioncode = $(shell sh ./script/version.sh) 6 | 7 | debug: ir_library/res/raw/tasker.prj 8 | ant debug 9 | 10 | release: ir_library/res/raw/tasker.prj 11 | $(MAKE) clean 12 | ant release 13 | mkdir -p releases 14 | install -v -m 0444 bin/IntentRadio-release.apk releases/IntentRadio-release-general-$(versioncode).apk 15 | rsync -v releases/*.apk $(HOME)/storage/Dropbox/Public/ 16 | rsync -v bin/IntentRadio-release.apk $(srv):$(www) 17 | 18 | clean: 19 | cd ./ir_library && ant clean 20 | cd ./google-play-store && ant clean 21 | ant clean 22 | 23 | install: 24 | $(MAKE) debug 25 | adb install -r bin/IntentRadio-debug.apk 26 | 27 | install-release: 28 | $(MAKE) release 29 | adb install -r bin/IntentRadio-release.apk 30 | 31 | update-project: 32 | android update project --name "IntentRadio" --target android-19 --path . --subprojects 33 | cd ./ir_library/ && $(MAKE) update-project 34 | 35 | google: 36 | cd ./google-play-store && $(MAKE) debug 37 | 38 | google-release: 39 | cd ./google-play-store && $(MAKE) release 40 | 41 | logcat: 42 | adb logcat 43 | 44 | log: 45 | adb logcat -s IntentRadio -s MediaPlayer 46 | 47 | logfile: 48 | adb shell cat /sdcard/Android/data/org.smblott.intentradio/files/intent-radio.log 49 | 50 | ir_library/res/raw/tasker.prj: ./ir_library/misc/Radio.prj.xml 51 | cd ./ir_library/ && make res/raw/tasker.prj 52 | 53 | version: 54 | @echo $(versioncode) 55 | 56 | .PHONY: debug release clean install install-release update-project logcat log google google-release version 57 | 58 | include ./Makefile.test 59 | 60 | # Release process: 61 | # 62 | # - bump: 63 | # - version code and name in BOTH ./AndroidManifest.xml and ./google-play-store/AndroidManifest.xml 64 | # - update release notes in web/index.ascii: 65 | # make install 66 | # - git commit; git push 67 | # - gtag vX.Y.Z 68 | # - build release APKs: 69 | # make release 70 | # make google-release 71 | # - F-Droid: 72 | # - just updating the tag (above) seems to trigger F-Droid to fetch 73 | # and build the new version 74 | # - Google Play Store: 75 | # - upload new APK 76 | # 77 | 78 | -------------------------------------------------------------------------------- /Makefile.test: -------------------------------------------------------------------------------- 1 | 2 | broadcast = adb shell am broadcast -a 3 | debug = -e debug yes 4 | 5 | play = $(broadcast) org.smblott.intentradio.PLAY $(debug) 6 | stop = $(broadcast) org.smblott.intentradio.STOP 7 | pause = $(broadcast) org.smblott.intentradio.PAUSE 8 | restart = $(broadcast) org.smblott.intentradio.RESTART 9 | click = $(broadcast) org.smblott.intentradio.CLICK 10 | 11 | play: 12 | $(play) 13 | 14 | stop: 15 | $(stop) 16 | 17 | r4: 18 | $(play) -e url http://www.bbc.co.uk/mediaselector/playlists/hls/radio/nonuk/lo/ak/bbc_radio_fourfm.m3u8 -e name "BBC Radio 4 (Fast)" 19 | 20 | # This blocks for quite some time, and cannot be cancelled until buffering has 21 | # completed; Android bug. 22 | r4aac: 23 | $(play) -e url http://www.bbc.co.uk/radio/listen/live/r4_heaacv2.pls -e name "BBC Radio 4" 24 | 25 | ufm: 26 | $(play) -e url http://192.168.3.3/cgi-bin/sc/wav -e name "Elsa Sound Card" 27 | 28 | wnyc: 29 | $(play) -e url http://www.wnyc.org/stream/fm.pls -e name "WNYC" 30 | 31 | newstalk: 32 | $(play) -e url http://communicorp.mp3.miisolutions.net:8000/communicorp/Newstalk_low.m3u -e name "Newstalk" 33 | 34 | lyric: 35 | $(play) -e url http://icecast2.rte.ie/lyric -e name "RTE Lyric FM" 36 | 37 | shoutcast: 38 | $(play) -e url 'http://yp.shoutcast.com/sbin/tunein-station.pls?id=230816' -e name "Shoutcast" 39 | 40 | file: 41 | $(play) -e url file:///sdcard/x.mp3 -e name "/sdcard/x/mp3" 42 | 43 | pause: 44 | $(pause) 45 | 46 | restart: 47 | $(restart) 48 | 49 | click: 50 | $(click) 51 | 52 | .PHONY: play stop r4 r4aac ufm wnyc newstalk lyric shoutcast file pause restart click 53 | 54 | wien: 55 | # $(play) -e url 'http://mp3stream2.apasf.apa.at:8000/listen.pls' -e name "Wien" 56 | # $(play) -e url 'http://mp3stream3.apasf.apa.at:8000/listen.pls' -e name "Oe 1" 57 | # $(play) -e url 'http://mp3stream1.apasf.apa.at:8000/listen.pls' -e name "FM4" 58 | # $(play) -e url 'http://amber.streamguys.com:4020/live' -e name "WEAA" 59 | # $(play) -e url 'http://live.str3am.com:2410/wypr.m3u' -e name "WYPR" 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Intent Radio 2 | ============ 3 | 4 | What? 5 | ---- 6 | 7 | *Intent Radio* is an android internet radio app without a graphical user 8 | interface. It is controlled exclusively through the delivery of 9 | [broadcast intents](http://developer.android.com/reference/android/content/BroadcastReceiver.html). 10 | If you do not know what a broadcast intent is, then this is probably not the 11 | app for you. 12 | 13 | *Intent Radio* was written primarily to be driven by shortcuts, tasks and 14 | events triggered from [Tasker](http://tasker.dinglisch.net/). 15 | 16 | Despite the name, *Intent Radio* will happily play local audio media too. 17 | 18 | Where? 19 | ------ 20 | 21 | More information, downloads and a sample Tasker project are available on: 22 | 23 | - the project [home page](http://intent-radio.smblott.org/) 24 | 25 | The download is also available on: 26 | 27 | - The Google [Play Store](https://play.google.com/store/apps/details?id=org.smblott.intentradioio). 28 | - [F-Droid](https://f-droid.org/repository/browse/?fdid=org.smblott.intentradio). 29 | 30 | -------------------------------------------------------------------------------- /RELEASE.txt: -------------------------------------------------------------------------------- 1 | 2 | Next Release 3 | ------------ 4 | 5 | - Better (but not yet perfect) handling of drops due to connectivity changes. 6 | - Fixed minor bugs and/or inconsistencies. 7 | - Check out the web page for information on how to handle playlists. 8 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | 2 | Better playlist detection: 3 | 4 | - check the mime type header received by HTTP/get. 5 | 6 | Some links in the app seem to not be working? 7 | 8 | - but they work in the emulator? 9 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 56 | 57 | 69 | 70 | 71 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /google-play-store/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/google-play-store/.gitignore -------------------------------------------------------------------------------- /google-play-store/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /google-play-store/Makefile: -------------------------------------------------------------------------------- 1 | 2 | versioncode = $(shell sh ../script/version.sh) 3 | 4 | debug: ../ir_library/res/raw/tasker.prj 5 | ant debug 6 | 7 | release: ../ir_library/res/raw/tasker.prj 8 | $(MAKE) clean 9 | ant release 10 | mkdir -p ../releases 11 | install -v -m 0444 bin/IntentRadio-release.apk ../releases/IntentRadio-release-google-$(versioncode).apk 12 | 13 | install: 14 | $(MAKE) debug 15 | adb install -r bin/IntentRadio-debug.apk 16 | 17 | install-release: 18 | $(MAKE) release 19 | adb install -r bin/IntentRadio-release.apk 20 | 21 | clean: 22 | cd .. && $(MAKE) clean 23 | 24 | logcat: 25 | cd .. && $(MAKE) $@ 26 | 27 | log: 28 | cd .. && $(MAKE) $@ 29 | 30 | update-project: 31 | android update project --name "IntentRadio" --target android-19 --path . --subprojects 32 | cd ../ir_library/ && $(MAKE) update-project 33 | 34 | ../ir_library/res/raw/tasker.prj: ../ir_library/misc/Radio.prj.xml 35 | cd ../ir_library/ && $(MAKE) res/raw/tasker.prj 36 | 37 | version: 38 | @echo $(versioncode) 39 | 40 | .PHONY: debug release clean install install-release logcat log update-project versioncode 41 | include ../Makefile.test 42 | 43 | # Release process: 44 | # 45 | # - bump 46 | # ** version code in ./AndroidManifest.xml 47 | # ** version name in ./res/values/strings.xml 48 | # - update release notes in web/index.ascii 49 | # - git commit/push 50 | # - build release APK 51 | # - git tag -a vX.Y 52 | # - git push origin --tags 53 | # - on GitHub, publish release 54 | # including upload of release APK 55 | # - add link to release APK to web/index.ascii (at bottom) 56 | # - in web: make install 57 | # - git commit/push 58 | # 59 | 60 | -------------------------------------------------------------------------------- /google-play-store/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 56 | 57 | 69 | 70 | 71 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /google-play-store/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /google-play-store/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-19 15 | 16 | key.store=/home/blott/.android/intent_radio.keystore 17 | key.alias=intent_radio 18 | 19 | android.library.reference.1=../ir_library 20 | manifestmerger.enabled=true 21 | -------------------------------------------------------------------------------- /google-play-store/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google Play Store 4 | 5 | -------------------------------------------------------------------------------- /google-play-store/src/.nothing: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/google-play-store/src/.nothing -------------------------------------------------------------------------------- /icons/intent_radio_large_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/icons/intent_radio_large_1024x1024.png -------------------------------------------------------------------------------- /icons/intent_radio_large_1024x500-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/icons/intent_radio_large_1024x500-0.png -------------------------------------------------------------------------------- /ir_library/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | gen/ 3 | local.properties 4 | player 5 | -------------------------------------------------------------------------------- /ir_library/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /ir_library/Makefile: -------------------------------------------------------------------------------- 1 | 2 | debug: res/raw/tasker.prj 3 | cd .. && $(MAKE) $@ 4 | 5 | clean: 6 | cd .. && $(MAKE) $@ 7 | 8 | install: 9 | cd .. && $(MAKE) $@ 10 | 11 | install-release: 12 | cd .. && $(MAKE) $@ 13 | 14 | logcat: 15 | cd .. && $(MAKE) $@ 16 | 17 | log: 18 | $(MAKE) logcat 19 | 20 | update-project: 21 | android update lib-project --target android-19 --path . 22 | 23 | res/raw/tasker.prj: ./misc/Radio.prj.xml 24 | install -m 0444 $< $@ 25 | 26 | .PHONY: debug clean install install-release logcat log update-project 27 | include ../Makefile.test 28 | 29 | -------------------------------------------------------------------------------- /ir_library/ant.properties: -------------------------------------------------------------------------------- 1 | # This file is used to override default values used by the Ant build system. 2 | # 3 | # This file must be checked into Version Control Systems, as it is 4 | # integral to the build system of your project. 5 | 6 | # This file is only used by the Ant script. 7 | 8 | # You can use this to override default values such as 9 | # 'source.dir' for the location of your java source folder and 10 | # 'out.dir' for the location of your output folder. 11 | 12 | # You can also use it define how the release builds are signed by declaring 13 | # the following properties: 14 | # 'key.store' for the location of your keystore and 15 | # 'key.alias' for the name of the key to use. 16 | # The password will be asked during the build when you use the 'release' target. 17 | 18 | -------------------------------------------------------------------------------- /ir_library/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 56 | 57 | 69 | 70 | 71 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /ir_library/misc/Metadata.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.content.Context; 4 | import android.os.AsyncTask; 5 | import android.media.MediaMetadataRetriever; 6 | import android.net.Uri; 7 | 8 | public class Metadata extends AsyncTask 9 | { 10 | private int then = 0; 11 | private Context context = null; 12 | private String url = null; 13 | 14 | Metadata(Context a_context, String a_url) 15 | { 16 | super(); 17 | then = Counter.now(); 18 | context = a_context; 19 | url = a_url; 20 | log("Metadata: then=" + then); 21 | } 22 | 23 | public void start() 24 | { executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } 25 | 26 | @Override 27 | protected String doInBackground(Void... args) 28 | { 29 | try 30 | { 31 | log("Metadata start: ", url); 32 | MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 33 | log("Metadata 1."); 34 | retriever.setDataSource(context,Uri.parse(url)); 35 | // FIXME: 36 | // This is broken! 37 | // Never reaches here! 38 | log("Metadata 2."); 39 | String title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); 40 | log("Metadata done."); 41 | return title != null ? title : null; 42 | } 43 | catch (Exception e) 44 | { return null; } 45 | } 46 | 47 | @Override 48 | protected void onPostExecute(String title) 49 | { 50 | if ( title != null && ! isCancelled() && Counter.still(then) ) 51 | { 52 | Notify.name("XX" + title); 53 | // Notify.note(); 54 | } 55 | } 56 | 57 | /* ******************************************************************** 58 | * Logging... 59 | */ 60 | 61 | private static void log(String... msg) 62 | { Logger.log(msg); } 63 | } 64 | -------------------------------------------------------------------------------- /ir_library/misc/Now.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.os.AsyncTask; 4 | import java.lang.Thread; 5 | 6 | public abstract class Now extends AsyncTask 7 | { 8 | Now() 9 | { super(); } 10 | 11 | public abstract void now(); 12 | 13 | protected Void doInBackground(Integer... args) 14 | { return null; } 15 | 16 | protected void onPostExecute(Void ignored) 17 | { 18 | if ( ! isCancelled() ) 19 | now(); 20 | } 21 | 22 | public AsyncTask start() 23 | { return executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } 24 | } 25 | -------------------------------------------------------------------------------- /ir_library/misc/PlaylistM3u.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.content.Intent; 4 | import android.content.Context; 5 | import android.net.Uri; 6 | 7 | public class PlaylistM3u extends Playlist 8 | { 9 | PlaylistM3u(IntentPlayer player) 10 | { super(player); } 11 | 12 | public static boolean is_playlist(String url) 13 | { return is_playlist_suffix(url,".m3u") || is_playlist_suffix(url,".m3u8"); } 14 | 15 | @Override 16 | String filter(String line) 17 | { return line.indexOf('#') == 0 ? "" : line; } 18 | } 19 | -------------------------------------------------------------------------------- /ir_library/misc/PlaylistPls.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.content.Intent; 4 | import android.content.Context; 5 | 6 | public class PlaylistPls extends Playlist 7 | { 8 | PlaylistPls(IntentPlayer player) 9 | { super(player); } 10 | 11 | public static boolean is_playlist(String url) 12 | { return is_playlist_suffix(url,".pls"); } 13 | 14 | @Override 15 | String filter(String line) 16 | { 17 | if ( line.startsWith("File") && 0 < line.indexOf('=') ) 18 | return line; 19 | 20 | return ""; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ir_library/misc/Radio.prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1395050613280 4 | true 5 | 1396081182750 6 | 186 7 | 180 8 | 206 9 | Radio Headset Plugged 10 | 11 | 30 12 | 13 | 14 | 15 | 110 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 1396080742875 24 | 1396084742165 25 | 23 26 | 24 27 | Radio State Received 28 | 29 | 599 30 | org.smblott.intentradio.STATE 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 1392739136719 39 | Radio 40 | 23,186 41 | Alpha 42 | 184,25,24,181,185,177,180,179,178,182,176,183,206 43 | 44 | 45 | 1392739152451 46 | 1395523281564 47 | 176 48 | Radio Play R4 49 | 10 50 | 51 | 877 52 | org.smblott.intentradio.PLAY 53 | 54 | 55 | 56 | url: http://www.bbc.co.uk/mediaselector/playlists/hls/radio/nonuk/lo/ak/bbc_radio_fourfm.m3u8 57 | name:BBC Radio 4 (FM) 58 | 59 | 60 | 61 | 62 | 63 | hd_av_play_over_video 64 | 65 | 66 | 67 | 1392739152451 68 | 1395645542998 69 | 177 70 | Radio Stop 71 | 10 72 | 73 | 877 74 | org.smblott.intentradio.STOP 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | hd_av_pause_over_video 86 | 87 | 88 | 89 | 1392739152451 90 | 1394365485093 91 | 178 92 | Radio Restart 93 | 10 94 | 95 | 877 96 | org.smblott.intentradio.RESTART 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | hd_av_pause_over_video 108 | 109 | 110 | 111 | 1392739152451 112 | 1392983714500 113 | 179 114 | Radio Play R5 115 | 10 116 | 117 | 877 118 | org.smblott.intentradio.PLAY 119 | 120 | 121 | 122 | url: http://www.bbc.co.uk/radio/listen/live/r5l_heaacv2.pls 123 | name:BBC Radio Five Live 124 | 125 | 126 | 127 | 128 | 129 | hd_av_play_over_video 130 | 131 | 132 | 133 | 1392822644101 134 | 1395645547967 135 | 180 136 | 137 | 130 138 | Radio Play 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 1392739152451 148 | 1393517374001 149 | 181 150 | Radio Play 151 | 10 152 | 153 | 877 154 | org.smblott.intentradio.PLAY 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | hd_av_play_over_video 166 | 167 | 168 | 169 | 1392739152451 170 | 1395050618188 171 | 182 172 | Radio Play Elsa 173 | 10 174 | 175 | 877 176 | org.smblott.intentradio.PLAY 177 | 178 | 179 | 180 | url:http://192.168.3.3/cgi-bin/sc/wav 181 | name:Elsa/UFM 182 | 183 | 184 | 185 | 186 | 187 | hd_av_play_over_video 188 | 189 | 190 | 191 | 1392739152451 192 | 1394365477106 193 | 183 194 | Radio Pause 195 | 10 196 | 197 | 877 198 | org.smblott.intentradio.PAUSE 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | hd_av_pause_over_video 210 | 211 | 212 | 213 | 1392739152451 214 | 1392984044055 215 | 184 216 | Radio Play RR1 217 | 10 218 | 219 | 877 220 | org.smblott.intentradio.PLAY 221 | 222 | 223 | 224 | url: http://icecast2.rte.ie/radio1 225 | name:RTÉ Radio 1 226 | 227 | 228 | 229 | 230 | 231 | hd_av_play_over_video 232 | 233 | 234 | 235 | 1392739152451 236 | 1394376527889 237 | 185 238 | Radio Play Lyric 239 | 10 240 | 241 | 877 242 | org.smblott.intentradio.PLAY 243 | 244 | 245 | 246 | url: http://icecast2.rte.ie/lyric 247 | name:RTÉ Lyric FM 248 | 249 | 250 | 251 | 252 | 253 | hd_av_play_over_video 254 | 255 | 256 | 257 | 1395645555984 258 | 1395645570132 259 | 206 260 | 261 | 130 262 | Radio Stop 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 1396080749081 272 | 1396084742165 273 | 24 274 | 275 | 548 276 | false 277 | Intent radio state change: %state. 278 | 279 | 280 | 281 | 547 282 | %IRSTATE 283 | %state 284 | 285 | 286 | 287 | 288 | 547 289 | %IRURL 290 | %url 291 | 292 | 293 | 294 | 295 | 547 296 | %IRNAME 297 | %name 298 | 299 | 300 | 301 | 302 | 303 | 1392739152451 304 | 1396082205898 305 | 25 306 | Radio State Request 307 | 10 308 | 309 | 877 310 | org.smblott.intentradio.STATE_REQUEST 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | hd_av_pause_over_video 322 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /ir_library/misc/old-README.md: -------------------------------------------------------------------------------- 1 | Intent Radio 2 | ============ 3 | 4 | Who? 5 | ---- 6 | 7 | You might be interested in *Intent Radio* if: 8 | 9 | - you use Android, 10 | - you use Tasker, 11 | - you listen to internet radio, and 12 | - you're a geek. 13 | 14 | What? 15 | ---- 16 | 17 | *Intent Radio* is an android internet radio app without a graphical user 18 | interface. It is controlled exclusively through the delivery of 19 | [broadcast intents](http://developer.android.com/reference/android/content/BroadcastReceiver.html). 20 | If you do not know what a broadcast intent is, then this is probably not the 21 | app for you. 22 | 23 | Download 24 | -------- 25 | 26 | I haven't figured out the details of putting *Intent Radio* onto the Play 27 | Store yet. So, for the moment, the download is on the GitHub project's [release page](https://github.com/smblott-github/intent_radio/releases). 28 | 29 | Or, of course, you can build the app yourself. 30 | 31 | Why? 32 | ---- 33 | 34 | There are already many internet radio apps for Android; so, why another 35 | one? 36 | 37 | Well, I couldn't find one that worked just right for me... 38 | 39 | I tried (and like) [xiialaive](http://xiialive.com/). And it supports external 40 | broadcast intents. However, I was finding it would hang irredeemably 41 | on start up about two times in five, mainly when on mobile data. 42 | 43 | And I particularly like [tunein](http://tunein.com/). However, it doesn't 44 | support either shortcuts or broadcast intents, so I have no way to 45 | start and stop it automatically, say when a headset is plugged in or 46 | out. 47 | 48 | The 49 | [BBC iPlayer Radio](https://play.google.com/store/apps/details?id=uk.co.bbc.android.iplayerradio&hl=en) 50 | app is pretty slick; and most of what I listen to is BBC. Again, however, 51 | there's no way to control playback without much pointy-pressy action through 52 | the GUI. 53 | 54 | And then there's [Tasker](http://tasker.dinglisch.net/). Tasker is an 55 | automation app for Android. It's like a small graphical programming 56 | language combined with a mechanism to fire off tasks in response to various 57 | events. 58 | 59 | *Intent Radio* was written primarily to be driven by Tasker, either via 60 | task shortcuts on the home screen, or via Tasker's response to events such as 61 | a headset being plugged in or out. 62 | 63 | How? 64 | ---- 65 | 66 | *Intent Radio* supports the following broadcast intents... 67 | 68 | `org.smblott.intentradio.PLAY` 69 | 70 | - start playback 71 | - extra: `url` -- the URL to play 72 | - extra: `name` -- the display name for the station 73 | 74 | Both extras are strings, and both are optional. If `name` is omitted, 75 | then the URL is used as the display name. If `url` is omitted, then 76 | a built-in URL for BBC Radio 4 is used. 77 | 78 | `org.smblott.intentradio.STOP` 79 | 80 | - stop playback 81 | - extras: none 82 | 83 | During playback, *Intent Radio* places a notification in the notification 84 | area. Clicking on the notification broadcasts the "`...STOP`" intent, which 85 | causes playback to stop. 86 | 87 | *Intent Radio* uses the built-in Android 88 | [media player](http://developer.android.com/reference/android/media/MediaPlayer.html) for playback. So all audio codecs supported natively by Android 89 | are supported by *Intent Radio*. 90 | 91 | Additionally, *Intent Radio* supports 92 | [playlists](http://en.wikipedia.org/wiki/PLS_(file_format)) (whose URL must 93 | end with the suffix `.pls`). For example: 94 | 95 | - `http://www.bbc.co.uk/.../xxx.pls` 96 | 97 | Warnings! 98 | --------- 99 | 100 | Although *Intent Radio* has no graphical user interface, you must 101 | nevertheless start up the app *at least once*. Otherwise, Android will not 102 | deliver broadcast intents to the app. This is an Android security feature. 103 | 104 | Also, start up can be slow for some streams. BBC Radio 4, for example, 105 | takes in excess of 30 seconds for playback to begin. I do not know the 106 | source of this delay. Please be patient. 107 | 108 | Finally, *Intent Radio* is built for Android API level 16, so only for 4.1 109 | (Jelly Bean) devices and above. 110 | 111 | A Sample Tasker Project 112 | ----------------------- 113 | 114 | If you're using Tasker, then this [Tasker project](https://github.com/smblott-github/intent_radio/tree/master/misc) may 115 | be helpful in getting started with *Intent Radio*. 116 | 117 | Release Notes 118 | ------------- 119 | 120 | ### Version 1.1 121 | 122 | - Use `httpURLConnection`. 123 | - Fetch playlists on an asynchronous thread (so, non-blocking). 124 | 125 | ### Version 1.0 126 | 127 | - Initial release. 128 | 129 | -------------------------------------------------------------------------------- /ir_library/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /ir_library/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-19 15 | android.library=true 16 | -------------------------------------------------------------------------------- /ir_library/res/drawable-hdpi/intent_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/ir_library/res/drawable-hdpi/intent_radio.png -------------------------------------------------------------------------------- /ir_library/res/drawable-ldpi/intent_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/ir_library/res/drawable-ldpi/intent_radio.png -------------------------------------------------------------------------------- /ir_library/res/drawable-mdpi/intent_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/ir_library/res/drawable-mdpi/intent_radio.png -------------------------------------------------------------------------------- /ir_library/res/drawable-xhdpi/intent_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/ir_library/res/drawable-xhdpi/intent_radio.png -------------------------------------------------------------------------------- /ir_library/res/drawable-xxhdpi/intent_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/ir_library/res/drawable-xxhdpi/intent_radio.png -------------------------------------------------------------------------------- /ir_library/res/drawable-xxxhdpi/intent_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/ir_library/res/drawable-xxxhdpi/intent_radio.png -------------------------------------------------------------------------------- /ir_library/res/layout/buttons.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 16 | 20 | 21 | 25 | 26 | 34 | 35 | 43 | 44 | 52 | 53 | 61 | 62 | 70 | 71 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /ir_library/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 21 | 22 | 31 | 32 | 41 | 42 | 43 | 44 | 51 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /ir_library/res/layout/prefs.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 15 | 16 | 17 | 18 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /ir_library/res/menu/prefs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /ir_library/res/raw/message.html: -------------------------------------------------------------------------------- 1 | Introduction 2 | 3 | 4 | Intent Radio is a streaming internet radio app without a graphical user 5 | interface. It is controlled exclusively through the delivery of broadcast 6 | intents. Automation apps, such as Tasker, should be used to control 7 | playback by issuing suitable broadcast intents. 8 | 9 | 10 | 11 | This app must be run once before Android will deliver intents to its background 12 | service. This is an Android security feature. 13 | 14 | 15 | 16 | Thereafter, including after reboots, it should not be necessary the run this 17 | app at all. Its service runs in the background. 18 | 19 | 20 | Media Player 21 | 22 | 23 | All aspects of media playback are handled by the built-in Android media player. 24 | Additionally, playlist URLs (whose URL/name must end with ".pls" or ".m3u") 25 | are also supported. 26 | 27 | 28 | 29 | During playback, Intent Radio adds a notification to the notification area. 30 | Clicking on this notification stops or restarts playback, as approrpiate. 31 | 32 | 33 | Broadcast Intents Received by IR 34 | 35 | 36 | 37 | org.smblott.intentradio.PLAY 38 | 39 | 40 | 41 | Extras: 42 | url: the URL to play 43 | name: the display name 44 | 45 | 46 | 47 | If no name extra is provided, then the URL is used as the display 48 | name. If no url is provided, then a built-in URL for BBC Radio 4 is 49 | used. 50 | 51 | 52 | 53 | 54 | 55 | org.smblott.intentradio.STOP 56 | 57 | 58 | 59 | Stop playback. 60 | No extras. 61 | 62 | 63 | 64 | 65 | 66 | org.smblott.intentradio.PAUSE 67 | 68 | 69 | 70 | Pause playback, but only if playing. 71 | No extras. 72 | 73 | 74 | 75 | 76 | 77 | org.smblott.intentradio.RESTART 78 | 79 | 80 | 81 | Restart playback, but only if paused. 82 | No extras. 83 | 84 | 85 | 86 | 87 | 88 | org.smblott.intentradio.STATE_REQUEST 89 | 90 | 91 | 92 | Request that Intent Radio broadcast its state (see below). 93 | No extras. 94 | 95 | 96 | 97 | This intent is not required to receive state updates. 98 | Intent Radio broadcasts its state automatically whenever its state changes. 99 | 100 | 101 | 102 | Broadcast Intents Sent by IR 103 | 104 | 105 | 106 | org.smblott.intentradio.STATE 107 | 108 | 109 | 110 | Informs listeners (possibly Tasker) of changes in state. 111 | Extras: 112 | state: the current state (see below) 113 | url: the current url 114 | name: the current name 115 | 116 | 117 | The state is one 118 | stop, 119 | play, 120 | play/buffering, 121 | play/pause, 122 | play/dim or 123 | error. 124 | 125 | 126 | 127 | More Information 128 | 129 | 130 | There is considerably more information, 131 | including instructions for configuring Tasker, 132 | on the project home page. 133 | 134 | 135 | 136 | See also the source code on 137 | GitHub, and the 138 | release notes/change log. 139 | 140 | 141 | 142 | Please report issues on 143 | GitHub. 144 | 145 | 146 | 147 | See also: 148 | Tasker. 149 | 150 | -------------------------------------------------------------------------------- /ir_library/res/raw/playing.html: -------------------------------------------------------------------------------- 1 | Playing... 2 | 3 | 4 | REPLACE_URL 5 | 6 | -------------------------------------------------------------------------------- /ir_library/res/raw/tasker.prj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1395050613280 4 | true 5 | 1396081182750 6 | 186 7 | 180 8 | 206 9 | Radio Headset Plugged 10 | 11 | 30 12 | 13 | 14 | 15 | 110 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 1396080742875 24 | 1396084742165 25 | 23 26 | 24 27 | Radio State Received 28 | 29 | 599 30 | org.smblott.intentradio.STATE 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 1392739136719 39 | Radio 40 | 23,186 41 | Alpha 42 | 184,25,24,181,185,177,180,179,178,182,176,183,206 43 | 44 | 45 | 1392739152451 46 | 1395523281564 47 | 176 48 | Radio Play R4 49 | 10 50 | 51 | 877 52 | org.smblott.intentradio.PLAY 53 | 54 | 55 | 56 | url: http://www.bbc.co.uk/mediaselector/playlists/hls/radio/nonuk/lo/ak/bbc_radio_fourfm.m3u8 57 | name:BBC Radio 4 (FM) 58 | 59 | 60 | 61 | 62 | 63 | hd_av_play_over_video 64 | 65 | 66 | 67 | 1392739152451 68 | 1395645542998 69 | 177 70 | Radio Stop 71 | 10 72 | 73 | 877 74 | org.smblott.intentradio.STOP 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | hd_av_pause_over_video 86 | 87 | 88 | 89 | 1392739152451 90 | 1394365485093 91 | 178 92 | Radio Restart 93 | 10 94 | 95 | 877 96 | org.smblott.intentradio.RESTART 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | hd_av_pause_over_video 108 | 109 | 110 | 111 | 1392739152451 112 | 1392983714500 113 | 179 114 | Radio Play R5 115 | 10 116 | 117 | 877 118 | org.smblott.intentradio.PLAY 119 | 120 | 121 | 122 | url: http://www.bbc.co.uk/radio/listen/live/r5l_heaacv2.pls 123 | name:BBC Radio Five Live 124 | 125 | 126 | 127 | 128 | 129 | hd_av_play_over_video 130 | 131 | 132 | 133 | 1392822644101 134 | 1395645547967 135 | 180 136 | 137 | 130 138 | Radio Play 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 1392739152451 148 | 1393517374001 149 | 181 150 | Radio Play 151 | 10 152 | 153 | 877 154 | org.smblott.intentradio.PLAY 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | hd_av_play_over_video 166 | 167 | 168 | 169 | 1392739152451 170 | 1395050618188 171 | 182 172 | Radio Play Elsa 173 | 10 174 | 175 | 877 176 | org.smblott.intentradio.PLAY 177 | 178 | 179 | 180 | url:http://192.168.3.3/cgi-bin/sc/wav 181 | name:Elsa/UFM 182 | 183 | 184 | 185 | 186 | 187 | hd_av_play_over_video 188 | 189 | 190 | 191 | 1392739152451 192 | 1394365477106 193 | 183 194 | Radio Pause 195 | 10 196 | 197 | 877 198 | org.smblott.intentradio.PAUSE 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | hd_av_pause_over_video 210 | 211 | 212 | 213 | 1392739152451 214 | 1392984044055 215 | 184 216 | Radio Play RR1 217 | 10 218 | 219 | 877 220 | org.smblott.intentradio.PLAY 221 | 222 | 223 | 224 | url: http://icecast2.rte.ie/radio1 225 | name:RTÉ Radio 1 226 | 227 | 228 | 229 | 230 | 231 | hd_av_play_over_video 232 | 233 | 234 | 235 | 1392739152451 236 | 1394376527889 237 | 185 238 | Radio Play Lyric 239 | 10 240 | 241 | 877 242 | org.smblott.intentradio.PLAY 243 | 244 | 245 | 246 | url: http://icecast2.rte.ie/lyric 247 | name:RTÉ Lyric FM 248 | 249 | 250 | 251 | 252 | 253 | hd_av_play_over_video 254 | 255 | 256 | 257 | 1395645555984 258 | 1395645570132 259 | 206 260 | 261 | 130 262 | Radio Stop 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 1396080749081 272 | 1396084742165 273 | 24 274 | 275 | 548 276 | false 277 | Intent radio state change: %state. 278 | 279 | 280 | 281 | 547 282 | %IRSTATE 283 | %state 284 | 285 | 286 | 287 | 288 | 547 289 | %IRURL 290 | %url 291 | 292 | 293 | 294 | 295 | 547 296 | %IRNAME 297 | %name 298 | 299 | 300 | 301 | 302 | 303 | 1392739152451 304 | 1396082205898 305 | 25 306 | Radio State Request 307 | 10 308 | 309 | 877 310 | org.smblott.intentradio.STATE_REQUEST 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | hd_av_pause_over_video 322 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /ir_library/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | IntentRadio 4 | Intent Radio 5 | General 6 | 7 | http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_vlow/llnw/bbc_radio_fourfm.m3u8 8 | 9 | 10 | BBC Radio 4 (FM) 11 | 12 | org.smblott.intentradio.PLAY 13 | org.smblott.intentradio.STOP 14 | org.smblott.intentradio.PAUSE 15 | org.smblott.intentradio.RESTART 16 | 17 | org.smblott.intentradio.STATE 18 | org.smblott.intentradio.STATE_REQUEST 19 | org.smblott.intentradio.CLICK 20 | 21 | intent-radio.log 22 | 23 | -------------------------------------------------------------------------------- /ir_library/res/values/version.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 16 9 | 1.9.6 10 | 11 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Build.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.content.Context; 4 | import android.content.pm.ApplicationInfo; 5 | import java.util.zip.ZipFile; 6 | import java.util.zip.ZipEntry; 7 | import java.text.SimpleDateFormat; 8 | import android.content.pm.ApplicationInfo; 9 | import android.content.pm.PackageInfo; 10 | 11 | public class Build 12 | { 13 | static private String build = null; 14 | 15 | // source: http://stackoverflow.com/questions/7607165/how-to-write-build-time-stamp-into-apk 16 | // 17 | public static String getBuildDate(Context context) 18 | { 19 | if ( build != null ) 20 | return build; 21 | 22 | try 23 | { 24 | ApplicationInfo info = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); 25 | ZipFile file = new ZipFile(info.sourceDir); 26 | ZipEntry entry = file.getEntry("classes.dex"); 27 | long time = entry.getTime(); 28 | build = SimpleDateFormat.getInstance().format(new java.util.Date(time)); 29 | } 30 | catch (Exception e) 31 | { build = "Unknown"; } 32 | 33 | if ( debug_build(context) ) 34 | build += " [debug]"; 35 | 36 | return build; 37 | } 38 | 39 | public static boolean debug_build(Context context) 40 | { 41 | int DEBUGGABLE = ApplicationInfo.FLAG_DEBUGGABLE; 42 | return (context.getApplicationInfo().flags & DEBUGGABLE) == DEBUGGABLE; 43 | } 44 | 45 | public static String version_string(Context context) 46 | { 47 | try 48 | { 49 | PackageInfo pinfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); 50 | int version_code = pinfo.versionCode; 51 | String version_name = pinfo.versionName; 52 | return version_code + "-" + version_name; 53 | } 54 | catch (Exception e) {} 55 | 56 | return "Unknown"; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/ClipButtons.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | import android.content.Context; 7 | 8 | import android.view.View; 9 | import android.widget.Button; 10 | 11 | public class ClipButtons extends PreferenceActivity 12 | { 13 | 14 | private static String intent_play = null; 15 | private static String intent_stop = null; 16 | private static String intent_pause = null; 17 | private static String intent_restart = null; 18 | private static String intent_state_request = null; 19 | private static String intent_state = null; 20 | 21 | private static Context context = null; 22 | 23 | @Override 24 | public void onCreate(Bundle savedInstanceState) 25 | { 26 | super.onCreate(savedInstanceState); 27 | Logger.init(getApplicationContext()); 28 | context = getApplicationContext(); 29 | 30 | intent_play = getString(R.string.intent_play); 31 | intent_stop = getString(R.string.intent_stop); 32 | intent_pause = getString(R.string.intent_pause); 33 | intent_restart = getString(R.string.intent_restart); 34 | intent_state_request = getString(R.string.intent_state_request); 35 | intent_state = getString(R.string.intent_state); 36 | 37 | setContentView(R.layout.buttons); 38 | } 39 | 40 | /* ******************************************************************** 41 | * Clip buttons... 42 | */ 43 | 44 | public static void clip_play(View view) { Clipper.clip(context,intent_play); } 45 | public static void clip_stop(View view) { Clipper.clip(context,intent_stop); } 46 | public static void clip_pause(View view) { Clipper.clip(context,intent_pause); } 47 | public static void clip_restart(View view) { Clipper.clip(context,intent_restart); } 48 | public static void clip_state_request(View view) { Clipper.clip(context,intent_state_request); } 49 | public static void clip_state(View view) { Clipper.clip(context,intent_state); } 50 | 51 | /* ******************************************************************** 52 | * Utilities... 53 | */ 54 | 55 | private static void toast(String msg) 56 | { Logger.toast(msg); } 57 | } 58 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Clipper.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.content.Context; 4 | 5 | import android.content.ClipData; 6 | import android.content.ClipboardManager; 7 | 8 | public class Clipper extends Logger 9 | { 10 | static void clip(Context context, String text) 11 | { 12 | if ( text == null || context == null ) 13 | return; 14 | 15 | init(context); 16 | 17 | ClipboardManager clip_manager = (ClipboardManager) context.getSystemService(context.CLIPBOARD_SERVICE); 18 | ClipData clip_data = ClipData.newPlainText("text", text); 19 | clip_manager.setPrimaryClip(clip_data); 20 | toast("Clipboard:\n" + text); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Connectivity.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.content.Context; 4 | import android.content.BroadcastReceiver; 5 | 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | 9 | import android.net.ConnectivityManager; 10 | import android.net.NetworkInfo; 11 | 12 | import android.os.AsyncTask; 13 | 14 | import android.preference.PreferenceManager; 15 | import android.content.SharedPreferences; 16 | 17 | public class Connectivity extends BroadcastReceiver 18 | { 19 | private static ConnectivityManager connectivity = null; 20 | 21 | private Context context = null; 22 | private IntentPlayer player = null; 23 | private static final int TYPE_NONE = -1; 24 | 25 | Connectivity(Context a_context, IntentPlayer a_player) 26 | { 27 | Logger.log("Connectivity: created"); 28 | context = a_context; 29 | player = a_player; 30 | 31 | init_connectivity(context); 32 | context.registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); 33 | } 34 | 35 | static private void init_connectivity(Context context) 36 | { 37 | if ( connectivity == null ) 38 | connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 39 | if ( connectivity != null ) 40 | previous_type = getType(); 41 | } 42 | 43 | void destroy() 44 | { context.unregisterReceiver(this); } 45 | 46 | static private int getType() 47 | { return getType(null); } 48 | 49 | static private int getType(Intent intent) 50 | { 51 | if (connectivity == null) 52 | return TYPE_NONE; 53 | 54 | if ( intent != null && intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false) ) 55 | return TYPE_NONE; 56 | 57 | NetworkInfo network = connectivity.getActiveNetworkInfo(); 58 | if ( network != null && network.isConnected() ) 59 | { 60 | int type = network.getType(); 61 | switch (type) 62 | { 63 | // These cases all fall through. 64 | case ConnectivityManager.TYPE_WIFI: 65 | case ConnectivityManager.TYPE_MOBILE: 66 | case ConnectivityManager.TYPE_WIMAX: 67 | if ( network.getState() == NetworkInfo.State.CONNECTED ) 68 | return type; 69 | } 70 | } 71 | 72 | return TYPE_NONE; 73 | } 74 | 75 | static boolean onWifi() 76 | { return previous_type == ConnectivityManager.TYPE_WIFI; } 77 | 78 | static public boolean isConnected(Context context){ 79 | init_connectivity(context); 80 | return (getType() != TYPE_NONE); 81 | } 82 | 83 | private static AsyncTask disable_task = null; 84 | private static int previous_type = TYPE_NONE; 85 | private int then = 0; 86 | 87 | @Override 88 | public void onReceive(Context context, Intent intent) 89 | { 90 | int type = getType(intent); 91 | boolean want_network_playing = State.is_want_playing() && player.isNetworkUrl(); 92 | Logger.log("Connectivity: " + type + " " + want_network_playing); 93 | 94 | if ( type == TYPE_NONE && previous_type != TYPE_NONE && want_network_playing ) 95 | dropped_connection(); 96 | 97 | if ( previous_type == TYPE_NONE 98 | && type != previous_type 99 | && Counter.still(then) 100 | ) 101 | { // We have become reconnected, and we're still in the window to resume playback. 102 | Logger.log("Connectivity: connected"); 103 | restart(); 104 | } 105 | 106 | // We can get from mobile data to WiFi without going through TYPE_NONE. 107 | // So the counter does not help. 108 | // && Counter.still(then) 109 | if ( previous_type != TYPE_NONE && type != TYPE_NONE && type != previous_type && want_network_playing ) 110 | { // We have moved to a different type of network. 111 | Logger.log("Connectivity: different network type"); 112 | restart(); 113 | } 114 | 115 | previous_type = type; 116 | return; 117 | } 118 | 119 | public void dropped_connection() 120 | { // We've lost connectivity. 121 | Logger.log("Connectivity: disconnected"); 122 | player.stop(); 123 | then = Counter.now(); 124 | State.set_state(context, State.STATE_DISCONNECTED, true); 125 | 126 | if ( disable_task != null ) 127 | disable_task.cancel(true); 128 | 129 | disable_task = 130 | new Later(300) 131 | { 132 | @Override 133 | public void later() 134 | { 135 | player.stop(); 136 | disable_task = null; 137 | } 138 | }.start(); 139 | } 140 | 141 | private void restart() 142 | { 143 | if ( disable_task != null ) 144 | { 145 | disable_task.cancel(true); 146 | disable_task = null; 147 | } 148 | 149 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 150 | if ( settings.getBoolean("reconnect", false) ) 151 | player.play(); 152 | } 153 | } 154 | 155 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/CopyResource.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | import android.os.Process; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.InputStream; 10 | 11 | public class CopyResource extends Logger 12 | { 13 | private static final String prefix = ".IntentRadio."; 14 | 15 | /* Install raw file resource "id" into location "path" on external storage. 16 | * Returns null on success, or an error message on failure. 17 | */ 18 | 19 | public static String copy(Context context, int id, String path) 20 | { return copy(context, id, path, false); } 21 | 22 | public static String copy(Context context, int id, String path, boolean overwrite) 23 | { 24 | log("CopyResource id: ", ""+id); 25 | log("CopyResource path: ", path); 26 | 27 | File tmp = null; 28 | InputStream input = null; 29 | FileOutputStream output = null; 30 | boolean success = true; 31 | 32 | File sdcard = Environment.getExternalStorageDirectory(); 33 | if ( sdcard == null || ! Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ) 34 | { return "SD card not found or not ready."; } 35 | 36 | path = sdcard.getAbsolutePath() + "/" + path; 37 | log("CopyResource full path: ", path); 38 | 39 | File file = new File(path); 40 | if ( file.exists() && ! overwrite ) 41 | { return "File already exists, not copied..."; } 42 | 43 | File directory = new File(file.getParent()); 44 | if ( ! directory.isDirectory() ) 45 | { return "Directory does not exist..."; } 46 | 47 | try 48 | { 49 | tmp = File.createTempFile(prefix, null, directory); 50 | log("CopyResource tmp path: ", tmp.toString()); 51 | 52 | input = context.getResources().openRawResource(id); 53 | output = new FileOutputStream(tmp); 54 | 55 | byte[] buffer = new byte[1024]; 56 | int count = 0; 57 | 58 | while ( 0 < (count = input.read(buffer)) ) 59 | output.write(buffer, 0, count); 60 | } 61 | catch (Exception e1) 62 | { 63 | success = false; 64 | } 65 | finally 66 | { 67 | try { if ( output != null ) output.close(); } catch (Exception e2) { success = false; } 68 | try { if ( input != null ) input.close(); } catch (Exception e2) {} 69 | } 70 | 71 | if ( success ) 72 | success = tmp.renameTo(file); 73 | 74 | if ( tmp != null && tmp.exists() ) 75 | if ( ! tmp.delete() ) 76 | log("CopyResource failed to delete: ", tmp.toString()); 77 | 78 | return success ? null : "Unknown error..."; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Counter.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | public class Counter extends Logger 4 | { 5 | private static volatile int counter = 1; 6 | 7 | public static int now() 8 | { return counter; } 9 | 10 | public static void time_passes() 11 | { counter += 1; } 12 | 13 | public static boolean still(int then) 14 | { return then == now(); } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/HttpGetter.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import java.net.URL; 4 | import java.net.HttpURLConnection; 5 | import java.io.BufferedInputStream; 6 | import java.io.BufferedReader; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | 10 | import java.util.List; 11 | import java.util.ArrayList; 12 | 13 | public class HttpGetter 14 | { 15 | public static List httpGet(String str) 16 | { 17 | HttpURLConnection connection = null; 18 | List lines = new ArrayList(); 19 | 20 | try 21 | { 22 | URL url = new URL(str); 23 | connection = (HttpURLConnection) url.openConnection(); 24 | 25 | if ( Playlist.is_playlist_mime_type(connection.getContentType()) ) 26 | { 27 | InputStream stream = new BufferedInputStream(connection.getInputStream()); 28 | readStream(stream, lines); 29 | } 30 | connection.disconnect(); 31 | } 32 | catch ( Exception e ) 33 | { if ( connection != null ) connection.disconnect(); } 34 | 35 | return lines; 36 | } 37 | 38 | private static void readStream(InputStream stream, List lines) throws Exception 39 | { 40 | String line; 41 | BufferedReader buff = new BufferedReader(new InputStreamReader(stream)); 42 | 43 | while ((line = buff.readLine()) != null) 44 | lines.add(line); 45 | 46 | stream.close(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/IntentPlayer.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.content.Context; 6 | import android.content.SharedPreferences; 7 | import android.content.SharedPreferences.Editor; 8 | import android.os.AsyncTask; 9 | import android.os.IBinder; 10 | import android.os.PowerManager; 11 | import android.os.StrictMode; 12 | 13 | import android.media.AudioManager; 14 | import android.media.AudioManager.OnAudioFocusChangeListener; 15 | 16 | import android.media.MediaPlayer; 17 | import android.media.MediaPlayer.OnBufferingUpdateListener; 18 | import android.media.MediaPlayer.OnErrorListener; 19 | import android.media.MediaPlayer.OnInfoListener; 20 | import android.media.MediaPlayer.OnPreparedListener; 21 | import android.media.MediaPlayer.OnCompletionListener; 22 | 23 | import android.net.Uri; 24 | import android.os.Build.VERSION; 25 | import android.webkit.URLUtil; 26 | 27 | public class IntentPlayer extends Service 28 | implements 29 | OnBufferingUpdateListener, 30 | OnInfoListener, 31 | OnErrorListener, 32 | OnPreparedListener, 33 | OnAudioFocusChangeListener, 34 | OnCompletionListener 35 | { 36 | 37 | /* ******************************************************************** 38 | * Globals... 39 | */ 40 | 41 | private static final int note_id = 100; 42 | private static final String preference_file = "state"; 43 | private static SharedPreferences settings = null; 44 | 45 | private static Context context = null; 46 | 47 | private static String app_name = null; 48 | private static String app_name_long = null; 49 | private static String intent_play = null; 50 | private static String intent_stop = null; 51 | private static String intent_pause = null; 52 | private static String intent_restart = null; 53 | private static String intent_state_request = null; 54 | private static String intent_click = null; 55 | 56 | private static String default_url = null; 57 | private static String default_name = null; 58 | public static String name = null; 59 | public static String url = null; 60 | 61 | private static MediaPlayer player = null; 62 | private static AudioManager audio_manager = null; 63 | 64 | private static Playlist playlist_task = null; 65 | private static AsyncTask pause_task = null; 66 | 67 | private static Connectivity connectivity = null; 68 | private static int initial_failure_ttl = 5; 69 | private static int failure_ttl = 0; 70 | 71 | /* ******************************************************************** 72 | * Create service... 73 | */ 74 | 75 | @Override 76 | public void onCreate() { 77 | context = getApplicationContext(); 78 | Logger.init(context); 79 | Notify.init(this,context); 80 | 81 | app_name = getString(R.string.app_name); 82 | app_name_long = getString(R.string.app_name_long); 83 | intent_play = getString(R.string.intent_play); 84 | intent_stop = getString(R.string.intent_stop); 85 | intent_pause = getString(R.string.intent_pause); 86 | intent_restart = getString(R.string.intent_restart); 87 | intent_state_request = context.getString(R.string.intent_state_request); 88 | intent_click = getString(R.string.intent_click); 89 | default_url = getString(R.string.default_url); 90 | default_name = getString(R.string.default_name); 91 | 92 | settings = getSharedPreferences(preference_file, context.MODE_PRIVATE); 93 | url = settings.getString("url", default_url); 94 | name = settings.getString("name", default_name); 95 | 96 | audio_manager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 97 | connectivity = new Connectivity(context,this); 98 | } 99 | 100 | /* ******************************************************************** 101 | * Destroy service... 102 | */ 103 | 104 | public void onDestroy() 105 | { 106 | log("Destroyed."); 107 | stop(); 108 | 109 | if ( player != null ) 110 | { 111 | player.release(); 112 | player = null; 113 | } 114 | 115 | if ( connectivity != null ) 116 | { 117 | connectivity.destroy(); 118 | connectivity = null; 119 | } 120 | 121 | Logger.state("off"); 122 | super.onDestroy(); 123 | } 124 | 125 | /* ******************************************************************** 126 | * Main entry point... 127 | */ 128 | 129 | @Override 130 | public int onStartCommand(Intent intent, int flags, int startId) 131 | { 132 | if ( intent == null || ! intent.hasExtra("action") ) 133 | return done(); 134 | 135 | if ( intent.hasExtra("debug") ) 136 | Logger.state(intent.getStringExtra("debug")); 137 | 138 | if ( ! Counter.still(intent.getIntExtra("counter", Counter.now())) ) 139 | return done(); 140 | 141 | String action = intent.getStringExtra("action"); 142 | log("Action: ", action); 143 | 144 | if ( action.equals(intent_stop) ) return stop(); 145 | if ( action.equals(intent_pause) ) return pause(); 146 | if ( action.equals(intent_restart) ) return restart(); 147 | if ( action.equals(intent_click) ) return click(); 148 | 149 | if ( action.equals(intent_state_request) ) 150 | { 151 | State.get_state(context); 152 | return done(); 153 | } 154 | 155 | if ( action.equals(intent_play) ) 156 | { 157 | if ( intent.hasExtra("url") ) 158 | url = intent.getStringExtra("url"); 159 | 160 | if ( intent.hasExtra("name") ) 161 | name = intent.getStringExtra("name"); 162 | 163 | Editor editor = settings.edit(); 164 | editor.putString("url", url); 165 | editor.putString("name", name); 166 | editor.commit(); 167 | 168 | log("Name: ", name); 169 | log("URL: ", url); 170 | Notify.name(name); 171 | failure_ttl = initial_failure_ttl; 172 | return play(url); 173 | } 174 | 175 | log("unknown action: ", action); 176 | return done(); 177 | } 178 | 179 | /* ******************************************************************** 180 | * Play... 181 | */ 182 | 183 | public int play() 184 | { return play(url); } 185 | 186 | private int play(String url) 187 | { 188 | stop(false); 189 | 190 | toast(name); 191 | log("Play: ", url); 192 | 193 | if ( ! URLUtil.isValidUrl(url) ) 194 | { 195 | toast("Invalid URL."); 196 | return stop(); 197 | } 198 | 199 | if ( isNetworkUrl(url) && ! connectivity.isConnected(context) ) 200 | { 201 | toast("No internet connection."); 202 | // We'll pretend that we dropped the connection. That way, when we 203 | // get a connection, playback will start. 204 | connectivity.dropped_connection(); 205 | return done(); 206 | } 207 | 208 | int focus = audio_manager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 209 | if ( focus != AudioManager.AUDIOFOCUS_REQUEST_GRANTED ) 210 | { 211 | toast("Could not obtain audio focus."); 212 | return stop(); 213 | } 214 | 215 | // ///////////////////////////////////////////////////////////////// 216 | // Set up media player... 217 | 218 | if ( player == null ) 219 | { 220 | log("Creating media player..."); 221 | player = new MediaPlayer(); 222 | player.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); 223 | player.setAudioStreamType(AudioManager.STREAM_MUSIC); 224 | player.setOnPreparedListener(this); 225 | player.setOnBufferingUpdateListener(this); 226 | player.setOnInfoListener(this); 227 | player.setOnErrorListener(this); 228 | player.setOnCompletionListener(this); 229 | } 230 | 231 | if ( isNetworkUrl(url) ) 232 | WifiLocker.lock(context, app_name_long); 233 | 234 | log("Connecting..."); 235 | playlist_task = new Playlist(this,url).start(); 236 | 237 | // The Playlist object calls play_launch(url), when it's ready. 238 | start_buffering(); 239 | return done(State.STATE_BUFFER); 240 | } 241 | 242 | /* ******************************************************************** 243 | * Launch player... 244 | */ 245 | 246 | // The launch_url may be different from the original URL. For example, it 247 | // could be the URL extracted from a playlist, whereas the original url is 248 | // that of the playlist itself. 249 | private static String launch_url = null; 250 | 251 | public int play_launch(String url) 252 | { 253 | log("Launching: ", url); 254 | 255 | launch_url = null; 256 | if ( ! URLUtil.isValidUrl(url) ) 257 | { 258 | toast("Invalid URL."); 259 | return stop(); 260 | } 261 | 262 | launch_url = url; 263 | 264 | // Note: Because of the way we handle network connectivity, the player 265 | // always stops and then restarts as we move between network types. 266 | // Therefore, stop() and start() are always called. So we always have 267 | // the WiFi lock if we're on WiFi and we need it, and don't otherwise. 268 | // 269 | // Here, we could be holding a WiFi lock because the playlist URL was a 270 | // network URL, but perhaps now the launch URL is not. Or the other way 271 | // around. So release the WiFi lock (if it's being held) and reaquire 272 | // it, if necessary. 273 | WifiLocker.unlock(); 274 | if ( isNetworkUrl(url) ) 275 | WifiLocker.lock(context, app_name_long); 276 | 277 | try 278 | { 279 | player.setVolume(1.0f, 1.0f); 280 | player.setDataSource(context, Uri.parse(url)); 281 | player.prepareAsync(); 282 | } 283 | catch (Exception e) 284 | { return stop(); } 285 | 286 | start_buffering(); 287 | return done(State.STATE_BUFFER); 288 | } 289 | 290 | @Override 291 | public void onPrepared(MediaPlayer mp) 292 | { 293 | if ( mp == player ) 294 | { 295 | log("Starting...."); 296 | player.start(); 297 | // Invalidate any outstanding stop_soon threads, or the like. 298 | Counter.time_passes(); 299 | // Allow future restarts after failure. 300 | failure_ttl = initial_failure_ttl; 301 | State.set_state(context, State.STATE_PLAY, isNetworkUrl()); 302 | } 303 | } 304 | 305 | public boolean isNetworkUrl() 306 | { return isNetworkUrl(launch_url); } 307 | 308 | public boolean isNetworkUrl(String check_url) 309 | { return ( check_url != null && URLUtil.isNetworkUrl(check_url) ); } 310 | 311 | /* ******************************************************************** 312 | * Stop... 313 | */ 314 | 315 | public int stop() 316 | { return stop(true); } 317 | 318 | private int stop(boolean update_state) 319 | { 320 | log("Stopping"); 321 | 322 | Counter.time_passes(); 323 | launch_url = null; 324 | audio_manager.abandonAudioFocus(this); 325 | WifiLocker.unlock(); 326 | 327 | if ( player != null ) 328 | { 329 | log("Stopping/releasing player..."); 330 | if ( player.isPlaying() ) 331 | player.stop(); 332 | player.reset(); 333 | player.release(); 334 | player = null; 335 | } 336 | 337 | if ( playlist_task != null ) 338 | { 339 | playlist_task.cancel(true); 340 | playlist_task = null; 341 | } 342 | 343 | if ( update_state ) 344 | return done(State.STATE_STOP); 345 | else 346 | return done(); 347 | } 348 | 349 | /* ******************************************************************** 350 | * Reduce volume, for a short while, for a notification. 351 | */ 352 | 353 | private int duck(String msg) 354 | { 355 | log("Duck: ", State.current()); 356 | 357 | if ( State.is(State.STATE_DUCK) || ! State.is_playing() ) 358 | return done(); 359 | 360 | player.setVolume(0.1f, 0.1f); 361 | return done(State.STATE_DUCK); 362 | } 363 | 364 | /* ******************************************************************** 365 | * Pause/restart... 366 | */ 367 | 368 | private int pause() 369 | { 370 | log("Pause: ", State.current()); 371 | 372 | if ( player == null || State.is(State.STATE_PAUSE) || ! State.is_playing() ) 373 | return done(); 374 | 375 | if ( pause_task != null ) 376 | pause_task.cancel(true); 377 | 378 | // We're still holding resources, including a possibly a Wifi Wakelock 379 | // and the player itself. Spin off a task to convert this "pause" 380 | // into a stop, soon. 381 | pause_task = 382 | new Later() 383 | { 384 | @Override 385 | public void later() 386 | { 387 | pause_task = null; 388 | stop(); 389 | } 390 | }.start(); 391 | 392 | player.pause(); 393 | return done(State.STATE_PAUSE); 394 | } 395 | 396 | private int restart() 397 | { 398 | log("Restart: ", State.current()); 399 | 400 | if ( player == null || State.is_stopped() ) 401 | return play(); 402 | 403 | // Always reset the volume. 404 | // There's something broken about the state model. 405 | // For example, we could be in state DUCK, then buffering starts, so 406 | // suddenly we're in state BUFFERING, although we're also still ducked. 407 | // The probelm is that one state is being used to model two different 408 | // things. Until that's fixed, it is nevertheless always safe (??) 409 | // reset the volume on restart. 410 | // 411 | player.setVolume(1.0f, 1.0f); 412 | 413 | if ( State.is(State.STATE_PLAY) || State.is(State.STATE_BUFFER) ) 414 | return done(); 415 | 416 | if ( State.is(State.STATE_DUCK) ) 417 | return done(State.STATE_PLAY); 418 | 419 | int focus = audio_manager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 420 | if ( focus != AudioManager.AUDIOFOCUS_REQUEST_GRANTED ) 421 | { 422 | toast("Failed to acquire audio focus."); 423 | return done(); 424 | } 425 | 426 | if ( pause_task != null ) 427 | { pause_task.cancel(true); pause_task = null; } 428 | 429 | player.start(); 430 | return done(State.STATE_PLAY); 431 | } 432 | 433 | /* ******************************************************************** 434 | * Respond to click events from the notification. 435 | */ 436 | 437 | private int click() 438 | { 439 | log("Click: ", State.current()); 440 | 441 | if ( State.is(State.STATE_DISCONNECTED) ) 442 | { 443 | stop(); 444 | Notify.cancel(); 445 | return done(); 446 | } 447 | 448 | if ( State.is_playing() && ! isNetworkUrl() ) 449 | return pause(); 450 | 451 | if ( State.is_playing() ) 452 | { 453 | stop(); 454 | Notify.cancel(); 455 | return done(); 456 | } 457 | 458 | if ( player == null || State.is_stopped() ) 459 | return play(); 460 | 461 | if ( State.is(State.STATE_PAUSE) ) 462 | return restart(); 463 | 464 | log("Unhandled click: ", State.current()); 465 | return done(); 466 | } 467 | 468 | /* ******************************************************************** 469 | * All onStartCommand() invocations end here... 470 | */ 471 | 472 | private int done(String state) 473 | { 474 | if ( state != null ) 475 | State.set_state(context, state, isNetworkUrl()); 476 | 477 | return done(); 478 | } 479 | 480 | private int done() 481 | { return START_NOT_STICKY; } 482 | 483 | /* ******************************************************************** 484 | * Listeners... 485 | */ 486 | 487 | @Override 488 | public void onBufferingUpdate(MediaPlayer player, int percent) 489 | { 490 | /* 491 | // Notifications of buffer state seem to be unreliable. 492 | if ( 0 <= percent && percent <= 100 ) 493 | log("Buffering: ", ""+percent, "%"); 494 | */ 495 | } 496 | 497 | @Override 498 | public boolean onInfo(MediaPlayer player, int what, int extra) 499 | { 500 | switch (what) 501 | { 502 | case MediaPlayer.MEDIA_INFO_BUFFERING_START: 503 | State.set_state(context, State.STATE_BUFFER, isNetworkUrl()); 504 | break; 505 | 506 | case MediaPlayer.MEDIA_INFO_BUFFERING_END: 507 | failure_ttl = initial_failure_ttl; 508 | State.set_state(context, State.STATE_PLAY, isNetworkUrl()); 509 | break; 510 | } 511 | return true; 512 | } 513 | 514 | private Later start_buffering_task = null; 515 | 516 | private void start_buffering() 517 | { 518 | if ( start_buffering_task != null ) 519 | start_buffering_task.cancel(true); 520 | 521 | // We'll give it 90 seconds for the stream to start. Otherwise, we'll 522 | // declare an error. onError() tries to restart, in some cases. 523 | start_buffering_task = (Later) 524 | new Later(90) 525 | { 526 | @Override 527 | public void later() 528 | { 529 | start_buffering_task = null; 530 | onError(null,0,0); 531 | } 532 | }.start(); 533 | } 534 | 535 | private Later stop_soon_task = null; 536 | 537 | private void stop_soon() 538 | { 539 | if ( stop_soon_task != null ) 540 | stop_soon_task.cancel(true); 541 | 542 | stop_soon_task = (Later) 543 | new Later(300) 544 | { 545 | @Override 546 | public void later() 547 | { 548 | stop_soon_task = null; 549 | stop(); 550 | } 551 | }.start(); 552 | } 553 | 554 | private void try_recover() 555 | { 556 | stop_soon(); 557 | if ( isNetworkUrl() && 0 < failure_ttl ) 558 | { 559 | failure_ttl -= 1; 560 | if ( connectivity.isConnected(context) ) 561 | play(); 562 | else 563 | connectivity.dropped_connection(); 564 | } 565 | } 566 | 567 | // Waring: onError is called, by start_buffering(), with null arguments. 568 | // Do not rely upon these arguments being meaningful. 569 | @Override 570 | public boolean onError(MediaPlayer player, int what, int extra) 571 | { 572 | log("Error: ", ""+what); 573 | 574 | State.set_state(context,State.STATE_ERROR, isNetworkUrl()); 575 | try_recover(); // This calls stop_soon(). 576 | 577 | // Returning true, here, prevents the onCompletionlistener from being called. 578 | return true; 579 | } 580 | 581 | /* ******************************************************************** 582 | * On completion listener... 583 | */ 584 | 585 | @Override 586 | public void onCompletion(MediaPlayer mp) 587 | { 588 | log("Completion: " + State.current()); 589 | 590 | // We only enter STATE_COMPLETE for non-network URLs, and only if we 591 | // really were playing (so not, for example, if we are in STATE_ERROR, or 592 | // STATE_DISCONNECTED). This simplifies connectivity management, in 593 | // Connectivity.java. 594 | log("onCompletion: isNetworkUrl: " + isNetworkUrl()); 595 | if ( ! isNetworkUrl() && (State.is(State.STATE_PLAY) || State.is(State.STATE_DUCK)) ) 596 | State.set_state(context, State.STATE_COMPLETE, isNetworkUrl()); 597 | 598 | // Don't stay completed for long. stop(), soon, to free up resources. 599 | stop_soon(); 600 | } 601 | 602 | /* ******************************************************************** 603 | * Audio focus listeners... 604 | */ 605 | 606 | @Override 607 | public void onAudioFocusChange(int change) 608 | { 609 | log("onAudioFocusChange: ", ""+change); 610 | 611 | if ( player != null ) 612 | switch (change) 613 | { 614 | case AudioManager.AUDIOFOCUS_GAIN: 615 | log("Audiofocus_gain"); 616 | restart(); 617 | break; 618 | 619 | case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 620 | log("Transient"); 621 | // pause(); 622 | // break; 623 | // Drop through. 624 | 625 | case AudioManager.AUDIOFOCUS_LOSS: 626 | log("Audiofocus_loss"); 627 | pause(); 628 | break; 629 | 630 | case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 631 | log("Audiofocus_loss_transient_can_duck"); 632 | duck("Audio focus lost, ducking..."); 633 | break; 634 | } 635 | } 636 | 637 | /* ******************************************************************** 638 | * Logging... 639 | */ 640 | 641 | private void log(String... msg) 642 | { Logger.log(msg); } 643 | 644 | private void toast(String msg) 645 | { Logger.toast(msg); } 646 | 647 | /* ******************************************************************** 648 | * Required abstract method... 649 | */ 650 | 651 | @Override 652 | public IBinder onBind(Intent intent) 653 | { return null; } 654 | } 655 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/IntentRadio.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.os.Bundle; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.content.Context; 7 | 8 | import android.os.AsyncTask;; 9 | 10 | import android.text.Html; 11 | import android.text.method.LinkMovementMethod; 12 | import android.text.Spanned; 13 | import android.view.View; 14 | import android.view.Menu; 15 | import android.view.MenuItem; 16 | import android.view.MenuInflater; 17 | import android.widget.Button; 18 | import android.widget.TextView; 19 | import android.widget.PopupMenu; 20 | 21 | import android.content.ClipData; 22 | import android.content.ClipboardManager; 23 | 24 | public class IntentRadio extends PreferenceActivity 25 | { 26 | private static Context context = null; 27 | 28 | private AsyncTask draw_task = null; 29 | private AsyncTask install_task = null; 30 | private String url = null; 31 | 32 | @Override 33 | public void onCreate(Bundle savedInstanceState) 34 | { 35 | super.onCreate(savedInstanceState); 36 | context = getApplicationContext(); 37 | Logger.init(context); 38 | 39 | // Handle app activity... 40 | // 41 | draw_task = null; 42 | install_task = null; 43 | 44 | setContentView(R.layout.main); 45 | 46 | TextView view = (TextView) findViewById(R.id.text); 47 | view.setMovementMethod(LinkMovementMethod.getInstance()); 48 | view.setText("Loading..."); 49 | 50 | // Read file contents and build date for main screen asyncronously... 51 | // 52 | draw_task = new AsyncTask() 53 | { 54 | private TextView view = null; 55 | private Integer id = null; 56 | private String url = null; 57 | 58 | @Override 59 | protected Spanned doInBackground(Object... args) 60 | { 61 | view = (TextView) args[0]; 62 | id = (Integer) args[1]; 63 | url = (String) args[2]; 64 | 65 | String text = ReadRawTextFile.read(getApplicationContext(),id.intValue()); 66 | 67 | if ( url != null ) 68 | text = text.replace("REPLACE_URL", url); 69 | else 70 | text += 71 | "\n" 72 | + "\n" 73 | + "Distribution: " + getString(R.string.distribution) + "\n" 74 | + "Version: " + Build.version_string(context) + "\n" 75 | + "Build: " + Build.getBuildDate(context) + "\n" 76 | + "\n"; 77 | 78 | 79 | return Html.fromHtml(text); 80 | } 81 | 82 | @Override 83 | protected void onPostExecute(Spanned html) 84 | { 85 | if ( ! isCancelled() ) 86 | view.setText(html); 87 | } 88 | 89 | }; 90 | 91 | // Handle intent... 92 | // 93 | Intent intent = getIntent(); 94 | String action = intent.getAction(); 95 | if ( action.equals(Intent.ACTION_VIEW) ) 96 | { 97 | url = intent.getDataString(); 98 | Intent msg = new Intent(context, IntentPlayer.class); 99 | msg.putExtra("action", getString(R.string.intent_play)); 100 | msg.putExtra("url", url); 101 | context.startService(msg); 102 | findViewById(R.id.clip_url).setVisibility(View.VISIBLE); 103 | draw_task.execute(view, R.raw.playing, url); 104 | return; 105 | } 106 | 107 | // Open app... 108 | // 109 | findViewById(R.id.clip_buttons).setVisibility(View.VISIBLE); 110 | findViewById(R.id.install_tasker).setVisibility(View.VISIBLE); 111 | draw_task.execute(view, R.raw.message, null); 112 | } 113 | 114 | /* ******************************************************************** 115 | * Destroy activity: clean up any remaining tasks... 116 | */ 117 | 118 | public void onDestroy() 119 | { 120 | if ( draw_task != null && draw_task.getStatus() != AsyncTask.Status.FINISHED ) 121 | draw_task.cancel(true); 122 | 123 | if ( install_task != null && install_task.getStatus() != AsyncTask.Status.FINISHED ) 124 | install_task.cancel(true); 125 | 126 | draw_task = null; 127 | install_task = null; 128 | 129 | super.onDestroy(); 130 | } 131 | 132 | /* ******************************************************************** 133 | * Launch clip buttons... 134 | */ 135 | 136 | public void clip_buttons(View v) 137 | { 138 | Intent clipper = new Intent(IntentRadio.this, ClipButtons.class); 139 | startActivity(clipper); 140 | } 141 | 142 | /* ******************************************************************** 143 | * Install sample Tasker project... 144 | * 145 | * This currently assumes that Tasker *always* stores projects in: 146 | * 147 | * - /sdcard/Tasker/projects 148 | * 149 | * Does it? 150 | * 151 | * File I/O is more blocking than anything else we're doing, so we'll do it 152 | * asyncronously. 153 | */ 154 | 155 | private static final String project_file = "Tasker/projects/IntentRadio.prj.xml"; 156 | 157 | public void install_tasker(View v) 158 | { 159 | if ( install_task != null && install_task.getStatus() != AsyncTask.Status.FINISHED ) 160 | return; 161 | 162 | install_task = new AsyncTask() 163 | { 164 | @Override 165 | protected String doInBackground(Void... unused) 166 | { 167 | return CopyResource.copy(context, R.raw.tasker, project_file); 168 | } 169 | 170 | @Override 171 | protected void onPostExecute(String error) 172 | { 173 | if ( isCancelled() ) 174 | return; 175 | 176 | if ( error == null /* so, success */ ) 177 | { 178 | toast("Project file installed...\n\n/sdcard/" + project_file); 179 | toast("Next, import this project into Tasker."); 180 | } 181 | else 182 | toast("Install error:\n" + error + "\n\n/sdcard/" + project_file); 183 | } 184 | 185 | }; 186 | install_task.execute(); 187 | } 188 | 189 | /* ******************************************************************** 190 | * Clip url... 191 | */ 192 | 193 | public void clip_url(View view) 194 | { Clipper.clip(context,url); } 195 | 196 | /* ******************************************************************** 197 | * Toasts... 198 | */ 199 | 200 | static private void toast(String msg) 201 | { Logger.toast_long(msg); } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Intents.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.BroadcastReceiver; 6 | 7 | public class Intents extends BroadcastReceiver { 8 | 9 | @Override 10 | public void onReceive(Context context, Intent intent) { 11 | Intent msg = new Intent(context, IntentPlayer.class); 12 | msg.putExtra("action", intent.getAction()); 13 | passExtra("url", intent, msg); 14 | passExtra("name", intent, msg); 15 | passExtra("debug", intent, msg); 16 | msg.putExtra("broadcast", true); 17 | context.startService(msg); 18 | } 19 | 20 | private static void passExtra(String key, Intent intent, Intent msg) 21 | { 22 | if ( intent.hasExtra(key) ) 23 | { 24 | String str = intent.getStringExtra(key); 25 | if ( str != null ) 26 | msg.putExtra(key, str); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Later.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import android.os.AsyncTask; 4 | import java.lang.Thread; 5 | 6 | public abstract class Later extends AsyncTask 7 | { 8 | private static final int default_seconds = 120; 9 | private int seconds = default_seconds; 10 | private int then; 11 | 12 | public abstract void later(); 13 | 14 | // secs < 0: execute immediately 15 | // secs == 0: delay for default_seconds 16 | // otherwise: delay for secs 17 | // 18 | Later(int secs) 19 | { 20 | super(); 21 | if ( secs == 0 ) 22 | secs = default_seconds; 23 | seconds = secs; 24 | then = Counter.now(); 25 | } 26 | 27 | Later() 28 | { this(default_seconds); } 29 | 30 | protected Void doInBackground(Integer... args) 31 | { 32 | try { if ( 0 < seconds ) Thread.sleep(seconds * 1000); } 33 | catch ( Exception e ) { } 34 | return null; 35 | } 36 | 37 | protected void onPostExecute(Void ignored) 38 | { 39 | if ( ! isCancelled() && Counter.still(then) ) 40 | later(); 41 | } 42 | 43 | public AsyncTask start() 44 | { return executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } 45 | } 46 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Logger.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | 6 | import java.util.Date; 7 | import java.text.DateFormat; 8 | import java.text.SimpleDateFormat; 9 | import android.text.TextUtils; 10 | 11 | import android.content.Context; 12 | import android.widget.Toast; 13 | import android.util.Log; 14 | 15 | import android.os.Process; 16 | import android.content.pm.ApplicationInfo; 17 | 18 | public class Logger 19 | { 20 | private static Context context = null; 21 | private static String name = null; 22 | private static boolean debugging = false; 23 | private static int pid = 0; 24 | 25 | private static final boolean use_file = true; 26 | private static final boolean append = false; 27 | private static final int DEBUGGABLE = ApplicationInfo.FLAG_DEBUGGABLE; 28 | 29 | /* ******************************************************************** 30 | * Initialisation... 31 | */ 32 | 33 | public static void init(Context a_context) 34 | { 35 | if ( context != null ) 36 | return; 37 | 38 | context = a_context; 39 | name = context.getString(R.string.app_name); 40 | 41 | // Always enable debugging on debug builds... 42 | // 43 | if ( Build.debug_build(context) ) 44 | { 45 | state("debug"); 46 | log("Debug build: debugging enabled"); 47 | } 48 | } 49 | 50 | /* ******************************************************************** 51 | * Enable/disable... 52 | */ 53 | 54 | public static void state(String s) 55 | { 56 | if ( s.equals("debug") || s.equals("yes") || s.equals("on") || s.equals("start") ) 57 | { start(); return; } 58 | 59 | if ( s.equals("nodebug") || s.equals("no") || s.equals("off") || s.equals("stop") ) 60 | { stop(); return; } 61 | 62 | Log.d(name, "Logger: invalid state change: " + s); 63 | } 64 | 65 | /* ******************************************************************** 66 | * State changes... 67 | */ 68 | 69 | private static DateFormat format = null; 70 | private static FileOutputStream file = null; 71 | 72 | private static void start() 73 | { 74 | if ( debugging ) 75 | return; 76 | 77 | debugging = true; 78 | pid = Process.myPid(); 79 | 80 | if ( format == null ) 81 | format = new SimpleDateFormat("HH:mm:ss "); 82 | 83 | if ( use_file ) 84 | try 85 | { 86 | File log_file = new File(context.getExternalFilesDir(null), context.getString(R.string.intent_log_file)); 87 | file = new FileOutputStream(log_file, append); 88 | } 89 | catch (Exception e) 90 | { file = null; } 91 | 92 | log("Logger: -> on"); 93 | } 94 | 95 | private static void stop() 96 | { 97 | log("Logger: -> off"); 98 | 99 | debugging = false; 100 | 101 | if ( file != null ) 102 | { 103 | try { file.close(); } 104 | catch (Exception e) { } 105 | file = null; 106 | } 107 | } 108 | 109 | /* ******************************************************************** 110 | * Public logging methods... 111 | */ 112 | 113 | public static void log(String... msg) 114 | { 115 | if ( ! debugging || msg == null ) 116 | return; 117 | 118 | String text = format.format(new Date()) + pid + " " + TextUtils.join("",msg); 119 | 120 | Log.d(name, text); 121 | log_file(text); 122 | } 123 | 124 | public static void toast(String msg) 125 | { toast(msg,false); } 126 | 127 | public static void toast_long(String msg) 128 | { toast(msg,true); } 129 | 130 | /* ******************************************************************** 131 | * Private logging method... 132 | */ 133 | 134 | public static void toast(String msg, boolean vlong) 135 | { 136 | if ( msg == null || context == null ) 137 | return; 138 | 139 | Toast.makeText(context, msg, (vlong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT)).show(); 140 | log(msg); 141 | } 142 | 143 | private static void log_file(String msg) 144 | { 145 | if ( file != null ) 146 | try 147 | { 148 | file.write((msg + "\n").getBytes()); 149 | file.flush(); 150 | } catch (Exception e) {} 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Makefile: -------------------------------------------------------------------------------- 1 | 2 | root = ../../../../.. 3 | 4 | debug install release clean google google-release: 5 | cd $(root) && $(MAKE) $@ 6 | 7 | .PHONY: debug install release clean google google-release 8 | 9 | # Use path (again) so that completion works. 10 | # 11 | include ../../../../../Makefile.test 12 | 13 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Notify.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import java.lang.System; 4 | import android.app.Service; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | 8 | import android.app.Notification; 9 | import android.app.NotificationManager; 10 | import android.app.Notification.Builder; 11 | 12 | import android.app.PendingIntent; 13 | 14 | import android.preference.PreferenceManager; 15 | import android.content.SharedPreferences; 16 | 17 | public class Notify 18 | { 19 | private static final int note_id = 100; 20 | 21 | private static Service service = null; 22 | private static Context context = null; 23 | 24 | private static NotificationManager note_manager = null; 25 | private static Builder builder = null; 26 | private static Notification note = null; 27 | 28 | public static void init(Service a_service, Context a_context) 29 | { 30 | service = a_service; 31 | context = a_context; 32 | 33 | note_manager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE); 34 | 35 | PendingIntent pending_click = null; 36 | String intent_click = context.getString(R.string.intent_click); 37 | 38 | log("Notify: using broadcasts to deliver clicks."); 39 | Intent click = new Intent(intent_click); 40 | pending_click = PendingIntent.getBroadcast(context, 0, click, 0); 41 | 42 | builder = 43 | new Notification.Builder(context) 44 | .setOngoing(false) 45 | .setSmallIcon(R.drawable.intent_radio) 46 | .setPriority(Notification.PRIORITY_HIGH) 47 | .setContentIntent(pending_click) 48 | .setContentTitle(service.getString(R.string.app_name_long)) 49 | ; 50 | } 51 | 52 | private static String previous_state = null; 53 | private static String previous_name = null; 54 | private static boolean previous_foreground = false; 55 | private static boolean notification_created = false; 56 | 57 | public static void name(String name) 58 | { 59 | if ( previous_name == null || ! name.equals(previous_name) ) 60 | { 61 | builder.setContentText(name); 62 | previous_name = name; 63 | } 64 | } 65 | 66 | public static void note(boolean isNetworkUrl) 67 | { 68 | boolean current_foreground = State.is_playing(); 69 | 70 | if ( current_foreground != previous_foreground || ! notification_created ) 71 | { 72 | if ( current_foreground ) 73 | { 74 | log("Starting foreground."); 75 | Notification note = 76 | builder 77 | .setContentInfo(isNetworkUrl ? "(touch to stop)" : "(touch to pause)") 78 | .setOngoing(true) 79 | .setPriority(Notification.PRIORITY_HIGH) 80 | .setWhen(System.currentTimeMillis()) 81 | .build(); 82 | service.startForeground(note_id, note); 83 | } 84 | else 85 | { 86 | log("Stopping foreground."); 87 | // It would be nice to use "false", below. However, while that 88 | // gives nice smooth notification transitions, the resulting 89 | // notification is *always* "ongoing", so it cannot be dismissed. 90 | // 91 | service.stopForeground(true); 92 | Notification note = 93 | builder 94 | .setContentInfo(State.is(State.STATE_PAUSE) ? "(touch to resume)" : "(touch to restart)") 95 | .setOngoing(false) 96 | .setPriority(Notification.PRIORITY_DEFAULT) 97 | .setWhen(System.currentTimeMillis()) 98 | .build(); 99 | note_manager.notify(note_id, note); 100 | } 101 | previous_foreground = current_foreground; 102 | notification_created = true; 103 | } 104 | 105 | String state = State.text(); 106 | if ( previous_state == null || ! state.equals(previous_state) ) 107 | { 108 | log("Notify state: ", state); 109 | Notification note = 110 | builder 111 | .setSubText(state+".") 112 | .setWhen(System.currentTimeMillis()) 113 | .build(); 114 | note_manager.notify(note_id, note); 115 | previous_state = state; 116 | } 117 | 118 | // Cludge. 119 | // Cancel the notification if we're now stopped and the user has indicated 120 | // a preference for no on-going notification. 121 | if ( State.is(State.STATE_STOP) ) 122 | { 123 | SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); 124 | if ( ! settings.getBoolean("persistent_notification", true) ) 125 | note_manager.cancel(note_id); 126 | } 127 | } 128 | 129 | public static void cancel() 130 | { 131 | log("Notify cancel()."); 132 | note_manager.cancelAll(); 133 | } 134 | 135 | /* ******************************************************************** 136 | * Logging... 137 | */ 138 | 139 | private static void log(String... msg) 140 | { Logger.log(msg); } 141 | 142 | private static void toast(String msg) 143 | { Logger.toast(msg); } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /ir_library/src/org/smblott/intentradio/Playlist.java: -------------------------------------------------------------------------------- 1 | package org.smblott.intentradio; 2 | 3 | import java.util.Random; 4 | import java.util.ArrayList; 5 | import java.util.regex.Pattern; 6 | import java.util.regex.Matcher; 7 | import java.util.List; 8 | import android.os.AsyncTask; 9 | import android.webkit.URLUtil; 10 | import android.net.Uri; 11 | // import java.util.concurrent.ThreadPoolExecutor; 12 | 13 | public class Playlist extends AsyncTask 14 | { 15 | private static final int max_ttl = 10; 16 | 17 | // Enumeration for playlist types. 18 | // 19 | private static final int NONE = 0; 20 | private static final int M3U = 1; 21 | private static final int PLS = 2; 22 | 23 | private IntentPlayer player = null; 24 | private String start_url = null; 25 | private int then = 0; 26 | 27 | Playlist(IntentPlayer a_player, String a_url) 28 | { 29 | super(); 30 | player = a_player; 31 | start_url = a_url; 32 | then = Counter.now(); 33 | log("Playlist: then=" + then); 34 | } 35 | 36 | public Playlist start() 37 | { 38 | return (Playlist) executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 39 | } 40 | 41 | protected String doInBackground(Void... args) 42 | { 43 | String url = start_url; 44 | int ttl = max_ttl; 45 | int type = NONE; 46 | 47 | if ( url != null && url.length() != 0 && URLUtil.isValidUrl(url) ) 48 | type = playlist_type(url); 49 | else 50 | url = null; 51 | 52 | if ( 0 < ttl && url != null && type != NONE ) 53 | { 54 | ttl -= 1; 55 | log("Playlist url: ", url); 56 | log("Playlist type: ", ""+type); 57 | 58 | url = select_url_from_playlist(url,type); 59 | if ( url != null && url.length() != 0 && URLUtil.isValidUrl(url) ) 60 | type = playlist_type(url); 61 | else 62 | url = null; 63 | } 64 | 65 | if ( url == null ) { log("Playlist: failed to extract url." ); } 66 | if ( ttl == 0 ) { log("Playlist: too many playlists (TTL)." ); url = null; } 67 | if ( url != null ) { log("Playlist final url: ", url ); } 68 | 69 | return url; 70 | } 71 | 72 | protected void onPostExecute(String url) { 73 | if ( url != null && player != null && ! isCancelled() && Counter.still(then) ) 74 | player.play_launch(url); 75 | else 76 | log("Playlist: launch cancelled"); 77 | } 78 | 79 | /* ******************************************************************** 80 | * Filter lines of a playlist... 81 | */ 82 | 83 | static String filter(String line, int type) 84 | { 85 | switch (type) 86 | { 87 | // 88 | case M3U: 89 | return line.indexOf('#') == 0 ? "" : line; 90 | // 91 | case PLS: 92 | if ( line.startsWith("File") && 0 < line.indexOf('=') ) 93 | return line; 94 | return ""; 95 | // 96 | default: 97 | // Should not happen. 98 | log("Playlist invalid filter type: ", line); 99 | return line; 100 | } 101 | } 102 | 103 | /* ******************************************************************** 104 | * Select a single (random) url from a playlist... 105 | */ 106 | 107 | private static Random random = null; 108 | 109 | private String select_url_from_playlist(String url, int type) 110 | { 111 | List lines = HttpGetter.httpGet(url); 112 | 113 | for (int i=0; i links = select_urls_from_list(lines); 124 | if ( links.size() == 0 ) 125 | return null; 126 | 127 | for (int i=0; i select_urls_from_list(List lines) 146 | { 147 | ArrayList links = new ArrayList(); 148 | 149 | if ( url_pattern == null ) 150 | url_pattern = Pattern.compile(url_regex); 151 | 152 | for (int i=0; i 2 | 3 | 4 | -------------------------------------------------------------------------------- /script/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | < ./AndroidManifest.xml \ 4 | sed -n '/android:version/ {s/.*="//; s/".*//; p}' \ 5 | | tr '\n' '-' \ 6 | | sed 's/-$//' 7 | -------------------------------------------------------------------------------- /src/.nothing: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/src/.nothing -------------------------------------------------------------------------------- /tasker_scripts/.gitignore: -------------------------------------------------------------------------------- 1 | numbers.txt 2 | sdcard 3 | -------------------------------------------------------------------------------- /tasker_scripts/Makefile: -------------------------------------------------------------------------------- 1 | 2 | dest = /sdcard/intent_radio 3 | 4 | install: 5 | adb push playlist $(dest) 6 | 7 | log: 8 | adb shell cat /sdcard/Tasker/.intent_radio/log.txt 9 | 10 | state: 11 | adb shell cat /sdcard/Tasker/.intent_radio/state.txt 12 | 13 | stop: 14 | $(MAKE) -C .. stop 15 | -------------------------------------------------------------------------------- /tasker_scripts/playlist: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Version 0.1.5 3 | 4 | # ######################################################################## 5 | # Instructions: 6 | # See http://intent-radio.smblott.org/playlist.html 7 | 8 | # ######################################################################## 9 | # Constraints: 10 | # This script has to work with the standard Android shell, and with only 11 | # the utilities available on a vanilla Android installation (which 12 | # doesn't even include head and tail). 13 | # 14 | # For URL encoding we use sed, if it's available. Otherwise, we use a 15 | # simple shell function to replace the most important non-URL characters. 16 | 17 | root='/sdcard/Tasker/.intent_radio' 18 | state="$root/state.txt" 19 | seendir="$root/seen" 20 | randomfile="$root/random" 21 | stopfile="$root/stop" 22 | loopfile="$root/loop" 23 | tmp="$state.tmp" 24 | log="$root/log.txt" 25 | 26 | # Bail early out early (to avoid even having to parse this script) if 27 | # there's nothing to be done. 28 | [ $# = 0 ] && exit 1 29 | [ $# = 1 ] && ! [ -f "$state" ] && exit 30 | [ $# = 1 ] && [ -f "$stopfile" ] && [ "$1" != 'next' ] && exit 31 | 32 | mkdir -p "$root" 33 | exec 4>> $log 2>&4 34 | 35 | # For testing, it is convenient to be able to run this script on 36 | # non-Android systems too. ir_on_android succeeds only if calibre is not 37 | # available. calibre is unlikely to be installed on an Android device. 38 | ir_on_android () 39 | { 40 | ! type calibre > /dev/null 41 | } 42 | 43 | log () 44 | { 45 | ir_on_android || echo "$*" 46 | date +"%H:%M.%S $*" >&4 47 | } 48 | 49 | log "args $*" 50 | 51 | ir_cleanup () 52 | { 53 | rm -fr "$state" "$seendir" "$loopfile" "$randomfile" "$stopfile" 54 | } 55 | 56 | # ######################################################################## 57 | # Options. 58 | # The vanilla shell on Android does not have getopt. 59 | 60 | command="$1" 61 | shift 62 | 63 | opt_random='' 64 | opt_loop='' 65 | 66 | while [ 0 -lt $# ] 67 | do 68 | case "$1" in 69 | "-r" ) opt_random='yes'; shift ;; 70 | "-l" ) opt_loop='yes'; shift ;; 71 | * ) break 2 ;; 72 | esac 73 | done 74 | 75 | # ######################################################################## 76 | # Identify file types. 77 | 78 | ir_is_url () 79 | { 80 | case "$1" in 81 | file://* ) true ;; 82 | http://* ) true ;; 83 | https://* ) true ;; 84 | content://* ) true ;; 85 | * ) false ;; 86 | esac 87 | } 88 | 89 | ir_is_playlist () 90 | { 91 | case "$1" in 92 | *.m3u ) [ -f "$1" ] ;; 93 | * ) false ;; 94 | esac 95 | } 96 | 97 | ir_is_audio () 98 | { 99 | case "$1" in 100 | *.mp3 ) [ -f "$1" ] ;; 101 | *.aac ) [ -f "$1" ] ;; 102 | *.m4a ) [ -f "$1" ] ;; 103 | *.ogg ) [ -f "$1" ] ;; 104 | *.oga ) [ -f "$1" ] ;; 105 | *.flac ) [ -f "$1" ] ;; 106 | *.wav ) [ -f "$1" ] ;; 107 | * ) false ;; 108 | esac 109 | } 110 | 111 | # ######################################################################## 112 | # Take a text file and randomize the lines. 113 | # It would be so much simpler if we had "sort -R". 114 | 115 | ir_randomize () 116 | { 117 | if [ -n "$RANDOM" ] && type sort > /dev/null 118 | then 119 | log "randomizing..." 120 | file="$1" 121 | tmp_file_1="$file.1.rtmp" 122 | tmp_file_2="$file.2.rtmp" 123 | 124 | log "before randomizing... $file...:" 125 | cat "$file" >&4 126 | 127 | while read line 128 | do 129 | echo "$RANDOM $line" 130 | done < "$file" > "$tmp_file_1" 131 | 132 | sort -g "$tmp_file_1" > "$tmp_file_2" 133 | rm "$tmp_file_1" 134 | 135 | while read random line 136 | do 137 | echo "$line" 138 | done < "$tmp_file_2" > "$tmp_file_1" 139 | 140 | rm "$tmp_file_2" 141 | mv "$tmp_file_1" "$file" 142 | 143 | log "after randomizing...:" 144 | cat "$file" >&4 145 | 146 | else 147 | log "not randomizing... sort is not available" 148 | fi 149 | } 150 | 151 | # ######################################################################## 152 | # URL encoding. 153 | 154 | ir_urlencode_sed () 155 | { 156 | # Everything except: s/\//%2f/g; 157 | # Was sed 's/%/%25/g; s/ /%20/g; s/\t/%09/g; s/!/%21/g; s/"/%22/g; s/#/%23/g; s/\$/%24/g; s/\&/%26/g; s/(/%28/g; s/)/%29/g; s/\*/%2a/g; s/+/%2b/g; s/,/%2c/g; s/-/%2d/g; s/\./%2e/g; s/:/%3a/g; s/;/%3b/g; s//%3e/g; s/?/%3f/g; s/@/%40/g; s/\[/%5b/g; s/\\/%5c/g; s/\]/%5d/g; s/\^/%5e/g; s/_/%5f/g; s/`/%60/g; s/{/%7b/g; s/|/%7c/g; s/}/%7d/g; s/~/%7e/g; s/ /%09/g' | sed "s/'/%27/g" 158 | sed '/^\// {s/%/%25/g; s/ /%20/g; s/\t/%09/g;s/"/%22/g; s/#/%23/g; s/\&/%26/g; s/(/%28/g; s/)/%29/g; s/\*/%2a/g; s/+/%2b/g; s/,/%2c/g; s/:/%3a/g; s/;/%3b/g; s/>/%3e/g; s/?/%3f/g; s/@/%40/g; s/\[/%5b/g; s/\\/%5c/g; s/\]/%5d/g; s/\^/%5e/g; s/`/%60/g; s/{/%7b/g; s/|/%7c/g; s/}/%7d/g; s/~/%7e/g}' \ 159 | | sed "/^\// {s/'/%27/g}" 160 | } 161 | 162 | # Copy stdin to stdout, replacing every instance of "$1" withe "$2". 163 | ir_replace () 164 | { 165 | symbol="$1" 166 | replacement="$2" 167 | 168 | while read line 169 | do 170 | done_first='' 171 | OLDIFS="$IFS" 172 | IFS="$symbol" 173 | for tok in $line 174 | do 175 | [ -n "$done_first" ] && echo -n "$replacement" 176 | echo -n "$tok" 177 | done_first='yes' 178 | done 179 | echo 180 | IFS="$OLDIFS" 181 | done 182 | } 183 | 184 | # URL encode, but only replacing "%" and " ". 185 | ir_urlencode_poor () 186 | { 187 | ir_replace "%" "%25" \ 188 | | ir_replace " " "%20" 189 | } 190 | 191 | ir_url_encode () 192 | { 193 | if type sed > /dev/null 194 | then 195 | ir_urlencode_sed 196 | else 197 | ir_urlencode_poor 198 | fi 199 | } 200 | 201 | ir_make_file_url () 202 | { 203 | while read f 204 | do 205 | case "$f" in 206 | /* ) echo "file://$f" ;; 207 | * ) echo "$f" ;; 208 | esac 209 | done 210 | } 211 | 212 | ir_urlencode_files () 213 | { 214 | ir_url_encode | ir_make_file_url 215 | } 216 | 217 | # ######################################################################## 218 | # Play. 219 | 220 | IR_PLAY_INTENT='org.smblott.intentradio.PLAY' 221 | 222 | ir_send_intent () 223 | { 224 | log "" 225 | echo " " am broadcast -a "$@" >&4 226 | ir_on_android \ 227 | && am broadcast -a "$@" 228 | } 229 | 230 | ir_play () 231 | { 232 | echo "$1" 233 | ir_send_intent $IR_PLAY_INTENT -e url "$1" -e name "$1" 234 | } 235 | 236 | # ######################################################################## 237 | # Play next item. 238 | 239 | ir_next_picker () 240 | { 241 | while [ -f "$state" ] && [ -s "$state" ] 242 | do 243 | { 244 | read item 245 | cat >&3 246 | } < "$state" 3> "$tmp" && mv "$tmp" "$state" 247 | 248 | if [ -n "$item" ] && ir_is_url "$item" 249 | then 250 | ir_play "$item" 251 | return 0 252 | fi 253 | done 254 | return 1 255 | } 256 | 257 | ir_next () 258 | { 259 | ir_next_picker && return 260 | 261 | if [ -f "$loopfile" ] 262 | then 263 | cp "$loopfile" "$state" 264 | [ -f "$randomfile" ] && ir_randomize "$state" 265 | ir_next_picker && return 266 | fi 267 | 268 | ir_cleanup 269 | } 270 | 271 | # ######################################################################## 272 | # Construct playlists for various types of thing. 273 | # 274 | # Each function assumes: 275 | # - The current working directory is the location of the file or directory. 276 | # - It's first (and only) argument is the absolute path of the file or directory. 277 | # Callers arrange this by using ir_cd. 278 | 279 | ir_playlist () 280 | { 281 | while read f 282 | do 283 | case "$f" in 284 | \#* ) true ;; 285 | /* ) ir_process "$f" ;; 286 | * ) [ -n "$f" ] && ir_process "$(pwd)/$f" ;; 287 | esac 288 | done < "$1" 289 | } 290 | 291 | ir_directory () 292 | { 293 | dir="$1" 294 | ls | while read file 295 | do 296 | ir_process "$dir/$file" 297 | done 298 | } 299 | 300 | ir_file () 301 | { 302 | echo "$1" 303 | } 304 | 305 | # ######################################################################## 306 | # Because playlist and directory handling is recursive, it is possible to 307 | # get stuck in an infinite loop. Here, we provide tests to ensure that 308 | # that cannot happen. 309 | 310 | ir_see () 311 | { 312 | for thing 313 | do 314 | [ -d "$thing" ] && mkdir -p "$seendir/$thing" 315 | [ -f "$thing" ] && true > "$seendir/$thing" 316 | done 317 | } 318 | 319 | ir_seen () 320 | { 321 | [ -d "$seendir/$1" ] || [ -f "$seendir/$1" ] 322 | } 323 | 324 | # ######################################################################## 325 | # Change directory to that of the thing we're currently handling. 326 | 327 | ir_absolute_path () 328 | { 329 | if type readlink > /dev/null 330 | then 331 | # Amazingly, we have readlink in /system/bin (at least on my phone). 332 | readlink -f "$1" 333 | else 334 | # If we don't have readlink, then we'll try building an absolute path by hand. 335 | case "$1" in 336 | /* ) echo $1 ;; 337 | * ) echo "$(pwd)/$1" 338 | esac 339 | fi 340 | } 341 | 342 | # The first argument is either a directory or a file. 343 | # cd to the directory or (in the case of a file) the containing directory. 344 | # Then call the remaining command/arguments with the absolute path of the first 345 | # argument appended. 346 | # This allows us to handle both absolute and relative paths uniformly. 347 | # We assume that "$thing" exists. 348 | ir_cd () 349 | { 350 | thing=$(ir_absolute_path "$1") 351 | shift 352 | 353 | if [ -f "$thing" ] 354 | then 355 | directory="${thing%/*}" 356 | else 357 | directory="$thing" 358 | fi 359 | 360 | if ! ir_seen "$thing" 361 | then 362 | ir_see "$directory" "$thing" 363 | if [ "$(pwd)" = "$directory" ] 364 | then 365 | # Don't create a new process and change directory if we don't need 366 | # to. 367 | "$@" "$thing" 368 | else 369 | ( 370 | if cd "$directory" 371 | then 372 | "$@" "$thing" 373 | fi 374 | ) 375 | fi 376 | else 377 | log "skipping $thing" 378 | fi 379 | } 380 | 381 | # ######################################################################## 382 | # Handle various types of thing. 383 | 384 | ir_process () 385 | { 386 | for arg 387 | do 388 | ir_is_url "$arg" && echo $arg 389 | [ -d "$arg" ] && ir_cd "$arg" ir_directory 390 | ir_is_playlist "$arg" && ir_cd "$arg" ir_playlist 391 | ir_is_audio "$arg" && ir_cd "$arg" ir_file 392 | done 393 | } 394 | 395 | # ######################################################################## 396 | # External operations. 397 | 398 | ir_append () 399 | { 400 | log "append $*" 401 | 402 | if [ -f "$stopfile" ] 403 | then 404 | rm "$stopfile" 405 | fi 406 | 407 | # rm -fr "$seendir" 408 | ir_process "$*" | ir_urlencode_files > "$tmp" 409 | 410 | if [ -f "$loopfile" ] 411 | then 412 | cat "$tmp" >> "$loopfile" 413 | fi 414 | 415 | cat "$tmp" >> "$state" 416 | rm "$tmp" 417 | 418 | if [ -f "$randomfile" ] 419 | then 420 | # We randomize the entire playlist, not just the newly appended part. 421 | # Is this the best thing to do? 422 | ir_randomize "$state" 423 | fi 424 | 425 | log "state..." 426 | cat "$state" >&4 427 | log "...end" 428 | 429 | ir_next 430 | } 431 | 432 | ir_start () 433 | { 434 | ir_cleanup 435 | 436 | [ -n "$opt_loop" ] && true > "$loopfile" 437 | [ -n "$opt_random" ] && true > "$randomfile" 438 | 439 | ir_append "$@" 440 | } 441 | 442 | ir_stop () 443 | { 444 | true > "$stopfile" 445 | } 446 | 447 | ir_resume () 448 | { 449 | [ -f "$stopfile" ] && rm "$stopfile" 450 | ir_next 451 | } 452 | 453 | case "$command" in 454 | 'start' ) ir_start "$@" ;; 455 | 'append' ) ir_append "$@" ;; 456 | 'next' ) ir_resume ;; 457 | 'complete' ) ir_next ;; 458 | 'stop' ) ir_stop ;; 459 | # 'resume' ) ir_resume ;; 460 | esac 461 | 462 | true 463 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | Radio.prj.xml 3 | IntentRadio-release.apk 4 | playlist 5 | -------------------------------------------------------------------------------- /web/IR_Playlist.prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1410867261331 4 | 1410949154382 5 | 76 6 | 74 7 | IR State 8 | 9 | 599 10 | org.smblott.intentradio.STATE 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 1410864887952 19 | IR Playlist 20 | 76 21 | Alpha 22 | 58,78,79,74 23 | 24 | 25 | 1410864916248 26 | 1410949093374 27 | 58 28 | IRP Play Playlist 29 | 10 30 | 31 | 123 32 | false 33 | sh /sdcard/intent_radio/playlist start /sdcard/xy.m3u 34 | 35 | 36 | %STD_OUT 37 | %STD_ERR 38 | %CMD_EXIT 39 | 40 | 41 | hd_aaa_ext_glasses 42 | 43 | 44 | 45 | 1410949138362 46 | 1410949154382 47 | 74 48 | 49 | 123 50 | false 51 | sh /sdcard/intent_radio/playlist %state 52 | 53 | 54 | %STD_OUT 55 | %STD_ERR 56 | %CMD_EXIT 57 | 58 | 59 | 60 | 1410864916248 61 | 1410949097343 62 | 78 63 | IRP Next 64 | 10 65 | 66 | 123 67 | false 68 | sh /sdcard/intent_radio/playlist next 69 | 70 | 71 | %STD_OUT 72 | %STD_ERR 73 | %CMD_EXIT 74 | 75 | 76 | hd_aaa_ext_glasses 77 | 78 | 79 | 80 | 1410864916248 81 | 1410949859394 82 | 79 83 | IRP Play Directory 84 | 10 85 | 86 | 123 87 | false 88 | sh /sdcard/intent_radio/playlist start /sdcard/test 89 | 90 | 91 | %STD_OUT 92 | %STD_ERR 93 | %CMD_EXIT 94 | 95 | 96 | hd_aaa_ext_glasses 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /web/Makefile: -------------------------------------------------------------------------------- 1 | 2 | include $(HOME)/.gnumake.mk 3 | 4 | .PHONY: build install 5 | 6 | target += Radio.prj.xml 7 | target += IR_Playlist.prj.xml 8 | target += index.html 9 | target += playlist.html 10 | target += debug.html 11 | target += tasker.html 12 | target += privacy.html 13 | target += release.html 14 | target += favicon.ico 15 | target += playlist 16 | 17 | build: $(target) 18 | @true 19 | 20 | srv = smblott.computing.dcu.ie 21 | www = /home/www/smblott.org/intent_radio/ 22 | 23 | install: 24 | $(MAKE) build 25 | chmod ugo+r * 26 | rsync -av $(target) $(srv):$(www) 27 | 28 | Radio.prj.xml: ../ir_library/misc/Radio.prj.xml 29 | install -m 0444 $< $@ 30 | 31 | playlist: ../tasker_scripts/playlist 32 | install -m 0444 $< $@ 33 | 34 | -------------------------------------------------------------------------------- /web/debug.ascii: -------------------------------------------------------------------------------- 1 | = Intent Radio -- Debugging = 2 | Stephen Blott 3 | :toc2: 4 | :theme: smblott 5 | 6 | // ///////////////////////////////////////////////////// 7 | == Enable Debugging == 8 | 9 | .Enable/disable 10 | **** 11 | - Debugging is disabled, by default, on all signed release builds. 12 | - Debugging is enabled, by default, on builds that are signed with a debug key. 13 | 14 | - Debugging is enabled whenever an intent is received with a string extra 15 | `debug` with the value `on`. 16 | 17 | - Debugging is disabled whenever an intent is received with a string extra 18 | `debug` with the value `off`. 19 | **** 20 | 21 | // ///////////////////////////////////////////////////// 22 | == Debugging Features == 23 | 24 | .Logcat 25 | **** 26 | When debugging is enabled, various informational messages are written to 27 | the Android log with the tag `IntentRadio`. These can be viewed as 28 | follows: 29 | 30 | [source, sh] 31 | ---- 32 | adb logcat -s IntentRadio 33 | ---- 34 | 35 | 36 | [TIP] 37 | ==== 38 | Of course, the Android SDK is required. 39 | 40 | If you don't already know what `adb` and `logcat` are, then this probably 41 | isn't for you. 42 | ==== 43 | **** 44 | 45 | .File logging 46 | **** 47 | Additionally, whenever debugging is enabled, the same messages are written 48 | to a log file in the app's private file space on the SD card. For me, 49 | that's: 50 | 51 | - `/sdcard/Android/data/org.smblott.intentradio/files/intent-radio.log` 52 | 53 | [NOTE] 54 | ==== 55 | This file is truncated each time the _Intent Radio_ service starts, and 56 | each time debugging is enabled after previously being disabled. 57 | However, the file is never removed by the app itself. 58 | ==== 59 | **** 60 | 61 | // vim: set syntax=asciidoc: 62 | -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/web/favicon.ico -------------------------------------------------------------------------------- /web/index.ascii: -------------------------------------------------------------------------------- 1 | = Intent Radio = 2 | Stephen Blott 3 | :toc2: 4 | :theme: smblott 5 | 6 | .Quick links: 7 | **** 8 | - Jump straight to broadcast intent link:#how[instructions]. 9 | - Jump straight to link:#downloads[downloads]. 10 | **** 11 | 12 | // ///////////////////////////////////////////////////// 13 | == What? == 14 | 15 | **** 16 | _Intent Radio_ (or _IR_) is an android audio media app without a graphical user 17 | interface. It is controlled exclusively through the delivery of 18 | http://developer.android.com/reference/android/content/BroadcastReceiver.html[broadcast 19 | intents], typically generated by an automation app such as 20 | http://tasker.dinglisch.net/[Tasker]. If you don't know what a broadcast 21 | intent is, then this is probably not the app for you. 22 | 23 | Despite the name, _IR_ will happily play local audio media too. Once 24 | you're done reading here, take a look at link:playlist.html[this approach] 25 | to having _IR_ manage a playlist of songs. 26 | **** 27 | 28 | // // ///////////////////////////////////////////////////// 29 | // == Who? == 30 | // 31 | // **** 32 | // You might be interested in _Intent Radio_ if: 33 | // 34 | // - you use Android, 35 | // - you use Tasker, 36 | // - you listen to internet radio or other audio, and 37 | // - well, you're a geek. 38 | // **** 39 | 40 | // // ///////////////////////////////////////////////////// 41 | // == Download == 42 | // 43 | // **** 44 | // I'm too tight to pay Google $25 for the honour of hosting this on the 45 | // Play Store. So, for the moment, the download is on 46 | // https://github.com/smblott-github/intent_radio/releases[GitHub] (and direct 47 | // `apk` download links are link:#downloads[below]). 48 | // 49 | // Alternatively, you can download and build the app from 50 | // https://github.com/smblott-github/intent_radio[source]. 51 | // **** 52 | 53 | // ///////////////////////////////////////////////////// 54 | == Why? == 55 | 56 | // **** 57 | // There are already many internet radio apps for Android; so, why another 58 | // one? 59 | // 60 | // Well, I couldn't find one that worked just right for me... 61 | // 62 | // Xiialive:: 63 | // I tried (and like) http://xiialive.com/[xiialive]. And it supports external 64 | // broadcast intents. However, I was finding it would hang irredeemably 65 | // on start up about two times in five, mainly when on mobile data. 66 | // 67 | // Tunein:: 68 | // And I particularly like http://tunein.com/[tunein]. However, it doesn't 69 | // support either shortcuts or broadcast intents, so I have no way to 70 | // start and stop it automatically, say when a headset is plugged in or 71 | // out. 72 | // 73 | // BBC IPlayer Radio:: 74 | // The 75 | // https://play.google.com/store/apps/details?id=uk.co.bbc.android.iplayerradio&hl=en[BBC 76 | // IPlayer Radio] app is pretty slick; and most of what I listen to is BBC. 77 | // Again, however, there's no way to control playback without much 78 | // pointing and pressing. 79 | // **** 80 | 81 | **** 82 | It's simple: 83 | 84 | - _Automation_. 85 | 86 | http://tasker.dinglisch.net/[Tasker] is an automation app for Android. 87 | It's a small graphical programming language combined with a mechanism to 88 | fire off tasks in response to various events. 89 | 90 | _IR_ was written primarily to be driven by Tasker, either via task 91 | shortcuts, or in response to events such as a headset plugged in or out. 92 | Or perhaps based on the type of network to which you're attached. Or the 93 | network's SSID . Or the time of day. Or the day of the week. Or some 94 | combination of the above. Or whatever... 95 | **** 96 | 97 | // ///////////////////////////////////////////////////// 98 | [[how]] 99 | == How? == 100 | 101 | **** 102 | _Intent Radio_ supports the following broadcast intents... 103 | **** 104 | 105 | .`org.smblott.intentradio.PLAY` 106 | **** 107 | - start playback 108 | - extras: 109 | ** `url` -- the URL to play 110 | ** `name` -- the display name for the station 111 | 112 | Both extras are strings, and both are optional. 113 | 114 | If `url` is omitted, then Intent Radio restarts the previous 115 | station. If there is no such station, then it plays BBC Radio 4 (because, 116 | well, why not?). 117 | **** 118 | 119 | .`org.smblott.intentradio.STOP` 120 | **** 121 | - stop playback 122 | - extras: none 123 | **** 124 | 125 | .`org.smblott.intentradio.PAUSE` 126 | **** 127 | - pause playback, if playing 128 | - extras: none 129 | **** 130 | 131 | .`org.smblott.intentradio.RESTART` 132 | **** 133 | - restart playback, if paused 134 | - extras: none 135 | **** 136 | 137 | [[state-request]] 138 | .`org.smblott.intentradio.STATE_REQUEST` 139 | **** 140 | - request that _IR_ broadcast its state (see below) 141 | - extras: none 142 | 143 | This intent is _not required_ to receive state updates. 144 | _IR_ always broadcasts its state whenever it changes. 145 | **** 146 | 147 | [[state]] 148 | .`org.smblott.intentradio.STATE` 149 | **** 150 | - this is broadcast by Intent Radio itself to inform listeners (such as Tasker) of its state 151 | - extras: 152 | ** `state`: one of `stop`, `play`, `play/buffering`, `play/pause`, `play/duck`, `complete`, or `error` 153 | ** `url`: the current URL 154 | ** `name`: the current station name 155 | 156 | These are all strings. 157 | 158 | The sample Tasker project includes a listener which sets the global Tasker 159 | variables `%IRSTATE`, `%IRURL` and `%IRNAME` whenever such an intent is 160 | received. 161 | 162 | If you are not using the sample project (or have installed an earlier 163 | version and edited it to meet your own needs), then set up Tasker as follows: 164 | 165 | - Create a new Tasker profile: `event/System/Intent Received`. 166 | - Set _Action_ to `org.smblott.intentradio.STATE`. 167 | - Within the associated task, the local variable `%state` is the current 168 | state, `%url` the current URL, and `%name` the current name. Assign these to 169 | global variables of your choice, such as `%IRSTATE`, `%IRURL` and `%IRNAME`. 170 | **** 171 | 172 | .More Information: 173 | **** 174 | - During playback, _IR_ places a notification in the notification area. 175 | Clicking on the notification causes playback to either stop or 176 | (re-)start, as appropriate. 177 | 178 | - _IR_ uses the built-in Android 179 | http://developer.android.com/reference/android/media/MediaPlayer.html[media 180 | player] for playback. So all audio codecs supported natively by Android 181 | are supported. 182 | 183 | - Additionally, _Intent Radio_ supports 184 | http://en.wikipedia.org/wiki/PLS_(file_format)[PLS playlists] (suffix `.pls`) and http://en.wikipedia.org/wiki/M3U[M3U 185 | playlists] (suffix `.m3u` or `.m3u8`). 186 | For example: 187 | ** `http://www.bbc.co.uk/.../xxx.pls` 188 | ** `http://www.bbc.co.uk/.../xxx.m3u` 189 | ** `http://www.bbc.co.uk/.../xxx.m3u8` 190 | 191 | // - When a playlist is encountered, _IR_ selects a random link from the 192 | // playlist. Additionally, playlists can themselves contain playlists 193 | // (nested up to ten times). 194 | **** 195 | 196 | .Important! 197 | **** 198 | - Although _IR_ has no graphical user interface, you must 199 | nevertheless _launch the app at least once_. Otherwise, Android will 200 | not deliver broadcast intents to the app's background service. This is 201 | an Android security feature. + 202 | _Thereafter, it should not be necessary to launch the app at all_, even after a reboot or an upgrade. 203 | 204 | - _IR_ is built for Android API level 16, so only for 4.1 (Jelly Bean) 205 | devices and above. 206 | 207 | // Also, start up can be slow for some streams. BBC Radio 4, for example, 208 | // takes in excess of 30 seconds for playback to begin. I do not know the 209 | // source of this delay. Please be patient. 210 | **** 211 | 212 | // ///////////////////////////////////////////////////// 213 | == More... == 214 | 215 | .If you're using Tasker: 216 | **** 217 | - Then this link:./Radio.prj.xml[Tasker project] may be helpful in getting 218 | started with _Intent Radio_. + 219 | This project can also be installed from within the app itself. 220 | - Or, there are some basic instructions link:./tasker.html[here]. 221 | **** 222 | 223 | // .Debugging: 224 | // **** 225 | // - See link:./debug.html[here]. 226 | // **** 227 | 228 | // ///////////////////////////////////////////////////// 229 | [[downloads]] 230 | == Downloads == 231 | 232 | .Downloads: 233 | **** 234 | - Available on the Google 235 | https://play.google.com/store/apps/details?id=org.smblott.intentradioio[Play Store]. 236 | 237 | - Also available on https://f-droid.org/repository/browse/?fdid=org.smblott.intentradio[F-Droid]. + 238 | If you use this version, then also install the 239 | https://f-droid.org/[F-Droid app] in order to receive notifications of new releases. 240 | 241 | Do not install versions from both of these sources. Pick one and stick 242 | with it. They're the same thing. 243 | 244 | // Alternatively: 245 | // 246 | // - The latest release is 247 | // link:IntentRadio-release.apk[here] 248 | // or on https://dl.dropboxusercontent.com/u/41898306/IntentRadio-release.apk[Dropbox]. + 249 | // If you use this version, then you will _not_ be notified about updates. 250 | 251 | Alternatively: 252 | 253 | - Pick up the latest release from https://www.dropbox.com/sh/8f4tpun8zynqorz/AABLoQhgUBF2OsNmsSNCI5tia?dl=0[Dropbox]. 254 | **** 255 | 256 | // ///////////////////////////////////////////////////// 257 | [[notes]] 258 | == Release Notes == 259 | 260 | .Version 1.9.11: 261 | **** 262 | - Add permission to read external storage. This should allow IR to play content stored in external storage. 263 | **** 264 | 265 | .Version 1.9.10: 266 | **** 267 | - Just change the default URL (because the BBC have been diddling with 268 | their URLs). 269 | **** 270 | 271 | .Version 1.9.9: 272 | **** 273 | - Considerably better (but not yet perfect) handling of drops due to 274 | intermittent network connectivity. 275 | - Now defaults to reconnect on network drops. 276 | - Fixed minor bugs and/or inconsistencies. 277 | - Check out the web page for information on how to handle playlists. 278 | **** 279 | 280 | .Version 1.9.8: 281 | **** 282 | - Add option (disabled by default) to restart playback if the network 283 | connection is lost and then re-established. 284 | **** 285 | 286 | .Version 1.9.7: 287 | **** 288 | - Better playlist detection and handling. 289 | - Bug fixes for playlist handling. 290 | - Bug fixes for audio focus. 291 | - Better default BBC Radio 4 URL. 292 | - Clarify in-app messages. 293 | - Disable recursive playlist fetch. + 294 | Without recursive playlist fetch, we can use better URLs for some 295 | stations, including some important BBC radio stations. 296 | - Use better (faster start up) URL for default radio station. 297 | - Add in-app preference for cancelling the notification. 298 | **** 299 | 300 | .Version 1.8: 301 | **** 302 | - Major reworking of notifications, including look and click behaviour. + 303 | When stopped or paused, clicking on the notification now restarts. 304 | - Substantial reworking of the state/audio-focus/thread model. 305 | - Recursive playlist fetch. + 306 | If a playlist contains a playlist, then fetch that too. + 307 | Post a playlist of your favourite stations online somewhere, even containing 308 | playlists itself, point Intent Radio towards it, and listen to a random 309 | favourite station. 310 | - Changed name of state `play/dim` to `play/duck`; added state `complete`. 311 | - Add .M3u8 playlist type (for BBC Media Selector). 312 | **** 313 | 314 | .Version 1.7: 315 | **** 316 | - Add button to install sample Tasker project. 317 | - Improved (extended) the sample Tasker project. 318 | - Broadcast state (can be tracked in Tasker). 319 | - Sample project sets Tasker global variables `%IRSTATE`, `IRURL` and `IRNAME` to broadcast state. 320 | - Handle implicit intents. 321 | - A PLAY request without a "url" plays last station, if possible. 322 | - Re-use `MediaPlayer` instance. 323 | - Better notifications. 324 | **** 325 | 326 | .Version 1.6: 327 | **** 328 | - Separate page for clipping intents to clipboard. 329 | - Better layout in app. 330 | - Notification persists after errors + 331 | (but then it is not _ongoing_, so it can be dismissed). 332 | - Version and build date visible in app. 333 | - Much code refactoring and clean up. 334 | **** 335 | 336 | .Version 1.5: 337 | **** 338 | - Pause/restart broadcast intents. 339 | - Improved handling of audio focus. 340 | - Improved notifications. 341 | **** 342 | 343 | .Version 1.3: 344 | **** 345 | - M3u playlist support. 346 | **** 347 | 348 | .Version 1.2: 349 | **** 350 | - Obtain WiFi lock when on WiFi. 351 | - Handle audio focus events. 352 | **** 353 | 354 | .Version 1.1 355 | **** 356 | - Use `httpURLConnection`. 357 | - Fetch playlists on an asynchronous thread (so, non-blocking). 358 | **** 359 | 360 | .Version 1.0 361 | **** 362 | - Initial release. 363 | **** 364 | 365 | // vim: set syntax=asciidoc: 366 | -------------------------------------------------------------------------------- /web/intent_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smblott-github/intent_radio/3a6b8a7e98093a6daf0c2f2b70b52d12c61e5afd/web/intent_radio.png -------------------------------------------------------------------------------- /web/playlist.ascii: -------------------------------------------------------------------------------- 1 | = Intent Radio -- Playlists = 2 | Stephen Blott 3 | :toc2: 4 | :theme: smblott 5 | 6 | // ///////////////////////////////////////////////////// 7 | == Why? == 8 | 9 | **** 10 | I was asked by a user (thank you, Dan) to add playlist support to _IR_: 11 | play one song, then another, and so on. However, I didn't want to do this. 12 | The whole idea of _IR_ is to _keep things simple_, and allow users to 13 | control playback (and hence get the behaviour they want) via external apps 14 | such as Tasker. 15 | **** 16 | 17 | // ///////////////////////////////////////////////////// 18 | == What? == 19 | 20 | **** 21 | This page describes how implement playlists for _IR_, but without extending 22 | _IR_ itself. 23 | 24 | The approach is to write a shell script (which is called by Tasker) to 25 | launch the next track when the current track finishes. 26 | **** 27 | 28 | // ///////////////////////////////////////////////////// 29 | == How? == 30 | 31 | // ///////////////////////////////////////////////////// 32 | === First: A Tasker Project === 33 | 34 | **** 35 | First: 36 | 37 | . download link:./IR_Playlist.prj.xml[this Tasker project] and copy it to your Tasker 38 | project directory (`/sdcard/Tasker/projects`); then 39 | 40 | . using Tasker, import the project (`IR_Playlist.prj.xml`). 41 | 42 | The project won't do much yet, but take a look around. Eventually, you 43 | will have to edit the example tasks such that they reference suitable paths for your system. 44 | 45 | You do not need to make any changes to the other task or the `IR State` profile. 46 | **** 47 | 48 | // ///////////////////////////////////////////////////// 49 | === Next: A Shell Script === 50 | 51 | **** 52 | Next: 53 | 54 | . create a directory `/sdcard/intent_radio`, and 55 | 56 | . download link:playlist[this shell script] and copy it to that directory + 57 | (so, to `/sdcard/intent_radio/playlist`). 58 | 59 | The name of the directory (`/sdcard/intent_radio`) and the name of the 60 | script (`playlist`) matter; they're used in the sample Tasker project. 61 | **** 62 | 63 | [NOTE] 64 | ==== 65 | Warning: + 66 | Only run this shell script from within Tasker. Do not run it 67 | manually. (Otherwise, the state file it creates will end up being owned by the wrong user -- I think). 68 | ==== 69 | 70 | // ///////////////////////////////////////////////////// 71 | === Finally: Give it a Go === 72 | 73 | **** 74 | The Tasker project includes a Task `IRP Playlist Play`. All this does, is 75 | call the shell script above with suitable arguments. In the sample 76 | project, it's: 77 | 78 | sh /sdcard/intent_radio/playlist start /sdcard/xy.m3u 79 | 80 | Change the last part, the playlist file, to your own playlist (subject to 81 | the rules, below), and give it a go. 82 | **** 83 | 84 | **** 85 | You can also provide a directory name: 86 | 87 | sh /sdcard/intent_radio/playlist start /sdcard/Music/Yes/Tormato 88 | 89 | In this case, all audio files _in or under_ `/sdcard/Music/Yes/Tormato` will be added to the playlist. 90 | **** 91 | 92 | **** 93 | If you provide multiple arguments after `start`, then they are joined 94 | together (with spaces) and treated as a single thing. 95 | 96 | sh /sdcard/intent_radio/playlist start /sdcard/Music/Yes/The Yes Album 97 | 98 | This will be treated as a single directory (with spaces in its name). 99 | **** 100 | 101 | [NOTE] 102 | ==== 103 | The argument to `start` is _not_ a URL. It is the absolute path of 104 | a playlist file, a directory, or an audio file. 105 | ==== 106 | 107 | .Playlist files 108 | **** 109 | Roughly, the supported playlist format is http://en.wikipedia.org/wiki/M3U[M3U]: 110 | 111 | - Playlist file names must end with the extension `.m3u` (all lower case). 112 | - Empty lines and lines on which the first non-whitespace character is `#` are ignored. 113 | **** 114 | 115 | .Playlist entries 116 | **** 117 | URLs:: 118 | Entries beginning `file://`, `http://`, `https://` or `content://` are 119 | appended verbatim to the active playlist. Any necessary URL encoding 120 | must already have been applied. + 121 | + 122 | Suggestion: + 123 | Don't use `file://` for local files. If `sed` is available, 124 | then the `playlist` script handles URL encoding itself. 125 | 126 | Directories:: 127 | Every audio file and the contents of every playlist file _in or under_ 128 | the indicated directory are appended to the active playlist. Directory 129 | names can be either relative or absolute, and should not be URL encoded. 130 | 131 | Audio Files:: 132 | The audio file is appended to the active playlist. File names can be 133 | either relative or absolute, and should not be URL encoded. 134 | 135 | Other playlist files:: 136 | The contents of the playlist are appended to the active playlist, 137 | recursively. 138 | Playlist file names can be either relative or absolute, and should not 139 | be URL encoded. 140 | **** 141 | 142 | .Tips 143 | **** 144 | If you have existing M3U playlists which use relative paths, then there's a good chance 145 | that they will just work. 146 | 147 | It's easy to generate playlist files with standard Unix utilities (if you have them). Here are a couple of 148 | examples: 149 | 150 | Create a playlist with absolute file names: 151 | 152 | find /sdcard/Music/Yes/Tormato -type f -name '*.mp3' /sdcard/tormato.m3u 153 | 154 | Create a playlist with relative file names: 155 | 156 | cd /sdcard/Music/Yes/Tormato 157 | ls *.mp3 > Tormato.m3u 158 | **** 159 | 160 | .Techy tips 161 | **** 162 | Take a look at the shell script itself and you'll get a better idea of 163 | what's going on. The shell script leaves a log of its runs in: 164 | 165 | - `/sdcard/Tasker/.intent_radio/log.txt` 166 | 167 | The state file itself is: 168 | 169 | - `/sdcard/Tasker/.intent_radio/state.txt` 170 | 171 | (Yes, those are a leading dots in those file names.) 172 | **** 173 | 174 | // ///////////////////////////////////////////////////// 175 | == Options/Operations == 176 | 177 | The shell script supports the following options. 178 | 179 | .Start: 180 | **** 181 | - `start [-l] [-r] THING` 182 | 183 | `THING` is the absolute path of a playlist file, directory or audio file. 184 | 185 | If `-l` is provided, then the playlist is looped. If `-r` is provided, 186 | then the playlist is randomized. 187 | 188 | Note, however, that randomization is only possible if you have 189 | `sort` on your device. Which probably means you have to have 190 | `busybox` installed. Which probably means you need root. 191 | 192 | Duplicate items encountered are silently discarded. 193 | **** 194 | 195 | .Append 196 | **** 197 | - `append THING` 198 | 199 | Like `start`, but appends to an existing playlist. Also advances to the 200 | next track. `append` does not support the `-l` and `-r` options. 201 | 202 | New items will _not_ be added to the playlist if they are already on the 203 | playlist. 204 | **** 205 | 206 | .Next 207 | **** 208 | - `next` 209 | 210 | Move on to the next item on the playlist. If the playlist is stopped, then 211 | it is resumed. 212 | **** 213 | 214 | .Stop 215 | **** 216 | - `stop` 217 | 218 | Suspend the playlist. 219 | 220 | [NOTE] 221 | ==== 222 | _Do not use this option manually_. It is 223 | generated automatically when the Tasker project receives the `stop` state 224 | from the player. 225 | 226 | Also, `stop`, here, does not actually stop playback. It just disables the 227 | playlist. 228 | ==== 229 | **** 230 | 231 | // .Resume 232 | // **** 233 | // - `resume` 234 | // 235 | // Resume a previously-stopped playlist. Playback starts at the track after 236 | // the one which was playing when the playlist was suspended. 237 | // **** 238 | 239 | // ///////////////////////////////////////////////////// 240 | == How does it work? == 241 | 242 | **** 243 | _IR_ broadcasts its state. The `IR State` profile in the sample Tasker 244 | project listens for these broadcasts and forwards them to the `playlist` 245 | shell script. 246 | 247 | When the shell script sees a `completed` state, it fires off 248 | the next track. When it sees a `stop` state, it suspends the active 249 | playlist (so that subsequently, when a non-playlist track completes, it 250 | does not incorrectly fire off the next track on the active playlist). 251 | **** 252 | 253 | // vim: set syntax=asciidoc: 254 | -------------------------------------------------------------------------------- /web/privacy.ascii: -------------------------------------------------------------------------------- 1 | = Intent Radio -- Privacy = 2 | Stephen Blott 3 | :toc2: 4 | :theme: smblott 5 | 6 | .Privacy 7 | **** 8 | This app does not collect any data from you whatsoever. 9 | **** 10 | 11 | // vim: set syntax=asciidoc: 12 | -------------------------------------------------------------------------------- /web/release.ascii: -------------------------------------------------------------------------------- 1 | = Intent Radio -- Bleeding Edge = 2 | Stephen Blott 3 | :toc2: 4 | :theme: smblott 5 | 6 | .The bleeding edge release is available: 7 | **** 8 | - link:IntentRadio-release.apk[] 9 | **** 10 | 11 | // vim: set syntax=asciidoc: 12 | -------------------------------------------------------------------------------- /web/tasker.ascii: -------------------------------------------------------------------------------- 1 | = Intent Radio -- Tasker = 2 | Stephen Blott 3 | :toc2: 4 | :theme: smblott 5 | 6 | .The simple way 7 | **** 8 | - Download and experiment with this link:Radio.prj.xml[sample Tasker project]. 9 | - Or, extract this project from within the app itself. 10 | **** 11 | 12 | .Alternatively, create a Tasker task as follows 13 | **** 14 | - Create a new task. 15 | - Add an action: 16 | ** select _Misc/Send Intent_. 17 | - Under _Action_, insert the name of the intent. 18 | - Leave _Cat_, _Mime Type_ and _Data_ at their default values; either 19 | `None` or empty. 20 | - For the `PLAY` intent, under _Extra_ insert: + 21 | + 22 | `url: INSERT-YOUR-URL-HERE` + 23 | + 24 | and, under the second _Extra_, insert: + 25 | + 26 | `name: INSERT-YOUR-NAME-HERE` + 27 | - Leave everything else empty, except ensure that the _Target_ is 28 | set to `Broadcast Receiver`. 29 | 30 | Save the task and run it. 31 | 32 | [TIP] 33 | ==== 34 | You can save on typing the long intent names (and possible typos) by using 35 | the _Copy_ buttons within the app itself. 36 | ==== 37 | **** 38 | 39 | // vim: set syntax=asciidoc: 40 | --------------------------------------------------------------------------------
30
110
599
877
130
548
547
4 | Intent Radio is a streaming internet radio app without a graphical user 5 | interface. It is controlled exclusively through the delivery of broadcast 6 | intents. Automation apps, such as Tasker, should be used to control 7 | playback by issuing suitable broadcast intents. 8 |
11 | This app must be run once before Android will deliver intents to its background 12 | service. This is an Android security feature. 13 |
16 | Thereafter, including after reboots, it should not be necessary the run this 17 | app at all. Its service runs in the background. 18 |
23 | All aspects of media playback are handled by the built-in Android media player. 24 | Additionally, playlist URLs (whose URL/name must end with ".pls" or ".m3u") 25 | are also supported. 26 |
29 | During playback, Intent Radio adds a notification to the notification area. 30 | Clicking on this notification stops or restarts playback, as approrpiate. 31 |
36 | 37 | org.smblott.intentradio.PLAY 38 | 39 | 40 | 41 | Extras: 42 | url: the URL to play 43 | name: the display name 44 | 45 | 46 | 47 | If no name extra is provided, then the URL is used as the display 48 | name. If no url is provided, then a built-in URL for BBC Radio 4 is 49 | used. 50 | 51 |
37 | org.smblott.intentradio.PLAY 38 |
41 | Extras: 42 | url: the URL to play 43 | name: the display name 44 |
47 | If no name extra is provided, then the URL is used as the display 48 | name. If no url is provided, then a built-in URL for BBC Radio 4 is 49 | used. 50 |
54 | 55 | org.smblott.intentradio.STOP 56 | 57 | 58 | 59 | Stop playback. 60 | No extras. 61 | 62 |
55 | org.smblott.intentradio.STOP 56 |
59 | Stop playback. 60 | No extras. 61 |
65 | 66 | org.smblott.intentradio.PAUSE 67 | 68 | 69 | 70 | Pause playback, but only if playing. 71 | No extras. 72 | 73 |
66 | org.smblott.intentradio.PAUSE 67 |
70 | Pause playback, but only if playing. 71 | No extras. 72 |
76 | 77 | org.smblott.intentradio.RESTART 78 | 79 | 80 | 81 | Restart playback, but only if paused. 82 | No extras. 83 | 84 |
77 | org.smblott.intentradio.RESTART 78 |
81 | Restart playback, but only if paused. 82 | No extras. 83 |
87 | 88 | org.smblott.intentradio.STATE_REQUEST 89 | 90 | 91 | 92 | Request that Intent Radio broadcast its state (see below). 93 | No extras. 94 | 95 | 96 | 97 | This intent is not required to receive state updates. 98 | Intent Radio broadcasts its state automatically whenever its state changes. 99 | 100 |
88 | org.smblott.intentradio.STATE_REQUEST 89 |
92 | Request that Intent Radio broadcast its state (see below). 93 | No extras. 94 |
97 | This intent is not required to receive state updates. 98 | Intent Radio broadcasts its state automatically whenever its state changes. 99 |
105 | 106 | org.smblott.intentradio.STATE 107 | 108 | 109 | 110 | Informs listeners (possibly Tasker) of changes in state. 111 | Extras: 112 | state: the current state (see below) 113 | url: the current url 114 | name: the current name 115 | 116 | 117 | The state is one 118 | stop, 119 | play, 120 | play/buffering, 121 | play/pause, 122 | play/dim or 123 | error. 124 | 125 |
106 | org.smblott.intentradio.STATE 107 |
110 | Informs listeners (possibly Tasker) of changes in state. 111 | Extras: 112 | state: the current state (see below) 113 | url: the current url 114 | name: the current name 115 |
117 | The state is one 118 | stop, 119 | play, 120 | play/buffering, 121 | play/pause, 122 | play/dim or 123 | error. 124 |
130 | There is considerably more information, 131 | including instructions for configuring Tasker, 132 | on the project home page. 133 |
136 | See also the source code on 137 | GitHub, and the 138 | release notes/change log. 139 |
142 | Please report issues on 143 | GitHub. 144 |
147 | See also: 148 | Tasker. 149 |
4 | REPLACE_URL 5 |
\n" 73 | + "Distribution: " + getString(R.string.distribution) + "\n" 74 | + "Version: " + Build.version_string(context) + "\n" 75 | + "Build: " + Build.getBuildDate(context) + "\n" 76 | + "
123