Sabtu, 19 Juli 2014

Creating a Media App for AndroidTV

    One of the newest technologies released by Google this year at I/O is the AndroidTV, a platform that takes what they learned from the Chromecast and Google TV and makes it even better by building on the Android OS. For this tutorial I will go over putting together a basic media app for the AndroidTV in order to help other developers get their own apps and content out for this platform in order to push for its success. While the base Android project from Android Studio does let you create a media app, it provides far more than the basics and needs a lot of clean up, so I went ahead and did that clean up and will go over the different methods used in order to hopefully provide some clarity. While I happened to have received my AndroidTV ADT-1 developer box at I/O by luckily sitting in on the session, other developers can request a kit through Google's site. The source code for this project can be found on GitHub.

    The first thing we need to do in a new Android project is set up our gradle file. AndroidTV is currently using the Android-L SDK and has dependencies on the new support library for leanback, recyclerview, and appcompat, then third party libraries used are GSON and Picasso

android {
compileSdkVersion 'android-L'
buildToolsVersion "20.0.0"
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:recyclerview-v7:+'
compile 'com.android.support:leanback-v17:+'
compile 'com.android.support:appcompat-v7:+'
compile 'com.squareup.picasso:picasso:2.3.2'
compile 'com.google.code.gson:gson:2.2.4'
}

    Next we need to set up our manifest to use the Internet permission and know what activities we will use in the project. The main two activities for this project are the MainActivity and PlayerActivity, though I also include Bonus.SlothActivity, which I added to this project to play around with creating a custom activity and decided to leave in the app to show that you can use any activity on this platform (I'll go over this at the end, since I had fun with it). Note that the category the the MainActivity intent-filter is leanback_launcher, as this is what allows the app icon to show up on the television.

<uses-permission android:name="android.permission.INTERNET" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />

<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".Activity.MainActivity"
android:label="@string/app_name"
android:logo="@drawable/giraffes"
android:screenOrientation="landscape" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".Activity.PlayerActivity"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
<activity android:name=".Activity.DetailsActivity" />

<activity
android:name=".Bonus.SlothActivity"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />

</application>

    Under our styles file, we need to set AppTheme to extends the Leanback theme, otherwise the app will not load, however it's still not certain if this will be the case when Android L and AndroidTV officially launch.

<style name="AppTheme" parent="@style/Theme.Leanback">

    Now that the general configuration is set, we should be able to start moving into the actual java code. MainActivity is simply a base activity with a layout that contains a fragment, so we'll put together the layout for now and then jump into how MainFragment works.

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}


<?xml version="1.0" encoding="utf-8"?>
<fragment
xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_browse_fragment"
android:name="com.ptrprograms.androidtvmediaplayer.Fragment.MainFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:deviceIds="tv"
tools:ignore="MergeRootFrame" />


    MainFragment.java extends the new BrowseFragment provided in the support library, which gives us a lot of the functionality we need for a media app such as the fast lane (side navigation bar) and view for the rows of content. The only member variable that we'll need here is a List of Movie objects, where Movie is just a standard serialized model object containing information about a movie like the title, category, description and URLs to associated images and the video.

public class Movie implements Serializable {
private String title;
private String description;
private String studio;
private String videoUrl;
private String category;
private String cardImageUrl;
private String backgroundImageUrl;
...

    The first thing that MainFragment does in onActivityCreated is load the data for the list of Movie objects. In this case I'm simply using a locally stored JSON file and converting it into the list using GSON, but this method can be used however works for your project to load in data, be it locally or from an online API.

private void loadData() {
String json = Utils.loadJSONFromResource( getActivity(), R.raw.movies );
Gson gson = new Gson();
Type collection = new TypeToken<ArrayList<Movie>>(){}.getType();
mMovies = gson.fromJson( json, collection );
}

   Next we can go ahead and initialize the base UI by setting the title in the top right of the screen, enabling category headers, setting the action the back button on the remote should take (opening the fast lane or returning to the home screen), setting fragment colors and background. I should mention that I only set the background once in MainFragment, however one set of polish that you can add to your app is changing the background whenever a new item is selected in order to match the selected item. Also, there is a method built into BrowseFragment called setBadgeDrawable which lets you pass a drawable resource id to display an image in the top right corner rather than plain text.

private void initUI() {
setTitle( getString( R.string.browse_title ) );
setHeadersState( HEADERS_ENABLED );

//Back button goes to the fast lane, rather than home screen
setHeadersTransitionOnBackEnabled( true );

setBrandColor( getResources().getColor( R.color.fastlane_background ) );
setSearchAffordanceColor( getResources().getColor( R.color.search_button_color ) );
setBackground();
}

Example of the fast lane with background color, search circle with background color and title text. I stuck with a black/grey theme, which may not be the best for examples, but was far easier on my eyes without having to be great at design :)
    After our colors and base UI are set, we can start populated data in order to get the categories in the fast lane and cards that you see in the image above. We do this by creating a new adapter of ListRowPresenters to hold the rows, then create sets of ListRows containing our cards to put into that adapter (think linked list of linked lists) before assigning the adapter of ListRows to the fragment, similar to using setAdapter in a ListFragment. For this method each row will consist of cards representing Movie objects from the same category - comedy, horror, or action.

private void loadRows() {

ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter( new ListRowPresenter() );
CardPresenter cardPresenter = new CardPresenter();

List<String> categories = getCategories();
if( categories == null || categories.isEmpty() )
return;

for( String category : categories ) {
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter( cardPresenter );
for( Movie movie : mMovies ) {
if( category.equalsIgnoreCase( movie.getCategory() ) )
listRowAdapter.add( movie );
}
if( listRowAdapter.size() > 0 ) {
HeaderItem header = new HeaderItem( rowsAdapter.size() - 1, category, null );
rowsAdapter.add( new ListRow( header, listRowAdapter ) );
}
}

setupPreferences( rowsAdapter );
setAdapter( rowsAdapter );

}

    As you can see in the second loop, for each movie we check to see if the movie category matches the current row category, and if it does we add it to our adapter of CardPresenter objects. The CardPresenter extends the Presenter class to bind our movie object to a programmatically created view and also implements the ViewHolder pattern. The main methods for the card presenter are shown here and are from CardPresenter.java

static class ViewHolder extends Presenter.ViewHolder {
private ImageCardView mCardView;
private PicassoImageCardViewTarget mImageCardViewTarget;

public ViewHolder( View view ) {
super( view );
mCardView = (ImageCardView) view;
mImageCardViewTarget = new PicassoImageCardViewTarget( mCardView );
}

public ImageCardView getCardView() {
return mCardView;
}

protected void updateCardViewImage( Context context, String link ) {
Picasso.with( context )
.load(link)
.resize( mCardView.getResources().getInteger( R.integer.card_presenter_width ), mCardView.getResources().getInteger( R.integer.card_presenter_height ) )
.centerCrop()
.error( R.drawable.default_background )
.into( mImageCardViewTarget );
}
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
ImageCardView cardView = new ImageCardView( parent.getContext() );
cardView.setFocusable( true );
cardView.setFocusableInTouchMode( true );
return new ViewHolder(cardView);
}

@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
Movie movie = (Movie) item;

if ( !TextUtils.isEmpty( movie.getCardImageUrl() ) ) {
((ViewHolder) viewHolder).mCardView.setTitleText( movie.getTitle() );
((ViewHolder) viewHolder).mCardView.setContentText( movie.getStudio() );
((ViewHolder) viewHolder).mCardView.setMainImageDimensions(
( (ViewHolder) viewHolder ).mCardView.getContext().getResources().getInteger( R.integer.card_presenter_width ),
( (ViewHolder) viewHolder ).mCardView.getContext().getResources().getInteger( R.integer.card_presenter_height ) );
( (ViewHolder) viewHolder ).updateCardViewImage( ( (ViewHolder) viewHolder ).getCardView().getContext(), movie.getCardImageUrl() );
}
}

Switching between rows using the fast lane
Example of the cards full view in a row with their category header
    Returning to our loadRows method in MainFragment.java, we have a method called setupPreferences. All this does is low another row at the end of our rows adapter that contains a PreferenceCardPresenter (similar to the CardPresenter class above, but greatly simplified to just display text in a square). I'm only adding one preference here as an example.

private void setupPreferences( ArrayObjectAdapter adapter ) {

HeaderItem gridHeader = new HeaderItem( adapter.size(), "Preferences", null );
PreferenceCardPresenter mGridPresenter = new PreferenceCardPresenter();
ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter( mGridPresenter );
gridRowAdapter.add( getResources().getString( R.string.sloth ) );
adapter.add( new ListRow( gridHeader, gridRowAdapter ) );

}


    The final thing that we need to do once MainActivity/MainFragment are created is initialize our event listeners. The search button in BrowseFragment has a special method for this called setOnSearchClickedListener, however I will leave implementing search for another post. The other event listener that needs to be set is the BrowseFragment's setOnItemClickedListener like so

private void setupEventListeners() {
setOnItemClickedListener( getDefaultItemClickedListener() );
setOnSearchClickedListener( new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getActivity(), "Implement your own in-app search", Toast.LENGTH_LONG).show();
}
});
}

    where the default item clicked listener checks to see if the data item is a Movie object, and if it is then it packages it into an intent to launch the DetailsActivity which simply houses VideoDetailsFragment. If it is a string, then we know it belongs to our Preference row (though we could just as easily have created a special Preference object to populate that row) and we launch the SlothActivity that I will show later.

protected OnItemClickedListener getDefaultItemClickedListener() {
return new OnItemClickedListener() {
@Override
public void onItemClicked( Object item, Row row ) {
if( item instanceof Movie ) {
Movie movie = (Movie) item;
Intent intent = new Intent( getActivity(), DetailsActivity.class );
intent.putExtra( VideoDetailsFragment.EXTRA_MOVIE, movie );
startActivity( intent );
} else if( item instanceof String ) {
if( ((String) item).equalsIgnoreCase( getString( R.string.sloth ) ) ) {
Intent intent = new Intent( getActivity(), SlothActivity.class );
startActivity( intent );
}
}
}
};
}

    The VideoDetailsFragment extends the new DetailsFragment class and provides a quick overview of a media object with options for the user to select. The first thing we do in this fragment is retrieve the Movie object from the intent that started the DetailsActivity and set up the background for the activity using Picasso from the Movie's background URL property

private void initBackground() {
BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
backgroundManager.attach(getActivity().getWindow());
mBackgroundTarget = new PicassoBackgroundManagerTarget( backgroundManager );

mMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);

if( mSelectedMovie != null && !TextUtils.isEmpty( mSelectedMovie.getBackgroundImageUrl() ) ) {
try {
updateBackground(new URI(mSelectedMovie.getBackgroundImageUrl()));
} catch (URISyntaxException e) { }
}
}

protected void updateBackground(URI uri) {
if( uri.toString() == null ) {
try {
uri = new URI("");
} catch( URISyntaxException e ) {}
}

Picasso.with(getActivity())
.load( uri.toString() )
.error( getResources().getDrawable( R.drawable.default_background ) )
.resize( mMetrics.widthPixels, mMetrics.heightPixels )
.into( mBackgroundTarget );
}

    Next we use an AsyncTask to create the DetailsOverviewRow and assign the selected Movie object to it, populate information and set up our actions (the buttons at the bottom of the details card) to do their jobs. In this case I only have one action, and it sends an intent to the PlayerActivity in order to play the selected item.

@Override
protected DetailsOverviewRow doInBackground( Movie... movies ) {
mSelectedMovie = movies[0];
DetailsOverviewRow row = null;
try {
row = new DetailsOverviewRow( mSelectedMovie );
Bitmap poster = Picasso.with( getActivity() )
.load( mSelectedMovie.getCardImageUrl() )
.resize(Utils.dpToPx( getActivity().getResources().getInteger( R.integer.detail_thumbnail_square_size ), getActivity().getApplicationContext() ),
Utils.dpToPx( getActivity().getResources().getInteger( R.integer.detail_thumbnail_square_size ), getActivity().getApplicationContext() ) )
.centerCrop()
.get();
row.setImageBitmap( getActivity(), poster );
} catch ( IOException e ) {
getActivity().finish();
return null;
} catch( NullPointerException e ) {
getActivity().finish();
return null;
}

row.addAction( new Action( ACTION_WATCH, getResources().getString(
R.string.watch ), getResources().getString( R.string.watch_subtext) ) );

return row;
}

@Override
protected void onPostExecute(DetailsOverviewRow detailRow) {
if( detailRow == null )
return;

ClassPresenterSelector ps = new ClassPresenterSelector();
DetailsOverviewRowPresenter dorPresenter =
new DetailsOverviewRowPresenter( new DetailsDescriptionPresenter() );
// set detail background and style
dorPresenter.setBackgroundColor( getResources().getColor( R.color.detail_background ) );
dorPresenter.setStyleLarge( true );
dorPresenter.setOnActionClickedListener( new OnActionClickedListener() {
@Override
public void onActionClicked( Action action ) {
if (action.getId() == ACTION_WATCH ) {
Intent intent = new Intent( getActivity(), PlayerActivity.class );
intent.putExtra( EXTRA_MOVIE, mSelectedMovie );
intent.putExtra( EXTRA_SHOULD_AUTO_START, true );
startActivity( intent );
}
}
});

ps.addClassPresenter( DetailsOverviewRow.class, dorPresenter );
ps.addClassPresenter( ListRow.class,
new ListRowPresenter() );


ArrayObjectAdapter adapter = new ArrayObjectAdapter( ps );
adapter.add( detailRow );
loadRelatedMedia( adapter );
setAdapter( adapter );
}



    Notice the loadRelatedMedia method in onPostExecute. This method is used to (you guessed it) load related media onto the details screen. The only thing I'm doing in this method is extracting all of the movies of the same category as the selected movie from the Movies List and displaying them using the CardPresenter presenter from MainFragment, though more appropriate related movies can be gathered or media apps that have that logic in place.

private void loadRelatedMedia( ArrayObjectAdapter adapter ) {

String json = Utils.loadJSONFromResource( getActivity(), R.raw.movies );
Gson gson = new Gson();
Type collection = new TypeToken<ArrayList<Movie>>(){}.getType();
List<Movie> movies = gson.fromJson( json, collection );
List<Movie> related = new ArrayList<Movie>();
for( Movie movie : movies ) {
if( movie.getCategory().equals( mSelectedMovie.getCategory() ) ) {
related.add( movie );
}
}

if( related.isEmpty() )
return;

ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter( new CardPresenter() );
for( Movie movie : related ) {
listRowAdapter.add( movie );
}

HeaderItem header = new HeaderItem( 0, "Related", null );
adapter.add( new ListRow( header, listRowAdapter ) );
}



    The final important part to a base media project is the actual video player. In this case all of that logic is handled by PlayerActivity.java (which I took from the Android sample app, rather than writing my own, though I do have a post on making a native video player from a few months ago that isn't as great as the Android sample player) and there isn't anything too out of the norm for a video player in this class. The most important part of this video player is the addition of a onKeyDown method that listens for input from the remote control dpad for controlling playback:

@Override
public boolean onKeyDown( int keyCode, KeyEvent event ) {
int currentPos = 0;
int delta = mDuration / getResources().getInteger( R.integer.scrub_segment_divisor );
if ( delta < getResources().getInteger( R.integer.min_scrub_time ) )
delta = getResources().getInteger( R.integer.min_scrub_time );

if ( mControllers.getVisibility() != View.VISIBLE ) {
mControllers.setVisibility( View.VISIBLE );
}
switch ( keyCode ) {
case KeyEvent.KEYCODE_DPAD_CENTER:
return true;
case KeyEvent.KEYCODE_DPAD_DOWN:
return true;
case KeyEvent.KEYCODE_DPAD_LEFT:
currentPos = mVideoView.getCurrentPosition();
currentPos -= delta;
if (currentPos > 0)
play(currentPos);
return true;
case KeyEvent.KEYCODE_DPAD_RIGHT:
currentPos = mVideoView.getCurrentPosition();
currentPos += delta;
if( currentPos < mDuration )
play( currentPos );
return true;
case KeyEvent.KEYCODE_DPAD_UP:
return true;
}

return super.onKeyDown(keyCode, event);
}

Let there be lips!
    And with that you can have a pretty solid media app for the AndroidTV. Now that the technical part is done, if you're not interested in some random ridiculousness, you're alright to stop reading now :)

    Still with me? Alright, cool. So earlier I added a preference square item to MainFragment, and when selected it launches an intent to SlothActivity. I wanted to play around with AndroidTV a bit and see if you can throw in a custom activity, and it turns out you can. With a bit of stylized changes to this project from Bakhtiyor Khodjayev from a couple years ago that originally displayed animated snowflakes falling, I threw together a great custom activity of a sloth putting in a hard days work. Sadly recording though ADB doesn't capture audio, and I removed the MP4 from the project because I didn't want the hassle of being busted for more copyright infringement than I'm (probably, who knows?) doing, but the original audio to this was Led Zeppelin's Boogie with Stu. Anywho, for your viewing pleasure, the custom SlothActivity



Read More..

Minggu, 06 Juli 2014

*** Deprecated. Official API has been released, so please use that :) Here's a sample project to replicate this blog's end product: https://github.com/PaulTR/AndroidDemoProjects/tree/master/WatchFaceWithOptions ***

    For this tutorial I will go over creating a custom watch face for the Android Wear. There are rumors that Google will be releasing a watch face builder class, but right now no one is sure when/if that will happen. The point of this tutorial is to be a central place for information on building custom watch faces with the current system, as the information is currently scattered across Reddit and Google+ posts.

    This demo app allows the user to select a school from a list and then displays the time in the corner with the school logo below it. The settings activity for selecting the school is not a requirement for the watch face, as you can choose to only display one type of face, but I included it to show an additional feature. I also removed the mobile activity launcher, as it's not a necessity for the app. The code for this app is available on GitHub.
Watch face for Fresno State
    The first thing that needs to be done is to open up the mobile and wear AndroidManifest.xml files and add the permissions that will be needed for selecting a watch face:

<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

    In the wear AndroidManifest.xml file, add the following properties to the application tag:

<application
android:allowBackup="true"
android:icon="@drawable/bulldog_wallpaper"
android:label="@string/app_name"
android:theme="@android:style/Theme.DeviceDefault" >

    and the following properties to the activity tag for the watch face activity. Note the intent-filter and meta-data tags in the activity section, as those are required for determining if the activity should be selectable as a watch face. I use a general icon for the preview here, but generally a screen shot of the actual watch face (like above) should be used:

<activity
android:name=".WatchFaceActivity"
android:label="@string/app_name"
android:enabled="true"
android:taskAffinity=""
android:allowEmbedded="true"
android:theme="@android:style/Theme.DeviceDefault.NoActionBar" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="com.google.android.clockwork.home.category.HOME_BACKGROUND" />
</intent-filter>
<meta-data
android:name="com.google.android.clockwork.home.preview"
android:resource="@drawable/bulldog_wallpaper"/>
</activity>

Preview icon for the watch face

    In order to have the list of schools in the application area, we need to have an intent-filter added to the SettingsActivity activity tag:

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

    Once the manifest is put together, we can go ahead and fill in SettingsActivity to display a list of schools and save the selected school in a shared preference. In this example I use an object called SchoolObj that simply loads a list of school names and codes for organizing data:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
setContentView( R.layout.activity_settings );
final SchoolObj schools = new SchoolObj();
ListView listView = (ListView) findViewById( R.id.list );
ArrayAdapter<String> adapter = new ArrayAdapter( this, android.R.layout.simple_list_item_1, schools.schoolList );
listView.setAdapter( adapter );
listView.setOnItemClickListener( new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
SharedPreferences pref = getSharedPreferences( WatchFaceActivity.SHARED_PREFERENCE, Context.MODE_PRIVATE );
SharedPreferences.Editor editor = pref.edit();
editor.putString( SHARED_PREFERENCE_SCHOOL, schools.schoolCodeList.get( position ) );
editor.commit();
finish();
}
});
}

List of schools for customizing the watch face
    The final part to this application is the actual watch face activity. In onCreate() we can take a reference to the containing view, the TextClock object and the ImageView where the school logo will be stored. When onResume() is called we to see if SharedPreferences contains a key for a selected school and then set the color of the TextClock, ImageView drawable and background color for the watch face. If the user has just installed the application and has not selected a watch face, we set a default face.

@Override
protected void onResume() {
super.onResume();
SharedPreferences pref = getSharedPreferences( SHARED_PREFERENCE, Context.MODE_PRIVATE );

if( pref.contains( SettingsActivity.SHARED_PREFERENCE_SCHOOL ) ) {
String schoolCode = pref.getString( SettingsActivity.SHARED_PREFERENCE_SCHOOL, "" );
loadSchoolWatchFace( schoolCode );
} else {
mBackground.setImageResource( R.drawable.bulldog_wallpaper );
mClock.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
mContainer.setBackgroundColor(getResources().getColor( android.R.color.white ) );
}
}

    It is important that we set these properties every time that the activity resumes, as we must set the watch face to a "blank state" when the watch is put into a power conserving off state and onPause() is called

@Override
protected void onPause() {
super.onPause();
mBackground.setImageDrawable( null );
mClock.setTextColor( getResources().getColor( android.R.color.white ) );
mContainer.setBackgroundColor( getResources().getColor( android.R.color.black ) );
}

Watch Blank State
    The loadSchoolWatchFace( schoolCode ) method simply checks the school code in a set of if/else statements to set the proper image and colors, just like the default else portion of onResume()

private void loadSchoolWatchFace( String schoolCode ) {
if( "cuboulder".equals( schoolCode ) ) {
mBackground.setImageResource( R.drawable.cuboulder_wallpaper );
mClock.setTextColor(getResources().getColor(android.R.color.holo_orange_light));
} else if( "fsu".equals( schoolCode ) ) {
mBackground.setImageResource( R.drawable.floridastate_wallpaper );
mClock.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
} else if( "ucsc".equals( schoolCode ) ) {
mBackground.setImageResource( R.drawable.bananaslugs_logo );
mClock.setTextColor(getResources().getColor(android.R.color.holo_orange_light));
} else if( "berkeley".equals( schoolCode ) ) {
mBackground.setImageResource( R.drawable.berkeley_wallpaper );
mClock.setTextColor(getResources().getColor(android.R.color.holo_orange_light ) );
} else {
//Default to Fresno
mBackground.setImageResource( R.drawable.bulldog_wallpaper );
mClock.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
}

mContainer.setBackgroundColor(getResources().getColor(android.R.color.white));
}
Florida State University watch face
UC Santa Cruz Banana Slugs watch face

    And that's all there is to it. The important parts to add are the manifest intent-filter and metadata items, and onResume()/onPause() for the watch face, then the rest adds functionality to the app. This can also work with an analog watch face and other views, so there's a lot more possibilities for developers to add some great features.


Read More..

Minggu, 29 Juni 2014

    In this tutorial, I will go over writing a program for Android Wear physical devices. This app will allow the user to select a time interval on the Wear, and then set off a vibration on the user's wrist when the timer goes off before repeating. The goal of this app is to hopefully assist drivers on long road trips to stay alert (an idea that I had while driving back along I80 from San Francisco to Denver) and has been tested on the Samsung Gear Live physical device (which is where all of the screen shots have come from). The source code for this app is available on GitHub.

    For those interested, the app is available on the Play Store for free and working now that Play Services 5.0 is available.

    Also, before I begin, I should also throw out a thank you to ReadWrite, as I ended up getting to I/O last minute with a registration code through a contest on their awesome site.


    The first step is to download and install the latest Android Studio with SDK packages, then follow this documentation for putting together the broiler plate code for the mobile and wear version of the app. For this tutorial I named the main activity for the Wear portion 'IterationActivity', as this will be the activity screen that is displayed when the user opens the app on their Wear device. In the broiler plate code as of the current release, this activity extends WatchActivity, which is not available in the SDK, so we can safely change this to a regular Activity class. Next we can create an IntentService called TimerService that handles all of the background operations. We'll get back to filling this out once the infrastructure is ready to go.

    In the Wear and Mobile (it's important to have it in both, or the wear portion will not unpackage onto the user's device. I learned this the hard way with a lot of frustration :) ) sections, open AndroidManifest.xml and add the vibrate permission:

<uses-permission android:name="android.permission.VIBRATE" />

    Then add the service tag for our TimerService.class in the Wear manifest:

<service android:name=".Services.TimerService" />

    Next, under the Mobile section in build.gradle, add the following line to dependencies in order to package the Wear portion of the app with the Mobile:

wearApp project(':wear')

    Now that this app should compile and run directly on a Wear device, it's time to make it actually do something. The layout xml file used by IterativeActivity is a simple list view with some text above it, and should look like this:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Select an interval"
/>

<ListView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>

</LinearLayout>

    Under rect_activity_iteration and round_activity_iteration layout files, we can replace the text view with a GridViewPager for containing our view:

<GridViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

    Back to the IterationActivity file, we can start adding content to onCreate(). The following block is added when the activity is started. If the SharedPreference for a selected duration is set, then the timer is started with that value, otherwise we continue with the block.

if( ( getIntent().getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK ) == Intent.FLAG_ACTIVITY_NEW_TASK ) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences( this );
long duration = pref.getLong( SAVED_STATE_SELECTED_DURATION, 0 );
if( duration != 0 ) {
setupTimer( duration );
finish();
return;
}
}

    If the activity hasn't been started before and there is no preselected interval time, then we can set up the array of times that the user can select from:

private void setupIterationArray() {
int[] minutes = getResources().getIntArray( R.array.interation_minutes );
for( int i = 0; i < minutes.length; i++ ) {
IterationListItem item = new IterationListItem( getResources().getQuantityString( R.plurals.label_minutes, minutes[i], minutes[i] ),
minutes[i] * 60 * 1000 );
mIterationTimes.add( item );
}
}

    Next we set the content view to activity_iteration.xml:

setContentView(R.layout.activity_iteration);

    After the view is set, we can create the list of labels and times that the user can select from:

private void setupIterationArray() {
int[] minutes = getResources().getIntArray( R.array.interation_minutes );
for( int i = 0; i < minutes.length; i++ ) {
IterationListItem item = new IterationListItem( getResources().getQuantityString( R.plurals.label_minutes, minutes[i], minutes[i] ),
minutes[i] * 60 * 1000 );
mIterationTimes.add( item );
}
}

    Finally under onCreate() we can initialize the list view and GoogleAPIClient. This will provide the activity for selecting durations in Wear:

private void initList() {
mListView = (ListView) findViewById( R.id.list_view );
mListView.setAdapter( new ArrayAdapter<IterationListItem>( this,
android.R.layout.simple_list_item_1, mIterationTimes ) );
mListView.setOnItemClickListener( this );
}

private void initGoogleApiClient() {
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi( Wearable.API )
.addConnectionCallbacks( this )
.addOnConnectionFailedListener( this )
.build();
}


    When one of these list items is clicked, the value is saved in a shared preference and the duration is sent to the setupTimer method:

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences( this );
SharedPreferences.Editor editor = pref.edit();
editor.putLong( SAVED_STATE_SELECTED_DURATION, mIterationTimes.get( position ).getDuration() );
editor.commit();
setupTimer( mIterationTimes.get( position ).getDuration() );
}

    setupTimer accepts a duration in milliseconds and cancels any notifications that this app may have created before, then builds and launches a new timer notification and registers an alarm before closing the list activity:

private void setupTimer( long duration ) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from( this );
notificationManager.cancel( 1 );
notificationManager.notify( 1, buildNotification( duration ) );
registerAlarmManager( duration );
finish();
}

    The buildNotification method is pretty standard and similar to the three other notification posts I wrote about when going over the developers preview. The important part to note here is the PendingIntent that goes to TimerService and closes down the timer:

private Notification buildNotification( long duration ) {
Intent removeIntent = new Intent( ACTION_REMOVE_TIMER, null, this, TimerService.class );
PendingIntent pendingRemoveIntent = PendingIntent.getService( this, 0, removeIntent, PendingIntent.FLAG_UPDATE_CURRENT );

return new NotificationCompat.Builder( this )
.setSmallIcon( R.drawable.ic_launcher )
.setContentTitle( "Stay Awake" )
.setContentText(TimeUtil.getTimeString( duration ) )
.setUsesChronometer( true )
.setLargeIcon( BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher ) )
.setWhen( System.currentTimeMillis() + duration )
.addAction( R.drawable.ic_launcher, "Remove Timer", pendingRemoveIntent )
.setDeleteIntent( pendingRemoveIntent )
.setLocalOnly( true )
.build();
}


    The final thing to do in IterationActivity is register an alarm that fires off an intent to TimerService once the duration has finished:

private void registerAlarmManager( long duration ) {
AlarmManager alarmManager = (AlarmManager) getSystemService( Context.ALARM_SERVICE );
Intent intent = new Intent( ACTION_SHOW_ALARM, null, this, TimerService.class );
PendingIntent pendingIntent = PendingIntent.getService( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT );

long time = System.currentTimeMillis() + duration;
alarmManager.setExact( AlarmManager.RTC_WAKEUP, time, pendingIntent );
}

    Now that IterationActivity is set, we can focus on TimerService.class. HandleIntent listens for two types of actions: starting an alarm and removing an alarm.

@Override
protected void onHandleIntent(Intent intent) {
String action = intent.getAction();

if( IterationActivity.ACTION_SHOW_ALARM.equals( action ) ) {
showAlarm();
} else if( IterationActivity.ACTION_REMOVE_TIMER.equals( action ) ) {
removeAlarm();
}
}

    showAlarm() is only called after the first timer has ended, so it simply vibrates the Wear on call and fires an intent back to IterationActivity with a flag stating that the next notification timer should be posted.  It should be noted that after a lot of time and frustration, I switched to using a pattern here rather than simply passing in the duration, as Wear won't stop the vibration on the watch unless the screen is active if a pattern isn't used.

private void showAlarm() {
final Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
long[] pattern = { 0, getResources().getInteger( R.integer.vibration_duration ) };
v.vibrate( pattern, -1 );
Intent intent = new Intent( this, IterationActivity.class );
intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
startActivity( intent );
}


    removeAlarm() cancels any notification timers that may be visible, any running alarm manager for our specified PendingIntent and sets the selected duration shared preference to 0.

private void removeAlarm() {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from( this );
notificationManager.cancel( 1 );

AlarmManager alarmManager = (AlarmManager) getSystemService( Context.ALARM_SERVICE );

Intent intent = new Intent( IterationActivity.ACTION_SHOW_ALARM, null, this, TimerService.class );
PendingIntent pendingIntent = PendingIntent.getService( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT );

alarmManager.cancel( pendingIntent );

SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = pref.edit();
editor.putLong( IterationActivity.SAVED_STATE_SELECTED_DURATION, 0 );
editor.apply();
}

    And with that we should have a fully functioning basic Android Wear application. Devices go on sale on the 7th, and I can honestly say from my few days using one it has been useful and will only get more useful as developers make additional apps.
Read More..

Sabtu, 21 Juni 2014

    Google has released street view functionality in the most recent version of the Play Services, allowing Android developers another amazing feature in their maps skill set. I decided to dive right in because of my excitement for this feature, and luckily Google made it incredibly easy to implement street view into an app. The code for this sample can be found on my GitHub account. This app will take the user's current location and display their nearest street view panorama location.


    Before starting, we need to do the necessary project setup. The gradle file should import the latest version of Google Play Services, and the manifest should list necessary permissions and meta-data:

<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.ptrprograms.streetview.permission.MAPS_RECEIVE" />

<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="@string/maps_api_key" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />

compile 'com.google.android.gms:play-services:4.+'

    The easiest way to get street view into an app is by implementing a fragment. While the fragment can easily be extended, I chose to simply embed the pre-made StreetViewPanoramaFragment and use it for my main layout.

<fragment

xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/street_view_panorama_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.StreetViewPanoramaFragment" />

    While I would usually implement initial code in onCreate, for this example I call a function from onRestore that starts the location client connection.

private void updateStreetViewPosition() {
if( mLocationClient == null )
mLocationClient = new LocationClient( this, this, this );
if( !mLocationClient.isConnected() && !mLocationClient.isConnecting() ) {
mLocationClient.connect();
}
}

    Using the GooglePlayServicesClient.ConnectionCallbacks listeners, I save the user's current location and initialize the street view panorama view.

@Override
public void onConnected(Bundle bundle) {
if( mCurrentLocation == null ) {
mCurrentLocation = new LatLng( mLocationClient.getLastLocation().getLatitude(), mLocationClient.getLastLocation().getLongitude() );
}
initStreetView();
}

    In initStreetView(), I get a reference to the StreetViewPanormaFragment, and extract the StreetViewPanorama from it. StreetViewPanorama is the control structure for everything we will do in street view, and it can only be obtained from the fragment by using getStreetViewPanorama:

StreetViewPanoramaFragment fragment = ( (StreetViewPanoramaFragment) getFragmentManager().findFragmentById( R.id.street_view_panorama_fragment ) );
if( mPanorama == null ) {
if( fragment != null ) {
mPanorama = fragment.getStreetViewPanorama();

    The next part of initStreetView() takes into account member variables that are used for keeping track of camera properties on rotation or other configuration changes. By using the StreetViewPanormaCamera.Builder object, I set the tilt, bearing and zoom levels to either the default values or the user's saved values, and then assign the camera to the panorama view.

if( mPanorama != null && mCurrentLocation != null ) {
StreetViewPanoramaCamera.Builder builder = new StreetViewPanoramaCamera.Builder( mPanorama.getPanoramaCamera() );
if( mBearing != builder.bearing )
builder.bearing = mBearing;
if( mTilt != builder.tilt )
builder.tilt = mTilt;
if( mZoom != builder.zoom )
builder.zoom = mZoom;
mPanorama.animateTo(builder.build(), 0);
mPanorama.setPosition( mCurrentLocation, 300 );
mPanorama.setStreetNamesEnabled( true );
}

    This is where other properties can be set to the street view, like setStreetNamesEnabled above, such restricting the user's ability to zoom, leave their current panorama or implement listeners for user interactions. Another important portion above is setPosition, as it must be set for the street view to work, and the accepted values are a LatLng or a LatLng with an integer. The second parameter tells the system to find the nearest street viewable point within a set number of meters from the provided LatLng.



    The final portion in this demo app is onSavedInstanceState, where I save the user location if they have explored a bit in street view, as well as the tilt, bearing and zoom of the camera, in order to not reset their view on rotate.

if( mPanorama != null && mPanorama.getLocation() != null && mPanorama.getLocation().position != null ) {
outState.putDouble( EXTRA_LAT, mPanorama.getLocation().position.latitude );
outState.putDouble( EXTRA_LONG, mPanorama.getLocation().position.longitude );
}
if( mPanorama != null && mPanorama.getPanoramaCamera() != null ) {
outState.putFloat( EXTRA_TILT, mPanorama.getPanoramaCamera().tilt );
outState.putFloat( EXTRA_BEARING, mPanorama.getPanoramaCamera().bearing );
outState.putFloat( EXTRA_ZOOM, mPanorama.getPanoramaCamera().zoom );
}

    And with that, we have a basic street view placed into an app. This opens the door for a lot of developers to improve their apps with another great feature.
Read More..

Sabtu, 31 Mei 2014

*** I have this TDK available for sale. Anyone interested, shoot me an email or a comment to discuss ***

    Back in March I won a Ford Sync Test Development Kit (TDK), a device for testing mobile integration with Ford vehicles. As of January, 2014, there are 1.5 million vehicles in North America that support AppLink, and the AppLink functionality will be released in Europe and Asia this year. 

    For this tutorial I will go over creating an app that plays an online audio stream (Denver's Comedy 103.1 icecast stream) from a mobile device over the Ford Sync system. The app supports audio playback, console and steering wheel button interactions, displaying text on the console and opening with a voice command. Below is a video I recorded today showing the app in use. Notice that the app itself is never brought to the foreground on the phone, as the only requirement is that the phone be on and paired with the system. All code for this demo is on GitHub.


    The first thing that we need to do is grab the AppLinkSDKAndroid-2-2 .jar file from http://developer.ford.com and place it into the project's lib folder. We then need to open the manifest to register required permissions, a receiver for the Sync bluetooth events and the service that controls the interactions between the Android device and the Ford Sync console. 

Permissions:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

Service and Receiver:
<service android:name=".Service.AppLinkService"></service>

<receiver android:name=".Receiver.AppLinkReceiver">
<intent-filter>
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>

    It should be noted that the receiver should listen for five different actions pertaining to connection and signal noise. These actions help us determine when the audio service should be stopped or started.

    Once the manifest has been filled in, we can move over to our MainActivity file. In onCreate, we just need to call a method to start the AppLink service and proxy, which listens for events from the Ford Sync console. This allows the app to account for when the app is opened and then the user enters their vehicle, whereas our receiver will handle starting the service when the app is not open.

private void startSyncProxyService() {
boolean isPaired = false;
BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();

if( btAdapter != null ) {
if( btAdapter.isEnabled() && btAdapter.getBondedDevices() != null && !btAdapter.getBondedDevices().isEmpty() ) {
for( BluetoothDevice device : btAdapter.getBondedDevices() ) {
if( device.getName() != null && device.getName().contains( getString( R.string.device_name ) ) ) {
isPaired = true;
break;
}
}
}

if( isPaired ) {
if( AppLinkService.getInstance() == null ) {
Intent appLinkServiceIntent = new Intent( this, AppLinkService.class );
startService( appLinkServiceIntent );
} else {
SyncProxyALM proxyInstance = AppLinkService.getInstance().getProxy();
if( proxyInstance == null ) {
AppLinkService.getInstance().startProxy();
}
}
}
}
}

    The inverse to startSyncProxyService in onCreate is endSyncProxyService, which is called when MainActivity is destroyed.

private void endSyncProxyInstance() {
if( AppLinkService.getInstance() != null ) {
SyncProxyALM proxy = AppLinkService.getInstance().getProxy();
if( proxy != null ) {
AppLinkService.getInstance().reset();
} else {
AppLinkService.getInstance().startProxy();
}
}
}

    Next we need to fill in the AppLinkReceiver class, which extends BroadcastReceiver. The only method we need from the receiver is onReceive, which takes an intent and checks the actions associated with that intent in order to determine if the AppLinkService should be started or stopped.

@Override
public void onReceive(Context context, Intent intent) {
if( intent == null || intent.getAction() == null || context == null )
return;

BluetoothDevice device = intent.getParcelableExtra( BluetoothDevice.EXTRA_DEVICE );
String action = intent.getAction();

Intent serviceIntent = new Intent( context, AppLinkService.class );
serviceIntent.putExtras( intent );


//Should start service
if( action.compareTo(BluetoothDevice.ACTION_ACL_CONNECTED) == 0 &&
device != null &&
device.getName() != null &&
device.getName().contains( context.getString( R.string.device_name ) ) &&
AppLinkService.getInstance() == null )
{
context.startService(serviceIntent);
}

else if( action.equals( Intent.ACTION_BOOT_COMPLETED ) &&
BluetoothAdapter.getDefaultAdapter() != null &&
BluetoothAdapter.getDefaultAdapter().isEnabled() ) {
context.startService(serviceIntent);

}

//Should stop service
else if( action.equals( BluetoothDevice.ACTION_ACL_DISCONNECTED ) &&
device != null &&
device.getName() != null &&
device.getName().contains( context.getString( R.string.device_name ) ) &&
AppLinkService.getInstance() != null )
{
context.stopService( intent );
}

else if( action.equals(BluetoothAdapter.ACTION_STATE_CHANGED ) &&
intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_TURNING_OFF &&
AppLinkService.getInstance() != null )
{
context.stopService( serviceIntent );
}

else if( action.equals( AudioManager.ACTION_AUDIO_BECOMING_NOISY ) ) {
context.stopService( serviceIntent );
}
}

    Now that we have the infrastructure for starting and stopping our AppLinkService, it's time to build the service file. We start off by extending Service and implementing Ford's IProxyListenerALM interface, then importing all of the required stub methods associated with it. At the top of the class we should define our class variables:

private static AppLinkService mInstance = null;
private MediaPlayer mPlayer = null;
private SyncProxyALM mProxy = null;
private int mCorrelationId = 0;

    Because of the way Ford handles the AppLinkService and the requirement that there only be one AppLinkService and SyncProxyALM to control all interactions between an app and the console, we must keep an instance of our service and proxy. We also need to keep track of an integer value (mCorrelationId) to differentiate messages between the console and app. When the service is started in onStartCommand, we need to check that bluetooth is available and enabled, and then start the proxy.

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if( intent != null &&
BluetoothAdapter.getDefaultAdapter() != null &&
BluetoothAdapter.getDefaultAdapter().isEnabled() ) {
startProxy();
}

return START_STICKY;
}

    The startProxy method simply creates a SyncProxyALM object with a title and number for identifying the app. The title used here is what's used for starting the app with vocal commands from the console.

public void startProxy() {
if( mProxy == null ) {
try {
mProxy = new SyncProxyALM( this, getString( R.string.display_title ), true, getString( R.string.app_link_id ) );
} catch( SyncException e ) {
if( mProxy == null ) {
stopSelf();
}
}
}
}

    When the app connects to Sync and starts on the console, the onOnHMIStatus method is called with an OnHMIStatus object passed in. This is where we determine if audio should be played or stopped, display initial text on the screen and we determine what buttons to subscribe to.

@Override
public void onOnHMIStatus(OnHMIStatus onHMIStatus) {
switch( onHMIStatus.getSystemContext() ) {
case SYSCTXT_MAIN:
case SYSCTXT_VRSESSION:
case SYSCTXT_MENU:
break;
default:
return;
}

switch( onHMIStatus.getAudioStreamingState() ) {
case AUDIBLE: {
playAudio();
break;
}
case NOT_AUDIBLE: {
stopAudio();
break;
}
}

if( mProxy == null )
return;

if( onHMIStatus.getHmiLevel().equals( HMILevel.HMI_FULL ) && onHMIStatus.getFirstRun() ) {
//setup app with SYNC
try {
mProxy.show( "Welcome to Paul's", "Ford AppLink Demo", TextAlignment.CENTERED, mCorrelationId++ );
} catch( SyncException e ) {}
subscribeToButtons();
}
}

    subscribeToButtons simply subscribes through the control proxy, and the service listens for button presses and performs actions based on which button comes through Sync's event listener

private void subscribeToButtons() {
if( mProxy == null )
return;

try {
mProxy.subscribeButton( ButtonName.OK, mCorrelationId++ );
} catch( SyncException e ) {}
}

@Override
public void onOnButtonPress(OnButtonPress notification) {
if( ButtonName.OK == notification.getButtonName() ) {
if( mPlayer != null ) {
if( mPlayer.isPlaying() ) {
stopAudio();
} else {
playAudio();
}
}
}
}

    The final steps to implementing audio over AppLink is playing and stopping the audio, which is handled exactly as it would be in any other app, where the MediaPlayer object is created and set with a URL, then started once the stream has loaded. The only difference here is that the proxy is used to display text to the user's console as the operations happen.

private void playAudio() {
String url = "http://173.231.136.91:8060/";
if( mPlayer == null )
mPlayer = new MediaPlayer();

try {
mProxy.show("Loading...", "", TextAlignment.CENTERED, mCorrelationId++);
} catch( SyncException e ) {}

mPlayer.reset();
mPlayer.setAudioStreamType( AudioManager.STREAM_MUSIC );
try {
mPlayer.setDataSource(url);
mPlayer.setOnPreparedListener( new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared( MediaPlayer mediaPlayer ) {
mediaPlayer.start();
try {
mProxy.show("Playing online audio", "", TextAlignment.CENTERED, mCorrelationId++);
} catch( SyncException e ) {}
}
});
mPlayer.prepare();
} catch (IllegalArgumentException e) {
} catch (SecurityException e) {
} catch (IllegalStateException e) {
} catch (IOException e) {
}
}

private void stopAudio() {
if( mPlayer == null )
return;
mPlayer.pause();
try {
mProxy.show("Press OK", "to play audio", TextAlignment.CENTERED, mCorrelationId++);
} catch( SyncException e ) {}
}

    And with that, we have a simple audio service integrated with Ford Sync AppLink. There's a lot more that can be done with the system, such as text to speech, using GPS, and integrating any information available from the phone via sensors or online data. As more vehicles end up with this system in the world, it'll be interesting to see what else people do with it.
Read More..

Jumat, 30 Mei 2014

Introduction to Geofencing with a Service

    Geofencing is a feature in Google Play Services that allows for checking when a user has entered or left a circular area. This feature is implemented using Google's Location Services, so it relies on location data from cellular towers, wireless networks and GPS. While this can be a powerful tool, it should be used conservatively as continuously polling for a users location can be taxing on the device battery. It should also be noted that geofencing can take some time to register if the area has been entered or left, so you should plan accordingly when designing any apps that use this feature. For this post I have decided to put together an app that creates a geofence around the users starting location and post a notification when the user enters or leaves the fence. All code for this demo can be found on GitHub.

    The first thing that should be done is to update the gradle.build file to include Google Play Services.

compile 'com.google.android.gms:play-services:4.+'

    Next the manifest should be edited to allow for the ACCESS_FINE_LOCATION permission in order to use the device's GPS functionality. Meta-data for the Google Play Services version should be included as a tag, and the service that will be used for handling what action to take on a geofence event should be declared. For this demo I also force the app to be in portrait mode in order to avoid the broiler plate code required for reconnecting to Play Services on rotate.

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ptrprograms.geofencing" >

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".Activity.MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service android:name=".Service.GeofencingService" />

<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />

</application>

</manifest>


    Once the manifest file is set, then the main activity for the application should be set to implement GooglePlayServices connection callbacks and LocationClient Geofencing result and removal listeners. These are generally used for doing specific actions when PlayServices has connected or disconnected, as well as handling geofencing events.

public class MainActivity extends Activity implements 
GooglePlayServicesClient.ConnectionCallbacks,
GooglePlayServicesClient.OnConnectionFailedListener,
LocationClient.OnAddGeofencesResultListener,
LocationClient.OnRemoveGeofencesResultListener {

     For now we'll leave the required methods as stubs and get back to them once everything else is ready. The first thing we should do in our activity, after calling setContentView, is verify that the device has Google Play Services available. If not, we can take action to alert the user to download it from the play store, or in this case simply end the activity.

private void verifyPlayServices() {
switch ( GooglePlayServicesUtil.isGooglePlayServicesAvailable( this ) ) {
case ConnectionResult.SUCCESS: {
break;
}
case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED: {
finish();
}
default: {
finish();
}
}
}

    Once Play Services have been confirmed, we can create a reference to the device LocationClient, and create a PendingIntent to start our service (GeofencingService.class) that handles performing an action when the user interacts with a geofence.

mLocationClient = new LocationClient( this, this, this );
mIntent = new Intent( this, GeofencingService.class );
mPendingIntent = PendingIntent.getService( this, 0, mIntent, PendingIntent.FLAG_UPDATE_CURRENT );

    After this point you can start the geofence operations whenever appropriate for your application. In this case the operations are handled by a simple ToggleButton. In order to start listening for geofence events, you need to use the Geofence.Builder class to create a fence at a given lat/lng pair with a radius (in meters), specify if the fence should listen for entering, exiting or both interactions, and set an expiration time on it. For this example I am using the user's current location, a radius of 100 meters, listening for both entering and exiting events, and never letting the geofence listeners expire since I am controlling them myself. Once the geofence is built, it is placed in an ArrayList of Geofence objects and added to the location client with our pending intent so that when the fence is triggered, the PendingIntent is fired off.

private void startGeofence() {
Location location = mLocationClient.getLastLocation();

Geofence.Builder builder = new Geofence.Builder();
mGeofence = builder.setRequestId( FENCE_ID )
.setCircularRegion( location.getLatitude(), location.getLongitude(), RADIUS )
.setTransitionTypes( Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT )
.setExpirationDuration( Geofence.NEVER_EXPIRE )
.build();

ArrayList<Geofence> geofences = new ArrayList<Geofence>();
geofences.add( mGeofence );
mLocationClient.addGeofences( geofences, mPendingIntent, this );
}

    At this point we need to populate the LocationClient.OnAddGeofencesResultListener onAddGeofencesResult method. This method only has one responsibility: initiate the IntentService that will handle geofence events.

public void onAddGeofencesResult(int status, String[] geofenceIds ) {
if( status == LocationStatusCodes.SUCCESS ) {
Intent intent = new Intent( mIntent );
startService( intent );
}
}

    Once the service is created and the PendingIntent is associated with LocationClient, our service should receive intents every time the geofence is entered or exited, even if the app itself is in the background. The only thing we're doing with the service in this case is posting a notification for the user to inform them that they have entered or exited our specified area by checking the LocationClient.getGeofenceTransition( intent ) method. An introduction to notifications can be found in one of my earlier posts.

Entering the geofenced area
@Override
protected void onHandleIntent( Intent intent ) {
NotificationCompat.Builder builder = new NotificationCompat.Builder( this );
builder.setSmallIcon( R.drawable.ic_launcher );
builder.setDefaults( Notification.DEFAULT_ALL );
builder.setOngoing( true );

int transitionType = LocationClient.getGeofenceTransition( intent );
if( transitionType == Geofence.GEOFENCE_TRANSITION_ENTER ) {
builder.setContentTitle( "Geofence Transition" );
builder.setContentText( "Entering Geofence" );
mNotificationManager.notify( 1, builder.build() );
}
else if( transitionType == Geofence.GEOFENCE_TRANSITION_EXIT ) {
builder.setContentTitle( "Geofence Transition" );
builder.setContentText( "Exiting Geofence" );
mNotificationManager.notify( 1, builder.build() );
}
}

Exiting the geofenced area 
    In order to remove the fences, we simply call removeGeofences with the PendingIntent and context passed in.

private void stopGeofence() {
mLocationClient.removeGeofences( mPendingIntent, this );
}

    One important thing to notice here is that while the fence is removed, we also need to kill the background service that is waiting for geofence events. This is where the LocationClient.OnRemoveGeofenceListener interface comes into play. Since we are removing the geofences by passing in our intent, we need to populate the onRemoveGeofencesByPendingIntentResult method.

@Override
public void onRemoveGeofencesByPendingIntentResult(int status, PendingIntent pendingIntent) {
if( status == LocationStatusCodes.SUCCESS ) {
stopService( mIntent );
}
}

    And with that, we have everything we need to create and use geofences within an Android application. There's a lot that can be done with this feature, such as added a "warmer/colder" feature to a scavenger hunt, keeping track of when a user enters a store, and monitoring when a user is near a landmark to name a few. I hope this tutorial helps others create something awesome, and good luck!
Read More..