Minggu, 27 April 2014

Since Android Jelly Bean, notifications have had the ability to be expanded into a larger view with a custom layout. One of the most common uses for this comes from media applications, such as Pandora and YouTube, that allow the user to interact with buttons in the custom layout in order to control their media services without being in the app. Given the usefulness of this technique, I have put together a demo service that creates a notification with a custom layout containing buttons, and added listeners for those buttons in order to call functions within the service. As with all of my other posts, the demo project can be found on my GitHub account here.

The entry point for this demo (MainActivity) uses a simple layout containing a single button that creates an intent with an action to start the background service that will be doing all of the work in our example.

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

mLaunchNotificationButton = (Button) findViewById( R.id.launch_notification );
mLaunchNotificationButton.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick( View view ) {
Intent intent = new Intent( getApplicationContext(), CustomNotificationService.class );
intent.setAction( CustomNotificationService.ACTION_NOTIFICATION_PLAY_PAUSE );
startService( intent );
}
});
}

Once the intent for the service is fired, onStartCommand is called in the service, which is where I pass the intent to a function that handles filtering out the action and calling the appropriate methods.

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleIntent( intent );
return super.onStartCommand(intent, flags, startId);
}

private void handleIntent( Intent intent ) {
if( intent != null && intent.getAction() != null )
{
if( intent.getAction().equalsIgnoreCase( ACTION_NOTIFICATION_PLAY_PAUSE ) )
{
mIsPlaying = !mIsPlaying;
showNotification(mIsPlaying);
} else if( intent.getAction().equalsIgnoreCase( ACTION_NOTIFICATION_FAST_FORWARD ) )
{
//fast forward function
} else if( intent.getAction().equalsIgnoreCase( ACTION_NOTIFICATION_REWIND ) )
{
//rewind action
}
}
}

If the action is to play or pause the service, then the service would perform these actions and then display a new notification with the updated UI from the showNotification function.

private void showNotification( boolean isPlaying ) {
Notification notification = new NotificationCompat.Builder( getApplicationContext() )
.setAutoCancel( true )
.setSmallIcon( R.drawable.ic_launcher )
.setContentTitle( getString( R.string.app_name ) )
.build();

if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN )
notification.bigContentView = getExpandedView( isPlaying );

NotificationManager manager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
manager.notify( 1, notification );
}

The code here follows the same convention as my previous post for standard notifications, with the exception of the Jelly Bean code to create a bigContentView Remote Views. The code for pre-Jelly Bean devices creates a notification that looks like this:


The getExpandedView function returns a RemoteViews object that consists of an inflated custom view that has PendingIntents associated with each of the buttons that will be sent to the existing service in order to control the operations within with the service. The layout is fairly standard with a fixed size of 128dp and three buttons under some custom text:

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

<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/notification_expanded_height">

<ImageView
android:id="@+id/large_icon"
android:layout_width="@dimen/notification_expanded_height"
android:layout_height="@dimen/notification_expanded_height"
android:scaleType="centerCrop"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
/>
<LinearLayout
android:id="@+id/buttons_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@id/large_icon"
android:orientation="horizontal">

<ImageButton
android:id="@+id/ib_rewind"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/notification_button_height"
android:scaleType="fitCenter" />

<ImageButton
android:id="@+id/ib_play_pause"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/notification_button_height"
android:scaleType="fitCenter" />

<ImageButton
android:id="@+id/ib_fast_forward"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="@dimen/notification_button_height"
android:scaleType="fitCenter" />
</LinearLayout>

<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/app_name"
android:textSize="@dimen/notification_text_size"
android:layout_gravity="center"
android:gravity="center"
android:layout_toRightOf="@+id/large_icon"
android:layout_above="@+id/buttons_row"/>

</RelativeLayout>

which in turn looks like this when it is created:


In the getExpandedView method, each image in the notification is set using the setImageViewResource method

customView.setImageViewResource( R.id.ib_rewind, R.drawable.ic_rewind );

and each button has a pending intent with action associated with it

Intent intent = new Intent( getApplicationContext(), CustomNotificationService.class );
intent.setAction( ACTION_NOTIFICATION_PLAY_PAUSE );
PendingIntent pendingIntent = PendingIntent.getService( getApplicationContext(), 1, intent, 0 );
customView.setOnClickPendingIntent( R.id.ib_play_pause, pendingIntent );

Since each of these pending intents goes back to the service, they are filtered through the handleIntent method and actions can be carried out based on the button clicks from this notification. Since the notification and operations are controlled through a service, the notification can control media when the app has been closed out or the Android device is locked. As this demo is not using an actual audio service to determine what controls should be shown, the mIsPlaying flag is set and passed to the custom view creation method in order to show either the play or pause button.


And that's how simple it is to create a custom expanded view with buttons! 

Read More..

Senin, 14 April 2014

Notifications Part 1: Introduction

One of the most useful techniques for any developer's mobile toolkit is building notifications. They allow you to quickly get information to your user, bring them into your app, provide controls for media and do a variety of other pretty cool things. They are also the basis for interacting with the new Google Wear hardware. The first part of my notification posts will go over the basics of using the NotificationCompat builder, which is compatible back to Android v4, to display a notification in the status drawer, enable vibrations, show icons and fire an intent to open a specified activity. As with the other posts, all source code for this project can be found on GitHub.

First and foremost, the demo project that I made allows the user to populate different information and enable different features in a notification, as seen here:


The title, content text, subtext and content info are straight forward and demonstrated here:


 and the ticker text is the text that is displayed in the status bar when the notification comes in:


Notifications are created using the builder pattern with NotificationCombat.Builder. Each characteristic is then added to the notification through a series of functions, followed by returning the built notification to the NotificationManager. An example notification can be built as simply as this:

NotificationCompat.Builder builder = new NotificationCompat.Builder( this );
builder.setContentTitle(getString(R.string.app_name));
builder.setContentText(mNotificationTextEditText.getText());
builder.setSmallIcon( R.drawable.ic_launcher );
NotificationManager manager =
(NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
manager.notify( 1, builder.build() );

where the '1' in the notify function is an int that can be incremented to stack notifications, or use the same number to replace any currently active notifications from your activity.


When only the small icon is set, it fills the roll of the large icon on the left. If both the large and small image are defined, then the large image is the left image, and the image next to the content info is the small image. 

Notification sounds can be triggered using the builder.setSound method. Unless you have a compelling reason, you should use the notification sound defined by the user if your notification is to be audible.

builder.setSound( RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION ) );

Vibrations can also be set for the notification using the builder.setVibrate method. This method takes an array of longs where every even index is the number of milliseconds that the device should not vibrate, and every odd index is the number of milliseconds that the device should vibrate. In the demo, the notification will vibrate for half of a second, pause for a quarter of a second, and then vibrate a second time for half of a second. The 0 at index 0 means that the vibration will start as soon as the notification is received, rather than waiting.

builder.setSound( RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION ) );

One of the more useful features of Android notifications is that they can be set with an intent that allows the user to open an activity on click, and then the notification can be cleared from the notification drawer.

Intent intent = new Intent( this, MainActivity.class );
TaskStackBuilder stackBuilder = TaskStackBuilder.create( this );
stackBuilder.addNextIntent( intent );
PendingIntent resultIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent( resultIntent );
builder.setAutoCancel( true );

The time section of the notification can be overwritten to count the time since the notification posted by using the builder.setUsesChronometer method.


The last feature I want to go over that comes with basic notifications is the ability to use some predefined styles. In the demo project, I apply the Big Picture style and apply an image for the picture area

NotificationCompat.BigPictureStyle style = new NotificationCompat.BigPictureStyle();
style.bigPicture(BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher));
builder.setStyle(style);

This creates a notification that contains a large image under all of the standard information:


Aside from the basic features that I have just gone over, notifications allow for custom views that can contain items, such as buttons, to perform special actions. They can also be used with the new Google Wear hardware. I plan to go over these features in a later post, as notifications are one of the most powerful tools in the Android SDK.

Read More..

Minggu, 02 Maret 2014

Making a Gallery

    During my last summer of college, I had an interview for a possible internship that asked me how I would allow users to see a series of images quickly before allowing them to see a better quality one. Given that I knew a lot less then than I do now (which still isn't much :)), I gave an answer that described how it would work with a view pager, without knowing what a view pager was. While this was close, it didn't cover the 'quick' aspect and didn't cover any of the implementation details. Since I didn't get the job (which in hindsight is awesome, as I ended up in gorgeous Boulder, Colorado, with a great team rather than heading down to Los Angeles), I decided to at least work out how I would solve this problem.  As with all of my posts, the source code for this project is available on my GitHub.

    For this project I am going to use online images so that their URLs can be passed to the app from a JSON stream with some additional data. This JSON stream is then parsed using GSON into a Gallery model object that contains an ArrayList of Image objects. Each Image object contains a caption for the image, the URL for the higher resolution version of the image, and a thumbnail for the image. The thumbnails are displayed in a GridView, and the higher resolution images with captions are displayed in a separate activity with a ViewPager. The final product with very little styling (mind you, if this were to be a production app then more styling would be a necessity) will look like this:


    As can be seen in the first image, the main activity is simply a grid of images. Instead of using a standard ImageView, I have a SquareImageView class that takes the images and displays them in a square in order to keep the rows uniform. This is done by calling setMeasuredDimension to make the image height equal the width when the view calls onMeasure

SquareImageView.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int dimension = getDefaultSize( getSuggestedMinimumWidth(), widthMeasureSpec );
setMeasuredDimension(dimension, dimension);
}

    The feed is pulled down in MainActivity using Volley and GSON to put the JSON data into the model objects:

MainActivity.java
private void loadFeed() {
String feedUrl = getString( R.string.feed_url );
GsonRequest<Gallery> request = new GsonRequest<Gallery>( Request.Method.GET,
feedUrl, Gallery.class, successListener(), errorListener() );
Volley.newRequestQueue( getApplicationContext() ).add( request );
}
public Response.Listener successListener()
{
return new Response.Listener<Gallery>()
{
@Override
public void onResponse( Gallery gallery ) {
mGallery.setImages( gallery.getImages() );
mGallery.setDescription( gallery.getDescription() );
setupUI();
}
};
}

   setupUI() adds all of the images from the gallery into an ArrayAdapter for the GridView, and adds a click listener to open an activity for viewing the full resolution images. The adapter for the grid uses the viewholder pattern and the Picasso library to load the thumbnail images into the grid, as can be seen in GalleryGridAdapter.java. When an image is clicked in the grid, all images are passed to the next activity as well as the clicked position, allowing the ImageDetailsActivity to display the ViewPager at the correct location

MainActivity.java
mGridView.setOnItemClickListener( new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> adapterView,
View view, int position, long id) {
Intent intent = new Intent( getApplicationContext(), ImageActivity.class );
intent.putExtra( ImageActivity.EXTRA_IMAGE_LIST,
(ArrayList) mGallery.getImages() );
intent.putExtra( ImageActivity.EXTRA_CUR_IMAGE, position );
startActivity( intent );
}

ImageDetailsActivity.java
if( getIntent() == null || getIntent().getExtras() == null )
return;

List<Image> tmpList = getIntent().getExtras()
.getParcelableArrayList( EXTRA_IMAGE_LIST );
mCurrentImagePosition = getIntent().getExtras().getInt( EXTRA_CUR_IMAGE, 0 );
mAdapter = new ImageStateViewPager( getSupportFragmentManager(), tmpList );

    ImageActivity takes the images and loads them into a FragmentStatePagerAdapter which returns a new instance of ImageDetailsFragment with passed Image object from getItem.

ImageStateViewPager.java
@Override
public Fragment getItem( int position ) {
return( position < 0 || position > ( mImageList.size() - 1 ) ) ? null :
ImageFragment.newInstance( mImageList.get( position ) );
}

    This fragment displays a progress spinner until Picasso has loaded the external image, and displays a caption that can have visibility toggled by tapping on the image. By using a ViewPager, the user is able to swipe left or right to view additional images while only keeping a max of three images in memory at a time.

    And with that, we have a simple gallery component for Android. There's a lot of modifications that can be made to this, such as adding progress spinners for the grid as the initial feed is loaded, and spinners for each image object as Picasso loads them into the grid, as well as styles to show overlays and change spacing in the grid. Overall the gallery is a useful component for displaying graphical content to your users, and makes a great addition to any Android developer's arsenal of UI components.
Read More..

Minggu, 16 Februari 2014

Writing a Muzei Plugin for Online Images

    Three days ago Roman Nurik released Muzei, an app that allows users to add plugins that change device wallpaper images after a set amount of time. Now that I have built my own plugin, I want to share what I learned and show how easy it is to build a plugin to interact with the Muzei app. The goal of this app is to pull a random cat image from the CatAPI and display it for the user. All source can be found here and the released plugin can be found here.


    Plugins for Muzei work by extending either the RemoteMuzeiArtSource or MuzeiArtSource service classes and registering them in the manifest. For this tutorial I will go over using RemoteMuzeiArtSource in order to use online resources for changing the wallpaper. The key method of RemoteMuzeiArtSource that must be implemented is onTryUpdate( int reason ). 

    @Override
protected void onTryUpdate( int reason ) throws RetryException {
String link;
try {
do {
link = getLink();
} while( !imageHasAcceptableExtension( link ) );
} catch( Exception e ) {
throw new RetryException();
}

setMuzeiImage( link );
}

    When a timer in Muzei goes off, this method is called. This is where an image should be pulled from an online resource and published. If an image cannot be pulled down, the RetryException exception is thrown, and Muzei handles it by using an exponential delay to attempt recalling onTryUpdate().

    In order to load the image, the image URL is needed. I do this through the getLink() method. It uses the CatAPI to get a random redirecting link, and returns the permanent link.

    public String getLink() throws IOException {

String link = BASE_URL;
URL url = new URL( link );
HttpURLConnection ucon = (HttpURLConnection) url.openConnection();
ucon.setInstanceFollowRedirects( false );
URL secondURL = new URL( ucon.getHeaderField( "Location" ) );

return secondURL.toString();
}

    Once that link is verified as a valid jpg image, it is passed to the setMuzeiImage method.

private void setMuzeiImage( String link ) {
publishArtwork(new Artwork.Builder()
.title(getApplication().getResources().getString(R.string.title))
.imageUri(Uri.parse(link))
.viewIntent( new Intent(Intent.ACTION_VIEW, Uri.parse( link ) ) )
.build() );

scheduleUpdate(System.currentTimeMillis() + ROTATE_TIME_MILLIS);
}

    This method uses the Muzei method publishArtwork to create a new Artwork object with a title, the image URL, and an intent to open the original image in a browser. Had another API been used with more information associated with each image, then the title can be more related to the specific image, and the intent URL could go to a web page describing the source. Another method that Artwork.Builder could use is token, which allows for tagging image that is currently being displayed. This is useful for selecting images from a stream in order to not use the same image twice in a row. The final thing handled in setMuzeiImage is scheduleUpdate, which tells Muzei when it should attempt to call onTryUpdate() in order to load in the next new image.

    Once the Muzei service is built, we must declare it in the manifest and have it accept intents for "com.google.android.apps.muzei.api.MuzeiArtSource"

    <service android:name=".MuzeiImageGenerator"
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:description="@string/description">
<intent-filter>
<action android:name="com.google.android.apps.muzei.api.MuzeiArtSource" />
</intent-filter>

<meta-data android:name="color" android:value="#67C7E2" />
</service>

    The icon used is displayed in Muzei when the user views their available plugins. The image should simple, be in a circle and have a transparent center image. The label and description are displayed below the icon, and the meta-data color attribute is used to change the display icon from white to a specified color. In this case I am using a cyan. The end result looks like this:


    And with that, we have a Muzei plugin! Other options include adding a settings activity so that the user choose which sources to pull from and how often the image should change, and integrating the service with existing apps to provide users wallpapers from images in the apps. I'm excited to see what comes from this, and hope others find this tutorial useful.

Read More..

Sabtu, 15 Februari 2014

Introduction to Using Google Maps v2

    Maps are arguably one of the most versatile and useful components when used correctly in a mobile application. For this project, I wanted to go over the basics of how to use a map in a fragment, as well as go over some of the tools that make maps useful. As with my other projects, the source code that I will be going over is available online here.


    To start, we're going to want to edit our manifest to include the Google Maps API key. There's plenty of tutorials out there already for getting this key, including this one from the Google documentation, so for the sake of brevity we'll skip over that process and assume that it exists as a value in the strings.xml file. We will want to include this key within the applications tag in the manifest as a piece of meta-data using the name "com.google.android.maps.v2.API_KEY".

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

    We're also going to want to put in our play services version number as meta-data between the closing tags for application and manifest using the name "com.google.android.gms.version".

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

    To finish up the manifest, the last thing we'll need is our set of permissions for the app, such as Internet access, location access, and optionally local storage permissions. While local storage is not a necessity for using maps, I used it for saving persistent information about the map.

<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.maps.permission.MAPS_RECEIVE" />

   The general layout of this project consist of three classes; the main activity, the map fragment, and a listener. The listener interface consists of two methods and is used for communicating from the fragment to the main activity.

public interface mapListener {
public void playServicesUnavailable();
public void longClickedMap( LatLng latLng );
}

    Both of these methods are fairly straight forward. One is called when the map determines that Google Play Services are unavailable, making the map unusable, and the other is called when the map detects a long click in order to send that location to the main activity.

    The purpose of the main activity is to control the high level functions of the application while leaving the fragment as a general tool that can be manipulated easily by the main activity. For this demonstration, MainActivity simply loads in our map fragment, handles the case of there being no Play Services ( puts up a dialog that leads the user to downloading services or closing the application ), and responds to a long click on the map by requesting that a marker be placed in the location.

    The bulk of the work in this application is handled through our map fragment. While Google provides a map fragment that can be accessed and manipulated from an activity, I decided to build my own fragment and include the GoogleMap in order to add my own helper functionality. I do include two listeners pertaining to GooglePlayServices in order to determine if the connection has succeeded, allowing the fragment to initialize, or failed.

public class PTRMapFragment extends Fragment implements GooglePlayServicesClient.ConnectionCallbacks, GooglePlayServicesClient.OnConnectionFailedListener

    Once the service has connected, the map is configured to show road and building names, as well as satellite images for the locations. The onMarkerClick and onMapLongClick listeners are also initialized on the map at this point.

    The action I have chosen when the user holds down on a location in the map is to simply add a marker to that location. This is done from the interface function longClickedMap( LatLng ) from our implemented MapListener class, and MainActivity simply calls the fragment's addMarker( LatLng ) function. addMarker( LatLng ) uses the GeoCoder object to retrieve the address listed for the given coordinates and adds a maker on that location with the address as a title for the marker.

public void addMarker( LatLng latLng ) {
if( latLng == null )
return;

Geocoder geocoder = new Geocoder( getActivity() );
String address;
try {
address = geocoder.getFromLocation( latLng.latitude, latLng.longitude, 1 ).get( 0 ).getAddressLine( 0 );
} catch( IOException e ) {
address = "";
}
Log.e( TAG, address );
addMarker( 0, latLng, address );
}

public void addMarker( float color, LatLng latLng, String title ) {
if( mMap == null )
mMap = ( (SupportMapFragment) getFragmentManager()
.findFragmentById( R.id.map ) )
.getMap();

if( latLng == null || mMap == null )
return;

MarkerOptions markerOptions = new MarkerOptions().position( latLng );
if( !title.isEmpty() )
markerOptions.title( title );

if( color == 0 )
color = BitmapDescriptorFactory.HUE_RED;

markerOptions.icon( BitmapDescriptorFactory.defaultMarker( color ) );
Marker marker = mMap.addMarker( markerOptions );
if( !markerLocations.contains( marker ) )
markerLocations.add( marker );

marker.showInfoWindow();
}



    Another important aspect of the map fragment is the camera. While I set the values for the camera in the fragment, they can also be set as attributes in the layout xml file. The method that I use checks for a shared preference to retrieve values that may have been changed by the user, or uses default values in order to focus the camera on the user's location and set the tilt, zoom and bearing.

     private void setInitialCameraPosition() {
double lng, lat;
float tilt, bearing, zoom;

SharedPreferences settings = getActivity().getSharedPreferences( EXTRAS_SHARED_PREFERENCES, 0 );
lng = Double.longBitsToDouble( settings.getLong( SAVED_STATE_LONG, Double.doubleToLongBits( mLocationClient.getLastLocation().getLongitude() ) ) );
lat = Double.longBitsToDouble( settings.getLong( SAVED_STATE_LAT, Double.doubleToLongBits( mLocationClient.getLastLocation().getLatitude() ) ) );
zoom = settings.getFloat( SAVED_STATE_ZOOM, 17 );
bearing = settings.getFloat( SAVED_STATE_BEARING, 0 );
tilt = settings.getFloat( SAVED_STATE_TILT, 30 );

CameraPosition cameraPosition = new CameraPosition.Builder()
.target( new LatLng( lat, lng) )
.zoom( zoom )
.bearing( bearing )
.tilt( tilt )
.build();
if( cameraPosition == null || mMap == null )
return;
mMap.animateCamera( CameraUpdateFactory.newCameraPosition( cameraPosition ) );
}

    While I only implemented some map functionality, others include the ability to add circles, lines, closed polygons and overlays to make maps even more useful for applications. I also touched on the capabilities of Geocoder, but there's a lot more that can be utilized from that package in order find locations based on addresses or common names. I highly recommend going through the official documentation and seeing the full capabilities of these classes in order to assist your users to the fullest capacity.
Read More..

Jumat, 31 Januari 2014

Android Random Quote Daydream

    Introduced with the release of Android 4.2, Daydream is the name of the screen saver functionality that activates when a device is docked or plugged into a charger. These daydreams can be purely visible, such as the sample program that I will be discussing, or they can interact with the user through gestures and touches in order to provide more functionality.

   In order to demonstrate the Daydream service, I have built a simple application that retrieves quotes from an online feed and displays them to the user. After a set period of time has passed, the displayed quote will fade out, and another will fade in to take its place. Some key components that I am using that are not part of the standard Android SDK are:
  • GSONRequest - a Volley adapter for JSON requests that will be parsed into Java objects by GSON. This file was written by Ognyan Bankov.
  • Volley - a Google library that handle's network requests.
    All code for this blog post can be found here.



    The starting point for all Daydream apps is a Java class that extends DreamService. The code in this example is very minimal, as all of the display is handled in our custom view, but the key function in this example is the onAttachedToWindow method that is called as soon as the Daydream is started.

public class DaydreamService extends DreamService {

@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();

final QuoteView view = new QuoteView( this );
view.setLayoutParams( new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) );

setContentView( view );
}
}

    QuoteView extends a simple TextView and uses a runnable with Volley and GSON to load quotes from an online API into a Quote model and animate their transitions as the quote is swapped out.

Quote model:
public class Quote {
private String json_class;
private String quote;
private String link;
private String source;

public void setJson_class( String json_class ) {
this.json_class = json_class;
}

public void setQuote( String quote ) {
this.quote = quote;
}

public void setLink( String link ) {
this.link = link;
}

public void setSource( String source ) {
this.source = source;
}

public String getJson_class() {
return json_class;
}

public String getQuote() {
return quote;
}

public String getLink() {
return link;
}

public String getSource() {
return source;
}
}

    While using the above Quote model object is not a necessity, it does keep data organized and easy to access in the application.

API Network Requests:
private void generateQuote() {
GsonRequest<Quote> request = new GsonRequest<Quote>(
Request.Method.GET,
getResources().getString( R.string.random_quote_url_json ),
Quote.class,
onSuccessListener(),
onErrorListener() );

Volley.newRequestQueue( getContext() ).add( request );
}

public Response.Listener onSuccessListener() {
return new Response.Listener<Quote>() {
@Override
public void onResponse( Quote quote ) {
if( quote == null )
return;

if( mQuote == null )
mQuote = new Quote();

mQuote.setJson_class( quote.getJson_class() );
mQuote.setLink( quote.getLink() );
mQuote.setSource( quote.getSource() );
mQuote.setQuote( quote.getQuote() );
startAnimation( mFadeOut );
}
};
}

protected Response.ErrorListener onErrorListener()
{
return new Response.ErrorListener()
{
@Override
public void onErrorResponse( VolleyError volleyError )
{
mQuote.setQuote( getResources().getString( R.string.volley_error ) );
startAnimation( mFadeOut );
}
};
}

The above code uses a simple Volley request as an argument to GSONRequest in order to pull the quote feed into a Quote object and fade out the currently displayed text. Once the text has completely faded out, the textview is set to the new quote and fades in using another animation.

view mid-transition

The animation objects are created and set with a listener in the initAnimation method

private void initAnimation() {
mFadeIn = new AlphaAnimation( 0.0f, 1.0f );
mFadeIn.setDuration( mFadeInTime );
mFadeIn.setFillAfter( true );

mFadeOut = new AlphaAnimation( 1.0f, 0.0f );
mFadeOut.setDuration( mFadeOutTime );
mFadeOut.setFillAfter( true );
mFadeOut.setAnimationListener( new Animation.AnimationListener() {

@Override
public void onAnimationStart(Animation animation) {
//Do nothing.
}

@Override
public void onAnimationEnd(Animation animation) {
displayQuote();
}

@Override
public void onAnimationRepeat(Animation animation) {
//Do nothing.
}
});
}

    The final portion of this app that must be considered is the manifest. Permission to use the client's network connection must be requested, the Daydream service must be declared and an intent for android.service.dreams.DreamService must filtered to the application in order for it to run within the system.

Manifest:
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ptrprograms.daydream">

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

<application>

<service
android:name=".services.DaydreamService"
android:exported="true"
android:label="@string/app_name">

<intent-filter>

<action android:name="android.service.dreams.DreamService" />
<category android:name="android.intent.category.DEFAULT" />

</intent-filter>

</service>

</application>

</manifest>

    As this example shows, most of the work in this Daydream application is handled through the view. Additional features, such as click listeners and gestures, can be applied as well in order to add user interaction. While this program was fairly minimalistic, it provides an introduction to Daydream that I hope others will find useful in building their own Android screen savers.
Read More..

Minggu, 26 Januari 2014

    One of the key characteristics of mobile devices that separates them from older platforms is that they carry a number of embedded sensors, which allow them to take readings from their environment. I have written a sample program that displays available sensors on an Android device, which then shows information and output from a selected sensor. All source code can be found here.

    The initial screen is a simple ListFragment that populates an adapter with a list of sensors on the device:

SensorListFragment.java
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

mAdapter = new SensorListAdapter( getActivity() );
setListAdapter(mAdapter);

mSensorManager = (SensorManager) getActivity()
.getSystemService( Context.SENSOR_SERVICE );

for( Sensor sensor : mSensorManager.getSensorList( Sensor.TYPE_ALL ) )
mAdapter.add( sensor );
}

    This adapter displays a simple_list_item_1 list row that is populated with the name of each sensor, so that the result looks like this on a Nexus 4:



    Using the ListFragment's built-in OnListItemClicked function, we can retrieve the sensor that has been selected by the user and pass it to another fragment that replaces the one currently attached to our activity.

SensorListFragment.java
    @Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);

Sensor sensor = (Sensor) getListAdapter().getItem( position );
SensorDetailFragment mFragment = SensorDetailFragment.newInstance( sensor );
FragmentTransaction transaction = getActivity().getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.container, mFragment)
.addToBackStack(null)
.commit();
}

    The sensor is passed to the SensorDetailFragment and the type is stored as an argument that can be retrieved when the fragment is attached and created.

SensorDetailFragment.java
public static SensorDetailFragment newInstance( Sensor sensor ) {
SensorDetailFragment mFragment = new SensorDetailFragment();
Bundle args = new Bundle();
args.putInt( EXTRA_SENSOR_TYPE , sensor.getType() );
mFragment.setArguments(args);
return mFragment;
}

    When the fragment is attached and onCreate() is called, the SensorManager system service is retrieved and stored, then the sensor is retrieved from the manager by its type.

SensorDetailFragment.java
        mSensorManager = (SensorManager) getActivity().getSystemService( Context.SENSOR_SERVICE );
mSensor = mSensorManager.getDefaultSensor( type );

    After the TextViews, which label and display information, are initialized, the static information from the sensor is displayed using the sensor's various functions found here. The dynamic information, such as the readings from the sensor, are retrieved using the SensorEventListener. This interface consists of two methods that respond to events: onAccuracyChanged( Sensor sensor, int accuracy ) and onSensorChanged( SensorEvent event ).


    onAccuracyChanged passes the sensor that triggered the event, as well as an integer that matches with one of four values representing high, medium, low and unreliable accuracy of the given sensor.

    onSensorChanged passes a SensorEvent item that consists of various attributes such as

  • accuracy
  • timestamp - useful for throwing out data that occurs more rapidly than needed by the application, but not already associated with a sampling speed
  • sensor - the sensor that triggered the event
  • values - an array of the actual readings from the sensor. This can have indices 0 - 3 populated, depending on the type of sensor giving the reading. Some, such as the gravity sensor, give readings for values[0] - values[2] for the X, Y and Z axis of the device, while others, such as the barometer, only give readings in values[0].
    As with any listener interface, it is important to remember to register and unregister your listeners in a practical place, so as to not waste the device battery while the sensors are not being checked and their data used. In this sensor program, the listener is registered in onStart

SensorDetailFragment.java
mSensorManager.registerListener( SensorDetailFragment.this, mSensor, SensorManager.SENSOR_DELAY_UI );

and unregistered when the fragment is hidden.

SensorDetailFragment.java
    @Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if( hidden )
mSensorManager.unregisterListener( this );
}

    While there is a lot more to sensor-use in the Android platform, especially in how the data can be interpreted and used to take your apps to the next level, I hope I have given a decent introduction to retrieving data from a list of sensors, and that the source code on GitHub can help someone out there looking to implement sensors in their own projects.
Read More..