├── src └── org │ └── tvheadend │ └── tvhguide │ ├── .gitignore │ ├── intent │ ├── SearchIMDbIntent.java │ └── SearchEPGIntent.java │ ├── htsp │ ├── HTSListener.java │ ├── HTSResponseHandler.java │ ├── HTSConnectionListener.java │ ├── HTSFileInputStream.java │ ├── SelectionThread.java │ ├── HTSConnection.java │ └── HTSMessage.java │ ├── model │ ├── SeriesInfo.java │ ├── HttpTicket.java │ ├── Packet.java │ ├── ChannelTag.java │ ├── Subscription.java │ ├── Stream.java │ ├── Programme.java │ ├── Recording.java │ └── Channel.java │ ├── TVHVideoView.java │ ├── SettingsActivity.java │ ├── ChannelListViewWrapper.java │ ├── ExternalPlaybackActivity.java │ ├── RecordingActivity.java │ ├── PlaybackActivity.java │ ├── ProgrammeActivity.java │ ├── TVHGuideApplication.java │ ├── ChannelListActivity.java │ └── RecordingListActivity.java ├── res ├── drawable │ ├── logo.png │ ├── ic_error.png │ ├── ic_rec.png │ ├── logo_72.png │ ├── ic_success.png │ ├── ic_rec_small.png │ ├── ic_schedule.png │ ├── ic_error_small.png │ ├── ic_schedule_small.png │ ├── ic_success_small.png │ ├── overlay_box.xml │ └── ic_background.xml ├── drawable-hdpi │ └── ic_menu_tags.png ├── drawable-ldpi │ └── ic_menu_tags.png ├── drawable-mdpi │ └── ic_menu_tags.png ├── xml │ ├── searchable.xml │ └── preferences.xml ├── menu │ ├── pr_menu.xml │ ├── rc_menu.xml │ └── main_menu.xml ├── values │ ├── styles.xml │ ├── arrays.xml │ └── strings.xml ├── layout │ ├── programme_title.xml │ ├── recording_title.xml │ ├── recording_list_title.xml │ ├── recording_layout.xml │ ├── programme_list_title.xml │ ├── search_result_title.xml │ ├── player_layout.xml │ ├── programme_list_widget.xml │ ├── channel_list_title.xml │ ├── search_result_widget.xml │ ├── channel_list_widget.xml │ ├── recording_list_widget.xml │ └── programme_layout.xml ├── values-de │ └── strings.xml ├── values-cs │ └── strings.xml └── values-sv │ └── strings.xml ├── .gitignore ├── README ├── default.properties ├── project.properties ├── ant.properties ├── proguard.cfg └── AndroidManifest.xml /src/org/tvheadend/tvhguide/.gitignore: -------------------------------------------------------------------------------- 1 | R.java 2 | -------------------------------------------------------------------------------- /res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/logo.png -------------------------------------------------------------------------------- /res/drawable/ic_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/ic_error.png -------------------------------------------------------------------------------- /res/drawable/ic_rec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/ic_rec.png -------------------------------------------------------------------------------- /res/drawable/logo_72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/logo_72.png -------------------------------------------------------------------------------- /res/drawable/ic_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/ic_success.png -------------------------------------------------------------------------------- /res/drawable/ic_rec_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/ic_rec_small.png -------------------------------------------------------------------------------- /res/drawable/ic_schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/ic_schedule.png -------------------------------------------------------------------------------- /res/drawable/ic_error_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/ic_error_small.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_menu_tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable-hdpi/ic_menu_tags.png -------------------------------------------------------------------------------- /res/drawable-ldpi/ic_menu_tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable-ldpi/ic_menu_tags.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_menu_tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable-mdpi/ic_menu_tags.png -------------------------------------------------------------------------------- /res/drawable/ic_schedule_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/ic_schedule_small.png -------------------------------------------------------------------------------- /res/drawable/ic_success_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-tornblom/TVHGuide/HEAD/res/drawable/ic_success_small.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | nbproject/private 4 | libs 5 | *~ 6 | *.o 7 | *.d 8 | *.so 9 | *.a 10 | /gen/ 11 | /bin/ 12 | /nbandroid/ 13 | local.properties 14 | -------------------------------------------------------------------------------- /res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /res/menu/pr_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /res/menu/rc_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | TVHGuide is an Android application that enables you to interact with a 2 | TVHeadend server. It allows you to browse programme listings, schedual 3 | recordings and search the EPG. Depending on your hardware, it can also 4 | do some actual playback of either live TV or a recorded stream. 5 | 6 | It is licensed under the GPLv3 (see COPYING). 7 | -------------------------------------------------------------------------------- /res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /default.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 use, 7 | # "build.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | # Project target. 11 | target=android-7 12 | -------------------------------------------------------------------------------- /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 use, 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | # Project target. 11 | target=android-7 12 | -------------------------------------------------------------------------------- /res/drawable/overlay_box.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /ant.properties: -------------------------------------------------------------------------------- 1 | # This file is used to override default values used by the Ant build system. 2 | # 3 | # This file must be checked in 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 | -------------------------------------------------------------------------------- /res/drawable/ic_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 | 14 | 15 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /res/layout/programme_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | 25 | 26 | -------------------------------------------------------------------------------- /res/layout/recording_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | 25 | 26 | -------------------------------------------------------------------------------- /res/layout/recording_list_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | 25 | 26 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/intent/SearchIMDbIntent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this template, choose Tools | Templates 3 | * and open the template in the editor. 4 | */ 5 | package org.tvheadend.tvhguide.intent; 6 | 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.net.Uri; 11 | import java.net.URLEncoder; 12 | 13 | /** 14 | * 15 | * @author john-tornblom 16 | */ 17 | public class SearchIMDbIntent extends Intent { 18 | 19 | public SearchIMDbIntent(Context ctx, String query) { 20 | super(Intent.ACTION_VIEW); 21 | 22 | setData(Uri.parse("imdb:///find?s=tt&q=" + URLEncoder.encode(query))); 23 | 24 | PackageManager packageManager = ctx.getPackageManager(); 25 | if (packageManager.queryIntentActivities(this, PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) { 26 | setData(Uri.parse("http://akas.imdb.org/find?s=tt&q=" + URLEncoder.encode(query))); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/htsp/HTSListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.htsp; 20 | 21 | /** 22 | * 23 | * @author john-tornblom 24 | */ 25 | public interface HTSListener { 26 | 27 | public void onMessage(String action, Object obj); 28 | } 29 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/htsp/HTSResponseHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.htsp; 20 | 21 | /** 22 | * 23 | * @author john-tornblom 24 | */ 25 | public interface HTSResponseHandler { 26 | 27 | public void handleResponse(HTSMessage response); 28 | } 29 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/model/SeriesInfo.java: -------------------------------------------------------------------------------- 1 | package org.tvheadend.tvhguide.model; 2 | 3 | public class SeriesInfo { 4 | public int seasonNumber; 5 | public int seasonCount; 6 | 7 | public int episodeNumber; 8 | public int episodeCount; 9 | 10 | public int partNumber; 11 | public int partCount; 12 | 13 | public String onScreen; 14 | 15 | public String toString() { 16 | if (onScreen != null && onScreen.length() > 0) 17 | return onScreen; 18 | 19 | String s = ""; 20 | 21 | if (seasonNumber > 0) { 22 | if (s.length() > 0) 23 | s += ", "; 24 | s += String.format("season %02d", seasonNumber); 25 | } 26 | if (episodeNumber > 0) { 27 | if (s.length() > 0) 28 | s += ", "; 29 | s += String.format("episode %02d", episodeNumber); 30 | } 31 | if (partNumber > 0) { 32 | if (s.length() > 0) 33 | s += ", "; 34 | s += String.format("part %d", partNumber); 35 | } 36 | 37 | if(s.length() > 0) { 38 | s = s.substring(0,1).toUpperCase() + s.substring(1); 39 | } 40 | 41 | return s; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/htsp/HTSConnectionListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.htsp; 20 | 21 | /** 22 | * 23 | * @author john-tornblom 24 | */ 25 | public interface HTSConnectionListener { 26 | 27 | public void onMessage(HTSMessage response); 28 | public void onError(int errorCode); 29 | public void onError(Exception ex); 30 | } 31 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/model/HttpTicket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.model; 20 | 21 | /** 22 | * 23 | * @author john-tornblom 24 | */ 25 | public class HttpTicket { 26 | public final String path; 27 | public final String ticket; 28 | 29 | public HttpTicket(String path, String ticket) { 30 | this.path = path; 31 | this.ticket = ticket; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/model/Packet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.model; 20 | 21 | /** 22 | * 23 | * @author john-tornblom 24 | */ 25 | public class Packet { 26 | 27 | public Subscription subscription; 28 | public Stream stream; 29 | public int frametype; 30 | public long dts; 31 | public long pts; 32 | public long duration; 33 | public byte[] payload; 34 | } 35 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/model/ChannelTag.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.model; 20 | 21 | import android.graphics.Bitmap; 22 | 23 | /** 24 | * 25 | * @author john-tornblom 26 | */ 27 | public class ChannelTag { 28 | 29 | public long id; 30 | public String name; 31 | public String icon; 32 | public Bitmap iconBitmap; 33 | 34 | @Override 35 | public String toString() { 36 | return name; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/model/Subscription.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.model; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | /** 25 | * 26 | * @author john-tornblom 27 | */ 28 | public class Subscription { 29 | 30 | public long id; 31 | public String status; 32 | public List streams = new ArrayList(); 33 | 34 | public long packetCount; 35 | public long queSize; 36 | public long delay; 37 | public long droppedBFrames; 38 | public long droppedIFrames; 39 | public long droppedPFrames; 40 | } 41 | -------------------------------------------------------------------------------- /proguard.cfg: -------------------------------------------------------------------------------- 1 | -optimizationpasses 5 2 | -dontusemixedcaseclassnames 3 | -dontskipnonpubliclibraryclasses 4 | -dontpreverify 5 | -verbose 6 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 7 | 8 | -keep public class * extends android.app.Activity 9 | -keep public class * extends android.app.Application 10 | -keep public class * extends android.app.Service 11 | -keep public class * extends android.content.BroadcastReceiver 12 | -keep public class * extends android.content.ContentProvider 13 | -keep public class * extends android.app.backup.BackupAgentHelper 14 | -keep public class * extends android.preference.Preference 15 | -keep public class com.android.vending.licensing.ILicensingService 16 | 17 | -keepclasseswithmembernames class * { 18 | native ; 19 | } 20 | 21 | -keepclasseswithmembers class * { 22 | public (android.content.Context, android.util.AttributeSet); 23 | } 24 | 25 | -keepclasseswithmembers class * { 26 | public (android.content.Context, android.util.AttributeSet, int); 27 | } 28 | 29 | -keepclassmembers class * extends android.app.Activity { 30 | public void *(android.view.View); 31 | } 32 | 33 | -keepclassmembers enum * { 34 | public static **[] values(); 35 | public static ** valueOf(java.lang.String); 36 | } 37 | 38 | -keep class * implements android.os.Parcelable { 39 | public static final android.os.Parcelable$Creator *; 40 | } 41 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/model/Stream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.model; 20 | 21 | /** 22 | * 23 | * @author john-tornblom 24 | */ 25 | public class Stream { 26 | 27 | public static final String STREAM_TYPE_AC3 = "AC3"; 28 | public static final String STREAM_TYPE_MPEG2AUDIO = "MPEG2AUDIO"; 29 | public static final String STREAM_TYPE_MPEG2VIDEO = "MPEG2VIDEO"; 30 | public static final String STREAM_TYPE_MPEG4VIDEO = "MPEG4VIDEO"; 31 | public static final String STREAM_TYPE_H264 = "H264"; 32 | public static final String STREAM_TYPE_VP8 = "VP8"; 33 | public static final String STREAM_TYPE_AAC = "AAC"; 34 | public int index; 35 | public String type; 36 | public String language; 37 | public int height; 38 | public int width; 39 | } 40 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/intent/SearchEPGIntent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.intent; 20 | 21 | import android.app.SearchManager; 22 | import android.content.Context; 23 | import android.content.Intent; 24 | 25 | import org.tvheadend.tvhguide.SearchResultActivity; 26 | import org.tvheadend.tvhguide.model.Channel; 27 | 28 | /** 29 | * 30 | * @author john-tornblom 31 | */ 32 | public class SearchEPGIntent extends Intent { 33 | 34 | public SearchEPGIntent(Context ctx, String query) { 35 | super(ctx, SearchResultActivity.class); 36 | setAction(Intent.ACTION_SEARCH); 37 | putExtra(SearchManager.QUERY, query); 38 | } 39 | 40 | public SearchEPGIntent(Context ctx, Channel ch, String query) { 41 | this(ctx, query); 42 | putExtra("channelId", ch.id); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | TVHGuide 4 | 5 | Hilfe 6 | Einstellungen 7 | Aktualisieren 8 | Tags 9 | Aufnahmen 10 | 11 | Hostname 12 | Hostnamen des Servers eingeben 13 | 14 | Port 15 | Port der Servers eingeben 16 | 17 | Benutzername 18 | Benutzernamen eingeben 19 | 20 | Passwort 21 | Passwort eingeben 22 | 23 | Kanal-Logos 24 | Kanal-Logos anzeigen? 25 | 26 | Zugriff verweigert 27 | Ungültige Antwort des Servers 28 | Konnte nicht mit dem Server verbinden 29 | Verbindung zum Server verloren 30 | 31 | Lade... 32 | Bitte warten Sie einen Moment... 33 | 34 | Alle Kanäle 35 | 36 | Geplant 37 | Aufnahme 38 | Fertig 39 | Verpasst 40 | Ungültig 41 | -------------------------------------------------------------------------------- /res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 576 5 | 480 6 | 384 7 | 288 8 | 192 9 | 10 | 11 | 576p 12 | 480p 13 | 384p 14 | 288p 15 | 192p 16 | 17 | 18 | 19 | AAC 20 | MPEG2AUDIO 21 | 22 | 23 | Advanced Audio Codec (AAC) 24 | MPEG-2 Audio Layer II (MP2) 25 | 26 | 27 | 28 | H264 29 | MPEG4VIDEO 30 | VP8 31 | 32 | 33 | MPEG-4 AVC (H.264) 34 | MPEG-4 Part 2 35 | VP8 36 | 37 | 38 | 39 | PASS 40 | NONE 41 | 42 | 43 | Pass-through 44 | None 45 | 46 | 47 | 48 | matroska 49 | mpegts 50 | mpegps 51 | pass 52 | 53 | 54 | 55 | Matroska 56 | MPEG-TS 57 | MPEG-PS 58 | Pass-through 59 | 60 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/model/Programme.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.model; 20 | 21 | import java.util.Date; 22 | 23 | /** 24 | * 25 | * @author john-tornblom 26 | */ 27 | public class Programme implements Comparable { 28 | 29 | public long id; 30 | public long nextId; 31 | public int contentType; 32 | public Date start; 33 | public Date stop; 34 | public String title; 35 | public String description; 36 | public String summary; 37 | public SeriesInfo seriesInfo; 38 | public int starRating; 39 | public Channel channel; 40 | public Recording recording; 41 | 42 | public int compareTo(Programme that) { 43 | return this.start.compareTo(that.start); 44 | } 45 | 46 | public boolean isRecording() { 47 | return recording != null && "recording".equals(recording.state); 48 | } 49 | 50 | public boolean isScheduled() { 51 | return recording != null && "scheduled".equals(recording.state); 52 | } 53 | 54 | @Override 55 | public boolean equals(Object o) { 56 | if (o instanceof Programme) { 57 | return ((Programme) o).id == id; 58 | } 59 | 60 | return false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /res/layout/recording_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 21 | 22 | 26 | 27 | 28 | 29 | 35 | 36 | 40 | 41 | 46 | 47 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/model/Recording.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.model; 20 | 21 | import java.util.Date; 22 | 23 | /** 24 | * 25 | * @author john-tornblom 26 | */ 27 | public class Recording implements Comparable { 28 | 29 | public long id; 30 | public Date start; 31 | public Date stop; 32 | public String title; 33 | public String summary; 34 | public String description; 35 | public Channel channel; 36 | public String state; 37 | public String error; 38 | 39 | @Override 40 | public int compareTo(Recording that) { 41 | if (this.state() == 1 && that.state() == 1) { 42 | return this.start.compareTo(that.start); 43 | } else { 44 | return that.start.compareTo(this.start); 45 | } 46 | } 47 | 48 | public boolean isRecording() { 49 | return state() == 0; 50 | } 51 | 52 | public boolean isScheduled() { 53 | return state() == 1; 54 | } 55 | 56 | private int state() { 57 | if ("recording".equals(state)) { 58 | return 0; 59 | } else if ("scheduled".equals(state)) { 60 | return 1; 61 | } 62 | return 2; 63 | } 64 | 65 | @Override 66 | public boolean equals(Object o) { 67 | if (o instanceof Recording) { 68 | return ((Recording) o).id == id; 69 | } 70 | 71 | return false; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/model/Channel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.model; 20 | 21 | import android.graphics.Bitmap; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import java.util.Set; 25 | import java.util.TreeSet; 26 | 27 | /** 28 | * 29 | * @author john-tornblom 30 | */ 31 | public class Channel implements Comparable { 32 | 33 | public long id; 34 | public String name; 35 | public String icon; 36 | public int number; 37 | public Set epg = Collections.synchronizedSortedSet(new TreeSet()); 38 | public Set recordings = Collections.synchronizedSortedSet(new TreeSet()); 39 | public List tags; 40 | public Bitmap iconBitmap; 41 | public boolean isTransmitting; 42 | 43 | public int compareTo(Channel that) { 44 | return this.number - that.number; 45 | } 46 | 47 | public boolean hasTag(long id) { 48 | if (id == 0) { 49 | return true; 50 | } 51 | 52 | for (Integer i : tags) { 53 | if (i == id) { 54 | return true; 55 | } 56 | } 57 | return false; 58 | } 59 | 60 | public boolean isRecording() { 61 | for (Recording rec : recordings) { 62 | if ("recording".equals(rec.state)) { 63 | return true; 64 | } 65 | } 66 | return false; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /res/layout/programme_list_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 16 | 22 | 32 | 33 | 34 | 48 | 49 | 58 | -------------------------------------------------------------------------------- /res/layout/search_result_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 16 | 22 | 32 | 33 | 34 | 48 | 49 | 58 | -------------------------------------------------------------------------------- /res/layout/player_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 22 | 23 | 29 | 30 | 36 | 37 | 38 | 44 | 49 | 50 | 51 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/TVHVideoView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.content.Context; 22 | import android.util.AttributeSet; 23 | import android.widget.VideoView; 24 | 25 | /** 26 | * 27 | * @author john-tornblom 28 | */ 29 | public class TVHVideoView extends VideoView { 30 | 31 | private int videoWidth; 32 | private int videoHeight; 33 | 34 | public TVHVideoView(Context context) { 35 | super(context); 36 | } 37 | 38 | public TVHVideoView(Context context, AttributeSet attrs) { 39 | super(context, attrs); 40 | } 41 | 42 | public TVHVideoView(Context context, AttributeSet attrs, int defStyle) { 43 | super(context, attrs, defStyle); 44 | } 45 | 46 | public void setAspectRatio(int num, int den) { 47 | videoHeight = getHeight(); 48 | videoWidth = (videoHeight * num) / den; 49 | 50 | requestLayout(); 51 | invalidate(); 52 | } 53 | 54 | @Override 55 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 56 | 57 | int width = getDefaultSize(videoWidth, widthMeasureSpec); 58 | int height = getDefaultSize(videoHeight, heightMeasureSpec); 59 | 60 | if (videoWidth > 0 && videoHeight > 0) { 61 | if (videoWidth * height > width * videoHeight) { 62 | height = width * videoHeight / videoWidth; 63 | } else if (videoWidth * height < width * videoHeight) { 64 | width = height * videoWidth / videoHeight; 65 | } 66 | } 67 | 68 | setMeasuredDimension(width, height); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /res/layout/programme_list_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 37 | 38 | 44 | 45 | 46 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /res/layout/channel_list_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 16 | 22 | 32 | 33 | 34 | 48 | 49 | 58 | 67 | -------------------------------------------------------------------------------- /res/layout/search_result_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 18 | 19 | 23 | 27 | 33 | 34 | 40 | 41 | 42 | 47 | 50 | 55 | 56 | 62 | 63 | 64 | 67 | 72 | 73 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /res/layout/channel_list_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 20 | 21 | 25 | 26 | 31 | 32 | 37 | 38 | 42 | 43 | 48 | 49 | 55 | 56 | 57 | 60 | 61 | 67 | 68 | 69 | 72 | 73 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.content.Intent; 22 | import android.content.SharedPreferences; 23 | import android.os.Bundle; 24 | import android.preference.PreferenceActivity; 25 | import android.preference.PreferenceManager; 26 | import android.util.Log; 27 | import android.view.Window; 28 | import org.tvheadend.tvhguide.R; 29 | import org.tvheadend.tvhguide.htsp.HTSService; 30 | 31 | /** 32 | * 33 | * @author john-tornblom 34 | */ 35 | public class SettingsActivity extends PreferenceActivity { 36 | 37 | private int oldPort; 38 | private String oldHostname; 39 | private String oldUser; 40 | private String oldPw; 41 | 42 | @Override 43 | public void onCreate(Bundle savedInstanceState) { 44 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 45 | Boolean theme = prefs.getBoolean("lightThemePref", false); 46 | setTheme(theme ? android.R.style.Theme_Light : android.R.style.Theme); 47 | 48 | requestWindowFeature(Window.FEATURE_LEFT_ICON); 49 | super.onCreate(savedInstanceState); 50 | 51 | addPreferencesFromResource(R.xml.preferences); 52 | setTitle(getString(R.string.app_name) + " - " + getString(R.string.menu_settings)); 53 | setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.logo_72); 54 | } 55 | 56 | @Override 57 | protected void onStart() { 58 | super.onStart(); 59 | 60 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 61 | oldHostname = prefs.getString("serverHostPref", ""); 62 | oldPort = Integer.parseInt(prefs.getString("serverPortPref", "")); 63 | oldUser = prefs.getString("usernamePref", ""); 64 | oldPw = prefs.getString("passwordPref", ""); 65 | } 66 | 67 | @Override 68 | protected void onPause() { 69 | super.onPause(); 70 | 71 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 72 | boolean reconnect = false; 73 | reconnect |= !oldHostname.equals(prefs.getString("serverHostPref", "")); 74 | reconnect |= oldPort != Integer.parseInt(prefs.getString("serverPortPref", "")); 75 | reconnect |= !oldUser.equals(prefs.getString("usernamePref", "")); 76 | reconnect |= !oldPw.equals(prefs.getString("passwordPref", "")); 77 | 78 | if (reconnect) { 79 | Log.d("SettingsActivity", "Connectivity settings chaned, forcing a reconnect"); 80 | Intent intent = new Intent(SettingsActivity.this, HTSService.class); 81 | intent.setAction(HTSService.ACTION_CONNECT); 82 | intent.putExtra("hostname", prefs.getString("serverHostPref", "")); 83 | intent.putExtra("port", Integer.parseInt(prefs.getString("serverPortPref", ""))); 84 | intent.putExtra("username", prefs.getString("usernamePref", "")); 85 | intent.putExtra("password", prefs.getString("passwordPref", "")); 86 | intent.putExtra("force", true); 87 | startService(intent); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /res/values-cs/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | TVHGuide 4 | 5 | Nápověda 6 | Nastavení 7 | Obnovit 8 | Štítky 9 | Nahrávání 10 | 11 | Nahrávky 12 | Zrušit nahrávání 13 | Vymazat nahrávání 14 | 15 | Hostname 16 | Nazev serveru (Hostname) 17 | 18 | Port 19 | Port serveru 20 | 21 | Uživatelské jméno 22 | Vložte jméno 23 | 24 | Heslo 25 | Vložte heslo 26 | 27 | Ikony kanálů 28 | Zobrazit ikony kanálů? 29 | 30 | Rozlišení 31 | Preferované maximální rozlišení videa 32 | 33 | Audio kodek 34 | Preferovaný audio kodek 35 | 36 | Video kodek 37 | Preferovaný video kodek 38 | 39 | Kodek pro titulky 40 | Preferovaný kodek pro titulky 41 | 42 | Media kontejner 43 | Preferovaný proudový kontejner 44 | 45 | Přehrávání 46 | 47 | HTTP port 48 | Vložte HTTP port Serveru 49 | 50 | Překódovat proud 51 | Požadovat překódování pro živý přenos 52 | 53 | Preferovaný externí přehrávač 54 | Použije se pro živé streamy (BSPlayer seems OK) 55 | 56 | Přístup odmítnut 57 | Neplatná odpověď serveru 58 | Nelze se připojit na server 59 | Spojení se serverem ztraceno 60 | 61 | Načítám… 62 | Prosím čekejte… 63 | 64 | Všechny kanály 65 | Více 66 | 67 | Filmy / Divadlo 68 | Zprávy / Aktuality 69 | Seriály / Hry 70 | Sport 71 | Dětské / Pro mladistvé 72 | Hudba 73 | Umění / Kultura 74 | Společnost / Politika / Ekonomie 75 | Vzdělání / Věda / Naučné 76 | Hobby 77 | 78 | 79 | Plánované 80 | Nahrávání 81 | Kompletní 82 | Ztracené 83 | Neplatné 84 | 85 | Přehrát 86 | Žádný přenos 87 | 88 | Hledat v EPG 89 | dnes 90 | -------------------------------------------------------------------------------- /res/values-sv/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | TVHGuide 4 | 5 | Hjälp 6 | Inställningar 7 | Uppdatera 8 | Taggar 9 | Inspelningar 10 | 11 | Spela in 12 | Ta bort inspelning 13 | Avbryt inspelning 14 | 15 | Värdnamn 16 | Skriv in värdnamn 17 | 18 | Port 19 | Skriv in portnummer 20 | 21 | Användarnamn 22 | Skriv in användarnamn 23 | 24 | Lösenord 25 | Skriv in lösenord 26 | 27 | Användargränssnitt 28 | 29 | Ljust tema 30 | Använd ett ljust tema 31 | 32 | Kanallogotyper 33 | Visa kanalers logotyper? 34 | 35 | Upplösning 36 | Maximal upplösning vid omkodning 37 | 38 | Ljud 39 | Kodek att använda vid omkodning av ljud 40 | 41 | Video 42 | Kodek att använda vid omkodning av video 43 | 44 | Undertexter 45 | Kodek att använda vid omkodning av undertexter 46 | 47 | Media container 48 | Media container att föredra 49 | 50 | Uppspelning 51 | 52 | HTTP port 53 | Skriv in HTTP portnummer 54 | 55 | Koda om ström 56 | Begär omkodning av direktsänd media 57 | 58 | Föredra extern spelare 59 | Använd en extern spelare för direktsänd media (BSPlayer verkar OK) 60 | 61 | Åtkomst nekad 62 | Felaktigt svar från servern 63 | Kan inte ansluta till servern 64 | Tappade anslutningen till servern 65 | 66 | Laddar… 67 | Var god vänta några sekunder… 68 | 69 | Alla kanaler 70 | Hämta flera 71 | Säsong 72 | Avsnitt 73 | Del 74 | Sänds 75 | Omdömme 76 | Typ 77 | Del 78 | 79 | Schemalagd 80 | Spelar in 81 | Avslutad 82 | Missad 83 | Ogiltig 84 | 85 | Spela 86 | Sändningsuppehåll 87 | 88 | Sök i programtablån 89 | idag 90 | -------------------------------------------------------------------------------- /res/layout/recording_list_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 18 | 19 | 20 | 24 | 28 | 34 | 35 | 41 | 42 | 43 | 48 | 51 | 54 | 59 | 60 | 66 | 67 | 68 | 74 | 75 | 76 | 79 | 84 | 85 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/htsp/HTSFileInputStream.java: -------------------------------------------------------------------------------- 1 | package org.tvheadend.tvhguide.htsp; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | import android.util.Log; 7 | 8 | public class HTSFileInputStream extends InputStream { 9 | private HTSConnection connection; 10 | private String path; 11 | 12 | private long fileId; 13 | private long fileSize; 14 | 15 | private byte[] buf; 16 | private int bufPos; 17 | private long offset; 18 | 19 | public HTSFileInputStream(HTSConnection conn, String path) 20 | throws IOException { 21 | this.connection = conn; 22 | this.path = path; 23 | 24 | this.fileId = -1; 25 | this.fileSize = -1; 26 | 27 | this.reset(); 28 | this.open(); 29 | } 30 | 31 | public int available() { 32 | return this.buf.length; 33 | } 34 | 35 | public boolean markSupported() { 36 | return false; 37 | } 38 | 39 | public void reset() { 40 | buf = new byte[0]; 41 | bufPos = 0; 42 | offset = 0; 43 | } 44 | 45 | class FileOpenResponse implements HTSResponseHandler { 46 | int id; 47 | long size; 48 | long mtime; 49 | 50 | @Override 51 | public void handleResponse(HTSMessage response) { 52 | id = response.getInt("id", 0); 53 | size = response.getLong("size", 0); 54 | mtime = response.getLong("mtime", 0); 55 | notifyAll(); 56 | } 57 | }; 58 | 59 | class FileReadResponse implements HTSResponseHandler { 60 | byte[] data; 61 | 62 | @Override 63 | public void handleResponse(HTSMessage response) { 64 | data = response.getByteArray("data"); 65 | notifyAll(); 66 | } 67 | }; 68 | 69 | class FileCloseResponse implements HTSResponseHandler { 70 | int id; 71 | 72 | @Override 73 | public void handleResponse(HTSMessage response) { 74 | notifyAll(); 75 | } 76 | }; 77 | 78 | private void open() throws IOException { 79 | HTSMessage request = new HTSMessage(); 80 | FileOpenResponse response = new FileOpenResponse(); 81 | 82 | request.setMethod("fileOpen"); 83 | request.putField("file", path); 84 | 85 | synchronized (response) { 86 | try { 87 | connection.sendMessage(request, response); 88 | response.wait(); 89 | fileId = response.id; 90 | fileSize = response.size; 91 | } catch (Throwable e) { 92 | Log.e("TVHGuide", "Timeout waiting for fileOpen", e); 93 | } 94 | } 95 | 96 | if (fileId < 0) { 97 | throw new IOException("Failed to open remote file"); 98 | } else if (fileId == 0) { 99 | throw new IOException("Remote file is missing"); 100 | } 101 | } 102 | 103 | public void close() { 104 | HTSMessage request = new HTSMessage(); 105 | FileCloseResponse response = new FileCloseResponse(); 106 | 107 | request.setMethod("fileClose"); 108 | request.putField("id", fileId); 109 | 110 | synchronized (response) { 111 | try { 112 | connection.sendMessage(request, response); 113 | response.wait(); 114 | fileId = -1; 115 | fileSize = -1; 116 | } catch (Throwable e) { 117 | Log.e("TVHGuide", "Timeout waiting for fileClose", e); 118 | } 119 | } 120 | } 121 | 122 | public int read(byte[] outBuf, int outOffset, int outLength) { 123 | fillBuffer(); 124 | 125 | int ret = Math.min(buf.length - bufPos, outLength - outOffset); 126 | if(ret > 0) { 127 | System.arraycopy(buf, bufPos, outBuf, outOffset, ret); 128 | bufPos += ret; 129 | return ret; 130 | } 131 | 132 | return -1; 133 | } 134 | 135 | @Override 136 | public int read() throws IOException { 137 | fillBuffer(); 138 | 139 | if (bufPos < buf.length) { 140 | return buf[bufPos++] & 0xff; 141 | } 142 | 143 | return -1; 144 | } 145 | 146 | private void fillBuffer() { 147 | if(bufPos < buf.length) { 148 | return; 149 | } 150 | 151 | HTSMessage request = new HTSMessage(); 152 | FileReadResponse response = new FileReadResponse(); 153 | 154 | request.setMethod("fileRead"); 155 | request.putField("id", fileId); 156 | request.putField("size", Math.min(fileSize, 1024 * 1024 * 8)); 157 | request.putField("offset", offset); 158 | 159 | synchronized (response) { 160 | try { 161 | connection.sendMessage(request, response); 162 | response.wait(); 163 | 164 | offset += buf.length; 165 | buf = response.data; 166 | bufPos = 0; 167 | } catch (Throwable e) { 168 | Log.e("TVHGuide", "Timeout waiting for fileRead", e); 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/ChannelListViewWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.content.SharedPreferences; 22 | import android.graphics.drawable.BitmapDrawable; 23 | import android.graphics.drawable.ClipDrawable; 24 | import android.preference.PreferenceManager; 25 | import android.text.format.DateFormat; 26 | import android.view.Gravity; 27 | import android.view.View; 28 | import android.widget.ImageView; 29 | import android.widget.TextView; 30 | import java.util.Date; 31 | import java.util.Iterator; 32 | import org.tvheadend.tvhguide.R; 33 | import org.tvheadend.tvhguide.model.Channel; 34 | import org.tvheadend.tvhguide.model.Programme; 35 | 36 | /** 37 | * 38 | * @author john-tornblom 39 | */ 40 | public class ChannelListViewWrapper { 41 | 42 | private TextView name; 43 | private TextView nowTitle; 44 | private TextView nowTime; 45 | private TextView nextTitle; 46 | private TextView nextTime; 47 | private ImageView icon; 48 | private ImageView nowProgressImage; 49 | private ClipDrawable nowProgress; 50 | 51 | public ChannelListViewWrapper(View base) { 52 | name = (TextView) base.findViewById(R.id.ch_name); 53 | nowTitle = (TextView) base.findViewById(R.id.ch_now_title); 54 | 55 | nowProgressImage = (ImageView) base.findViewById(R.id.ch_elapsedtime); 56 | nowProgress = new ClipDrawable(nowProgressImage.getDrawable(), Gravity.LEFT, ClipDrawable.HORIZONTAL); 57 | nowProgressImage.setBackgroundDrawable(nowProgress); 58 | 59 | nowTime = (TextView) base.findViewById(R.id.ch_now_time); 60 | nextTitle = (TextView) base.findViewById(R.id.ch_next_title); 61 | nextTime = (TextView) base.findViewById(R.id.ch_next_time); 62 | icon = (ImageView) base.findViewById(R.id.ch_icon); 63 | } 64 | 65 | public void repaint(Channel channel) { 66 | nowTime.setText(""); 67 | nowTitle.setText(""); 68 | nextTime.setText(""); 69 | nextTitle.setText(""); 70 | nowProgress.setLevel(0); 71 | 72 | name.setText(channel.name); 73 | name.invalidate(); 74 | 75 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(icon.getContext()); 76 | Boolean showIcons = prefs.getBoolean("showIconPref", false); 77 | icon.setVisibility(showIcons ? ImageView.VISIBLE : ImageView.GONE); 78 | icon.setBackgroundDrawable(new BitmapDrawable(channel.iconBitmap)); 79 | 80 | if (channel.isRecording()) { 81 | icon.setImageResource(R.drawable.ic_rec_small); 82 | } else { 83 | icon.setImageDrawable(null); 84 | } 85 | icon.invalidate(); 86 | 87 | Iterator it = channel.epg.iterator(); 88 | if (!channel.isTransmitting && it.hasNext()) { 89 | nowTitle.setText(R.string.ch_no_transmission); 90 | } else if (it.hasNext()) { 91 | Programme p = it.next(); 92 | nowTime.setText( 93 | DateFormat.getTimeFormat(nowTime.getContext()).format(p.start) 94 | + " - " 95 | + DateFormat.getTimeFormat(nowTime.getContext()).format(p.stop)); 96 | 97 | double duration = (p.stop.getTime() - p.start.getTime()); 98 | double elapsed = new Date().getTime() - p.start.getTime(); 99 | double percent = elapsed / duration; 100 | 101 | nowProgressImage.setVisibility(ImageView.VISIBLE); 102 | nowProgress.setLevel((int) Math.floor(percent * 10000)); 103 | nowTitle.setText(p.title); 104 | } else { 105 | nowProgressImage.setVisibility(ImageView.GONE); 106 | } 107 | nowProgressImage.invalidate(); 108 | nowTime.invalidate(); 109 | nowTitle.invalidate(); 110 | 111 | if (it.hasNext()) { 112 | Programme p = it.next(); 113 | nextTime.setText( 114 | DateFormat.getTimeFormat(nextTime.getContext()).format(p.start) 115 | + " - " 116 | + DateFormat.getTimeFormat(nextTime.getContext()).format(p.stop)); 117 | 118 | nextTitle.setText(p.title); 119 | } 120 | nextTime.invalidate(); 121 | nextTitle.invalidate(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/ExternalPlaybackActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.app.Activity; 22 | import android.content.Intent; 23 | import android.content.SharedPreferences; 24 | import android.net.Uri; 25 | import android.os.Bundle; 26 | import android.preference.PreferenceManager; 27 | import android.util.Log; 28 | 29 | import org.tvheadend.tvhguide.htsp.HTSListener; 30 | import org.tvheadend.tvhguide.htsp.HTSService; 31 | import org.tvheadend.tvhguide.model.HttpTicket; 32 | import org.tvheadend.tvhguide.model.Stream; 33 | 34 | /** 35 | * 36 | * @author john-tornblom 37 | */ 38 | public class ExternalPlaybackActivity extends Activity implements HTSListener { 39 | 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | 44 | Intent intent = new Intent(ExternalPlaybackActivity.this, HTSService.class); 45 | intent.setAction(HTSService.ACTION_GET_TICKET); 46 | 47 | intent.putExtras(getIntent().getExtras()); 48 | this.startService(intent); 49 | } 50 | 51 | @Override 52 | protected void onResume() { 53 | super.onResume(); 54 | 55 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 56 | app.addListener(this); 57 | } 58 | 59 | @Override 60 | protected void onPause() { 61 | super.onPause(); 62 | 63 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 64 | app.removeListener(this); 65 | } 66 | 67 | private void startPlayback(String path, String ticket) { 68 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 69 | 70 | String host = prefs.getString("serverHostPref", "localhost"); 71 | Integer port = Integer.parseInt(prefs.getString("httpPortPref", "9981")); 72 | 73 | Integer resolution = Integer.parseInt(prefs.getString("resolutionPref", "288")); 74 | Boolean transcode = false; //prefs.getBoolean("transcodePref", true); 75 | String acodec = prefs.getString("acodecPref", Stream.STREAM_TYPE_AAC); 76 | String vcodec = prefs.getString("vcodecPref", Stream.STREAM_TYPE_MPEG4VIDEO); 77 | String scodec = prefs.getString("scodecPref", "PASS"); 78 | String container = prefs.getString("containerPref", "matroska"); 79 | String mime = "application/octet-stream"; 80 | 81 | if ("mpegps".equals(container)) { 82 | mime = "video/mp2p"; 83 | } else if ("mpegts".equals(container)) { 84 | mime = "video/mp4"; 85 | } else if ("matroska".equals(container)) { 86 | mime = "video/x-matroska"; 87 | } else if ("pass".equals(container)) { 88 | mime = "video/mp2t"; //assume mpegts 89 | } 90 | 91 | String url = "http://" + host + ":" + port + path; 92 | url += "?ticket=" + ticket; 93 | url += "&mux=" + container; 94 | if (transcode) { 95 | url += "&transcode=1"; 96 | url += "&resolution=" + resolution; 97 | url += "&acodec=" + acodec; 98 | url += "&vcodec=" + vcodec; 99 | url += "&scodec=" + scodec; 100 | } 101 | 102 | final Intent playbackIntent = new Intent(Intent.ACTION_VIEW); 103 | playbackIntent.setDataAndType(Uri.parse(url), mime); 104 | 105 | 106 | this.runOnUiThread(new Runnable() { 107 | 108 | public void run() { 109 | try { 110 | startActivity(playbackIntent); 111 | } catch (Throwable t) { 112 | Log.e("TVHGuide", "Can't execute external media player", t); 113 | try { 114 | Intent installIntent = new Intent(Intent.ACTION_VIEW); 115 | installIntent.setData(Uri.parse("market://search?q=free%20video%20player&c=apps")); 116 | startActivity(installIntent); 117 | } catch (Throwable t2) { 118 | Log.e("TVHGuide", "Can't query market", t2); 119 | } 120 | } finally { 121 | finish(); 122 | } 123 | } 124 | }); 125 | } 126 | 127 | public void onMessage(String action, final Object obj) { 128 | if (action.equals(TVHGuideApplication.ACTION_TICKET_ADD)) { 129 | HttpTicket t = (HttpTicket) obj; 130 | startPlayback(t.path, t.ticket); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 19 | 20 | 26 | 27 | 34 | 35 | 36 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 56 | 64 | 73 | 74 | 125 | 126 | 127 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/RecordingActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.app.Activity; 22 | import android.app.AlertDialog; 23 | import android.content.DialogInterface; 24 | import android.content.Intent; 25 | import android.content.SharedPreferences; 26 | import android.os.Bundle; 27 | import android.preference.PreferenceManager; 28 | import android.text.format.DateFormat; 29 | import android.view.Menu; 30 | import android.view.MenuItem; 31 | import android.view.Window; 32 | import android.widget.ImageView; 33 | import android.widget.TextView; 34 | 35 | import org.tvheadend.tvhguide.R; 36 | import org.tvheadend.tvhguide.htsp.HTSService; 37 | import org.tvheadend.tvhguide.intent.SearchEPGIntent; 38 | import org.tvheadend.tvhguide.intent.SearchIMDbIntent; 39 | import org.tvheadend.tvhguide.model.Recording; 40 | 41 | /** 42 | * 43 | * @author john-tornblom 44 | */ 45 | public class RecordingActivity extends Activity { 46 | 47 | Recording rec; 48 | 49 | @Override 50 | public void onCreate(Bundle savedInstanceState) { 51 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 52 | Boolean theme = prefs.getBoolean("lightThemePref", false); 53 | setTheme(theme ? R.style.CustomTheme_Light : R.style.CustomTheme); 54 | 55 | super.onCreate(savedInstanceState); 56 | 57 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 58 | rec = app.getRecording(getIntent().getLongExtra("id", 0)); 59 | if (rec == null) { 60 | finish(); 61 | return; 62 | } 63 | 64 | requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); 65 | 66 | setContentView(R.layout.recording_layout); 67 | 68 | getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.recording_title); 69 | TextView t = (TextView) findViewById(R.id.ct_title); 70 | t.setText(rec.channel.name); 71 | 72 | if (rec.channel.iconBitmap != null) { 73 | ImageView iv = (ImageView) findViewById(R.id.ct_logo); 74 | iv.setImageBitmap(rec.channel.iconBitmap); 75 | } 76 | 77 | TextView text = (TextView) findViewById(R.id.rec_name); 78 | text.setText(rec.title); 79 | 80 | text = (TextView) findViewById(R.id.rec_summary); 81 | text.setText(rec.summary); 82 | if(rec.summary.length() == 0) 83 | text.setVisibility(TextView.GONE); 84 | 85 | text = (TextView) findViewById(R.id.rec_desc); 86 | text.setText(rec.description); 87 | 88 | text = (TextView) findViewById(R.id.rec_time); 89 | text.setText( 90 | DateFormat.getLongDateFormat(this).format(rec.start) 91 | + " " 92 | + DateFormat.getTimeFormat(this).format(rec.start) 93 | + " - " 94 | + DateFormat.getTimeFormat(this).format(rec.stop)); 95 | } 96 | 97 | @Override 98 | public boolean onCreateOptionsMenu(Menu menu) { 99 | MenuItem item = null; 100 | 101 | if (rec.title != null) { 102 | item = menu.add(Menu.NONE, android.R.string.search_go, Menu.NONE, android.R.string.search_go); 103 | item.setIntent(new SearchEPGIntent(this, rec.title)); 104 | item.setIcon(android.R.drawable.ic_menu_search); 105 | 106 | item = menu.add(Menu.NONE, Menu.NONE, Menu.NONE, "IMDb"); 107 | item.setIntent(new SearchIMDbIntent(this, rec.title)); 108 | item.setIcon(android.R.drawable.ic_menu_info_details); 109 | } 110 | 111 | Intent intent = new Intent(this, HTSService.class); 112 | 113 | if (rec.isRecording() || rec.isScheduled()) { 114 | intent.setAction(HTSService.ACTION_DVR_CANCEL); 115 | intent.putExtra("id", rec.id); 116 | item = menu.add(Menu.NONE, R.string.menu_record_cancel, Menu.NONE, R.string.menu_record_cancel); 117 | item.setIntent(intent); 118 | item.setIcon(android.R.drawable.ic_menu_close_clear_cancel); 119 | item.setIntent(intent); 120 | } else { 121 | intent.setAction(HTSService.ACTION_DVR_DELETE); 122 | intent.putExtra("id", rec.id); 123 | item = menu.add(Menu.NONE, R.string.menu_record_remove, Menu.NONE, R.string.menu_record_remove); 124 | item.setIntent(intent); 125 | item.setIcon(android.R.drawable.ic_menu_delete); 126 | item.setIntent(intent); 127 | 128 | intent = new Intent(this, ExternalPlaybackActivity.class); 129 | intent.putExtra("dvrId", rec.id); 130 | item = menu.add(Menu.NONE, R.string.ch_play, Menu.NONE, R.string.ch_play); 131 | item.setIntent(intent); 132 | item.setIcon(android.R.drawable.ic_menu_view); 133 | } 134 | 135 | return true; 136 | } 137 | 138 | @Override 139 | public boolean onPrepareOptionsMenu(Menu menu) { 140 | boolean rebuild = false; 141 | if (rec.isRecording() || rec.isScheduled()) { 142 | rebuild = menu.findItem(R.string.menu_record_cancel) == null; 143 | } else { 144 | rebuild = menu.findItem(R.string.menu_record_remove) == null; 145 | } 146 | 147 | if (rebuild) { 148 | menu.clear(); 149 | return onCreateOptionsMenu(menu); 150 | } 151 | 152 | return true; 153 | } 154 | 155 | @Override 156 | public boolean onOptionsItemSelected(final MenuItem item) { 157 | switch (item.getItemId()) { 158 | case R.string.menu_record_remove: { 159 | new AlertDialog.Builder(this) 160 | .setTitle(R.string.menu_record_remove) 161 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { 162 | 163 | public void onClick(DialogInterface dialog, int which) { 164 | startService(item.getIntent()); 165 | } 166 | }).setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { 167 | 168 | public void onClick(DialogInterface dialog, int which) { 169 | //NOP 170 | } 171 | }).show(); 172 | } 173 | case R.string.menu_record_cancel: 174 | startService(item.getIntent()); 175 | return true; 176 | default: 177 | return super.onOptionsItemSelected(item); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/PlaybackActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.app.Activity; 22 | import android.content.Intent; 23 | import android.content.SharedPreferences; 24 | import android.graphics.PixelFormat; 25 | import android.media.MediaPlayer; 26 | import android.media.MediaPlayer.OnErrorListener; 27 | import android.media.MediaPlayer.OnPreparedListener; 28 | import android.net.Uri; 29 | import android.os.Bundle; 30 | import android.preference.PreferenceManager; 31 | import android.text.format.DateFormat; 32 | import android.view.View.OnClickListener; 33 | import android.view.*; 34 | import android.widget.FrameLayout; 35 | import android.widget.LinearLayout; 36 | import android.widget.TextView; 37 | import java.util.Date; 38 | import org.tvheadend.tvhguide.R; 39 | import org.tvheadend.tvhguide.htsp.HTSListener; 40 | import org.tvheadend.tvhguide.htsp.HTSService; 41 | import org.tvheadend.tvhguide.model.Channel; 42 | import org.tvheadend.tvhguide.model.HttpTicket; 43 | import org.tvheadend.tvhguide.model.Stream; 44 | 45 | /** 46 | * 47 | * @author john-tornblom 48 | */ 49 | public class PlaybackActivity extends Activity implements HTSListener { 50 | 51 | private TVHVideoView videoView; 52 | 53 | @Override 54 | protected void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | 57 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 58 | if (prefs.getBoolean("externalPref", false)) { 59 | Intent intent = new Intent(this, ExternalPlaybackActivity.class); 60 | intent.putExtras(this.getIntent().getExtras()); 61 | startActivity(intent); 62 | finish(); 63 | return; 64 | } 65 | 66 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 67 | final Channel channel = app.getChannel(getIntent().getLongExtra("channelId", 0)); 68 | if (channel == null) { 69 | finish(); 70 | return; 71 | } 72 | 73 | setTitle(channel.name); 74 | 75 | requestWindowFeature(Window.FEATURE_NO_TITLE); 76 | getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 77 | WindowManager.LayoutParams.FLAG_FULLSCREEN); 78 | 79 | setContentView(R.layout.player_layout); 80 | 81 | getWindow().setFormat(PixelFormat.TRANSLUCENT); 82 | 83 | final TextView clock = (TextView) findViewById(R.id.pl_clock); 84 | final LinearLayout headerOverlay = (LinearLayout) findViewById(R.id.pl_header); 85 | final LinearLayout middleOverlay = (LinearLayout) findViewById(R.id.pl_middle); 86 | final LinearLayout footerOverlay = (LinearLayout) findViewById(R.id.pl_footer); 87 | 88 | videoView = (TVHVideoView) findViewById(R.id.pl_video); 89 | videoView.setOnPreparedListener(new OnPreparedListener() { 90 | 91 | public void onPrepared(MediaPlayer arg0) { 92 | middleOverlay.setVisibility(LinearLayout.GONE); 93 | } 94 | }); 95 | 96 | videoView.setOnErrorListener(new OnErrorListener() { 97 | 98 | public boolean onError(MediaPlayer arg0, int arg1, int arg2) { 99 | finish(); 100 | return true; 101 | } 102 | }); 103 | 104 | LayoutInflater inflater = getLayoutInflater(); 105 | View v = inflater.inflate(R.layout.channel_list_widget, null, false); 106 | final ChannelListViewWrapper w = new ChannelListViewWrapper(v); 107 | footerOverlay.addView(v); 108 | 109 | FrameLayout frameLayout = (FrameLayout) findViewById(R.id.pl_frame); 110 | frameLayout.setOnClickListener(new OnClickListener() { 111 | 112 | public void onClick(View arg0) { 113 | if (headerOverlay.getVisibility() == LinearLayout.VISIBLE) { 114 | headerOverlay.setVisibility(LinearLayout.INVISIBLE); 115 | footerOverlay.setVisibility(LinearLayout.INVISIBLE); 116 | } else { 117 | w.repaint(channel); 118 | clock.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.RIGHT); 119 | clock.setText(DateFormat.getTimeFormat(clock.getContext()).format(new Date())); 120 | headerOverlay.setVisibility(LinearLayout.VISIBLE); 121 | footerOverlay.setVisibility(LinearLayout.VISIBLE); 122 | } 123 | } 124 | }); 125 | 126 | Intent intent = new Intent(PlaybackActivity.this, HTSService.class); 127 | intent.setAction(HTSService.ACTION_GET_TICKET); 128 | intent.putExtras(getIntent().getExtras()); 129 | startService(intent); 130 | } 131 | 132 | @Override 133 | protected void onResume() { 134 | super.onResume(); 135 | 136 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 137 | app.addListener(this); 138 | } 139 | 140 | @Override 141 | protected void onPause() { 142 | super.onPause(); 143 | 144 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 145 | app.removeListener(this); 146 | } 147 | 148 | private void startPlayback(String path, String ticket) { 149 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 150 | 151 | String host = prefs.getString("serverHostPref", "localhost"); 152 | Integer port = Integer.parseInt(prefs.getString("httpPortPref", "9981")); 153 | Integer resolution = Integer.parseInt(prefs.getString("resolutionPref", "288")); 154 | Boolean transcode = prefs.getBoolean("transcodePref", true); 155 | String container = prefs.getString("containerPref", "matroska"); 156 | String acodec = prefs.getString("acodecPref", Stream.STREAM_TYPE_AAC); 157 | String vcodec = prefs.getString("vcodecPref", Stream.STREAM_TYPE_H264); 158 | String scodec = prefs.getString("scodecPref", "NONE"); 159 | 160 | String url = "http://" + host + ":" + port + path; 161 | url += "?ticket=" + ticket; 162 | url += "&mux=" + container; 163 | if (transcode) { 164 | url += "&transcode=1"; 165 | url += "&resolution=" + resolution; 166 | url += "&acodec=" + acodec; 167 | url += "&vcodec=" + vcodec; 168 | url += "&scodec=" + scodec; 169 | } 170 | 171 | videoView.setVideoURI(Uri.parse(url)); 172 | videoView.requestFocus(); 173 | videoView.start(); 174 | videoView.setAspectRatio(16, 9); 175 | 176 | TextView codecInfo = (TextView) findViewById(R.id.pl_codec); 177 | 178 | container = valueToName(R.array.pref_container_list, 179 | R.array.pref_container_list_display, container); 180 | 181 | String c = container; 182 | if (transcode) { 183 | c += " ("; 184 | c += acodec + ", "; 185 | c += vcodec + "@" + resolution; 186 | c += ")"; 187 | } 188 | 189 | codecInfo.setGravity(Gravity.CENTER_HORIZONTAL); 190 | codecInfo.setText(c); 191 | } 192 | 193 | private String valueToName(int valueRresouce, int nameResource, String val) { 194 | 195 | String[] names = getResources().getStringArray(nameResource); 196 | String[] values = getResources().getStringArray(valueRresouce); 197 | 198 | for (int i = 0; i < values.length; i++) { 199 | if (values[i].equals(val)) { 200 | return names[i]; 201 | } 202 | } 203 | return ""; 204 | } 205 | 206 | public void onMessage(String action, final Object obj) { 207 | if (action.equals(TVHGuideApplication.ACTION_TICKET_ADD)) { 208 | 209 | this.runOnUiThread(new Runnable() { 210 | 211 | public void run() { 212 | HttpTicket t = (HttpTicket) obj; 213 | startPlayback(t.path, t.ticket); 214 | } 215 | }); 216 | 217 | } 218 | } 219 | } -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | TVHGuide 4 | 5 | Help 6 | Settings 7 | Refresh 8 | Tags 9 | Recordings 10 | 11 | Record 12 | Cancel recording 13 | Remove recording 14 | 15 | Hostname 16 | Enter Server Hostname 17 | 18 | Port 19 | Enter Server Port 20 | 21 | Username 22 | Enter Username 23 | 24 | Password 25 | Enter Password 26 | 27 | User interface 28 | 29 | Channel icons 30 | Show channel icons? 31 | 32 | Light theme 33 | Use a light theme 34 | 35 | Resolution 36 | Maximum resolution for the transcoder 37 | 38 | Audio codec 39 | Audio codec to use with the transcoder 40 | 41 | Video codec 42 | Video codec to use with the transcoder 43 | 44 | Subtitle codec 45 | Subtitle codec to use with the transcoder 46 | 47 | Media container 48 | Prefered media container 49 | 50 | Playback 51 | 52 | HTTP port 53 | Enter Server HTTP Port 54 | 55 | Transcode stream 56 | Request a transcoded stream during live playback 57 | 58 | Prefer external player 59 | Use an external player for live streams (BSPlayer seems OK) 60 | 61 | Access denied 62 | Invalid response from server 63 | Can\'t connect to server 64 | Lost connection to server 65 | 66 | Loading… 67 | Please wait a few seconds… 68 | 69 | All channels 70 | Get more 71 | Season 72 | Episode 73 | Part 74 | Airing 75 | Rating 76 | Type 77 | Part 78 | 79 | 80 | Movie/Drama 81 | News/Current affairs 82 | Show/Game show 83 | Sports 84 | Children\'s / Youth 85 | Music/Ballet/Dance 86 | Arts/Culture 87 | Social/Political issues/Economics 88 | Education/Science/Factual 89 | Leisure hobbies 90 | Misc 91 | 92 | 93 | 94 | Movie/Drama 95 | Detective/Thriller 96 | Adventure/Western/War 97 | Science Fiction/Fantasy/Horror 98 | Comedy 99 | Soap/Melodrama/Folkloric 100 | Romance 101 | Serious/ClassicalReligion/Historical 102 | Adult Movie/Drama 103 | 104 | 105 | 106 | News/Current affairs 107 | News/Weather Report 108 | Magazine 109 | Documentary 110 | Discussion/Interview/Debate 111 | 112 | 113 | 114 | Show/Game show 115 | Game show/Quiz/Contest 116 | Variety 117 | Talk 118 | 119 | 120 | 121 | Sports 122 | Special Event 123 | Magazine 124 | Football/Soccer 125 | Tennis/Squash 126 | Team sports 127 | Athletics 128 | Motor Sport 129 | Water Sport 130 | Winter Sports 131 | Equestrian 132 | Martial sports 133 | 134 | 135 | 136 | Children\'s / Youth 137 | Pre-school 138 | Entertainment (6 to 14 year-olds) 139 | Entertainment (10 to 16 year-olds) 140 | Informational/Educational/Schools 141 | Cartoons/Puppets 142 | 143 | 144 | 145 | Music/Ballet/Dance 146 | Rock/Pop 147 | Serious music/Classical Music 148 | Folk/Traditional music 149 | Jazz 150 | Musical/Opera 151 | Ballet 152 | 153 | 154 | 155 | Arts/Culture 156 | Performing Arts 157 | Fine Arts 158 | Religion 159 | Popular Culture/Tradital Arts 160 | Literature 161 | Film/Cinema 162 | Experimental Film/Video 163 | Broadcasting/Press 164 | New Media 165 | Magazine 166 | Fashion 167 | 168 | 169 | 170 | Social/Political issues/Economics 171 | Magazine/Report/Domentary 172 | Economics/Social Advisory 173 | Remarkable People 174 | 175 | 176 | 177 | 178 | Education/Science/Factual 179 | Nature/Animals/Environment 180 | Technology/Natural sciences 181 | Medicine/Physiology/Psychology 182 | Foreign Countries/Expeditions 183 | Social/Spiritual Sciences 184 | Further Education 185 | Languages 186 | 187 | 188 | 189 | Leisure hobbies 190 | Tourism/Travel 191 | Handicraft 192 | Motoring 193 | Fitness and Health 194 | Cooking 195 | Advertisement/Shopping 196 | Gardening 197 | 198 | 199 | 200 | Misc 201 | Black and White 202 | Unpublished 203 | Live Broadcast 204 | 205 | 206 | Scheduled 207 | Recording 208 | Completed 209 | Missed 210 | Invalid 211 | 212 | Play 213 | No transmission 214 | 215 | Search the EPG 216 | today 217 | -------------------------------------------------------------------------------- /res/layout/programme_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 18 | 19 | 24 | 25 | 31 | 32 | 39 | 40 | 44 | 45 | 46 | 53 | 54 | 55 | 60 | 61 | 67 | 68 | 74 | 75 | 79 | 80 | 84 | 85 | 89 | 90 | 91 | 92 | 96 | 97 | 104 | 105 | 106 | 112 | 113 | 119 | 123 | 124 | 125 | 129 | 130 | 137 | 138 | 139 | 145 | 146 | 152 | 153 | 157 | 158 | 159 | 160 | 164 | 165 | 172 | 173 | 174 | 180 | 181 | 187 | 188 | 192 | 193 | 203 | 204 | 210 | 211 | 212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/htsp/SelectionThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.htsp; 20 | 21 | import android.util.Log; 22 | import java.io.IOException; 23 | import java.nio.channels.SelectionKey; 24 | import java.nio.channels.Selector; 25 | import java.nio.channels.ServerSocketChannel; 26 | import java.nio.channels.SocketChannel; 27 | import java.nio.channels.spi.AbstractSelectableChannel; 28 | import java.util.ArrayList; 29 | import java.util.HashMap; 30 | import java.util.Iterator; 31 | import java.util.concurrent.locks.Lock; 32 | import java.util.concurrent.locks.ReentrantLock; 33 | 34 | /** 35 | * 36 | * @author john-tornblom 37 | */ 38 | public abstract class SelectionThread extends Thread { 39 | 40 | private static final String TAG = "SelectionThread"; 41 | private Selector selector; 42 | private volatile boolean running; 43 | private HashMap regBuf; 44 | private Lock lock; 45 | 46 | public SelectionThread() { 47 | running = false; 48 | lock = new ReentrantLock(); 49 | regBuf = new HashMap(); 50 | } 51 | 52 | public void setRunning(boolean b) { 53 | try { 54 | lock.lock(); 55 | running = false; 56 | } finally { 57 | lock.unlock(); 58 | } 59 | } 60 | 61 | void close(AbstractSelectableChannel channel) throws IOException { 62 | lock.lock(); 63 | try { 64 | regBuf.remove(channel); 65 | channel.close(); 66 | } finally { 67 | lock.unlock(); 68 | } 69 | } 70 | 71 | public void register(AbstractSelectableChannel channel, int ops, boolean b) { 72 | lock.lock(); 73 | try { 74 | int oldOps = 0; 75 | if (regBuf.containsKey(channel)) { 76 | oldOps = regBuf.get(channel); 77 | } 78 | if (b) { 79 | ops |= oldOps; 80 | } else { 81 | ops = oldOps & ~ops; 82 | } 83 | regBuf.put(channel, ops); 84 | if (selector != null) { 85 | selector.wakeup(); 86 | } 87 | } finally { 88 | lock.unlock(); 89 | } 90 | } 91 | 92 | @Override 93 | public void run() { 94 | try { 95 | lock.lock(); 96 | selector = Selector.open(); 97 | running = true; 98 | } catch (IOException ex) { 99 | running = false; 100 | Log.e(TAG, "Can't open a selector", ex); 101 | } finally { 102 | lock.unlock(); 103 | } 104 | 105 | while (running) { 106 | select(5000); 107 | } 108 | 109 | try { 110 | lock.lock(); 111 | //Clean up 112 | for (SelectionKey key : selector.keys()) { 113 | try { 114 | key.channel().close(); 115 | } catch (IOException ex) { 116 | Log.e(TAG, "Can't close channel", ex); 117 | key.cancel(); 118 | } 119 | 120 | } 121 | try { 122 | selector.close(); 123 | } catch (IOException ex) { 124 | Log.e(TAG, "Can't close selector", ex); 125 | } 126 | } finally { 127 | lock.unlock(); 128 | } 129 | } 130 | 131 | private void select(int timeout) { 132 | try { 133 | selector.select(timeout); 134 | } catch (IOException ex) { 135 | Log.e(TAG, "Can't select socket", ex); 136 | return; 137 | } 138 | 139 | Iterator it = selector.selectedKeys().iterator(); 140 | 141 | //Process the selected keys 142 | while (it.hasNext()) { 143 | SelectionKey selKey = (SelectionKey) it.next(); 144 | it.remove(); 145 | processTcpSelectionKey(selKey); 146 | } 147 | 148 | try { 149 | lock.lock(); 150 | ArrayList tmp = new ArrayList(); 151 | for (AbstractSelectableChannel ch : regBuf.keySet()) { 152 | try { 153 | int ops = regBuf.get(ch); 154 | ch.register(selector, ops); 155 | } catch (Throwable t) { 156 | tmp.add(ch); 157 | Log.e(TAG, "Can't register channel", t); 158 | if (ch instanceof SocketChannel) { 159 | onError((SocketChannel) ch); 160 | } 161 | } 162 | } 163 | for (AbstractSelectableChannel ch : tmp) { 164 | regBuf.remove(ch); 165 | } 166 | } finally { 167 | lock.unlock(); 168 | } 169 | } 170 | 171 | private void processTcpSelectionKey(SelectionKey selKey) { 172 | //Incomming connection established 173 | if (selKey.isValid() && selKey.isAcceptable()) { 174 | try { 175 | ServerSocketChannel ssChannel = (ServerSocketChannel) selKey.channel(); 176 | SocketChannel sChannel = ssChannel.accept(); 177 | if (sChannel != null) { 178 | sChannel.configureBlocking(false); 179 | try { 180 | onAccept(sChannel); 181 | } catch (Throwable t) { 182 | Log.e(TAG, "Can't establish connection", t); 183 | onError(sChannel); 184 | return; 185 | } 186 | } 187 | } catch (Throwable t) { 188 | Log.e(TAG, "Can't establish connection", t); 189 | return; 190 | } 191 | } 192 | 193 | //Outgoing connection established 194 | if (selKey.isValid() && selKey.isConnectable()) { 195 | try { 196 | SocketChannel sChannel = (SocketChannel) selKey.channel(); 197 | if (!sChannel.finishConnect()) { 198 | onError(sChannel); 199 | selKey.cancel(); 200 | return; 201 | } 202 | try { 203 | onConnect(sChannel); 204 | } catch (Throwable t) { 205 | Log.e(TAG, "Can't establish connection", t); 206 | onError(sChannel); 207 | selKey.cancel(); 208 | return; 209 | } 210 | } catch (Throwable t) { 211 | Log.e(TAG, "Can't establish connection", t); 212 | selKey.cancel(); 213 | return; 214 | } 215 | } 216 | 217 | //Incomming data 218 | if (selKey.isValid() && selKey.isReadable()) { 219 | SocketChannel sChannel = (SocketChannel) selKey.channel(); 220 | try { 221 | onReadable(sChannel); 222 | } catch (Throwable t) { 223 | Log.e(TAG, "Can't read message", t); 224 | onError(sChannel); 225 | selKey.cancel(); 226 | return; 227 | } 228 | } 229 | 230 | //Clear to send 231 | if (selKey.isValid() && selKey.isWritable()) { 232 | SocketChannel sChannel = (SocketChannel) selKey.channel(); 233 | try { 234 | onWrtiable(sChannel); 235 | } catch (Throwable t) { 236 | Log.e(TAG, "Can't send message", t); 237 | onError(sChannel); 238 | selKey.cancel(); 239 | return; 240 | } 241 | } 242 | } 243 | 244 | private void onAccept(SocketChannel ch) throws Exception { 245 | onEvent(SelectionKey.OP_ACCEPT, ch); 246 | } 247 | 248 | private void onConnect(SocketChannel ch) throws Exception { 249 | onEvent(SelectionKey.OP_CONNECT, ch); 250 | } 251 | 252 | private void onReadable(SocketChannel ch) throws Exception { 253 | onEvent(SelectionKey.OP_READ, ch); 254 | } 255 | 256 | private void onError(SocketChannel ch) { 257 | try { 258 | lock.lock(); 259 | ch.close(); 260 | regBuf.remove(ch); 261 | } catch (Exception ex) { 262 | Log.e(TAG, null, ex); 263 | } finally { 264 | lock.unlock(); 265 | } 266 | try { 267 | onEvent(-1, ch); 268 | } catch (Exception ex) { 269 | } 270 | } 271 | 272 | private void onWrtiable(SocketChannel ch) throws Exception { 273 | onEvent(SelectionKey.OP_WRITE, ch); 274 | } 275 | 276 | public abstract void onEvent(int selectionKey, SocketChannel ch) throws Exception; 277 | } 278 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/ProgrammeActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.app.Activity; 22 | import android.content.Intent; 23 | import android.content.SharedPreferences; 24 | import android.os.Bundle; 25 | import android.preference.PreferenceManager; 26 | import android.text.format.DateFormat; 27 | import android.util.SparseArray; 28 | import android.view.Menu; 29 | import android.view.MenuItem; 30 | import android.view.View; 31 | import android.view.Window; 32 | import android.widget.ImageView; 33 | import android.widget.RatingBar; 34 | import android.widget.TextView; 35 | 36 | import org.tvheadend.tvhguide.R; 37 | import org.tvheadend.tvhguide.R.string; 38 | import org.tvheadend.tvhguide.htsp.HTSService; 39 | import org.tvheadend.tvhguide.intent.SearchEPGIntent; 40 | import org.tvheadend.tvhguide.intent.SearchIMDbIntent; 41 | import org.tvheadend.tvhguide.model.Channel; 42 | import org.tvheadend.tvhguide.model.Programme; 43 | import org.tvheadend.tvhguide.model.SeriesInfo; 44 | 45 | /** 46 | * 47 | * @author john-tornblom 48 | */ 49 | public class ProgrammeActivity extends Activity { 50 | 51 | private Programme programme; 52 | 53 | @Override 54 | public void onCreate(Bundle savedInstanceState) { 55 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 56 | Boolean theme = prefs.getBoolean("lightThemePref", false); 57 | setTheme(theme ? R.style.CustomTheme_Light : R.style.CustomTheme); 58 | 59 | super.onCreate(savedInstanceState); 60 | 61 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 62 | Channel channel = app.getChannel(getIntent().getLongExtra("channelId", 0)); 63 | if (channel == null) { 64 | finish(); 65 | return; 66 | } 67 | 68 | long eventId = getIntent().getLongExtra("eventId", 0); 69 | for (Programme p : channel.epg) { 70 | if (p.id == eventId) { 71 | programme = p; 72 | break; 73 | } 74 | } 75 | 76 | if (programme == null) { 77 | finish(); 78 | return; 79 | } 80 | 81 | requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); 82 | 83 | setContentView(R.layout.programme_layout); 84 | 85 | getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.programme_title); 86 | TextView t = (TextView) findViewById(R.id.ct_title); 87 | t.setText(channel.name); 88 | 89 | if (channel.iconBitmap != null) { 90 | ImageView iv = (ImageView) findViewById(R.id.ct_logo); 91 | iv.setImageBitmap(channel.iconBitmap); 92 | } 93 | 94 | 95 | TextView text = (TextView) findViewById(R.id.pr_title); 96 | text.setText(programme.title); 97 | 98 | text = (TextView) findViewById(R.id.pr_channel); 99 | text.setText(channel.name); 100 | 101 | text = (TextView) findViewById(R.id.pr_airing); 102 | text.setText( 103 | DateFormat.getLongDateFormat(text.getContext()).format(programme.start) 104 | + " " 105 | + DateFormat.getTimeFormat(text.getContext()).format(programme.start) 106 | + " - " 107 | + DateFormat.getTimeFormat(text.getContext()).format(programme.stop)); 108 | 109 | 110 | if(programme.summary.length() == 0 && programme.description.length() == 0) { 111 | View v = findViewById(R.id.pr_summay_and_desc_layout); 112 | v.setVisibility(View.GONE); 113 | } else { 114 | text = (TextView) findViewById(R.id.pr_summary); 115 | text.setText(programme.summary); 116 | if(programme.summary.length() == 0) 117 | text.setVisibility(View.GONE); 118 | 119 | text = (TextView) findViewById(R.id.pr_desc); 120 | text.setText(programme.description); 121 | if(programme.description.length() == 0) 122 | text.setVisibility(View.GONE); 123 | } 124 | 125 | String s = buildSeriesInfoString(programme.seriesInfo); 126 | if(s.length() > 0) { 127 | text = (TextView) findViewById(R.id.pr_series_info); 128 | text.setText(s); 129 | } else { 130 | View v = findViewById(R.id.pr_series_info_row); 131 | v.setVisibility(View.GONE); 132 | v = findViewById(R.id.pr_series_info_sep); 133 | v.setVisibility(View.GONE); 134 | } 135 | 136 | SparseArray contentTypes = TVHGuideApplication.getContentTypes(this); 137 | s = contentTypes.get(programme.contentType, ""); 138 | if(s.length() > 0) { 139 | text = (TextView) findViewById(R.id.pr_content_type); 140 | text.setText(s); 141 | } else { 142 | View v = findViewById(R.id.pr_content_type_row); 143 | v.setVisibility(View.GONE); 144 | v = findViewById(R.id.pr_content_type_sep); 145 | v.setVisibility(View.GONE); 146 | } 147 | 148 | if(programme.starRating > 0) { 149 | RatingBar starRating = (RatingBar)findViewById(R.id.pr_star_rating); 150 | starRating.setRating((float)programme.starRating / 10.0f); 151 | 152 | text = (TextView) findViewById(R.id.pr_star_rating_txt); 153 | text.setText("(" 154 | + programme.starRating 155 | + "/" 156 | + 100 157 | + ")"); 158 | } else { 159 | View v = findViewById(R.id.pr_star_rating_row); 160 | v.setVisibility(View.GONE); 161 | } 162 | } 163 | 164 | 165 | public String buildSeriesInfoString(SeriesInfo info) { 166 | if (info.onScreen != null && info.onScreen.length() > 0) 167 | return info.onScreen; 168 | 169 | String s = ""; 170 | String season = this.getResources().getString(string.pr_season); 171 | String episode = this.getResources().getString(string.pr_episode); 172 | String part = this.getResources().getString(string.pr_part); 173 | 174 | if(info.onScreen.length() > 0) { 175 | return info.onScreen; 176 | } 177 | 178 | if (info.seasonNumber > 0) { 179 | if (s.length() > 0) 180 | s += ", "; 181 | s += String.format("%s %02d", season.toLowerCase(), info.seasonNumber); 182 | if(info.seasonCount > 0) 183 | s += String.format("/%02d", info.seasonCount); 184 | } 185 | if (info.episodeNumber > 0) { 186 | if (s.length() > 0) 187 | s += ", "; 188 | s += String.format("%s %02d", episode.toLowerCase(), info.episodeNumber); 189 | if(info.episodeCount > 0) 190 | s += String.format("/%02d", info.episodeCount); 191 | } 192 | if (info.partNumber > 0) { 193 | if (s.length() > 0) 194 | s += ", "; 195 | s += String.format("%s %d", part.toLowerCase(), info.partNumber); 196 | if(info.partCount > 0) 197 | s += String.format("/%02d", info.partCount); 198 | } 199 | 200 | if(s.length() > 0) { 201 | s = s.substring(0,1).toUpperCase() + s.substring(1); 202 | } 203 | 204 | return s; 205 | } 206 | 207 | @Override 208 | public boolean onCreateOptionsMenu(Menu menu) { 209 | MenuItem item = null; 210 | 211 | if (programme.title != null) { 212 | item = menu.add(Menu.NONE, android.R.string.search_go, Menu.NONE, android.R.string.search_go); 213 | item.setIntent(new SearchEPGIntent(this, programme.title)); 214 | item.setIcon(android.R.drawable.ic_menu_search); 215 | 216 | item = menu.add(Menu.NONE, Menu.NONE, Menu.NONE, "IMDb"); 217 | item.setIntent(new SearchIMDbIntent(this, programme.title)); 218 | item.setIcon(android.R.drawable.ic_menu_info_details); 219 | } 220 | 221 | Intent intent = new Intent(this, HTSService.class); 222 | 223 | if (programme.recording == null) { 224 | intent.setAction(HTSService.ACTION_DVR_ADD); 225 | intent.putExtra("eventId", programme.id); 226 | intent.putExtra("channelId", programme.channel.id); 227 | item = menu.add(Menu.NONE, R.string.menu_record, Menu.NONE, R.string.menu_record); 228 | item.setIcon(android.R.drawable.ic_menu_save); 229 | } else if (programme.isRecording() || programme.isScheduled()) { 230 | intent.setAction(HTSService.ACTION_DVR_CANCEL); 231 | intent.putExtra("id", programme.recording.id); 232 | item = menu.add(Menu.NONE, R.string.menu_record_cancel, Menu.NONE, R.string.menu_record_cancel); 233 | item.setIcon(android.R.drawable.ic_menu_close_clear_cancel); 234 | } else { 235 | intent.setAction(HTSService.ACTION_DVR_DELETE); 236 | intent.putExtra("id", programme.recording.id); 237 | item = menu.add(Menu.NONE, R.string.menu_record_remove, Menu.NONE, R.string.menu_record_remove); 238 | item.setIcon(android.R.drawable.ic_menu_delete); 239 | } 240 | 241 | item.setIntent(intent); 242 | 243 | return true; 244 | } 245 | 246 | @Override 247 | public boolean onPrepareOptionsMenu(Menu menu) { 248 | boolean rebuild = false; 249 | if (programme.recording == null) { 250 | rebuild = menu.findItem(R.string.menu_record) == null; 251 | } else if (programme.isRecording() || programme.isScheduled()) { 252 | rebuild = menu.findItem(R.string.menu_record_cancel) == null; 253 | } else { 254 | rebuild = menu.findItem(R.string.menu_record_remove) == null; 255 | } 256 | 257 | if (rebuild) { 258 | menu.clear(); 259 | return onCreateOptionsMenu(menu); 260 | } 261 | 262 | return true; 263 | } 264 | 265 | @Override 266 | public boolean onOptionsItemSelected(MenuItem item) { 267 | switch (item.getItemId()) { 268 | case R.string.menu_record_remove: 269 | case R.string.menu_record_cancel: 270 | case R.string.menu_record: 271 | startService(item.getIntent()); 272 | return true; 273 | default: 274 | return super.onOptionsItemSelected(item); 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/htsp/HTSConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.htsp; 20 | 21 | import android.util.Log; 22 | import java.io.IOException; 23 | import java.net.InetSocketAddress; 24 | import java.nio.ByteBuffer; 25 | import java.nio.channels.SelectionKey; 26 | import java.nio.channels.Selector; 27 | import java.nio.channels.SocketChannel; 28 | import java.security.MessageDigest; 29 | import java.security.NoSuchAlgorithmException; 30 | import java.util.HashMap; 31 | import java.util.Iterator; 32 | import java.util.LinkedList; 33 | import java.util.Map; 34 | import java.util.concurrent.locks.Lock; 35 | import java.util.concurrent.locks.ReentrantLock; 36 | 37 | /** 38 | * 39 | * @author john-tornblom 40 | */ 41 | public class HTSConnection extends Thread { 42 | 43 | public static final int TIMEOUT_ERROR = 1; 44 | public static final int CONNECTION_REFUSED_ERROR = 2; 45 | public static final int CONNECTION_LOST_ERROR = 3; 46 | public static final int HTS_AUTH_ERROR = 4; 47 | public static final int HTS_MESSAGE_ERROR = 5; 48 | private static final String TAG = "HTSPConnection"; 49 | private volatile boolean running; 50 | private Lock lock; 51 | private SocketChannel socketChannel; 52 | private ByteBuffer inBuf; 53 | private int seq; 54 | private String clientName; 55 | private String clientVersion; 56 | private int protocolVersion; 57 | private String webRoot; 58 | 59 | private HTSConnectionListener listener; 60 | private Map responseHandelers; 61 | private LinkedList messageQueue; 62 | private boolean auth; 63 | private Selector selector; 64 | 65 | public HTSConnection(HTSConnectionListener listener, String clientName, String clientVersion) { 66 | running = false; 67 | lock = new ReentrantLock(); 68 | inBuf = ByteBuffer.allocateDirect(1024 * 1024); 69 | inBuf.limit(4); 70 | responseHandelers = new HashMap(); 71 | messageQueue = new LinkedList(); 72 | 73 | this.listener = listener; 74 | this.clientName = clientName; 75 | this.clientVersion = clientVersion; 76 | } 77 | 78 | public void setRunning(boolean b) { 79 | try { 80 | lock.lock(); 81 | running = false; 82 | } finally { 83 | lock.unlock(); 84 | } 85 | } 86 | 87 | //sychronized, blocking connect 88 | public void open(String hostname, int port) { 89 | if (running) { 90 | return; 91 | } 92 | 93 | final Object signal = new Object(); 94 | 95 | lock.lock(); 96 | try { 97 | selector = Selector.open(); 98 | socketChannel = SocketChannel.open(); 99 | socketChannel.configureBlocking(false); 100 | socketChannel.socket().setKeepAlive(true); 101 | socketChannel.socket().setSoTimeout(5000); 102 | socketChannel.register(selector, SelectionKey.OP_CONNECT, signal); 103 | socketChannel.connect(new InetSocketAddress(hostname, port)); 104 | 105 | running = true; 106 | start(); 107 | } catch (Exception ex) { 108 | Log.e(TAG, "Can't open connection", ex); 109 | listener.onError(CONNECTION_REFUSED_ERROR); 110 | return; 111 | } finally { 112 | lock.unlock(); 113 | } 114 | 115 | synchronized (signal) { 116 | try { 117 | signal.wait(5000); 118 | if (socketChannel.isConnectionPending()) { 119 | listener.onError(TIMEOUT_ERROR); 120 | close(); 121 | } 122 | } catch (InterruptedException ex) { 123 | } 124 | } 125 | } 126 | 127 | public boolean isConnected() { 128 | return socketChannel != null 129 | && socketChannel.isOpen() 130 | && socketChannel.isConnected() 131 | && running; 132 | } 133 | 134 | //sycnronized, blocking auth 135 | public void authenticate(String username, final String password) { 136 | if (auth || !running) { 137 | return; 138 | } 139 | 140 | auth = false; 141 | final HTSMessage authMessage = new HTSMessage(); 142 | authMessage.setMethod("enableAsyncMetadata"); 143 | authMessage.putField("username", username); 144 | final HTSResponseHandler authHandler = new HTSResponseHandler() { 145 | 146 | public void handleResponse(HTSMessage response) { 147 | auth = response.getInt("noaccess", 0) != 1; 148 | if (!auth) { 149 | listener.onError(HTS_AUTH_ERROR); 150 | } 151 | synchronized (authMessage) { 152 | authMessage.notify(); 153 | } 154 | } 155 | }; 156 | 157 | HTSMessage helloMessage = new HTSMessage(); 158 | helloMessage.setMethod("hello"); 159 | helloMessage.putField("clientname", this.clientName); 160 | helloMessage.putField("clientversion", this.clientVersion); 161 | helloMessage.putField("htspversion", HTSMessage.HTSP_VERSION); 162 | helloMessage.putField("username", username); 163 | sendMessage(helloMessage, new HTSResponseHandler() { 164 | 165 | public void handleResponse(HTSMessage response) { 166 | 167 | protocolVersion = response.getInt("htspversion"); 168 | webRoot = response.getString("webroot", ""); 169 | 170 | MessageDigest md; 171 | try { 172 | md = MessageDigest.getInstance("SHA1"); 173 | md.update(password.getBytes()); 174 | md.update(response.getByteArray("challenge")); 175 | authMessage.putField("digest", md.digest()); 176 | sendMessage(authMessage, authHandler); 177 | } catch (NoSuchAlgorithmException ex) { 178 | return; 179 | } 180 | } 181 | }); 182 | 183 | synchronized (authMessage) { 184 | try { 185 | authMessage.wait(5000); 186 | if (!auth) { 187 | listener.onError(TIMEOUT_ERROR); 188 | } 189 | return; 190 | } catch (InterruptedException ex) { 191 | return; 192 | } 193 | } 194 | } 195 | 196 | public boolean isAuthenticated() { 197 | return auth; 198 | } 199 | 200 | public void sendMessage(HTSMessage message, HTSResponseHandler listener) { 201 | if (!isConnected()) { 202 | return; 203 | } 204 | 205 | lock.lock(); 206 | try { 207 | seq++; 208 | message.putField("seq", seq); 209 | responseHandelers.put(seq, listener); 210 | socketChannel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ | SelectionKey.OP_CONNECT); 211 | messageQueue.add(message); 212 | selector.wakeup(); 213 | } catch (Exception ex) { 214 | Log.e(TAG, "Can't transmit message", ex); 215 | this.listener.onError(ex); 216 | } finally { 217 | lock.unlock(); 218 | } 219 | } 220 | 221 | public void close() { 222 | lock.lock(); 223 | try { 224 | responseHandelers.clear(); 225 | messageQueue.clear(); 226 | auth = false; 227 | running = false; 228 | socketChannel.register(selector, 0); 229 | socketChannel.close(); 230 | } catch (Exception ex) { 231 | Log.e(TAG, "Can't close connection", ex); 232 | } finally { 233 | lock.unlock(); 234 | } 235 | } 236 | 237 | @Override 238 | public void run() { 239 | while (running) { 240 | try { 241 | selector.select(5000); 242 | } catch (IOException ex) { 243 | listener.onError(ex); 244 | running = false; 245 | continue; 246 | } 247 | 248 | lock.lock(); 249 | 250 | try { 251 | Iterator it = selector.selectedKeys().iterator(); 252 | while (it.hasNext()) { 253 | SelectionKey selKey = (SelectionKey) it.next(); 254 | it.remove(); 255 | processTcpSelectionKey(selKey); 256 | } 257 | 258 | int ops = SelectionKey.OP_READ; 259 | if (!messageQueue.isEmpty()) { 260 | ops |= SelectionKey.OP_WRITE; 261 | } 262 | socketChannel.register(selector, ops); 263 | } catch (Exception ex) { 264 | Log.e(TAG, "Can't read message", ex); 265 | listener.onError(ex); 266 | running = false; 267 | } finally { 268 | lock.unlock(); 269 | } 270 | } 271 | 272 | close(); 273 | } 274 | 275 | private void processTcpSelectionKey(SelectionKey selKey) throws IOException { 276 | if (selKey.isConnectable() && selKey.isValid()) { 277 | SocketChannel sChannel = (SocketChannel) selKey.channel(); 278 | sChannel.finishConnect(); 279 | final Object signal = selKey.attachment(); 280 | synchronized (signal) { 281 | signal.notify(); 282 | } 283 | sChannel.register(selector, SelectionKey.OP_READ); 284 | } 285 | 286 | if (selKey.isReadable() && selKey.isValid()) { 287 | SocketChannel sChannel = (SocketChannel) selKey.channel(); 288 | int len = sChannel.read(inBuf); 289 | if (len < 0) { 290 | throw new IOException("Server went down"); 291 | } 292 | 293 | HTSMessage msg = HTSMessage.parse(inBuf); 294 | if (msg != null) { 295 | handleMessage(msg); 296 | } 297 | } 298 | if (selKey.isWritable() && selKey.isValid()) { 299 | SocketChannel sChannel = (SocketChannel) selKey.channel(); 300 | HTSMessage msg = messageQueue.poll(); 301 | if (msg != null) { 302 | msg.transmit(sChannel); 303 | } 304 | } 305 | } 306 | 307 | private void handleMessage(HTSMessage msg) { 308 | if (msg.containsField("seq")) { 309 | int respSeq = msg.getInt("seq"); 310 | HTSResponseHandler handler = responseHandelers.get(respSeq); 311 | responseHandelers.remove(respSeq); 312 | 313 | if (handler != null) { 314 | synchronized (handler) { 315 | handler.handleResponse(msg); 316 | } 317 | return; 318 | } 319 | } 320 | 321 | listener.onMessage(msg); 322 | } 323 | 324 | public int getProtocolVersion() { 325 | return this.protocolVersion; 326 | } 327 | 328 | public String getWebRoot() { 329 | return this.webRoot; 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/htsp/HTSMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide.htsp; 20 | 21 | import java.io.IOException; 22 | import java.math.BigInteger; 23 | import java.nio.ByteBuffer; 24 | import java.nio.channels.SocketChannel; 25 | import java.util.ArrayList; 26 | import java.util.Collection; 27 | import java.util.Date; 28 | import java.util.HashMap; 29 | import java.util.List; 30 | import java.util.Map; 31 | 32 | /** 33 | * 34 | * @author john-tornblom 35 | */ 36 | public class HTSMessage extends HashMap { 37 | 38 | public static final long HTSP_VERSION = 8; 39 | private static final byte HMF_MAP = 1; 40 | private static final byte HMF_S64 = 2; 41 | private static final byte HMF_STR = 3; 42 | private static final byte HMF_BIN = 4; 43 | private static final byte HMF_LIST = 5; 44 | private ByteBuffer buf; 45 | 46 | public void putField(String name, Object value) { 47 | if (value != null) { 48 | put(name, value); 49 | } 50 | } 51 | 52 | public void setMethod(String name) { 53 | put("method", name); 54 | } 55 | 56 | public String getMethod() { 57 | return getString("method", ""); 58 | } 59 | 60 | public boolean containsField(String name) { 61 | return containsKey(name); 62 | } 63 | 64 | public BigInteger getBigInteger(String name) { 65 | return (BigInteger) get(name); 66 | } 67 | 68 | public long getLong(String name) { 69 | return getBigInteger(name).longValue(); 70 | } 71 | 72 | public long getLong(String name, long std) { 73 | if (!containsField(name)) { 74 | return std; 75 | } 76 | return getLong(name); 77 | } 78 | 79 | public int getInt(String name) { 80 | return getBigInteger(name).intValue(); 81 | } 82 | 83 | public int getInt(String name, int std) { 84 | if (!containsField(name)) { 85 | return std; 86 | } 87 | return getInt(name); 88 | } 89 | 90 | public String getString(String name, String std) { 91 | if (!containsField(name)) { 92 | return std; 93 | } 94 | return getString(name); 95 | } 96 | 97 | public String getString(String name) { 98 | Object obj = get(name); 99 | if (obj == null) { 100 | return null; 101 | } 102 | return obj.toString(); 103 | } 104 | 105 | public List getLongList(String name) { 106 | ArrayList list = new ArrayList(); 107 | 108 | if (!containsField(name)) { 109 | return list; 110 | } 111 | 112 | for (Object obj : (List) get(name)) { 113 | if (obj instanceof BigInteger) { 114 | list.add(((BigInteger) obj).longValue()); 115 | } 116 | } 117 | 118 | return list; 119 | } 120 | 121 | List getLongList(String name, List std) { 122 | if (!containsField(name)) { 123 | return std; 124 | } 125 | 126 | return getLongList(name); 127 | } 128 | 129 | public List getIntList(String name) { 130 | ArrayList list = new ArrayList(); 131 | 132 | if (!containsField(name)) { 133 | return list; 134 | } 135 | 136 | for (Object obj : (List) get(name)) { 137 | if (obj instanceof BigInteger) { 138 | list.add(((BigInteger) obj).intValue()); 139 | } 140 | } 141 | 142 | return list; 143 | } 144 | 145 | List getIntList(String name, List std) { 146 | if (!containsField(name)) { 147 | return std; 148 | } 149 | 150 | return getIntList(name); 151 | } 152 | 153 | public List getList(String name) { 154 | return (List) get(name); 155 | } 156 | 157 | public byte[] getByteArray(String name) { 158 | return (byte[]) get(name); 159 | } 160 | 161 | public Date getDate(String name) { 162 | return new Date(getLong(name) * 1000); 163 | } 164 | 165 | public boolean transmit(SocketChannel ch) throws IOException { 166 | if (buf == null) { 167 | byte[] data = serializeBinary(this); 168 | int len = data.length; 169 | buf = ByteBuffer.allocateDirect(len + 4); 170 | 171 | buf.put((byte) ((len >> 24) & 0xFF)); 172 | buf.put((byte) ((len >> 16) & 0xFF)); 173 | buf.put((byte) ((len >> 8) & 0xFF)); 174 | buf.put((byte) ((len) & 0xFF)); 175 | buf.put(data); 176 | buf.flip(); 177 | } 178 | 179 | if (ch.write(buf) < 0) { 180 | throw new IOException("Server went down"); 181 | } 182 | 183 | if (buf.hasRemaining()) { 184 | return false; 185 | } else { 186 | buf.flip(); 187 | return true; 188 | } 189 | } 190 | 191 | public static String getHexString(byte[] b) throws Exception { 192 | String result = ""; 193 | for (int i = 0; i < b.length; i++) { 194 | result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1); 195 | } 196 | return result; 197 | } 198 | 199 | private static byte[] toByteArray(BigInteger big) { 200 | byte[] b = big.toByteArray(); 201 | byte b1[] = new byte[b.length]; 202 | 203 | for (int i = 0; i < b.length; i++) { 204 | b1[i] = b[b.length - 1 - i]; 205 | } 206 | 207 | return b1; 208 | } 209 | 210 | private static BigInteger toBigInteger(byte b[]) { 211 | byte b1[] = new byte[b.length + 1]; 212 | 213 | for (int i = 0; i < b.length; i++) { 214 | b1[i + 1] = b[b.length - 1 - i]; 215 | } 216 | 217 | return new BigInteger(b1); 218 | } 219 | 220 | private static long uIntToLong(byte b1, byte b2, byte b3, byte b4) { 221 | long i = 0; 222 | i <<= 8; 223 | i ^= b1 & 0xFF; 224 | i <<= 8; 225 | i ^= b2 & 0xFF; 226 | i <<= 8; 227 | i ^= b3 & 0xFF; 228 | i <<= 8; 229 | i ^= b4 & 0xFF; 230 | return i; 231 | } 232 | 233 | public static HTSMessage parse(ByteBuffer buf) throws IOException { 234 | long len; 235 | 236 | if (buf.position() < 4) { 237 | return null; 238 | } 239 | 240 | len = uIntToLong(buf.get(0), buf.get(1), buf.get(2), buf.get(3)); 241 | 242 | if (len + 4 > buf.capacity()) { 243 | buf.clear(); 244 | throw new IOException("Mesage is to long"); 245 | } 246 | 247 | if (buf.limit() == 4) { 248 | buf.limit((int) (4 + len)); 249 | } 250 | 251 | //Message not yet fully read 252 | if (buf.position() < len + 4) { 253 | return null; 254 | } 255 | 256 | buf.flip(); 257 | buf.getInt(); //drops 4 bytes 258 | HTSMessage msg = deserializeBinary(buf); 259 | 260 | buf.limit(4); 261 | buf.position(0); 262 | return msg; 263 | } 264 | 265 | private static byte[] serializeBinary(String name, Object value) throws IOException { 266 | byte[] bName = name.getBytes(); 267 | byte[] bData = new byte[0]; 268 | byte type; 269 | 270 | if (value instanceof String) { 271 | type = HTSMessage.HMF_STR; 272 | bData = ((String) value).getBytes(); 273 | } else if (value instanceof BigInteger) { 274 | type = HTSMessage.HMF_S64; 275 | bData = toByteArray((BigInteger) value); 276 | } else if (value instanceof Integer) { 277 | type = HTSMessage.HMF_S64; 278 | bData = toByteArray(BigInteger.valueOf((Integer) value)); 279 | } else if (value instanceof Long) { 280 | type = HTSMessage.HMF_S64; 281 | bData = toByteArray(BigInteger.valueOf((Long) value)); 282 | } else if (value instanceof byte[]) { 283 | type = HTSMessage.HMF_BIN; 284 | bData = (byte[]) value; 285 | } else if (value instanceof Map) { 286 | type = HTSMessage.HMF_MAP; 287 | bData = serializeBinary((Map) value); 288 | } else if (value instanceof Collection) { 289 | type = HTSMessage.HMF_LIST; 290 | bData = serializeBinary((Collection) value); 291 | } else if (value == null) { 292 | throw new IOException("HTSP doesn't support null values"); 293 | } else { 294 | throw new IOException("Unhandled class for " + name + ": " + value 295 | + " (" + value.getClass().getSimpleName() + ")"); 296 | } 297 | 298 | byte[] buf = new byte[1 + 1 + 4 + bName.length + bData.length]; 299 | buf[0] = type; 300 | buf[1] = (byte) (bName.length & 0xFF); 301 | buf[2] = (byte) ((bData.length >> 24) & 0xFF); 302 | buf[3] = (byte) ((bData.length >> 16) & 0xFF); 303 | buf[4] = (byte) ((bData.length >> 8) & 0xFF); 304 | buf[5] = (byte) ((bData.length) & 0xFF); 305 | 306 | System.arraycopy(bName, 0, buf, 6, bName.length); 307 | System.arraycopy(bData, 0, buf, 6 + bName.length, bData.length); 308 | 309 | return buf; 310 | } 311 | 312 | private static byte[] serializeBinary(Collection list) throws IOException { 313 | ByteBuffer buf = ByteBuffer.allocate(Short.MAX_VALUE); 314 | 315 | for (Object value : list) { 316 | byte[] sub = serializeBinary("", value); 317 | buf.put(sub); 318 | } 319 | 320 | byte[] bBuf = new byte[buf.position()]; 321 | buf.flip(); 322 | buf.get(bBuf); 323 | 324 | return bBuf; 325 | } 326 | 327 | private static byte[] serializeBinary(Map map) throws IOException { 328 | ByteBuffer buf = ByteBuffer.allocate(Short.MAX_VALUE); 329 | 330 | for (Object key : map.keySet()) { 331 | Object value = map.get(key); 332 | byte[] sub = serializeBinary(key.toString(), value); 333 | buf.put(sub); 334 | } 335 | 336 | byte[] bBuf = new byte[buf.position()]; 337 | buf.flip(); 338 | buf.get(bBuf); 339 | 340 | return bBuf; 341 | } 342 | 343 | private static HTSMessage deserializeBinary(ByteBuffer buf) throws IOException { 344 | byte type, namelen; 345 | long datalen; 346 | 347 | HTSMessage msg = new HTSMessage(); 348 | int cnt = 0; 349 | 350 | while (buf.hasRemaining()) { 351 | type = buf.get(); 352 | namelen = buf.get(); 353 | datalen = uIntToLong(buf.get(), buf.get(), buf.get(), buf.get()); 354 | 355 | if (datalen > Integer.MAX_VALUE) { 356 | throw new IOException("Would get precision losses ;("); 357 | } 358 | if (buf.limit() < namelen + datalen) { 359 | throw new IOException("Buffer limit exceeded"); 360 | } 361 | 362 | //Get the key for the map (the name) 363 | String name = null; 364 | if (namelen == 0) { 365 | name = Integer.toString(cnt++); 366 | } else { 367 | byte[] bName = new byte[namelen]; 368 | buf.get(bName); 369 | name = new String(bName); 370 | } 371 | 372 | //Get the actual content 373 | Object obj = null; 374 | byte[] bData = new byte[(int) datalen]; //Should be long? 375 | buf.get(bData); 376 | 377 | switch (type) { 378 | case HTSMessage.HMF_STR: { 379 | obj = new String(bData); 380 | break; 381 | } 382 | case HMF_BIN: { 383 | obj = bData; 384 | break; 385 | } 386 | case HMF_S64: { 387 | obj = toBigInteger(bData); 388 | break; 389 | } 390 | case HMF_MAP: { 391 | ByteBuffer sub = ByteBuffer.allocateDirect((int) datalen); 392 | sub.put(bData); 393 | sub.flip(); 394 | obj = deserializeBinary(sub); 395 | break; 396 | } 397 | case HMF_LIST: { 398 | ByteBuffer sub = ByteBuffer.allocateDirect((int) datalen); 399 | sub.put(bData); 400 | sub.flip(); 401 | obj = new ArrayList(deserializeBinary(sub).values()); 402 | break; 403 | } 404 | default: 405 | throw new IOException("Unknown data type"); 406 | } 407 | msg.putField(name, obj); 408 | } 409 | return msg; 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/TVHGuideApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.app.Application; 22 | import android.content.Context; 23 | import android.os.Handler; 24 | import android.util.SparseArray; 25 | import android.widget.Toast; 26 | import java.util.ArrayList; 27 | import java.util.Collections; 28 | import java.util.List; 29 | 30 | import org.tvheadend.tvhguide.R; 31 | import org.tvheadend.tvhguide.htsp.HTSListener; 32 | import org.tvheadend.tvhguide.model.Channel; 33 | import org.tvheadend.tvhguide.model.ChannelTag; 34 | import org.tvheadend.tvhguide.model.HttpTicket; 35 | import org.tvheadend.tvhguide.model.Packet; 36 | import org.tvheadend.tvhguide.model.Programme; 37 | import org.tvheadend.tvhguide.model.Recording; 38 | import org.tvheadend.tvhguide.model.Subscription; 39 | 40 | /** 41 | * 42 | * @author john-tornblom 43 | */ 44 | public class TVHGuideApplication extends Application { 45 | 46 | public static final String ACTION_CHANNEL_ADD = "org.me.tvhguide.CHANNEL_ADD"; 47 | public static final String ACTION_CHANNEL_DELETE = "org.me.tvhguide.CHANNEL_DELETE"; 48 | public static final String ACTION_CHANNEL_UPDATE = "org.me.tvhguide.CHANNEL_UPDATE"; 49 | public static final String ACTION_TAG_ADD = "org.me.tvhguide.TAG_ADD"; 50 | public static final String ACTION_TAG_DELETE = "org.me.tvhguide.TAG_DELETE"; 51 | public static final String ACTION_TAG_UPDATE = "org.me.tvhguide.TAG_UPDATE"; 52 | public static final String ACTION_DVR_ADD = "org.me.tvhguide.DVR_ADD"; 53 | public static final String ACTION_DVR_DELETE = "org.me.tvhguide.DVR_DELETE"; 54 | public static final String ACTION_DVR_UPDATE = "org.me.tvhguide.DVR_UPDATE"; 55 | public static final String ACTION_PROGRAMME_ADD = "org.me.tvhguide.PROGRAMME_ADD"; 56 | public static final String ACTION_PROGRAMME_DELETE = "org.me.tvhguide.PROGRAMME_DELETE"; 57 | public static final String ACTION_PROGRAMME_UPDATE = "org.me.tvhguide.PROGRAMME_UPDATE"; 58 | public static final String ACTION_SUBSCRIPTION_ADD = "org.me.tvhguide.SUBSCRIPTION_ADD"; 59 | public static final String ACTION_SUBSCRIPTION_DELETE = "org.me.tvhguide.SUBSCRIPTION_DELETE"; 60 | public static final String ACTION_SUBSCRIPTION_UPDATE = "org.me.tvhguide.SUBSCRIPTION_UPDATE"; 61 | public static final String ACTION_SIGNAL_STATUS = "org.me.tvhguide.SIGNAL_STATUS"; 62 | public static final String ACTION_PLAYBACK_PACKET = "org.me.tvhguide.PLAYBACK_PACKET"; 63 | public static final String ACTION_LOADING = "org.me.tvhguide.LOADING"; 64 | public static final String ACTION_TICKET_ADD = "org.me.tvhguide.TICKET"; 65 | public static final String ACTION_ERROR = "org.me.tvhguide.ERROR"; 66 | private final List listeners = new ArrayList(); 67 | private final List tags = Collections.synchronizedList(new ArrayList()); 68 | private final List channels = Collections.synchronizedList(new ArrayList()); 69 | private final List recordings = Collections.synchronizedList(new ArrayList()); 70 | private final List subscriptions = Collections.synchronizedList(new ArrayList()); 71 | private volatile boolean loading = false; 72 | private Handler handler = new Handler(); 73 | 74 | public void addListener(HTSListener l) { 75 | listeners.add(l); 76 | } 77 | 78 | public void removeListener(HTSListener l) { 79 | listeners.remove(l); 80 | } 81 | 82 | private void broadcastMessage(String action, Object obj) { 83 | synchronized (listeners) { 84 | for (HTSListener l : listeners) { 85 | l.onMessage(action, obj); 86 | } 87 | } 88 | } 89 | 90 | public void broadcastError(final String error) { 91 | //Don't show error if no views are open 92 | synchronized (listeners) { 93 | if (listeners.isEmpty()) { 94 | return; 95 | } 96 | } 97 | handler.post(new Runnable() { 98 | 99 | public void run() { 100 | 101 | try { 102 | Toast toast = Toast.makeText(TVHGuideApplication.this, error, Toast.LENGTH_LONG); 103 | toast.show(); 104 | } catch (Throwable ex) { 105 | } 106 | } 107 | }); 108 | broadcastMessage(ACTION_ERROR, error); 109 | } 110 | 111 | public void broadcastPacket(Packet p) { 112 | broadcastMessage(ACTION_PLAYBACK_PACKET, p); 113 | } 114 | 115 | public List getChannelTags() { 116 | return tags; 117 | } 118 | 119 | public void addChannelTag(ChannelTag tag) { 120 | tags.add(tag); 121 | 122 | if (!loading) { 123 | broadcastMessage(ACTION_TAG_ADD, tag); 124 | } 125 | } 126 | 127 | public void removeChannelTag(ChannelTag tag) { 128 | tags.remove(tag); 129 | 130 | if (!loading) { 131 | broadcastMessage(ACTION_TAG_DELETE, tag); 132 | } 133 | } 134 | 135 | public void removeChannelTag(long id) { 136 | for (ChannelTag tag : getChannelTags()) { 137 | if (tag.id == id) { 138 | removeChannelTag(tag); 139 | return; 140 | } 141 | } 142 | } 143 | 144 | public ChannelTag getChannelTag(long id) { 145 | for (ChannelTag tag : getChannelTags()) { 146 | if (tag.id == id) { 147 | return tag; 148 | } 149 | } 150 | return null; 151 | } 152 | 153 | public void updateChannelTag(ChannelTag tag) { 154 | if (!loading) { 155 | broadcastMessage(ACTION_TAG_UPDATE, tag); 156 | } 157 | } 158 | 159 | public void addChannel(Channel channel) { 160 | channels.add(channel); 161 | 162 | if (!loading) { 163 | broadcastMessage(ACTION_CHANNEL_ADD, channel); 164 | } 165 | } 166 | 167 | public List getChannels() { 168 | return channels; 169 | } 170 | 171 | public void removeChannel(Channel channel) { 172 | channels.remove(channel); 173 | 174 | if (!loading) { 175 | broadcastMessage(ACTION_CHANNEL_DELETE, channel); 176 | } 177 | } 178 | 179 | public Channel getChannel(long id) { 180 | for (Channel ch : getChannels()) { 181 | if (ch.id == id) { 182 | return ch; 183 | } 184 | } 185 | return null; 186 | } 187 | 188 | public void removeChannel(long id) { 189 | for (Channel ch : getChannels()) { 190 | if (ch.id == id) { 191 | removeChannel(ch); 192 | return; 193 | } 194 | } 195 | } 196 | 197 | public void updateChannel(Channel ch) { 198 | if (!loading) { 199 | broadcastMessage(ACTION_CHANNEL_UPDATE, ch); 200 | } 201 | } 202 | 203 | public void addProgramme(Programme p) { 204 | if (!loading) { 205 | broadcastMessage(ACTION_PROGRAMME_ADD, p); 206 | } 207 | } 208 | 209 | public void removeProgramme(Programme p) { 210 | if (!loading) { 211 | broadcastMessage(ACTION_PROGRAMME_DELETE, p); 212 | } 213 | } 214 | 215 | public void updateProgramme(Programme p) { 216 | if (!loading) { 217 | broadcastMessage(ACTION_PROGRAMME_UPDATE, p); 218 | } 219 | } 220 | 221 | public void addRecording(Recording rec) { 222 | recordings.add(rec); 223 | 224 | if (!loading) { 225 | broadcastMessage(ACTION_DVR_ADD, rec); 226 | } 227 | } 228 | 229 | public List getRecordings() { 230 | return recordings; 231 | } 232 | 233 | public void removeRecording(Recording rec) { 234 | recordings.remove(rec); 235 | 236 | if (!loading) { 237 | broadcastMessage(ACTION_DVR_DELETE, rec); 238 | } 239 | } 240 | 241 | public Recording getRecording(long id) { 242 | for (Recording rec : getRecordings()) { 243 | if (rec.id == id) { 244 | return rec; 245 | } 246 | } 247 | return null; 248 | } 249 | 250 | public void removeRecording(long id) { 251 | for (Recording rec : getRecordings()) { 252 | if (rec.id == id) { 253 | removeRecording(rec); 254 | return; 255 | } 256 | } 257 | } 258 | 259 | public void updateRecording(Recording rec) { 260 | if (!loading) { 261 | broadcastMessage(ACTION_DVR_UPDATE, rec); 262 | } 263 | } 264 | 265 | public void setLoading(boolean b) { 266 | if (loading != b) { 267 | broadcastMessage(ACTION_LOADING, b); 268 | } 269 | loading = b; 270 | } 271 | 272 | public void clearAll() { 273 | tags.clear(); 274 | recordings.clear(); 275 | 276 | for (Channel ch : channels) { 277 | ch.epg.clear(); 278 | ch.recordings.clear(); 279 | } 280 | channels.clear(); 281 | 282 | for (Subscription s : subscriptions) { 283 | s.streams.clear(); 284 | } 285 | subscriptions.clear(); 286 | 287 | ChannelTag tag = new ChannelTag(); 288 | tag.id = 0; 289 | tag.name = getString(R.string.pr_all_channels); 290 | tags.add(tag); 291 | } 292 | 293 | public void addSubscription(Subscription s) { 294 | subscriptions.add(s); 295 | 296 | if (!loading) { 297 | broadcastMessage(ACTION_SUBSCRIPTION_ADD, s); 298 | } 299 | } 300 | 301 | public List getSubscriptions() { 302 | return subscriptions; 303 | } 304 | 305 | public void removeSubscription(Subscription s) { 306 | s.streams.clear(); 307 | subscriptions.remove(s); 308 | 309 | if (!loading) { 310 | broadcastMessage(ACTION_SUBSCRIPTION_DELETE, s); 311 | } 312 | } 313 | 314 | public Subscription getSubscription(long id) { 315 | for (Subscription s : getSubscriptions()) { 316 | if (s.id == id) { 317 | return s; 318 | } 319 | } 320 | return null; 321 | } 322 | 323 | public void removeSubscription(long id) { 324 | for (Subscription s : getSubscriptions()) { 325 | if (s.id == id) { 326 | removeSubscription(s); 327 | return; 328 | } 329 | } 330 | } 331 | 332 | public void updateSubscription(Subscription s) { 333 | if (!loading) { 334 | broadcastMessage(ACTION_SUBSCRIPTION_UPDATE, s); 335 | } 336 | } 337 | 338 | 339 | public void addTicket(HttpTicket t) { 340 | broadcastMessage(ACTION_TICKET_ADD, t); 341 | } 342 | 343 | public boolean isLoading() { 344 | return loading; 345 | } 346 | 347 | 348 | public static SparseArray getContentTypes(Context ctx) { 349 | SparseArray ret = new SparseArray(); 350 | 351 | String[] s = ctx.getResources().getStringArray(R.array.pr_content_type0); 352 | for(int i=0; i. 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.app.Activity; 22 | import android.app.AlertDialog; 23 | import android.app.ListActivity; 24 | import android.content.DialogInterface; 25 | import android.content.Intent; 26 | import android.content.SharedPreferences; 27 | import android.os.Bundle; 28 | import android.preference.PreferenceManager; 29 | import android.view.ContextMenu.ContextMenuInfo; 30 | import android.view.*; 31 | import android.widget.*; 32 | import java.util.*; 33 | import org.tvheadend.tvhguide.R; 34 | import org.tvheadend.tvhguide.htsp.HTSListener; 35 | import org.tvheadend.tvhguide.htsp.HTSService; 36 | import org.tvheadend.tvhguide.model.Channel; 37 | import org.tvheadend.tvhguide.model.ChannelTag; 38 | 39 | /** 40 | * 41 | * @author john-tornblom 42 | */ 43 | public class ChannelListActivity extends ListActivity implements HTSListener { 44 | 45 | private ChannelListAdapter chAdapter; 46 | ArrayAdapter tagAdapter; 47 | private AlertDialog tagDialog; 48 | private TextView tagTextView; 49 | private ImageView tagImageView; 50 | private View tagBtn; 51 | private ProgressBar pb; 52 | private ChannelTag currentTag; 53 | 54 | @Override 55 | public void onCreate(Bundle icicle) { 56 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 57 | Boolean theme = prefs.getBoolean("lightThemePref", false); 58 | setTheme(theme ? R.style.CustomTheme_Light : R.style.CustomTheme); 59 | 60 | super.onCreate(icicle); 61 | 62 | requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); 63 | 64 | chAdapter = new ChannelListAdapter(this, new ArrayList()); 65 | setListAdapter(chAdapter); 66 | 67 | getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.channel_list_title); 68 | tagTextView = (TextView) findViewById(R.id.ct_title); 69 | tagImageView = (ImageView) findViewById(R.id.ct_logo); 70 | 71 | pb = (ProgressBar) findViewById(R.id.ct_loading); 72 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 73 | builder.setTitle(R.string.menu_tags); 74 | 75 | tagAdapter = new ArrayAdapter( 76 | this, 77 | android.R.layout.simple_dropdown_item_1line, 78 | new ArrayList()); 79 | 80 | builder.setAdapter(tagAdapter, new android.content.DialogInterface.OnClickListener() { 81 | 82 | public void onClick(DialogInterface arg0, int pos) { 83 | setCurrentTag(tagAdapter.getItem(pos)); 84 | populateList(); 85 | } 86 | }); 87 | 88 | tagDialog = builder.create(); 89 | tagBtn = findViewById(R.id.ct_btn); 90 | tagBtn.setOnClickListener(new android.view.View.OnClickListener() { 91 | 92 | public void onClick(View arg0) { 93 | tagDialog.show(); 94 | } 95 | }); 96 | 97 | registerForContextMenu(getListView()); 98 | } 99 | 100 | @Override 101 | public boolean onCreateOptionsMenu(Menu menu) { 102 | MenuInflater inflater = getMenuInflater(); 103 | inflater.inflate(R.menu.main_menu, menu); 104 | return true; 105 | } 106 | 107 | @Override 108 | public boolean onContextItemSelected(MenuItem item) { 109 | switch (item.getItemId()) { 110 | case R.string.ch_play: { 111 | startActivity(item.getIntent()); 112 | return true; 113 | } 114 | case R.string.search_hint: { 115 | startSearch(null, false, item.getIntent().getExtras(), false); 116 | return true; 117 | } 118 | default: { 119 | return false; 120 | } 121 | } 122 | } 123 | 124 | @Override 125 | public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 126 | super.onCreateContextMenu(menu, v, menuInfo); 127 | MenuItem item = menu.add(ContextMenu.NONE, R.string.ch_play, ContextMenu.NONE, R.string.ch_play); 128 | 129 | AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; 130 | Channel ch = chAdapter.getItem(info.position); 131 | 132 | menu.setHeaderTitle(ch.name); 133 | Intent intent = new Intent(this, PlaybackActivity.class); 134 | intent.putExtra("channelId", ch.id); 135 | item.setIntent(intent); 136 | 137 | item = menu.add(ContextMenu.NONE, R.string.search_hint, ContextMenu.NONE, R.string.search_hint); 138 | intent = new Intent(); 139 | intent.putExtra("channelId", ch.id); 140 | item.setIntent(intent); 141 | } 142 | 143 | void connect(boolean force) { 144 | if (force) { 145 | chAdapter.clear(); 146 | } 147 | 148 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 149 | String hostname = prefs.getString("serverHostPref", "localhost"); 150 | int port = Integer.parseInt(prefs.getString("serverPortPref", "9982")); 151 | String username = prefs.getString("usernamePref", ""); 152 | String password = prefs.getString("passwordPref", ""); 153 | 154 | Intent intent = new Intent(ChannelListActivity.this, HTSService.class); 155 | intent.setAction(HTSService.ACTION_CONNECT); 156 | intent.putExtra("hostname", hostname); 157 | intent.putExtra("port", port); 158 | intent.putExtra("username", username); 159 | intent.putExtra("password", password); 160 | intent.putExtra("force", force); 161 | 162 | startService(intent); 163 | } 164 | 165 | private void setCurrentTag(ChannelTag t) { 166 | currentTag = t; 167 | 168 | if (t == null) { 169 | tagTextView.setText(R.string.pr_all_channels); 170 | tagImageView.setImageResource(R.drawable.logo_72); 171 | } else { 172 | tagTextView.setText(currentTag.name); 173 | if (currentTag.iconBitmap != null) { 174 | tagImageView.setImageBitmap(currentTag.iconBitmap); 175 | } else { 176 | tagImageView.setImageResource(R.drawable.logo_72); 177 | } 178 | } 179 | } 180 | 181 | private void populateList() { 182 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 183 | 184 | chAdapter.clear(); 185 | 186 | for (Channel ch : app.getChannels()) { 187 | if (currentTag == null || ch.hasTag(currentTag.id)) { 188 | chAdapter.add(ch); 189 | } 190 | } 191 | 192 | chAdapter.sort(); 193 | chAdapter.notifyDataSetChanged(); 194 | } 195 | 196 | @Override 197 | public boolean onOptionsItemSelected(MenuItem item) { 198 | switch (item.getItemId()) { 199 | case R.id.mi_settings: { 200 | Intent intent = new Intent(getBaseContext(), SettingsActivity.class); 201 | startActivityForResult(intent, R.id.mi_settings); 202 | return true; 203 | } 204 | case R.id.mi_refresh: { 205 | connect(true); 206 | return true; 207 | } 208 | case R.id.mi_recordings: { 209 | Intent intent = new Intent(getBaseContext(), RecordingListActivity.class); 210 | startActivity(intent); 211 | return true; 212 | } 213 | case R.id.mi_search: { 214 | onSearchRequested(); 215 | return true; 216 | } 217 | case R.id.mi_tags: { 218 | tagDialog.show(); 219 | return true; 220 | } 221 | default: { 222 | return super.onOptionsItemSelected(item); 223 | } 224 | } 225 | } 226 | 227 | @Override 228 | protected void onResume() { 229 | super.onResume(); 230 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 231 | app.addListener(this); 232 | 233 | connect(false); 234 | setLoading(app.isLoading()); 235 | } 236 | 237 | @Override 238 | protected void onPause() { 239 | super.onPause(); 240 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 241 | app.removeListener(this); 242 | } 243 | 244 | @Override 245 | protected void onListItemClick(ListView l, View v, int position, long id) { 246 | Channel ch = (Channel) chAdapter.getItem(position); 247 | 248 | if (ch.epg.isEmpty()) { 249 | return; 250 | } 251 | 252 | Intent intent = new Intent(getBaseContext(), ProgrammeListActivity.class); 253 | intent.putExtra("channelId", ch.id); 254 | startActivity(intent); 255 | } 256 | 257 | private void setLoading(boolean loading) { 258 | tagBtn.setEnabled(!loading); 259 | if (loading) { 260 | pb.setVisibility(ProgressBar.VISIBLE); 261 | tagTextView.setText(R.string.inf_load); 262 | tagImageView.setVisibility(ImageView.INVISIBLE); 263 | } else { 264 | pb.setVisibility(ProgressBar.GONE); 265 | tagImageView.setVisibility(ImageView.VISIBLE); 266 | 267 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 268 | tagAdapter.clear(); 269 | for (ChannelTag t : app.getChannelTags()) { 270 | tagAdapter.add(t); 271 | } 272 | 273 | populateList(); 274 | setCurrentTag(currentTag); 275 | } 276 | } 277 | 278 | public void onMessage(String action, final Object obj) { 279 | if (action.equals(TVHGuideApplication.ACTION_LOADING)) { 280 | 281 | runOnUiThread(new Runnable() { 282 | 283 | public void run() { 284 | boolean loading = (Boolean) obj; 285 | setLoading(loading); 286 | } 287 | }); 288 | } else if (action.equals(TVHGuideApplication.ACTION_CHANNEL_ADD)) { 289 | runOnUiThread(new Runnable() { 290 | 291 | public void run() { 292 | chAdapter.add((Channel) obj); 293 | chAdapter.notifyDataSetChanged(); 294 | chAdapter.sort(); 295 | } 296 | }); 297 | } else if (action.equals(TVHGuideApplication.ACTION_CHANNEL_DELETE)) { 298 | runOnUiThread(new Runnable() { 299 | 300 | public void run() { 301 | chAdapter.remove((Channel) obj); 302 | chAdapter.notifyDataSetChanged(); 303 | } 304 | }); 305 | } else if (action.equals(TVHGuideApplication.ACTION_CHANNEL_UPDATE)) { 306 | runOnUiThread(new Runnable() { 307 | 308 | public void run() { 309 | Channel channel = (Channel) obj; 310 | chAdapter.updateView(getListView(), channel); 311 | } 312 | }); 313 | } else if (action.equals(TVHGuideApplication.ACTION_TAG_ADD)) { 314 | runOnUiThread(new Runnable() { 315 | 316 | public void run() { 317 | ChannelTag tag = (ChannelTag) obj; 318 | tagAdapter.add(tag); 319 | } 320 | }); 321 | } else if (action.equals(TVHGuideApplication.ACTION_TAG_DELETE)) { 322 | runOnUiThread(new Runnable() { 323 | 324 | public void run() { 325 | ChannelTag tag = (ChannelTag) obj; 326 | tagAdapter.remove(tag); 327 | } 328 | }); 329 | } else if (action.equals(TVHGuideApplication.ACTION_TAG_UPDATE)) { 330 | //NOP 331 | } 332 | } 333 | 334 | class ChannelListAdapter extends ArrayAdapter { 335 | 336 | ChannelListAdapter(Activity context, List list) { 337 | super(context, R.layout.channel_list_widget, list); 338 | } 339 | 340 | public void sort() { 341 | sort(new Comparator() { 342 | 343 | public int compare(Channel x, Channel y) { 344 | return x.compareTo(y); 345 | } 346 | }); 347 | } 348 | 349 | public void updateView(ListView listView, Channel channel) { 350 | for (int i = 0; i < listView.getChildCount(); i++) { 351 | View view = listView.getChildAt(i); 352 | int pos = listView.getPositionForView(view); 353 | Channel ch = (Channel) listView.getItemAtPosition(pos); 354 | 355 | if (view.getTag() == null || ch == null) { 356 | continue; 357 | } 358 | 359 | if (channel.id != ch.id) { 360 | continue; 361 | } 362 | 363 | ChannelListViewWrapper wrapper = (ChannelListViewWrapper) view.getTag(); 364 | wrapper.repaint(channel); 365 | break; 366 | } 367 | } 368 | 369 | @Override 370 | public View getView(int position, View convertView, ViewGroup parent) { 371 | View row = convertView; 372 | ChannelListViewWrapper wrapper; 373 | 374 | Channel ch = getItem(position); 375 | Activity activity = (Activity) getContext(); 376 | 377 | if (row == null) { 378 | LayoutInflater inflater = activity.getLayoutInflater(); 379 | row = inflater.inflate(R.layout.channel_list_widget, null, false); 380 | row.requestLayout(); 381 | wrapper = new ChannelListViewWrapper(row); 382 | row.setTag(wrapper); 383 | 384 | } else { 385 | wrapper = (ChannelListViewWrapper) row.getTag(); 386 | } 387 | 388 | wrapper.repaint(ch); 389 | return row; 390 | } 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/org/tvheadend/tvhguide/RecordingListActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 John Törnblom 3 | * 4 | * This file is part of TVHGuide. 5 | * 6 | * TVHGuide is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * TVHGuide is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with TVHGuide. If not, see . 18 | */ 19 | package org.tvheadend.tvhguide; 20 | 21 | import android.app.Activity; 22 | import android.app.AlertDialog; 23 | import android.app.ListActivity; 24 | import android.content.DialogInterface; 25 | import android.content.Intent; 26 | import android.content.SharedPreferences; 27 | import android.os.Bundle; 28 | import android.preference.PreferenceManager; 29 | import android.text.format.DateFormat; 30 | import android.text.format.DateUtils; 31 | import android.view.ContextMenu; 32 | import android.view.ContextMenu.ContextMenuInfo; 33 | import android.view.LayoutInflater; 34 | import android.view.MenuItem; 35 | import android.view.View; 36 | import android.view.ViewGroup; 37 | import android.view.Window; 38 | import android.widget.AdapterView; 39 | import android.widget.ArrayAdapter; 40 | import android.widget.ImageView; 41 | import android.widget.ListView; 42 | import android.widget.TextView; 43 | 44 | import java.text.SimpleDateFormat; 45 | import java.util.ArrayList; 46 | import java.util.Comparator; 47 | import java.util.List; 48 | import org.tvheadend.tvhguide.R; 49 | import org.tvheadend.tvhguide.htsp.HTSListener; 50 | import org.tvheadend.tvhguide.htsp.HTSService; 51 | import org.tvheadend.tvhguide.intent.SearchEPGIntent; 52 | import org.tvheadend.tvhguide.intent.SearchIMDbIntent; 53 | import org.tvheadend.tvhguide.model.Channel; 54 | import org.tvheadend.tvhguide.model.Recording; 55 | 56 | /** 57 | * 58 | * @author john-tornblom 59 | */ 60 | public class RecordingListActivity extends ListActivity implements HTSListener { 61 | 62 | private RecordingListAdapter recAdapter; 63 | 64 | @Override 65 | public void onCreate(Bundle icicle) { 66 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 67 | Boolean theme = prefs.getBoolean("lightThemePref", false); 68 | setTheme(theme ? R.style.CustomTheme_Light : R.style.CustomTheme); 69 | 70 | super.onCreate(icicle); 71 | 72 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 73 | 74 | requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); 75 | 76 | List recList = new ArrayList(); 77 | recList.addAll(app.getRecordings()); 78 | recAdapter = new RecordingListAdapter(this, recList); 79 | recAdapter.sort(); 80 | setListAdapter(recAdapter); 81 | registerForContextMenu(getListView()); 82 | getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.recording_list_title); 83 | TextView t = (TextView) findViewById(R.id.ct_title); 84 | 85 | t.setText(R.string.menu_recordings); 86 | } 87 | 88 | @Override 89 | protected void onResume() { 90 | super.onResume(); 91 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 92 | app.addListener(this); 93 | } 94 | 95 | @Override 96 | protected void onPause() { 97 | super.onPause(); 98 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 99 | app.removeListener(this); 100 | } 101 | 102 | @Override 103 | protected void onListItemClick(ListView l, View v, int position, long id) { 104 | Recording rec = (Recording) recAdapter.getItem(position); 105 | 106 | Intent intent = new Intent(this, RecordingActivity.class); 107 | intent.putExtra("id", rec.id); 108 | startActivity(intent); 109 | } 110 | 111 | @Override 112 | public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 113 | super.onCreateContextMenu(menu, v, menuInfo); 114 | 115 | MenuItem item = null; 116 | Intent intent = null; 117 | 118 | AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; 119 | Recording rec = recAdapter.getItem(info.position); 120 | 121 | menu.setHeaderTitle(rec.title); 122 | 123 | intent = new Intent(RecordingListActivity.this, HTSService.class); 124 | intent.putExtra("id", rec.id); 125 | 126 | if (rec.isRecording() || rec.isScheduled()) { 127 | intent.setAction(HTSService.ACTION_DVR_CANCEL); 128 | item = menu.add(ContextMenu.NONE, R.string.menu_record_cancel, ContextMenu.NONE, R.string.menu_record_cancel); 129 | item.setIntent(intent); 130 | } else { 131 | intent.setAction(HTSService.ACTION_DVR_DELETE); 132 | item = menu.add(ContextMenu.NONE, R.string.menu_record_remove, ContextMenu.NONE, R.string.menu_record_remove); 133 | item.setIntent(intent); 134 | 135 | item = menu.add(ContextMenu.NONE, R.string.ch_play, ContextMenu.NONE, R.string.ch_play); 136 | intent = new Intent(this, ExternalPlaybackActivity.class); 137 | intent.putExtra("dvrId", rec.id); 138 | item.setIntent(intent); 139 | item.setIcon(android.R.drawable.ic_menu_view); 140 | } 141 | 142 | item = menu.add(ContextMenu.NONE, R.string.search_hint, ContextMenu.NONE, R.string.search_hint); 143 | item.setIntent(new SearchEPGIntent(this, rec.title)); 144 | item.setIcon(android.R.drawable.ic_menu_search); 145 | 146 | item = menu.add(ContextMenu.NONE, ContextMenu.NONE, ContextMenu.NONE, "IMDb"); 147 | item.setIntent(new SearchIMDbIntent(this, rec.title)); 148 | item.setIcon(android.R.drawable.ic_menu_info_details); 149 | } 150 | 151 | @Override 152 | public boolean onContextItemSelected(final MenuItem item) { 153 | switch (item.getItemId()) { 154 | case R.string.menu_record: 155 | case R.string.menu_record_cancel: { 156 | startService(item.getIntent()); 157 | return true; 158 | } 159 | case R.string.menu_record_remove: { 160 | 161 | new AlertDialog.Builder(this) 162 | .setTitle(R.string.menu_record_remove) 163 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { 164 | public void onClick(DialogInterface dialog, int which) { 165 | startService(item.getIntent()); 166 | } 167 | }) 168 | .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { 169 | public void onClick(DialogInterface dialog, int which) { 170 | //NOP 171 | } 172 | }) 173 | .show(); 174 | 175 | return true; 176 | } 177 | default: { 178 | return super.onContextItemSelected(item); 179 | } 180 | } 181 | } 182 | 183 | public void onMessage(String action, final Object obj) { 184 | if (action.equals(TVHGuideApplication.ACTION_LOADING) && !(Boolean) obj) { 185 | 186 | runOnUiThread(new Runnable() { 187 | 188 | public void run() { 189 | TVHGuideApplication app = (TVHGuideApplication) getApplication(); 190 | recAdapter.list.clear(); 191 | recAdapter.list.addAll(app.getRecordings()); 192 | recAdapter.notifyDataSetChanged(); 193 | recAdapter.sort(); 194 | } 195 | }); 196 | } else if (action.equals(TVHGuideApplication.ACTION_DVR_ADD)) { 197 | runOnUiThread(new Runnable() { 198 | 199 | public void run() { 200 | recAdapter.add((Recording) obj); 201 | recAdapter.notifyDataSetChanged(); 202 | recAdapter.sort(); 203 | } 204 | }); 205 | } else if (action.equals(TVHGuideApplication.ACTION_DVR_DELETE)) { 206 | runOnUiThread(new Runnable() { 207 | 208 | public void run() { 209 | recAdapter.remove((Recording) obj); 210 | recAdapter.notifyDataSetChanged(); 211 | } 212 | }); 213 | } else if (action.equals(TVHGuideApplication.ACTION_DVR_UPDATE)) { 214 | runOnUiThread(new Runnable() { 215 | 216 | public void run() { 217 | Recording rec = (Recording) obj; 218 | recAdapter.updateView(getListView(), rec); 219 | } 220 | }); 221 | } 222 | } 223 | 224 | private class ViewWarpper { 225 | 226 | TextView title; 227 | TextView channel; 228 | TextView time; 229 | TextView date; 230 | TextView message; 231 | TextView desc; 232 | ImageView icon; 233 | ImageView state; 234 | 235 | public ViewWarpper(View base) { 236 | title = (TextView) base.findViewById(R.id.rec_title); 237 | channel = (TextView) base.findViewById(R.id.rec_channel); 238 | 239 | time = (TextView) base.findViewById(R.id.rec_time); 240 | date = (TextView) base.findViewById(R.id.rec_date); 241 | message = (TextView) base.findViewById(R.id.rec_message); 242 | desc = (TextView) base.findViewById(R.id.rec_desc); 243 | icon = (ImageView) base.findViewById(R.id.rec_icon); 244 | state = (ImageView) base.findViewById(R.id.rec_state); 245 | } 246 | 247 | public void repaint(Recording rec) { 248 | Channel ch = rec.channel; 249 | 250 | title.setText(rec.title); 251 | title.invalidate(); 252 | 253 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(icon.getContext()); 254 | Boolean showIcons = prefs.getBoolean("showIconPref", false); 255 | 256 | icon.setVisibility(showIcons ? ImageView.VISIBLE : ImageView.GONE); 257 | if(ch != null) { 258 | icon.setImageBitmap(ch.iconBitmap); 259 | channel.setText(ch.name); 260 | } else { 261 | icon.setImageBitmap(null); 262 | channel.setText(""); 263 | } 264 | channel.invalidate(); 265 | 266 | if (DateUtils.isToday(rec.start.getTime())) { 267 | date.setText(getString(R.string.today)); 268 | } else if(rec.start.getTime() < System.currentTimeMillis() + 1000*60*60*24*2 && 269 | rec.start.getTime() > System.currentTimeMillis() - 1000*60*60*24*2) { 270 | date.setText(DateUtils.getRelativeTimeSpanString(rec.start.getTime(), 271 | System.currentTimeMillis(), DateUtils.DAY_IN_MILLIS)); 272 | } else if(rec.start.getTime() < System.currentTimeMillis() + 1000*60*60*24*6 && 273 | rec.start.getTime() > System.currentTimeMillis() - 1000*60*60*24*2 274 | ) { 275 | date.setText(new SimpleDateFormat("EEEE").format(rec.start.getTime())); 276 | } else { 277 | date.setText(DateFormat.getDateFormat(date.getContext()).format(rec.start)); 278 | } 279 | 280 | date.invalidate(); 281 | 282 | String msg = ""; 283 | if (rec.error != null) { 284 | msg = rec.error; 285 | state.setImageResource(R.drawable.ic_error_small); 286 | } else if ("completed".equals(rec.state)) { 287 | msg = getString(R.string.pvr_completed); 288 | state.setImageResource(R.drawable.ic_success_small); 289 | } else if ("invalid".equals(rec.state)) { 290 | msg = getString(R.string.pvr_invalid); 291 | state.setImageResource(R.drawable.ic_error_small); 292 | } else if ("missed".equals(rec.state)) { 293 | msg = getString(R.string.pvr_missed); 294 | state.setImageResource(R.drawable.ic_error_small); 295 | } else if ("recording".equals(rec.state)) { 296 | msg = getString(R.string.pvr_recording); 297 | state.setImageResource(R.drawable.ic_rec_small); 298 | } else if ("scheduled".equals(rec.state)) { 299 | msg = getString(R.string.pvr_scheduled); 300 | state.setImageResource(R.drawable.ic_schedule_small); 301 | } else { 302 | state.setImageDrawable(null); 303 | } 304 | if (msg.length() > 0) { 305 | message.setText("(" + msg + ")"); 306 | } else { 307 | message.setText(msg); 308 | } 309 | message.invalidate(); 310 | 311 | desc.setText(rec.description); 312 | desc.invalidate(); 313 | 314 | icon.invalidate(); 315 | 316 | time.setText( 317 | DateFormat.getTimeFormat(time.getContext()).format(rec.start) 318 | + " - " 319 | + DateFormat.getTimeFormat(time.getContext()).format(rec.stop)); 320 | time.invalidate(); 321 | } 322 | } 323 | 324 | class RecordingListAdapter extends ArrayAdapter { 325 | 326 | Activity context; 327 | List list; 328 | 329 | RecordingListAdapter(Activity context, List list) { 330 | super(context, R.layout.recording_list_widget, list); 331 | this.context = context; 332 | this.list = list; 333 | } 334 | 335 | public void sort() { 336 | sort(new Comparator() { 337 | 338 | public int compare(Recording x, Recording y) { 339 | return x.compareTo(y); 340 | } 341 | }); 342 | } 343 | 344 | public void updateView(ListView listView, Recording recording) { 345 | for (int i = 0; i < listView.getChildCount(); i++) { 346 | View view = listView.getChildAt(i); 347 | int pos = listView.getPositionForView(view); 348 | Recording rec = (Recording) listView.getItemAtPosition(pos); 349 | 350 | if (view.getTag() == null || rec == null) { 351 | continue; 352 | } 353 | 354 | if (recording.id != rec.id) { 355 | continue; 356 | } 357 | 358 | ViewWarpper wrapper = (ViewWarpper) view.getTag(); 359 | wrapper.repaint(recording); 360 | break; 361 | } 362 | } 363 | 364 | @Override 365 | public View getView(int position, View convertView, ViewGroup parent) { 366 | View row = convertView; 367 | ViewWarpper wrapper = null; 368 | 369 | Recording rec = list.get(position); 370 | 371 | if (row == null) { 372 | LayoutInflater inflater = context.getLayoutInflater(); 373 | row = inflater.inflate(R.layout.recording_list_widget, null, false); 374 | 375 | wrapper = new ViewWarpper(row); 376 | row.setTag(wrapper); 377 | 378 | } else { 379 | wrapper = (ViewWarpper) row.getTag(); 380 | } 381 | 382 | wrapper.repaint(rec); 383 | return row; 384 | } 385 | } 386 | } 387 | --------------------------------------------------------------------------------