Minggu, 22 Februari 2015

Sending Messages to Android Auto

    In November of 2014, Google provided developers with two simulators, one for media apps and another for message notifications, to start updating and testing apps to work with the new Android Auto platform. In a previous post, I shared how to create a service that works with Android Auto in order to play media, and in this post I will go over how to interact with and send messages to the Android Auto notification screen. All source code for this tutorial can be found on GitHub. The previous post (linked above) has instructions for installing the Auto simulators, so I'm going to skip over that in this post for brevity.

Main message screen on Android Auto
    To start, we're going to want to get everything set up. We'll do this by first making sure our app is targeting SDK version 21 or higher in build.gradle. Next we'll move over to adding in a new xml file under /res/xml called automotive_app_desc.xml and add in the following content to let Android Auto know that this app supports notifications
<automotiveApp>
<uses name="notification"/>
</automotiveApp>
    Next we're going to want to go into AndroidManifest.xml and add a metadata tag within the application node to direct the OS towards our previously created xml file.
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
    In addition to our metadata tag, we're going to add two broadcast receivers with intent-filters that look for specific actions for 'read' or 'reply' user actions. While I'm using broadcast receivers here, it should be noted that you could also create Services to handle the read and reply situations instead, which I'll talk about a little more when we start tying in these broadcast receivers in code.
<receiver android:name=".AutoMessageReadReceiver" android:exported="false">
<intent-filter>
<action android:name="com.ptrprograms.androidautomessenger.ACTION_MESSAGE_READ"/>
</intent-filter>
</receiver>

<receiver android:name=".AutoMessageReplyReceiver" android:exported="false">
<intent-filter>
<action android:name="com.ptrprograms.androidautomessenger.ACTION_MESSAGE_REPLY"/>
</intent-filter>
</receiver>
    Now that the general setup is done, we can jump into our Java code. The way Android Auto's messaging system works is an Android application connected to Auto runs all of the logic for when a notification should be constructed, then it sends that notification to the Auto dashboard. To keep things simple, I'm going to build out the notification in our application's MainActivity.java file in order to point out what needs to be done. The first part of this should seem familiar if you've dealt with Android notifications before (and if not, I've written other posts on creating notifications): we're going to create a general NotificationCompat.Builder object.
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder( getApplicationContext() )
.setSmallIcon( R.drawable.ic_launcher )
.setLargeIcon( BitmapFactory.decodeResource( getResources(), R.drawable.ic_launcher ) )
.setContentText( "content text" )
.setWhen( Calendar.getInstance().get( Calendar.SECOND ) )
.setContentTitle( "content title" );
    With the NotificationCompat.Builder created, we can start adding in the Android Auto portions of the code. To do this, we need to use the .extend() method of NotificationCompat.Builder to add functionality from NotificationCompat.CarExtender(), which also uses a builder pattern to add functionality to our app (more options are available than shown here, such as setting text color for the Auto notification, but I'll leave that off for now). Each message group on the Auto dashboard is called a Conversation, so we also need to create an UnreadConversation to add to our notification.
notificationBuilder.extend( new NotificationCompat.CarExtender()
.setUnreadConversation( getUnreadConversation() ) );
   getUnreadConversation() handles creating the messages that we will display, the actions taken when the user has read them and if they reply
private NotificationCompat.CarExtender.UnreadConversation getUnreadConversation() {
NotificationCompat.CarExtender.UnreadConversation.Builder unreadConversationBuilder =
new NotificationCompat.CarExtender.UnreadConversation.Builder( UNREAD_CONVERSATION_BUILDER_NAME );

unreadConversationBuilder
.setReadPendingIntent( getMessageReadPendingIntent() )
.setReplyAction( getMessageReplyPendingIntent(), getVoiceReplyRemoteInput() )
.addMessage( "Message 1")
.addMessage( "Message 2" )
.addMessage( "Message 3" )
.setLatestTimestamp( Calendar.getInstance().get( Calendar.SECOND ) );

return unreadConversationBuilder.build();
}
    The important things to pay attention to here are getMessageReadPendingIntent(), getMessageReplyPendingIntent() and getVoiceReplyRemoteInput(). As mentioned before, I'm using a set of BroadcastReceivers to handle responding to when the user reads or replies to a message, but since we're using a system of PendingIntents here, we could just as easily create a set of intents that go to a service to deal with these actions. getMessageReadPendingIntent() is pretty straight forward, creating a pending intent with an action that will be caught by AutoMessageReadReceiver.java after the user has tapped on the message and it has been read aloud by the system.
private Intent getMessageReadIntent() {
return new Intent()
.addFlags( Intent.FLAG_INCLUDE_STOPPED_PACKAGES )
.setAction( MESSAGE_READ_ACTION )
.putExtra( MESSAGE_CONVERSATION_ID_KEY, 1 );
}

private PendingIntent getMessageReadPendingIntent() {
return PendingIntent.getBroadcast( getApplicationContext(),
1,
getMessageReadIntent(),
PendingIntent.FLAG_UPDATE_CURRENT );
}
    AutoMessageReadReceiver simply listens for our intent with the MESSAGE_READ_ACTION and dismisses the notification associated with the intent.
public class AutoMessageReadReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int conversationId = intent.getIntExtra( MainActivity.MESSAGE_CONVERSATION_ID_KEY, -1 );
Log.d( "Message", "id: " + conversationId );
NotificationManagerCompat.from( context ).cancel( conversationId );
}
}
Messages before they have been listened to
Message notification after all 'UnreadConversations' have been read
    The reply action is a little more interesting, though equally straight forward. We create another PendingIntent for a separate BroadcastReceiver, and also attach a RemoteInput for a voice reply with the setReplyAction method of our UnreadConversationBuilder.
private Intent getMessageReplyIntent() {
return new Intent()
.addFlags( Intent.FLAG_INCLUDE_STOPPED_PACKAGES )
.setAction( MESSAGE_REPLY_ACTION )
.putExtra( MESSAGE_CONVERSATION_ID_KEY, 1 );
}

private PendingIntent getMessageReplyPendingIntent() {
return PendingIntent.getBroadcast( getApplicationContext(),
1,
getMessageReplyIntent(),
PendingIntent.FLAG_UPDATE_CURRENT );
}

private RemoteInput getVoiceReplyRemoteInput() {
return new RemoteInput.Builder( VOICE_REPLY_KEY )
.setLabel( "Reply" )
.build();
}
    where our BroadcastReceiver, AutoMessageReplyReceiver.java, not only marks a message as read, but will also extract a voice reply provided by the user in order to use it within our app.
public class AutoMessageReplyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText( context, "Message Received", Toast.LENGTH_LONG ).show();

int conversationId = intent.getIntExtra( MainActivity.MESSAGE_CONVERSATION_ID_KEY, -1 );
Log.d( "Message", "id: " + conversationId );
NotificationManagerCompat.from(context).cancel( conversationId );

String message = getMessageFromIntent( intent );
}

private String getMessageFromIntent( Intent intent ) {
//Note that Android Auto does not currently allow voice responses in their simulator
Bundle remoteInput = RemoteInput.getResultsFromIntent( intent );
if( remoteInput != null && remoteInput.containsKey( "extra_voice_reply" ) ) {
return remoteInput.getCharSequence( "extra_voice_reply" ).toString();
}

return null;
}
}
Reply button pressed. Currently the Android Auto simulator does not allow for voice responses, so it sends an empty string value with the RemoteInput and posts a Toast with "Canned response sent".
    Once the notification is properly set up for displaying on Auto, we use NotificationManagerCompat to display it on our device and Auto from MainActivity.java
NotificationManagerCompat.from( this ).notify( 1, notificationBuilder.build() );
    Once the notification has been sent, it will be visible to the user on the Android Auto dashboard. While this system may be simple to work with, given a proper location or context aware app, it could be an invaluable feature for users as they travel. While there are still a few things missing from the official documentation that I'd like to see, such as how to hide the reply button, I'm definitely excited to see what else becomes available as Auto is released and matures, and hope this tutorial helps others create great apps.

Read More..

Senin, 16 Februari 2015

Using the Android Auto Media Browser

    During the 2014 I/O, Google made the announcement that Android would be available for in-vehicle systems, but left everyone hanging on the major details. Luckily, in November 2014 they released two simulator APKs for developers to begin testing different features of their apps for Android Auto: audio and messaging services. For this tutorial, I will go over using the Android Auto framework to browse media and create a simple media player (which can obviously be expanded on for a real application). Source code for this tutorial can be found on GitHub.

   In order to start, we'll first need to install the Media Browser Simulator APK for Android Auto onto a Lollipop device. I ended up using a 2013 Nexus 7, though a Lollipop emulator should also work for our purposes. This can be found under your Android SDK folder along the path /extras/google/simulators/media-browser-simulator.apk after installing Android Auto API Simulators from the SDK Manger. You can install this through the Android Device Bridge (ADB) with the following command:
adb install media-browser-simulator.apk
    Once this is loaded up, we should be able to open the application on our device to see a screen similar to this (minus the AndroidAutoMedia item that we'll build through this tutorial):


    Next we'll want to get going on our actual app. We'll start by creating an app in Android Studio for phone/tablet. Once that's done and we have our standard "Hello World" template together, we'll need to go under /res/xml and create a new xml file - under my source code it's called automotive_app_desc.xml, though you can call it whatever you'd like. This file will be used by our application to let Android Auto know that we're building components to work with it. For now, simply copy the following source code into the XML file that tells Android Auto that we're making a media Auto plugin.
<automotiveApp>
<uses name="media" />
</automotiveApp>
    With our XML file created, we'll move over and open AndroidManifest.xml to make a few additions to work with Auto. Within the application tag, we're going to want to add two meta-data items - one points to the XML file we just created, and the other provides the Android Auto launcher icon used in the media browser shown above (I just used the standard ic_launcher.png).
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>

<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@drawable/ic_launcher" />
    After the meta-data nodes are made, we'll want to add a new Service node to the manifest, and include an intent-filter for "android.media.browse.MediaBrowserService."
<service android:name="com.ptrprograms.androidautomedia.service.AutoMediaBrowserService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
    Now that the manifest is filled out, we can start working on our Java code. We'll create a service (I called mine AutoMediaBrowserService, as seen above) and extend the MediaBrowserService class provided in the SDK. There are two methods that we will want to override here in order to get started. The first is onGetRoot( String clientPackageName, int clientUid, Bundle rootHints ), which is called by the Auto media browser application when first interacting with our service. In this method, we're only going to add one line to return a new BrowserRoot object using our root identifier string (I've defined mine at the top of the class as a final variable of BROWSEABLE_ROOT), though you can also use this method to validate the calling package to verify that it should have access to your media assets and return null if it fails validation.
@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
return new BrowserRoot(BROWSEABLE_ROOT, null);
}
    The other method that we'll need to override is onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result). This method is called when the root item or any subsequent items are clicked on in the Android Auto media browser. This is where you will check the parentId parameter and build out the list of MediaBrowser.MediaItems based on that id in order to provide a file structure for your app. In order to simplify things, I've moved this logic into a separate method so that onLoadChildren() is easily digestible.
@Override
public void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result) {

List<MediaBrowser.MediaItem> items = getMediaItemsById( parentId );
if( items != null ) {
result.sendResult( items );
}

}
    Now before I go into explaining how getMediaItemsById() works for providing media items and folders, I need to go over a bit of the prep code that I placed into onCreate() for this service. I created a model object called Song that consists of general information about the media object, such as artist, title, genre, etc. and a quick generator class to create the media objects that will be used for this sample. How you create and work with your own media objects will depend on your implementation and applications, so I tried to keep mine as simple as possible The Song model object source can be found here, and the generator helper class can be found here. With that in mind, my onCreate() looks like this
 @Override
public void onCreate() {
super.onCreate();

mSongs = SongGenerator.generateSongs();

initMediaSession();
}
    where initMediaSession simply builds out the MediaSession and Token used for this sample:
private void initMediaSession() {
mMediaSession = new MediaSession( this, "Android Auto Audio Demo" );
mMediaSession.setActive( true );
mMediaSession.setCallback( mMediaSessionCallback );

mMediaSessionToken = mMediaSession.getSessionToken();
setSessionToken( mMediaSessionToken );
}
    With this foundation in mind, we can move over to looking at getMediaItemsById(). This method will take an id, either our root id or one associated with a clicked item in the Auto browser, and create a folder or file structure to display to the user.
 private List<MediaBrowser.MediaItem> getMediaItemsById( String id ) {
List<MediaBrowser.MediaItem> mediaItems = new ArrayList<MediaBrowser.MediaItem>();
if( BROWSEABLE_ROOT.equalsIgnoreCase( id ) ) {
mediaItems.add( generateBrowseableMediaItemByGenre(BROWSEABLE_CAJUN) );
mediaItems.add( generateBrowseableMediaItemByGenre(BROWSEABLE_JAZZ) );
mediaItems.add( generateBrowseableMediaItemByGenre(BROWSEABLE_ROCK) );
} else if( !TextUtils.isEmpty( id ) ) {
return getPlayableMediaItemsByGenre( id );
}

return mediaItems;
}
    As you can see, if BROWSEABLE_ROOT is the clicked item, we create three media items with the method generateBrowseableMediaItemByGenre() and pass in one of three strings defined at the top of the class, and then return those items as part of our MediaItems list. generateBrowseableMediaItemByGenre() simply creates a MediaItem with the FLAG_BROWSEABLE flag set and some basic information to define the folders, as shown here:
private MediaBrowser.MediaItem generateBrowseableMediaItemByGenre( String genre ) {
MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
mediaDescriptionBuilder.setMediaId( genre );
mediaDescriptionBuilder.setTitle( genre );
mediaDescriptionBuilder.setIconBitmap( BitmapFactory.decodeResource( getResources(), R.drawable.folder ) );

return new MediaBrowser.MediaItem( mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE );
}
    This will provide us with three folders for three different genres under our root structure, like so


    When one of these three folders is tapped on by a user, we return to onLoadChildren(), which in turn calls getMediaItemsById() again, but this getMediaItemsById() will have an id that is not our root folder, so getPlayableMediaItemsByGenre( String genre ) is called. This will loop through all of our songs and create a list of MediaItems based on the passed genre
private List<MediaBrowser.MediaItem> getPlayableMediaItemsByGenre( String genre ) {
if( TextUtils.isEmpty( genre ) )
return null;

List<MediaBrowser.MediaItem> mediaItems = new ArrayList();

for( Song song : mSongs ) {
if( !TextUtils.isEmpty( song.getGenre() ) && genre.equalsIgnoreCase( song.getGenre() ) ) {
mediaItems.add( generatePlayableMediaItem( song ) );
}
}
return mediaItems;
}
    where generatePlayableMediaItem( song ) creates a MediaItem with properties based on the song and the FLAG_PLAYABLE flag set
private MediaBrowser.MediaItem generatePlayableMediaItem( Song song ) {
if( song == null )
return null;

MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
mediaDescriptionBuilder.setMediaId( song.getuId() );

if( !TextUtils.isEmpty( song.getTitle() ) )
mediaDescriptionBuilder.setTitle( song.getTitle() );

if( !TextUtils.isEmpty( song.getArtist() ) )
mediaDescriptionBuilder.setSubtitle( song.getArtist() );

if( !TextUtils.isEmpty( song.getThumbnailUrl() ) )
mediaDescriptionBuilder.setIconUri( Uri.parse( song.getThumbnailUrl() ) );

return new MediaBrowser.MediaItem( mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE );
}
    At this point we should be able to click through the Auto browser to get to a screen similar to this:



    Now if you remember, back in initMediaSession() we had this line:
mMediaSession.setCallback( mMediaSessionCallback );
    mMediaSessionCallback is defined at the top of our class and is meant to handle actions from the buttons available in the Auto media player. This is where we start and stop our media, define which controls are available and set the metadata for our media that will be displayed by Android Auto.
private MediaSession.Callback mMediaSessionCallback = new MediaSession.Callback() {
@Override
public void onPlay() {
super.onPlay();

toggleMediaPlaybackState( true );
playMedia( PreferenceManager.getDefaultSharedPreferences( getApplicationContext() ).getInt( CURRENT_MEDIA_POSITION, 0 ), null );
}

//This is called when the pause button is pressed, or when onPlayFromMediaId is called in
//order to pause any currently playing media
@Override
public void onPause() {
super.onPause();

toggleMediaPlaybackState( false );
pauseMedia();
}

@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
super.onPlayFromMediaId(mediaId, extras);

initMediaMetaData( mediaId );
toggleMediaPlaybackState( true );
playMedia( 0, mediaId );
}
};
    As you can see, I have a few helper methods here that I'll go over next. onPlayFromMediaId( String mediaId, Bundle extras ) is the callback that the system uses when a MediaItem with the FLAG_PLAYABLE property is clicked. In this callback we first set up our metadata for displaying information about the MediaItem with initMediaMetaData()
private void initMediaMetaData( String id ) {

for( Song song : mSongs ) {
if( !TextUtils.isEmpty( song.getuId() ) && song.getuId().equalsIgnoreCase( id ) ) {
MediaMetadata.Builder builder = new MediaMetadata.Builder();

if( !TextUtils.isEmpty( song.getTitle() ) )
builder.putText( MediaMetadata.METADATA_KEY_TITLE, song.getTitle() );

if( !TextUtils.isEmpty( song.getArtist() ) )
builder.putText( MediaMetadata.METADATA_KEY_ARTIST, song.getArtist() );

if( !TextUtils.isEmpty( song.getGenre() ) )
builder.putText( MediaMetadata.METADATA_KEY_GENRE, song.getGenre() );

if( !TextUtils.isEmpty( song.getAlbum() ) )
builder.putText( MediaMetadata.METADATA_KEY_ALBUM, song.getAlbum() );

if( !TextUtils.isEmpty( song.getAlbumUrl() ) )
builder.putText( MediaMetadata.METADATA_KEY_ALBUM_ART_URI, song.getAlbumUrl() );

mMediaSession.setMetadata( builder.build() );
}
}
}
     Then we toggle our MediaState and set our controls with toggleMediaPlaybackState(). Note that the controls are set using setAction on our PlaybackState.Builder() by bitwise ORing our actions, so when our media is playing we'll display the Pause, Skip to Next and Skip to Previous, and when our media is paused we'll only display the Play button.
private void toggleMediaPlaybackState( boolean playing ) {
PlaybackState playbackState;
if( playing ) {
playbackState = new PlaybackState.Builder()
.setActions( PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS )
.setState( PlaybackState.STATE_PLAYING, 0, 1 )
.build();
} else {
playbackState = new PlaybackState.Builder()
.setActions( PlaybackState.ACTION_PLAY_PAUSE )
.setState(PlaybackState.STATE_PAUSED, 0, 1)
.build();
}

mMediaSession.setPlaybackState( playbackState );
}
 


        The final methods in our service simply control starting, pausing and resuming our media. For this sample I only have one MP3 available to keep things simple, though you can set what plays based on the media id passed to the callback methods. The MP3 I'm using here was provided by a good friend of mine, Geoff Ledak, who you can catch DJing online at AfterHoursDJs.org.
private void playMedia( int position, String id ) {
if( mMediaPlayer != null )
mMediaPlayer.reset();

//Should check id to determine what to play in a real app
int songId = getApplicationContext().getResources().getIdentifier("geoff_ledak_dust_array_preview", "raw", getApplicationContext().getPackageName());
mMediaPlayer = MediaPlayer.create(getApplicationContext(), songId);

if( position > 0 )
mMediaPlayer.seekTo( position );
mMediaPlayer.start();

}

private void pauseMedia() {
if( mMediaPlayer != null ) {
mMediaPlayer.pause();
PreferenceManager.getDefaultSharedPreferences( this ).edit().putInt( CURRENT_MEDIA_POSITION,
mMediaPlayer.getCurrentPosition() ).commit();
}
}

@Override
public void onDestroy() {
super.onDestroy();
if( mMediaPlayer != null ) {
pauseMedia();
mMediaPlayer.release();
PreferenceManager.getDefaultSharedPreferences( this ).edit().putInt( CURRENT_MEDIA_POSITION,
0 ).commit();
}

}
    One more thing we can do is add a colorAccent to our app theme in order to add a bit of personalization to fit our app under /res/values-v21
<style name="AppTheme" parent="android:Theme.Material.Light">
<item name="android:colorAccent">@android:color/holo_green_dark</item>
</style>
    And with that, we should have a working Android Auto browser service plugin. I hope you've enjoyed the tutorial, and good luck building your own implementations for this new platform!
Read More..

Minggu, 25 Januari 2015

Implementing and Using Custom Drawable States

    One of the things that Android comes with out of the box is pressed states, which allows colors and drawables to change what's displayed on a view based on whether or not a user is pressing down on that view. In this post I'm going to take that a step further and go over a simple example for how to implement custom drawable states that allow you to tailor your user experience based on data. All code for this post can be found on GitHub.

    To keep things simple, the first class we're going to define is an enum containing the expected states that we want to work with. In this case we're going to use a traffic signal set of "Go", "Slow Down" and "Stop".

public enum CustomState {
GO,
SLOW_DOWN,
STOP
}

    Next we're going to want to create an 'attrs.xml' file to store the attributes that we'll use for our states

<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="state_go" format="boolean" />
<attr name="state_slow_down" format="boolean" />
<attr name="state_stop" format="boolean" />
</resources>

    Once our attributes are defined, we're going to move to where the magic happens: our custom View (CustomDrawableTextView.java, in this example). The first thing we're going to do is add a set of int arrays that represent each state that we want to support as member variables. These int arrays are going to be used later to merge in with our drawable states. We're also going to want to create a variable to keep track of which state is currently active from our custom enum.

 protected static int[] STATE_GO = { R.attr.state_go };
protected static int[] STATE_SLOW_DOWN = { R.attr.state_slow_down };
protected static int[] STATE_STOP = { R.attr.state_stop };

private CustomState mState;

    In order to drive this view, I've added an update method that is manually called on a timer from MainActivity, though this View could just as easily be built to use an event bus or other technique to drive the data. The key thing to notice is that the new desired state is passed to and saved by the View, and refreshDrawableState() is called.

 public void update( CustomState state ) {

if( state != null )
mState = state;

if( CustomState.GO.equals( mState ) ) {
setText( "GO" );
} else if( CustomState.SLOW_DOWN.equals( mState ) ) {
setText( "SLOW DOWN" );
} else if( CustomState.STOP.equals( mState) ) {
setText( "STOP" );
}

refreshDrawableState();
}

    The final thing we need to do with our custom View is override onCreateDrawableState to add in our custom state attributes. We do this by saving the result of the super method with an incremented parameter passed to it, and then calling mergeDrawableStates with the result and our int array attribute to add our custom attribute to the active drawable states for our View.

 @Override
protected int[] onCreateDrawableState(int extraSpace) {
if( mState == null )
return super.onCreateDrawableState(extraSpace);

final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);

if( CustomState.GO.equals( mState ) ) {
mergeDrawableStates( drawableState, STATE_GO );
return drawableState;
} else if( CustomState.SLOW_DOWN.equals( mState ) ) {
mergeDrawableStates( drawableState, STATE_SLOW_DOWN );
return drawableState;
} else if( CustomState.STOP.equals( mState) ) {
mergeDrawableStates( drawableState, STATE_STOP );
return drawableState;
} else {
return super.onCreateDrawableState(extraSpace);
}
}

    Now that that's all set, let's define a new drawable selector file to take advantage of our custom drawable states

<selector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:customapp="http://schemas.android.com/apk/res-auto">
<item customapp:state_go="true" android:drawable="@android:color/holo_green_light" />
<item customapp:state_slow_down="true" android:drawable="@color/yellow" />
<item customapp:state_stop="true" android:drawable="@android:color/holo_red_light" />
<item android:drawable="@android:color/white" />
</selector>

    Notice the custom attributes of state_go, state_slow_down and state_stop that match our attrs.xml file and our custom int arrays from our view. Android will automatically handle displaying the proper color for this state drawable now that we've laid out the groundwork for it. Finally we'll assign this drawable as a background for our custom View, and we'll see the color change as our states change.

 <com.ptrprograms.customdrawablestates.CustomDrawableTextView
android:id="@+id/custom_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="40sp"
android:gravity="center"
android:text="@string/hello_world"
android:background="@drawable/custom_state_background"/>





Read More..

Minggu, 28 Desember 2014

Flightradar24 - Flight Tracker

Turn your phone or tablet into an air traffic radar and see planes around the world move in real-time on a detailed map. Or point your Android device at a plane in the sky to find out where it�s going and more. Discover today why millions are already using Flightradar24 - Flight Tracker.
Features that have helped make Flightradar24 - Flight Tracker the #1 selling app in 100+ countries and the #1 Travel app in 140+ countries (United States, France, United Kingdom, Germany and more) include:
* Watch planes move in real-time on detailed map
* Identify planes flying overhead by simply pointing your device at the sky
* Experience what the pilot of a an aircraft sees in real-time and in 3D
* Tap on a plane for comprehensive flight and aircraft information such as route, estimated time of arrival, actual time of departure, aircraft type, speed, altitude, and high-resolution picture
* Easy to search for individual flights using flight number, airport, or airline
* Easy to filter by airline, aircraft, altitude, speed, and more
* Easy to set bookmarks to enable quick navigation to areas of interest
* Turn the device into the arrivals and departures board of any major airport and get real-time status updates for flights plus current airport weather conditions (in-app purchase)
* Realistic aircraft symbols (in-app purchase)
* Set up custom alerts based on airline, aircraft type, flight number or registration (in-app purchase)
HOW IT WORKS
Most aircraft are equipped with so called ADS-B transponders that transmit positional data. Flightradar24 has a rapidly growing network of several thousand ground stations around the world to receive this data that then shows up as aircraft moving on a map in the app. In an expanding number of regions Flightradar24, with the help of multilateration, is able to calculate the positions of aircraft that don�t have ADS-B transponders.
Traditional radar data is also used in the app thanks to a direct feed from the US Federal Aviation Administration (FAA).
http://www.flightradar24.com/how-it-works
***IMPORTANT NOTICES***
Minimum required screen resolution is 320x480px.
If you are only interested in tracking air traffic in a specific region, we suggest that you check Flightradar24.com BEFORE purchasing the app. Flightradar24 provides unrivaled positional aircraft data coverage around the world but there are areas where we don�t have coverage.
Cover art
Overview of coverage as of December 2014:
* Europe: close to 100%
* North America: 100% of US and Canada via slightly delayed radar data. Real-time coverage for most of US, Canada and Mexico for ADS-B equipped aircraft
* South America: Substantial coverage in most countries including Argentina, Bolivia, Brazil, Chile, Colombia, Ecuador and Venezuela.
* Asia: Substantial coverage in most major Asian countries including Japan, India, Indonesia, South Korea, Thailand, UAE, Malaysia, Taiwan and many other countries. Rapidly expanding coverage in China
* Oceania: Nearly complete coverage in Australia and New Zealand
* Africa: Substantial coverage in South Africa. Limited but growing coverage in the rest of the region
Find out how you can help:
http://www.flightradar24.com/increase-coverage
Coverage is subject to change at any time.
CONNECT WITH FLIGHTRADAR24
Like us on Facebook http://www.facebook.com/flightradar24 and follow us on Twitter http://twitter.com/flightradar24 for the latest on Flightradar24.
HELP & SUPPORT
forum.flightradar24.com/threads/89-Important-Read-this-before-you-ask-questions
flightradar24.com/contact-us.
DISCLAIMER
The use of this app is strictly limited to entertainment purposes. This specifically excludes activities that might endanger yourself or the lives of others. Under no circumstances will the developer of this app be held responsible for incidents resulting from the use of the data or its interpretation or its use contrary to this agreement. We reserve the right to anonymously track and report a user's activity inside of app.



Read More..

The Room

Welcome to The Room, a physical puzzler, wrapped in a mystery game, inside a beautifully tactile 3D world.
*****************
Praise for The Room:
� Eurogamer �...resist hurrying and savour every sliding panel and twist of a dial. The Room is the cave of mysteries from everyone's childhood; a perfect encapsulation of the fear and pleasure of discovery."
� Gametrailers �The touchscreen controls really shine. A must play� Penny Arcade �This isn�t a title that is hindered by touch controls as much as its set free by them"
� Arcade Sushi "I couldn�t stop playing this fascinating brain teaser...beautifully crafted"
� Indie Game Magazine "Fireproof Games has hit a home run"
� Pocket Gamer �More suspense than a thousand bullet-riddled shooters"
� Game Informer �Excellently produced, clever puzzles and spine-tingling music"
� Jaysisgames "The immersion overrides any sense of your surroundings beyond the box" "The Room is eye candy for the mind."
� IGN �The most realistically rendered objects in a mobile title to date.�
Cover art
******
How are you, old friend? If you�re reading this, then it worked. I only hope you can still forgive me.
We�ve never seen eye to eye on my research, but you must put such things behind you. You are the only one to whom I can turn. You must come at once, for we are all in great peril. I trust you remember the house? My study is the highest room.
Press forward with heart. There is no way back now.
AS.
******
Fireproof Games are very proud to bring you our greatest creation, a mind-bending journey filled with beauty, peril and mystery in equal measure. Be transported into a unique space that blends spellbinding visuals with intriguing problems to solve.
� Unsettlingly realistic graphics: The most natural looking visuals ever seen on a mobile device.
� Spine-tingling single finger controls: touch controls so natural you can play with one digit, to fully navigate this mysteriously beautiful 3D world.
� Fantastical pick-up-and-play design: Easy to start, hard to put down, the secrets of The Room will immerse you before you even know you're playing.
� Compelling layers of mystery: think you know what you're looking at? Think again.


Read More..

Riptide GP2

Riptide GP�2 kicks everything into overdrive, with intense online multi-player races, upgradeable hydro jets and riders, improved graphics, an all-new career mode, and a whole new stunt system with dozens of new tricks!
Featuring rocket-powered hydro jets racing around futuristic tracks across a dynamic and interactive water surface, Riptide GP2 delivers a fast, fun, and visually stunning racing experience.
From Vector Unit, developers of acclaimed racing games Riptide GP, Beach Buggy Blitz, Shine Runner, and Hydro Thunder Hurricane!

� � GAME FEATURES � �
� ONLINE MULTI-PLAYER
� Show off your skills and your custom-tuned hydro jet in 4-way online battles with friends and players around the world!
� CHALLENGE YOUR FRIENDS
� Race against your friends' best times in the exciting VR Challenge mode.
� ALL NEW CAREER MODE
� Play through Race, Hot Lap, Elimination, and Freestyle events to earn XP and cash that can be used to upgrade your hydro jet, unlock new stunts, and increase your rider's performance.
� ALL NEW WATERCRAFT
� Collect 9 powerful new hydro jets, and upgrade their performance and colors to gain an edge on your competition.
� ALL NEW STUNT SYSTEM
� Unlock and master 25 outrageous new stunts. Wow the crowd, charge your boost and make your competition eat wake.
� GAME THE WAY YOU WANT
� Seamlessly supports multiple control configurations for tilt, touch-screen, and gamepad play.
� GOOGLE PLAY GAME SERVICES
� Race online, earn achievements and keep your game synced to the cloud with your Google account.
� CUTTING EDGE TECH
� Powered by the all-new Vector Engine 4, Riptide GP2 builds on the original game�s stunning visuals, with extra-detailed HD graphics!
Cover art

� � CUSTOMER SUPPORT � �
If you encounter a problem running the game, please email the device you're using, Android OS version, and a detailed description of your problem to support@vectorunit.com.
We GUARANTEE if we can't fix your problem we'll give you a refund. But we can't help you if you just leave your problem in a review.
For fastest support on most common issues please visit:
www.vectorunit.com/support

� � MORE INFORMATION � �
Be the first to hear about updates, download custom images, and interact with the developers!
Like us on Facebook at www.facebook.com/VectorUnit
Follow us on Google+ at www.vectorunit.com/+
Follow us on Twitter @vectorunit.
Visit our web page at www.vectorunit.com
We welcome comments and suggestions for future improvements. If you have a suggestion, or just want to say hi, email us at info@vectorunit.com.


Read More..

Hitman GO


Hitman GO is included in this year�s Google Play Games Sale so don�t miss this chance, grab your copy of this award winning and critically acclaimed mobile interpretation of the Hitman franchise today!
Get your daily fix of Agent 47 with this elegant, strategy-based Hitman game!
5/5 Slide to Play: "�this is one to check out."
4/5 Joystiq: "Hitman GO is an impressive debut for Square Enix Montreal.�
4/5 Pocket Gamer - SILVER AWARD: "Hitman GO is a clever and refreshingly original puzzle game"
4/5 Polygon: "Hitman GO is a great minimalist take on the series' big ideas"
5/5 Pocket-lint: "Hitman GO is a clever and intelligent way of bringing a much-loved franchise to a mobile platform"
Cover art
Hitman GO is a turn-based puzzle game with beautifully rendered diorama-style set pieces. You will strategically navigate fixed spaces on a grid to avoid enemies and take out your target or infiltrate well-guarded locations. You really have to think about each move and all the Hitman tools of the trade you would expect are included; disguises, distractions, sniper rifles and even 47�s iconic Silverballers.
With Hitman GO, you�ll experience:
� Challenging puzzles that put your assassination skills to the test
� Beautiful scale model-style visuals
� Environments with secret passageways and off-limit areas
� Agent 47�s tools of the trade: Distractions, disguises, hiding spots, sniper rifles and even the iconic Silverballers
� Different enemy types with unique and deadly behaviours
� Different ways of completing each level, silently or forcefully


Read More..