commit a0d3e184f948f0675672de3b853bcfc2bc292df0 Author: Harald Hoyer Date: Mon Apr 24 16:08:30 2017 +0200 initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39fb081 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android_antlib_4-14-0/android_antlib_4-14-0.jar b/android_antlib_4-14-0/android_antlib_4-14-0.jar new file mode 100644 index 0000000..062ffd9 Binary files /dev/null and b/android_antlib_4-14-0/android_antlib_4-14-0.jar differ diff --git a/android_antlib_4-14-0/build.gradle b/android_antlib_4-14-0/build.gradle new file mode 100644 index 0000000..04de0a9 --- /dev/null +++ b/android_antlib_4-14-0/build.gradle @@ -0,0 +1,2 @@ +configurations.maybeCreate("default") +artifacts.add("default", file('android_antlib_4-14-0.jar')) \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..075bcc9 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 24 + buildToolsVersion "25.0.0" + defaultConfig { + applicationId "xyz.hoyer.iconsole" + minSdkVersion 15 + targetSdkVersion 24 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:24.2.1' + compile 'com.android.support:support-v4:24.2.1' + testCompile 'junit:junit:4.12' + compile 'com.android.support.constraint:constraint-layout:1.0.2' + provided project(':android_antlib_4-14-0') +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..4da8cc6 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/harald/Android/Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..aa24833 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/dsi/ant/channel/PredefinedNetwork.java b/app/src/main/java/com/dsi/ant/channel/PredefinedNetwork.java new file mode 100644 index 0000000..24746e6 --- /dev/null +++ b/app/src/main/java/com/dsi/ant/channel/PredefinedNetwork.java @@ -0,0 +1,40 @@ +package com.dsi.ant.channel; + +/** + * Created by harald on 24.04.17. + */ + +public enum PredefinedNetwork { + INVALID(-1), + PUBLIC(0), + ANT_PLUS1(1), // + ANT_FS(2); + + private final int mRawValue; + private static final PredefinedNetwork[] sValues = values(); + + private PredefinedNetwork(int rawValue) { + this.mRawValue = rawValue; + } + + int getRawValue() { + return this.mRawValue; + } + + private boolean equals(int rawValue) { + return rawValue == this.mRawValue; + } + + static PredefinedNetwork create(int rawValue) { + PredefinedNetwork code = INVALID; + + for(int i = 0; i < sValues.length; ++i) { + if(sValues[i].equals(rawValue)) { + code = sValues[i]; + break; + } + } + + return code; + } +} diff --git a/app/src/main/java/xyz/hoyer/iconsole/ChannelController.java b/app/src/main/java/xyz/hoyer/iconsole/ChannelController.java new file mode 100644 index 0000000..57bced0 --- /dev/null +++ b/app/src/main/java/xyz/hoyer/iconsole/ChannelController.java @@ -0,0 +1,296 @@ +/* + * Copyright 2012 Dynastream Innovations Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package xyz.hoyer.iconsole; + +import android.os.RemoteException; +import android.util.Log; + +import com.dsi.ant.channel.AntChannel; +import com.dsi.ant.channel.AntCommandFailedException; +import com.dsi.ant.channel.IAntChannelEventHandler; +import com.dsi.ant.message.ChannelId; +import com.dsi.ant.message.ChannelType; +import com.dsi.ant.message.fromant.AcknowledgedDataMessage; +import com.dsi.ant.message.fromant.BroadcastDataMessage; +import com.dsi.ant.message.fromant.ChannelEventMessage; +import com.dsi.ant.message.fromant.MessageFromAntType; +import com.dsi.ant.message.ipc.AntMessageParcel; + +import java.util.Random; + +public class ChannelController +{ + // The device type and transmission type to be part of the channel ID message + private static final int CHANNEL_PROOF_DEVICE_TYPE = 0x08; + private static final int CHANNEL_PROOF_TRANSMISSION_TYPE = 1; + + // The period and frequency values the channel will be configured to + private static final int CHANNEL_PROOF_PERIOD = 32768; // 1 Hz + private static final int CHANNEL_PROOF_FREQUENCY = 77; + + private static final String TAG = ChannelController.class.getSimpleName(); + + private static Random randGen = new Random(); + + private AntChannel mAntChannel; + private ChannelBroadcastListener mChannelBroadcastListener; + + private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback(); + + private ChannelInfo mChannelInfo; + + private boolean mIsOpen; + + static public abstract class ChannelBroadcastListener + { + public abstract void onBroadcastChanged(ChannelInfo newInfo); + } + + public ChannelController(AntChannel antChannel, boolean isMaster, int deviceId, + ChannelBroadcastListener broadcastListener) + { + mAntChannel = antChannel; + mChannelInfo = new ChannelInfo(deviceId, isMaster, randGen.nextInt(256)); + mChannelBroadcastListener = broadcastListener; + + openChannel(); + } + + + boolean openChannel() + { + if(null != mAntChannel) + { + if(mIsOpen) + { + Log.w(TAG, "Channel was already open"); + } + else + { + /* + * Although this reference code sets ChannelType to either a transmitting master or a receiving slave, + * the standard for ANT is that channels communication is bidirectional. The use of single-direction + * communication in this app is for ease of understanding as reference code. For more information and + * any additional features on ANT channel communication, refer to the ANT Protocol Doc found at: + * http://www.thisisant.com/resources/ant-message-protocol-and-usage/ + */ + ChannelType channelType = (mChannelInfo.isMaster ? + ChannelType.BIDIRECTIONAL_MASTER : ChannelType.BIDIRECTIONAL_SLAVE); + + // Channel ID message contains device number, type and transmission type. In + // order for master (TX) channels and slave (RX) channels to connect, they + // must have the same channel ID, or wildcard (0) is used. + ChannelId channelId = new ChannelId(mChannelInfo.deviceNumber, + CHANNEL_PROOF_DEVICE_TYPE, CHANNEL_PROOF_TRANSMISSION_TYPE); + + try + { + // Setting the channel event handler so that we can receive messages from ANT + mAntChannel.setChannelEventHandler(mChannelEventCallback); + + // Performs channel assignment by assigning the type to the channel. Additional + // features (such as, background scanning and frequency agility) can be enabled + // by passing an ExtendedAssignment object to assign(ChannelType, ExtendedAssignment). + mAntChannel.assign(channelType); + + /* + * Configures the channel ID, messaging period and rf frequency after assigning, + * then opening the channel. + * + * For any additional ANT features such as proximity search or background scanning, refer to + * the ANT Protocol Doc found at: + * http://www.thisisant.com/resources/ant-message-protocol-and-usage/ + */ + mAntChannel.setChannelId(channelId); + mAntChannel.setPeriod(CHANNEL_PROOF_PERIOD); + mAntChannel.setRfFrequency(CHANNEL_PROOF_FREQUENCY); + mAntChannel.open(); + mIsOpen = true; + + Log.d(TAG, "Opened channel with device number: " + mChannelInfo.deviceNumber); + } catch (RemoteException e) { + channelError(e); + } catch (AntCommandFailedException e) { + // This will release, and therefore unassign if required + channelError("Open failed", e); + } + } + } + else + { + Log.w(TAG, "No channel available"); + } + + return mIsOpen; + } + + /** + * Implements the Channel Event Handler Interface so that messages can be + * received and channel death events can be handled. + */ + public class ChannelEventCallback implements IAntChannelEventHandler + { + private void updateData(byte[] data) { + mChannelInfo.broadcastData = data; + + mChannelBroadcastListener.onBroadcastChanged(mChannelInfo); + } + + @Override + public void onChannelDeath() + { + // Display channel death message when channel dies + displayChannelError("Channel Death"); + } + + @Override + public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) { + Log.d(TAG, "Rx: "+ antParcel); + + // Switching on message type to handle different types of messages + switch(messageType) + { + // If data message, construct from parcel and update channel data + case BROADCAST_DATA: + // Rx Data + updateData(new BroadcastDataMessage(antParcel).getPayload()); + break; + case ACKNOWLEDGED_DATA: + // Rx Data + updateData(new AcknowledgedDataMessage(antParcel).getPayload()); + break; + case CHANNEL_EVENT: + // Constructing channel event message from parcel + ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel); + + // Switching on event code to handle the different types of channel events + switch(eventMessage.getEventCode()) + { + case TX: + // Use old info as this is what remote device has just received + mChannelBroadcastListener.onBroadcastChanged(mChannelInfo); + + mChannelInfo.broadcastData[0]++; + + if(mIsOpen) + { + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(mChannelInfo.broadcastData); + } catch (RemoteException e) { + channelError(e); + } + } + break; + case RX_SEARCH_TIMEOUT: + // TODO May want to keep searching + displayChannelError("No Device Found"); + break; + case CHANNEL_CLOSED: + case CHANNEL_COLLISION: + case RX_FAIL: + case RX_FAIL_GO_TO_SEARCH: + case TRANSFER_RX_FAILED: + case TRANSFER_TX_COMPLETED: + case TRANSFER_TX_FAILED: + case TRANSFER_TX_START: + case UNKNOWN: + // TODO More complex communication will need to handle these events + break; + } + break; + case ANT_VERSION: + case BURST_TRANSFER_DATA: + case CAPABILITIES: + case CHANNEL_ID: + case CHANNEL_RESPONSE: + case CHANNEL_STATUS: + case SERIAL_NUMBER: + case OTHER: + // TODO More complex communication will need to handle these message types + break; + } + } + } + + public ChannelInfo getCurrentInfo() + { + return mChannelInfo; + } + + void displayChannelError(String displayText) + { + mChannelInfo.die(displayText); + mChannelBroadcastListener.onBroadcastChanged(mChannelInfo); + } + + void channelError(RemoteException e) { + String logString = "Remote service communication failed."; + + Log.e(TAG, logString); + + displayChannelError(logString); + } + + void channelError(String error, AntCommandFailedException e) { + StringBuilder logString; + + if(e.getResponseMessage() != null) { + String initiatingMessageId = "0x"+ Integer.toHexString( + e.getResponseMessage().getInitiatingMessageId()); + String rawResponseCode = "0x"+ Integer.toHexString( + e.getResponseMessage().getRawResponseCode()); + + logString = new StringBuilder(error) + .append(". Command ") + .append(initiatingMessageId) + .append(" failed with code ") + .append(rawResponseCode); + } else { + String attemptedMessageId = "0x"+ Integer.toHexString( + e.getAttemptedMessageType().getMessageId()); + String failureReason = e.getFailureReason().toString(); + + logString = new StringBuilder(error) + .append(". Command ") + .append(attemptedMessageId) + .append(" failed with reason ") + .append(failureReason); + } + + Log.e(TAG, logString.toString()); + + mAntChannel.release(); + + displayChannelError("ANT Command Failed"); + } + + public void close() + { + // TODO kill all our resources + if (null != mAntChannel) + { + mIsOpen = false; + + // Releasing the channel to make it available for others. + // After releasing, the AntChannel instance cannot be reused. + mAntChannel.release(); + mAntChannel = null; + } + + displayChannelError("Channel Closed"); + } +} diff --git a/app/src/main/java/xyz/hoyer/iconsole/ChannelInfo.java b/app/src/main/java/xyz/hoyer/iconsole/ChannelInfo.java new file mode 100644 index 0000000..caf7c29 --- /dev/null +++ b/app/src/main/java/xyz/hoyer/iconsole/ChannelInfo.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012 Dynastream Innovations Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package xyz.hoyer.iconsole; + +import com.dsi.ant.message.fromant.DataMessage; + +public class ChannelInfo +{ + public final int deviceNumber; + + /** Master / Slave */ + public final boolean isMaster; + + public byte[] broadcastData = new byte[DataMessage.LENGTH_STANDARD_PAYLOAD]; + + public boolean error; + private String mErrorMessage; + + public ChannelInfo(int deviceNumber, boolean isMaster, int initialBroadcastValue) + { + this.deviceNumber = deviceNumber; + this.isMaster = isMaster; + + // Not actually concerned with this value, so can cast to byte and lose data without issues + broadcastData[0] = (byte)initialBroadcastValue; + + error = false; + mErrorMessage = null; + } + + public void die(String errorMessage) + { + error = true; + mErrorMessage = errorMessage; + } + + public String getErrorString() + { + return mErrorMessage; + } +} diff --git a/app/src/main/java/xyz/hoyer/iconsole/ChannelList.java b/app/src/main/java/xyz/hoyer/iconsole/ChannelList.java new file mode 100644 index 0000000..0106fc5 --- /dev/null +++ b/app/src/main/java/xyz/hoyer/iconsole/ChannelList.java @@ -0,0 +1,387 @@ +/* + * Copyright 2012 Dynastream Innovations Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package xyz.hoyer.iconsole; + +import com.dsi.ant.channel.ChannelNotAvailableException; +import xyz.hoyer.iconsole.ChannelService.ChannelChangedListener; +import xyz.hoyer.iconsole.ChannelService.ChannelServiceComm; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.ListView; +import android.widget.Toast; +import android.widget.ToggleButton; + +import java.util.ArrayList; + +public class ChannelList extends Activity { + private static final String TAG = ChannelList.class.getSimpleName(); + + private final String PREF_TX_BUTTON_CHECKED_KEY = "ChannelList.TX_BUTTON_CHECKED"; + private boolean mCreateChannelAsMaster; + + private ChannelServiceComm mChannelService; + + private ArrayList mChannelDisplayList = new ArrayList(); + private ArrayAdapter mChannelListAdapter; + private SparseArray mIdChannelListIndexMap = new SparseArray(); + + private boolean mChannelServiceBound = false; + + private void initButtons() + { + Log.v(TAG, "initButtons..."); + + //Register Master/Slave Toggle handler + ToggleButton toggleButton_MasterSlave = (ToggleButton)findViewById(R.id.toggleButton_MasterSlave); + toggleButton_MasterSlave.setEnabled(mChannelServiceBound); + toggleButton_MasterSlave.setChecked(mCreateChannelAsMaster); + toggleButton_MasterSlave.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(CompoundButton arg0, boolean enabled) + { + mCreateChannelAsMaster = enabled; + } + }); + + //Register Add Channel Button handler + Button button_addChannel = (Button)findViewById(R.id.button_AddChannel); + button_addChannel.setEnabled(mChannelServiceBound); + button_addChannel.setOnClickListener(new OnClickListener() + { + @Override + public void onClick(View v) + { + addNewChannel(mCreateChannelAsMaster); + } + }); + + //Register Clear Channels Button handler + Button button_clearChannels = (Button)findViewById(R.id.button_ClearChannels); + button_clearChannels.setEnabled(mChannelServiceBound); + button_clearChannels.setOnClickListener(new OnClickListener() + { + @Override + public void onClick(View v) + { + clearAllChannels(); + } + }); + + Log.v(TAG, "...initButtons"); + } + + private void initPrefs() + { + Log.v(TAG, "initPrefs..."); + + // Retrieves the app's current state of channel transmission mode + // from preferences to handle app resuming. + SharedPreferences preferences = getPreferences(MODE_PRIVATE); + + mCreateChannelAsMaster = preferences.getBoolean(PREF_TX_BUTTON_CHECKED_KEY, true); + + Log.v(TAG, "...initPrefs"); + } + + private void savePrefs() + { + Log.v(TAG, "savePrefs..."); + + // Saves the app's current state of channel transmission mode to preferences + SharedPreferences preferences = getPreferences(MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + + editor.putBoolean(PREF_TX_BUTTON_CHECKED_KEY, mCreateChannelAsMaster); + + editor.commit(); + + Log.v(TAG, "...savePrefs"); + } + + private void doBindChannelService() + { + Log.v(TAG, "doBindChannelService..."); + + // Binds to ChannelService. ChannelService binds and manages connection between the + // app and the ANT Radio Service + Intent bindIntent = new Intent(this, ChannelService.class); + startService(bindIntent); + mChannelServiceBound = bindService(bindIntent, mChannelServiceConnection, Context.BIND_AUTO_CREATE); + + if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI + doUnbindChannelService(); + + Log.i(TAG, " Channel Service binding = "+ mChannelServiceBound); + + Log.v(TAG, "...doBindChannelService"); + } + + private void doUnbindChannelService() + { + Log.v(TAG, "doUnbindChannelService..."); + + if(mChannelServiceBound) + { + unbindService(mChannelServiceConnection); + + mChannelServiceBound = false; + } + + ((Button)findViewById(R.id.button_ClearChannels)).setEnabled(false); + ((Button)findViewById(R.id.button_AddChannel)).setEnabled(false); + ((Button)findViewById(R.id.toggleButton_MasterSlave)).setEnabled(false); + + Log.v(TAG, "...doUnbindChannelService"); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Log.v(TAG, "onCreate..."); + + mChannelServiceBound = false; + + setContentView(R.layout.activity_fullscreen); + + initPrefs(); + + mChannelListAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1, mChannelDisplayList); + ListView listView_channelList = (ListView)findViewById(R.id.listView_channelList); + listView_channelList.setAdapter(mChannelListAdapter); + + if(!mChannelServiceBound) doBindChannelService(); + + initButtons(); + + Log.v(TAG, "...onCreate"); + } + + public void onBack() { + finish(); + } + + @Override + public void onDestroy() + { + Log.v(TAG, "onDestroy..."); + + doUnbindChannelService(); + + if(isFinishing()) + { + stopService(new Intent(this, ChannelService.class)); + } + + mChannelServiceConnection = null; + + savePrefs(); + + Log.v(TAG, "...onDestroy"); + + super.onDestroy(); + } + + private ServiceConnection mChannelServiceConnection = new ServiceConnection() + { + @Override + public void onServiceConnected(ComponentName name, IBinder serviceBinder) + { + Log.v(TAG, "mChannelServiceConnection.onServiceConnected..."); + + mChannelService = (ChannelServiceComm) serviceBinder; + + // Sets a listener that handles channel events + mChannelService.setOnChannelChangedListener(new ChannelChangedListener() + { + // Occurs when a channel has new info/data + @Override + public void onChannelChanged(final ChannelInfo newInfo) + { + Integer index = mIdChannelListIndexMap.get(newInfo.deviceNumber); + + if(null != index && index.intValue() < mChannelDisplayList.size()) + { + mChannelDisplayList.set(index.intValue(), getDisplayText(newInfo)); + runOnUiThread(new Runnable() + { + @Override + public void run() + { + mChannelListAdapter.notifyDataSetChanged(); + } + }); + } + } + + // Updates the UI to allow/disallow acquiring new channels + @Override + public void onAllowAddChannel(boolean addChannelAllowed) { + // Enable Add Channel button and Master/Slave toggle if + // adding channels is allowed + ((Button)findViewById(R.id.button_AddChannel)).setEnabled(addChannelAllowed); + ((Button)findViewById(R.id.toggleButton_MasterSlave)).setEnabled(addChannelAllowed); + } + }); + + // Initial check when connecting to ChannelService if adding channels is allowed + boolean allowAcquireChannel = mChannelService.isAddChannelAllowed(); + ((Button)findViewById(R.id.button_AddChannel)).setEnabled(allowAcquireChannel); + ((Button)findViewById(R.id.toggleButton_MasterSlave)).setEnabled(allowAcquireChannel); + + refreshList(); + + Log.v(TAG, "...mChannelServiceConnection.onServiceConnected"); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) + { + Log.v(TAG, "mChannelServiceConnection.onServiceDisconnected..."); + + // Clearing and disabling when disconnecting from ChannelService + mChannelService = null; + + ((Button)findViewById(R.id.button_ClearChannels)).setEnabled(false); + ((Button)findViewById(R.id.button_AddChannel)).setEnabled(false); + ((Button)findViewById(R.id.toggleButton_MasterSlave)).setEnabled(false); + + Log.v(TAG, "...mChannelServiceConnection.onServiceDisconnected"); + } + }; + + // This method is called when 'Add Channel' button is clicked + private void addNewChannel(final boolean isMaster) + { + Log.v(TAG, "addNewChannel..."); + + if(null != mChannelService) + { + ChannelInfo newChannelInfo; + try + { + // Telling the ChannelService to add a new channel. This method + // in ChannelService contains code required to acquire an ANT + // channel from ANT Radio Service. + newChannelInfo = mChannelService.addNewChannel(isMaster); + } catch (ChannelNotAvailableException e) + { + // Occurs when a channel is not available. Printing out the + // stack trace will show why no channels are available. + Toast.makeText(this, "Channel Not Available", Toast.LENGTH_SHORT).show(); + return; + } + + if(null != newChannelInfo) + { + // Adding new channel info to the list + addChannelToList(newChannelInfo); + mChannelListAdapter.notifyDataSetChanged(); + } + } + + Log.v(TAG, "...addNewChannel"); + } + + private void refreshList() + { + Log.v(TAG, "refreshList..."); + + if(null != mChannelService) + { + ArrayList chInfoList = mChannelService.getCurrentChannelInfoForAllChannels(); + + mChannelDisplayList.clear(); + for(ChannelInfo i: chInfoList) + { + addChannelToList(i); + } + mChannelListAdapter.notifyDataSetChanged(); + } + + Log.v(TAG, "...refreshList"); + } + + private void addChannelToList(ChannelInfo channelInfo) + { + Log.v(TAG, "addChannelToList..."); + + mIdChannelListIndexMap.put(channelInfo.deviceNumber, mChannelDisplayList.size()); + mChannelDisplayList.add(getDisplayText(channelInfo)); + + Log.v(TAG, "...addChannelToList"); + } + + + private static String getDisplayText(ChannelInfo channelInfo) + { + Log.v(TAG, "getDisplayText..."); + String displayText = null; + + if(channelInfo.error) + { + displayText = String.format("#%-6d !:%s", channelInfo.deviceNumber, channelInfo.getErrorString()); + } + else + { + if(channelInfo.isMaster) + { + displayText = String.format("#%-6d Tx:[%2d]", channelInfo.deviceNumber, channelInfo.broadcastData[0] & 0xFF); + } + else + { + displayText = String.format("#%-6d Rx:[%2d]", channelInfo.deviceNumber, channelInfo.broadcastData[0] & 0xFF); + } + } + + Log.v(TAG, "...getDisplayText"); + + return displayText; + } + + + private void clearAllChannels() + { + Log.v(TAG, "clearAllChannels..."); + + if(null != mChannelService) + { + // Telling ChannelService to close all the channels + mChannelService.clearAllChannels(); + + mChannelDisplayList.clear(); + mIdChannelListIndexMap.clear(); + mChannelListAdapter.notifyDataSetChanged(); + } + + Log.v(TAG, "...clearAllChannels"); + } +} diff --git a/app/src/main/java/xyz/hoyer/iconsole/ChannelService.java b/app/src/main/java/xyz/hoyer/iconsole/ChannelService.java new file mode 100644 index 0000000..186ce63 --- /dev/null +++ b/app/src/main/java/xyz/hoyer/iconsole/ChannelService.java @@ -0,0 +1,379 @@ +/* + * Copyright 2012 Dynastream Innovations Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package xyz.hoyer.iconsole; + +import xyz.hoyer.iconsole.ChannelController.ChannelBroadcastListener; + +import com.dsi.ant.AntService; +import com.dsi.ant.channel.AntChannel; +import com.dsi.ant.channel.AntChannelProvider; +import com.dsi.ant.channel.ChannelNotAvailableException; +import com.dsi.ant.channel.PredefinedNetwork; + +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.SparseArray; + +import java.util.ArrayList; + +public class ChannelService extends Service +{ + private static final String TAG = "ChannelService"; + + private Object mCreateChannel_LOCK = new Object(); + + SparseArray mChannelControllerList = new SparseArray(); + + ChannelChangedListener mListener; + + int channelDeviceIdCounter = 0; + + private boolean mAntRadioServiceBound; + private AntService mAntRadioService = null; + private AntChannelProvider mAntChannelProvider = null; + private boolean mAllowAddChannel = false; + + private ServiceConnection mAntRadioServiceConnection = new ServiceConnection() + { + @Override + public void onServiceConnected(ComponentName name, IBinder service) + { + // Must pass in the received IBinder object to correctly construct an AntService object + mAntRadioService = new AntService(service); + + try { + // Getting a channel provider in order to acquire channels + mAntChannelProvider = mAntRadioService.getChannelProvider(); + + // Initial check for number of channels available + boolean mChannelAvailable = mAntChannelProvider.getNumChannelsAvailable() > 0; + // Initial check for if legacy interface is in use. If the + // legacy interface is in use, applications can free the ANT + // radio by attempting to acquire a channel. + boolean legacyInterfaceInUse = mAntChannelProvider.isLegacyInterfaceInUse(); + + // If there are channels OR legacy interface in use, allow adding channels + if(mChannelAvailable || legacyInterfaceInUse) { + mAllowAddChannel = true; + } + else { + // If no channels available AND legacy interface is not in use, disallow adding channels + mAllowAddChannel = false; + } + + if(mAllowAddChannel) { + if(null != mListener) { + // Send an event that indicates if adding channels is allowed + mListener.onAllowAddChannel(mAllowAddChannel); + } + } + + } catch (RemoteException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) + { + die("Binder Died"); + + mAntChannelProvider = null; + mAntRadioService = null; + + if(mAllowAddChannel) { mListener.onAllowAddChannel(false); } + mAllowAddChannel = false; + } + + }; + + public interface ChannelChangedListener + { + /** + * Occurs when a Channel's Info has changed (i.e. a newly created + * channel, channel has transmitted or received data, or if channel has + * been closed. + * + * @param newInfo The channel's updated info + */ + void onChannelChanged(ChannelInfo newInfo); + + /** + * Occurs when there is adding a channel is being allowed or disallowed. + * + * @param addChannelAllowed True if adding channels is allowed. False, otherwise. + */ + void onAllowAddChannel(boolean addChannelAllowed); + } + + /** + * The interface used to communicate with the ChannelService + */ + public class ChannelServiceComm extends Binder + { + /** + * Sets the listener to be used for channel changed event callbacks. + * + * @param listener The listener that will receive events + */ + void setOnChannelChangedListener(ChannelChangedListener listener) + { + mListener = listener; + } + + /** + * Retrieves the current info for all channels currently added. + * + * @return A list that contains info for all the channels + */ + ArrayList getCurrentChannelInfoForAllChannels() + { + ArrayList retList = new ArrayList(); + for(int i = 0; i < mChannelControllerList.size(); i++) + { + ChannelController channel = mChannelControllerList.valueAt(i); + + retList.add(channel.getCurrentInfo()); + } + + return retList; + } + + /** + * Acquires and adds a channel from ANT Radio Service + * + * @param isMaster True if channel is transmitting, False if channel is receiving + * @return The info for the newly acquired and added channel + * @throws ChannelNotAvailableException + */ + ChannelInfo addNewChannel(final boolean isMaster) throws ChannelNotAvailableException + { + return createNewChannel(isMaster); + } + + /** + * Closes all channels currently added. + */ + void clearAllChannels() { closeAllChannels(); } + + /** + * Queries if adding a channel is allowed. + * @return True if adding a channel is allowed. False, otherwise. + */ + boolean isAddChannelAllowed() { return mAllowAddChannel; } + } + + private void closeAllChannels() + { + synchronized (mChannelControllerList) + { + // Closing all channels in the list + for(int i = 0; i < mChannelControllerList.size(); i++) + { + mChannelControllerList.valueAt(i).close(); + } + mChannelControllerList.clear(); + } + + // Reset the device id counter + channelDeviceIdCounter = 0; + } + + AntChannel acquireChannel() throws ChannelNotAvailableException + { + AntChannel mAntChannel = null; + if(null != mAntChannelProvider) + { + try + { + /* + * If applications require a channel with specific capabilities + * (event buffering, background scanning etc.), a Capabilities + * object should be created and then the specific capabilities + * required set to true. Applications can specify both required + * and desired Capabilities with both being passed in + * acquireChannel(context, PredefinedNetwork, + * requiredCapabilities, desiredCapabilities). + */ + mAntChannel = mAntChannelProvider.acquireChannel(this, PredefinedNetwork.ANT_PLUS1); + /* + NetworkKey mNK = new NetworkKey(new byte[] { (byte)0xb9, (byte)0xa5, (byte)0x21, (byte)0xfb, + (byte)0xbd, (byte)0x72, (byte)0xc3, (byte)0x45 }); + Log.v(TAG, mNK.toString()); + mAntChannel = mAntChannelProvider.acquireChannelOnPrivateNetwork(this, mNK); + */ + } catch (RemoteException e) + { + die("ACP Remote Ex"); + } + } + return mAntChannel; + } + + public ChannelInfo createNewChannel(final boolean isMaster) throws ChannelNotAvailableException + { + ChannelController channelController = null; + + synchronized(mCreateChannel_LOCK) + { + // Acquiring a channel from ANT Radio Service + AntChannel antChannel = acquireChannel(); + + if(null != antChannel) + { + + channelDeviceIdCounter += 1; + + // Constructing a controller that will manage and control the channel + channelController = new ChannelController(antChannel, isMaster, channelDeviceIdCounter, + new ChannelBroadcastListener() + { + @Override + public void onBroadcastChanged(ChannelInfo newInfo) + { + // Sending a channel changed event when message from ANT is received + mListener.onChannelChanged(newInfo); + } + }); + + mChannelControllerList.put(channelDeviceIdCounter, channelController); + } + } + + if(null == channelController) return null; + + return channelController.getCurrentInfo(); + } + + @Override + public IBinder onBind(Intent arg0) + { + return new ChannelServiceComm(); + } + + /** + * Receives AntChannelProvider state changes being sent from ANT Radio Service + */ + private final BroadcastReceiver mChannelProviderStateChangedReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + if(AntChannelProvider.ACTION_CHANNEL_PROVIDER_STATE_CHANGED.equals(intent.getAction())) { + boolean update = false; + // Retrieving the data contained in the intent + int numChannels = intent.getIntExtra(AntChannelProvider.NUM_CHANNELS_AVAILABLE, 0); + boolean legacyInterfaceInUse = intent.getBooleanExtra(AntChannelProvider.LEGACY_INTERFACE_IN_USE, false); + + if(mAllowAddChannel) { + // Was a acquire channel allowed + // If no channels available AND legacy interface is not in use, disallow acquiring of channels + if(0 == numChannels && !legacyInterfaceInUse) { + mAllowAddChannel = false; + update = true; + } + } else { + // Acquire channels not allowed + // If there are channels OR legacy interface in use, allow acquiring of channels + if(numChannels > 0 || legacyInterfaceInUse) { + mAllowAddChannel = true; + update = true; + } + } + + if(update && (null != mListener)) { + // AllowAddChannel has been changed, sending event callback + mListener.onAllowAddChannel(mAllowAddChannel); + } + } + } + }; + + private void doBindAntRadioService() + { + if(BuildConfig.DEBUG) Log.v(TAG, "doBindAntRadioService"); + + // Start listing for channel available intents + registerReceiver(mChannelProviderStateChangedReceiver, new IntentFilter(AntChannelProvider.ACTION_CHANNEL_PROVIDER_STATE_CHANGED)); + + // Creating the intent and calling context.bindService() is handled by + // the static bindService() method in AntService + mAntRadioServiceBound = AntService.bindService(this, mAntRadioServiceConnection); + } + + private void doUnbindAntRadioService() + { + if(BuildConfig.DEBUG) Log.v(TAG, "doUnbindAntRadioService"); + + // Stop listing for channel available intents + try{ + unregisterReceiver(mChannelProviderStateChangedReceiver); + } catch (IllegalArgumentException exception) { + if(BuildConfig.DEBUG) Log.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver."); + } + + if(mAntRadioServiceBound) + { + try + { + unbindService(mAntRadioServiceConnection); + } + catch(IllegalArgumentException e) + { + // Not bound, that's what we want anyway + } + + mAntRadioServiceBound = false; + } + } + + @Override + public void onCreate() + { + super.onCreate(); + + mAntRadioServiceBound = false; + + doBindAntRadioService(); + } + + @Override + public void onDestroy() + { + closeAllChannels(); + + doUnbindAntRadioService(); + mAntChannelProvider = null; + + super.onDestroy(); + } + + static void die(String error) + { + Log.e(TAG, "DIE: "+ error); + } + +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b78a0b8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,23 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..aac7c9b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9bca508 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':android_antlib_4-14-0'