Most Android phones come stock with Infrared Light Emitting Diodes. My service provider also baked the Peel remote application into the Android OS. This remote works great… but nearly every initial button press invokes a shameless full screen advertisement (often with video and sound). Very annoying! Seems like a simple application I could just write for myself.
Here’s an overview of how we’ll implement this:
- Add the Android Manifest items.
- Add the required metadata and layout.
- Create a subclass of
AppWidgetProvider
. - Create an
IRCommand
andConsumerIrManager
helper/wrapper.
Android has already provided a tutorial for implementing widgets. If you’re familiar with widgets and just interested in the IR stuff, you can skip to the end of this post.
Add the manifest items
The ConsumerIrManager
requires the TRANSMIT_IR
permission. Add it to your manifest
<uses-permission android:name="android.permission.TRANSMIT_IR" /> |
To make the widget available to the Android system add the following:
1 2 3 4 5 6 7 | <receiver android:name=".widget.MyAppWidgetProvider" > <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/app_widget_provider" /> </receiver> |
Don’t worry about MyAppWidgetProvider
and @xml/app_widget_provider
we’ll create them later.
Add the metadata and layouts
We’ll start by adding the required meta-data
from above. Create a new xml
file in res/xml
. It should look something like this:
1 2 3 4 5 6 7 8 9 | <?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="250dp" android:minHeight="40dp" android:updatePeriodMillis="0" android:initialLayout="@layout/television_remote_appwidget_layout" android:resizeMode="horizontal|vertical" android:widgetCategory="home_screen"> </appwidget-provider> |
Note the minWidth
and minHeight
use set values for the widget size (1 x 1 -> 4 x 4 etc.). See the documentation for details.
Now we can add the layout the widget will use. Widgets don’t support all types of views. Refer to the official documentation if you’re experiencing trouble. I just used a LinearLayout
and some ImageView
s. I called my layout television_remote_appwidget_layout
and added it to the res/layout
directory. Note I also gave my images views ids to help identify them later. I’ve omitted the other ImageView
s for simplicity. They’re mentioned below in an enum
.
1 2 3 4 5 6 7 8 9 10 11 12 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/widget_tv_power" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/ic_action_power" android:contentDescription="@string/power" /> </LinearLayout> |
Android Studio provides lots of clip art I’ve found useful for applications like this. Right click the drawable
directory and choose “new Vector Asset”. Click the “clip art” button and choose an asset. Give it a name and hit add. I was able to find icons for power, mute, volume up/down and source to use in my widget. Once your application is deployed to the device you should be able to add the widget to your homescreen by long clicking and choosing widgets.
Create a subclass of AppWidgetProvider
We defined receiver android:name=".widget.MyAppWidgetProvider"
in the Android Manifest. We’ll create this now. Create a new class called MyAppWidgetProvider
with a superclass of AppWidgetProvider
. Override the onUpdate()
and onReceive()
methods. Your methods should look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.television_remote_appwidget_layout); // currently doesn't respond to any clicks -- we'll cover this momentarily appWidgetManager.updateAppWidget(appWidgetId, views); } super.onUpdate(context, appWidgetManager, appWidgetIds); } @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); } |
Transmitting IR Commands
The widget is all ready to go but it does nothing. We’ll come back and connect the ImageView
to respond to a click after this step.
I borrowed this object from http://stackoverflow.com/users/1679571/randy and created an abstract
class using it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public abstract class IRCommand { private int frequency; private int[] pattern; IRCommand(final String irData) { List<String> list = new ArrayList<>(Arrays.asList(irData.split(" "))); list.remove(0); int frequency = Integer.parseInt(list.remove(0), 16); // frequency list.remove(0); list.remove(0); frequency = (int) (1000000 / (frequency * 0.241246)); int pulses = 1000000 / frequency; int count; int[] pattern = new int[list.size()]; for (int i = 0; i < list.size(); i++) { count = Integer.parseInt(list.get(i), 16); pattern[i] = count * pulses; } this.frequency = frequency; this.pattern = pattern; } int getFrequency() { return frequency; } int[] getPattern() { return pattern; } } |
Let’s create an implementation of IRCommand
specific to the television model. I found all the codes I needed on Remote Central. I saved them as String
s describing their behavior. I kept the constructor private and create some static creation methods for now. This should probably be refactored as a singleton which caches the constructed commands… but I’m leaving it as is for this example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class SamsungTvIRCommand extends IRCommand { private final static String TV_POWER_HEX = "0000 006d 0022 0003 00a9 00a8 0015 003f 0015 003f 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 003f 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0040 0015 0015 0015 003f 0015 003f 0015 003f 0015 003f 0015 003f 0015 003f 0015 0702 00a9 00a8 0015 0015 0015 0e6e"; private final static String TV_MUTE_HEX = "0000 006d 0000 0022 00a9 00a8 0015 003f 0015 0015 0015 003f 0015 0016 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 0015 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 0015 0015 003f 0015 003f 0015 003f 0015 003f 0015 0015 0015 0015 0015 0015 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 003f 0015 075f"; private static final String TV_VOLUME_DOWN_HEX = "0000 006d 0022 0003 00a9 00a8 0015 003f 0015 003f 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 003f 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 003f 0015 0015 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 0015 0015 003f 0015 003f 0015 003f 0015 003f 0015 0702 00a9 00a8 0015 0015 0015 0e6e"; private final static String TV_VOLUME_UP_HEX = "0000 006d 0022 0003 00a9 00a8 0015 003f 0015 003f 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 003f 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 003f 0015 003f 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 0015 003f 0015 003f 0015 003f 0015 003f 0015 003f 0015 0702 00a9 00a8 0015 0015 0015 0e6e"; private final static String TV_SOURCE_HEX = "0000 006C 0000 0022 00AD 00AD 0016 0041 0016 0041 0016 0041 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0041 0016 0041 0016 0041 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0041 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 0041 0016 06FB"; public static final SamsungTvIRCommand TV_POWER = new SamsungTvIRCommand(TV_POWER_HEX); public static final SamsungTvIRCommand TV_MUTE = new SamsungTvIRCommand(TV_MUTE_HEX); public static final SamsungTvIRCommand TV_VOLUME_DOWN = new SamsungTvIRCommand(TV_VOLUME_DOWN_HEX); public static final SamsungTvIRCommand TV_VOLUME_UP = new SamsungTvIRCommand(TV_VOLUME_UP_HEX); public static final SamsungTvIRCommand TV_SOURCE = new SamsungTvIRCommand(TV_SOURCE_HEX); private SamsungTvIRCommand(String irData) { super(irData); } } |
This objects gives us everything we need to start transmitting IR commands using the ConsumerIrManager
. Let’s create a helper that works better with our IRCommand
object.
1 2 3 4 5 6 7 8 9 10 11 | public class ConsumerIrManagerHelper { private ConsumerIrManager consumerIrManager; public ConsumerIrManagerHelper(ConsumerIrManager consumerIrManager) { this.consumerIrManager = consumerIrManager; } public void transmitIRCommand(IRCommand irCommand) { consumerIrManager.transmit(irCommand.getFrequency(), irCommand.getPattern()); } } |
Now we can just pass our implementation of IRCommand
to transmitIRCommand
and it will handle the rest. Let’s finish by hooking up the widget button.
Back to MyAppWidgetProvider
I was blocked by this detail for awhile. The Android documentation instructs you to create a PendingIntent
by invoking getActivity()
which opens an activity. I wanted to process the command without opening any activity. You can accomplish this by calling getBroadcast()
instead. This will result in the onReceive()
method being invoked on any button click. I added the following enum and methods to MyAppWidgetProvider
(for now).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public enum SamsungTVActions { POWER(R.id.widget_tv_power, "tv_power_action", TV_POWER); // Add additional actions as needed // MUTE(R.id.widget_tv_mute, "tv_mute_action", TV_MUTE), // VOL_DOWN(R.id.widget_tv_volume_down, "tv_volume_down", TV_VOLUME_DOWN), // VOL_UP(R.id.widget_tv_volume_up, "tv_volume_up", TV_VOLUME_UP), // SOURCE(R.id.widget_tv_source, "tv_source_action", TV_SOURCE); private int viewId; private String action; private IRCommand commandHex; SamsungTVActions(int viewId, String action, IRCommand commandHex) { this.viewId = viewId; this.action = action; this.commandHex = commandHex; } } private void setupViewClickPendingIntents(Context context, RemoteViews views) { for (SamsungTVActions samsungTvActions : SamsungTVActions.values()) views.setOnClickPendingIntent(samsungTvActions.viewId, createTVActionIntent(context, samsungTvActions.action)); } private PendingIntent createTVActionIntent(Context context, String action) { Intent intent = new Intent(context, getClass()); intent.setAction(action); return PendingIntent.getBroadcast(context, 0, intent, 0); } |
I used a combination of the enum
and methods to reduce duplicated code. I’m not sure if creating multiple Intent
objects is necessary. Please leave a comment if you know.
Now you can just add the call to setupViewClickPendingIntents()
to the existing onUpdate()
so it looks something like this:
1 2 3 4 5 6 7 8 9 10 | @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.television_remote_appwidget_layout); setupViewClickPendingIntents(context, views); appWidgetManager.updateAppWidget(appWidgetId, views); } super.onUpdate(context, appWidgetManager, appWidgetIds); } |
Summary
Success, the television can now be controlled by a handy little widget on my homescreen (for all those times the pesky remote is out of reach). I took some more time to customize the layout to make it more aesthetically appealing.
I’m not happy with the SamsungTVActions
enum
. It’s not abstract enough. I would have put the enum
in the abstract class but Java restricts you from abstracting an enum
. Ideally the MyAppWidgetProvider
would program to an interface instead of an implementation. Down the road it will take more refactoring to add support for multiple televisions.