diff --git a/build/sdk.atree b/build/sdk.atree index 791461c14..b40c405a1 100644 --- a/build/sdk.atree +++ b/build/sdk.atree @@ -271,7 +271,20 @@ developers/build/prebuilts/gradle/SwipeRefreshMultipleViews sam developers/build/prebuilts/gradle/MediaRouter samples/${PLATFORM_NAME}/media/MediaRouter # Wearable sample tree -development/samples/wearable/Notifications samples/${PLATFORM_NAME}/wearable/Notifications +development/samples/wearable/AgendaData samples/${PLATFORM_NAME}/wearable/AgendaData +development/samples/wearable/DataLayer samples/${PLATFORM_NAME}/wearable/DataLayer +development/samples/wearable/DelayedConfirmation samples/${PLATFORM_NAME}/wearable/DelayedConfirmation +development/samples/wearable/ElizaChat samples/${PLATFORM_NAME}/wearable/ElizaChat +development/samples/wearable/FindMyPhone samples/${PLATFORM_NAME}/wearable/FindMyPhone +development/samples/wearable/Flashlight samples/${PLATFORM_NAME}/wearable/Flashlight +development/samples/wearable/Geofencing samples/${PLATFORM_NAME}/wearable/Geofencing +development/samples/wearable/JumpingJack samples/${PLATFORM_NAME}/wearable/JumpingJack +development/samples/wearable/Notifications samples/${PLATFORM_NAME}/wearable/Notifications +development/samples/wearable/Quiz samples/${PLATFORM_NAME}/wearable/Quiz +development/samples/wearable/RecipeAssistant samples/${PLATFORM_NAME}/wearable/RecipeAssistant +development/samples/wearable/SkeletonWearableApp samples/${PLATFORM_NAME}/wearable/SkeletonWearableApp +development/samples/wearable/Timer samples/${PLATFORM_NAME}/wearable/Timer +development/samples/wearable/WatchViewStub samples/${PLATFORM_NAME}/wearable/WatchViewStub # Old sample tree development/samples/AccelerometerPlay samples/${PLATFORM_NAME}/legacy/AccelerometerPlay diff --git a/samples/wearable/AgendaData/Application/build.gradle b/samples/wearable/AgendaData/Application/build.gradle new file mode 100644 index 000000000..7de0d36a1 --- /dev/null +++ b/samples/wearable/AgendaData/Application/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'android' + +android { + compileSdkVersion 20 + buildToolsVersion '20' + + defaultConfig { + minSdkVersion 18 + targetSdkVersion 20 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + + aaptOptions { + noCompress 'apk' + } +} + +dependencies { + compile 'com.google.android.gms:play-services:5.0.+@aar' + compile "com.android.support:support-v4:20.0.+" + wearApp project(':Wearable') +} \ No newline at end of file diff --git a/samples/wearable/AgendaData/Application/proguard-rules.txt b/samples/wearable/AgendaData/Application/proguard-rules.txt new file mode 100644 index 000000000..824a322c1 --- /dev/null +++ b/samples/wearable/AgendaData/Application/proguard-rules.txt @@ -0,0 +1,28 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} +-dontwarn android.support.wearable.view.DelayedConfirmationView +-dontwarn android.support.wearable.view.CircledImageView + +-keep class android.support.wearable.view.WearableListView { + private void setScrollAnimator(int); + private void setScrollVertically(int); +} + diff --git a/samples/wearable/AgendaData/Application/project.properties b/samples/wearable/AgendaData/Application/project.properties new file mode 100644 index 000000000..4ab125693 --- /dev/null +++ b/samples/wearable/AgendaData/Application/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-19 diff --git a/samples/wearable/AgendaData/Application/src/main/AndroidManifest.xml b/samples/wearable/AgendaData/Application/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d96efd8d5 --- /dev/null +++ b/samples/wearable/AgendaData/Application/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/wearable/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/CalendarQueryService.java b/samples/wearable/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/CalendarQueryService.java new file mode 100644 index 000000000..c55fb2c2e --- /dev/null +++ b/samples/wearable/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/CalendarQueryService.java @@ -0,0 +1,251 @@ +package com.example.android.wearable.agendadata; + + +import static com.example.android.wearable.agendadata.Constants.TAG; +import static com.example.android.wearable.agendadata.Constants.CONNECTION_TIME_OUT_MS; +import static com.example.android.wearable.agendadata.Constants.CAL_DATA_ITEM_PATH_PREFIX; +import static com.example.android.wearable.agendadata.Constants.ALL_DAY; +import static com.example.android.wearable.agendadata.Constants.BEGIN; +import static com.example.android.wearable.agendadata.Constants.DATA_ITEM_URI; +import static com.example.android.wearable.agendadata.Constants.DESCRIPTION; +import static com.example.android.wearable.agendadata.Constants.END; +import static com.example.android.wearable.agendadata.Constants.EVENT_ID; +import static com.example.android.wearable.agendadata.Constants.ID; +import static com.example.android.wearable.agendadata.Constants.PROFILE_PIC; +import static com.example.android.wearable.agendadata.Constants.TITLE; + +import android.app.IntentService; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.text.format.Time; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; +import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; +import com.google.android.gms.wearable.Asset; +import com.google.android.gms.wearable.DataMap; +import com.google.android.gms.wearable.PutDataMapRequest; +import com.google.android.gms.wearable.Wearable; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Queries calendar events using Android Calendar Provider API and creates a data item for each + * event. + */ +public class CalendarQueryService extends IntentService + implements ConnectionCallbacks, OnConnectionFailedListener { + + private static final String[] INSTANCE_PROJECTION = { + CalendarContract.Instances._ID, + CalendarContract.Instances.EVENT_ID, + CalendarContract.Instances.TITLE, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END, + CalendarContract.Instances.ALL_DAY, + CalendarContract.Instances.DESCRIPTION, + CalendarContract.Instances.ORGANIZER + }; + + private static final String[] CONTACT_PROJECTION = new String[] { Data._ID, Data.CONTACT_ID }; + private static final String CONTACT_SELECTION = Email.ADDRESS + " = ?"; + + private GoogleApiClient mGoogleApiClient; + + public CalendarQueryService() { + super(CalendarQueryService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + @Override + protected void onHandleIntent(Intent intent) { + mGoogleApiClient.blockingConnect(CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS); + // Query calendar events in the next 24 hours. + Time time = new Time(); + time.setToNow(); + long beginTime = time.toMillis(true); + time.monthDay++; + time.normalize(true); + long endTime = time.normalize(true); + + List events = queryEvents(this, beginTime, endTime); + for (Event event : events) { + final PutDataMapRequest putDataMapRequest = event.toPutDataMapRequest(); + if (mGoogleApiClient.isConnected()) { + Wearable.DataApi.putDataItem( + mGoogleApiClient, putDataMapRequest.asPutDataRequest()).await(); + } else { + Log.e(TAG, "Failed to send data item: " + putDataMapRequest + + " - Client disconnected from Google Play Services"); + } + } + mGoogleApiClient.disconnect(); + } + + private static String makeDataItemPath(long eventId, long beginTime) { + return CAL_DATA_ITEM_PATH_PREFIX + eventId + "/" + beginTime; + } + + private static List queryEvents(Context context, long beginTime, long endTime) { + ContentResolver contentResolver = context.getContentResolver(); + Uri.Builder builder = CalendarContract.Instances.CONTENT_URI.buildUpon(); + ContentUris.appendId(builder, beginTime); + ContentUris.appendId(builder, endTime); + + Cursor cursor = contentResolver.query(builder.build(), INSTANCE_PROJECTION, + null /* selection */, null /* selectionArgs */, null /* sortOrder */); + try { + int idIdx = cursor.getColumnIndex(CalendarContract.Instances._ID); + int eventIdIdx = cursor.getColumnIndex(CalendarContract.Instances.EVENT_ID); + int titleIdx = cursor.getColumnIndex(CalendarContract.Instances.TITLE); + int beginIdx = cursor.getColumnIndex(CalendarContract.Instances.BEGIN); + int endIdx = cursor.getColumnIndex(CalendarContract.Instances.END); + int allDayIdx = cursor.getColumnIndex(CalendarContract.Instances.ALL_DAY); + int descIdx = cursor.getColumnIndex(CalendarContract.Instances.DESCRIPTION); + int ownerEmailIdx = cursor.getColumnIndex(CalendarContract.Instances.ORGANIZER); + + List events = new ArrayList(cursor.getCount()); + while (cursor.moveToNext()) { + Event event = new Event(); + event.id = cursor.getLong(idIdx); + event.eventId = cursor.getLong(eventIdIdx); + event.title = cursor.getString(titleIdx); + event.begin = cursor.getLong(beginIdx); + event.end = cursor.getLong(endIdx); + event.allDay = cursor.getInt(allDayIdx) != 0; + event.description = cursor.getString(descIdx); + String ownerEmail = cursor.getString(ownerEmailIdx); + Cursor contactCursor = contentResolver.query(Data.CONTENT_URI, + CONTACT_PROJECTION, CONTACT_SELECTION, new String[] {ownerEmail}, null); + int ownerIdIdx = contactCursor.getColumnIndex(Data.CONTACT_ID); + long ownerId = -1; + if (contactCursor.moveToFirst()) { + ownerId = contactCursor.getLong(ownerIdIdx); + } + contactCursor.close(); + // Use event organizer's profile picture as the notification background. + event.ownerProfilePic = getProfilePicture(contentResolver, context, ownerId); + events.add(event); + } + return events; + } finally { + cursor.close(); + } + } + + @Override + public void onConnected(Bundle connectionHint) { + } + + @Override + public void onConnectionSuspended(int cause) { + } + + @Override + public void onConnectionFailed(ConnectionResult result) { + } + + private static Asset getDefaultProfile(Resources res) { + Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.nobody); + return Asset.createFromBytes(toByteArray(bitmap)); + } + + private static Asset getProfilePicture(ContentResolver contentResolver, Context context, + long contactId) { + if (contactId != -1) { + // Try to retrieve the profile picture for the given contact. + Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); + InputStream inputStream = Contacts.openContactPhotoInputStream(contentResolver, + contactUri, true /*preferHighres*/); + + if (null != inputStream) { + try { + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + if (bitmap != null) { + return Asset.createFromBytes(toByteArray(bitmap)); + } else { + Log.e(TAG, "Cannot decode profile picture for contact " + contactId); + } + } finally { + closeQuietly(inputStream); + } + } + } + // Use a default background image if the user has no profile picture or there was an error. + return getDefaultProfile(context.getResources()); + } + + private static byte[] toByteArray(Bitmap bitmap) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + byte[] byteArray = stream.toByteArray(); + closeQuietly(stream); + return byteArray; + } + + private static void closeQuietly(Closeable closeable) { + try { + closeable.close(); + } catch (IOException e) { + Log.e(TAG, "IOException while closing closeable.", e); + } + } + + private static class Event { + + public long id; + public long eventId; + public String title; + public long begin; + public long end; + public boolean allDay; + public String description; + public Asset ownerProfilePic; + + public PutDataMapRequest toPutDataMapRequest(){ + final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create( + makeDataItemPath(eventId, begin)); + DataMap data = putDataMapRequest.getDataMap(); + data.putString(DATA_ITEM_URI, putDataMapRequest.getUri().toString()); + data.putLong(ID, id); + data.putLong(EVENT_ID, eventId); + data.putString(TITLE, title); + data.putLong(BEGIN, begin); + data.putLong(END, end); + data.putBoolean(ALL_DAY, allDay); + data.putString(DESCRIPTION, description); + data.putAsset(PROFILE_PIC, ownerProfilePic); + + return putDataMapRequest; + } + } +} diff --git a/samples/wearable/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/Constants.java b/samples/wearable/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/Constants.java new file mode 100644 index 000000000..1b9a0f078 --- /dev/null +++ b/samples/wearable/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/Constants.java @@ -0,0 +1,23 @@ +package com.example.android.wearable.agendadata; + +/** Constants used in companion app. */ +public final class Constants { + private Constants() { + } + + public static final String TAG = "AgendaDataSample"; + + public static final String CAL_DATA_ITEM_PATH_PREFIX = "/event"; + // Timeout for making a connection to GoogleApiClient (in milliseconds). + public static final long CONNECTION_TIME_OUT_MS = 100; + + public static final String EVENT_ID = "event_id"; + public static final String ID = "id"; + public static final String TITLE = "title"; + public static final String DESCRIPTION = "description"; + public static final String BEGIN = "begin"; + public static final String END = "end"; + public static final String DATA_ITEM_URI = "data_item_uri"; + public static final String ALL_DAY = "all_day"; + public static final String PROFILE_PIC = "profile_pic"; +} diff --git a/samples/wearable/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/MainActivity.java b/samples/wearable/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/MainActivity.java new file mode 100644 index 000000000..046fc8d62 --- /dev/null +++ b/samples/wearable/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/MainActivity.java @@ -0,0 +1,186 @@ +package com.example.android.wearable.agendadata; + +import static com.example.android.wearable.agendadata.Constants.TAG; + +import android.app.Activity; +import android.content.Intent; +import android.content.IntentSender; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; +import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.data.FreezableUtils; +import com.google.android.gms.wearable.DataApi; +import com.google.android.gms.wearable.DataItem; +import com.google.android.gms.wearable.DataItemBuffer; +import com.google.android.gms.wearable.Node; +import com.google.android.gms.wearable.NodeApi; +import com.google.android.gms.wearable.Wearable; + +import java.util.List; + +public class MainActivity extends Activity implements NodeApi.NodeListener, ConnectionCallbacks, + OnConnectionFailedListener { + + /** Request code for launching the Intent to resolve Google Play services errors. */ + private static final int REQUEST_RESOLVE_ERROR = 1000; + + private GoogleApiClient mGoogleApiClient; + private boolean mResolvingError = false; + + private TextView mLogTextView; + ScrollView mScroller; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.main); + mLogTextView = (TextView) findViewById(R.id.log); + mScroller = (ScrollView) findViewById(R.id.scroller); + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + @Override + protected void onStart() { + super.onStart(); + if (!mResolvingError) { + mGoogleApiClient.connect(); + } + } + + @Override + protected void onStop() { + if (mGoogleApiClient.isConnected()) { + Wearable.NodeApi.removeListener(mGoogleApiClient, this); + } + mGoogleApiClient.disconnect(); + super.onStop(); + } + + public void onGetEventsClicked(View v) { + startService(new Intent(this, CalendarQueryService.class)); + } + + public void onDeleteEventsClicked(View v) { + if (mGoogleApiClient.isConnected()) { + Wearable.DataApi.getDataItems(mGoogleApiClient) + .setResultCallback(new ResultCallback() { + @Override + public void onResult(DataItemBuffer result) { + if (result.getStatus().isSuccess()) { + deleteDataItems(result); + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onDeleteEventsClicked(): failed to get Data Items"); + } + } + result.close(); + } + }); + } else { + Log.e(TAG, "Failed to delete data items" + + " - Client disconnected from Google Play Services"); + } + } + + private void deleteDataItems(DataItemBuffer dataItems) { + if (mGoogleApiClient.isConnected()) { + // Store the DataItem URIs in a List and close the buffer. Then use these URIs + // to delete the DataItems. + final List dataItemList = FreezableUtils.freezeIterable(dataItems); + dataItems.close(); + for (final DataItem dataItem : dataItemList) { + final Uri dataItemUri = dataItem.getUri(); + // In a real calendar application, this might delete the corresponding calendar + // event from the calendar data provider. In this sample, we simply delete the + // DataItem, but leave the phone's calendar data intact. + Wearable.DataApi.deleteDataItems(mGoogleApiClient, dataItemUri) + .setResultCallback(new ResultCallback() { + @Override + public void onResult(DataApi.DeleteDataItemsResult deleteResult) { + if (deleteResult.getStatus().isSuccess()) { + appendLog("Successfully deleted data item: " + dataItemUri); + } else { + appendLog("Failed to delete data item:" + dataItemUri); + } + } + }); + } + } else { + Log.e(TAG, "Failed to delete data items" + + " - Client disconnected from Google Play Services"); + } + } + + private void appendLog(final String s) { + mLogTextView.post(new Runnable() { + @Override + public void run() { + mLogTextView.append(s); + mLogTextView.append("\n"); + mScroller.fullScroll(View.FOCUS_DOWN); + } + }); + } + + @Override + public void onPeerConnected(Node peer) { + appendLog("Device connected"); + } + + @Override + public void onPeerDisconnected(Node peer) { + appendLog("Device disconnected"); + } + + @Override + public void onConnected(Bundle connectionHint) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Connected to Google Api Service"); + } + mResolvingError = false; + Wearable.NodeApi.addListener(mGoogleApiClient, this); + } + + @Override + public void onConnectionSuspended(int cause) { + // Ignore + } + + @Override + public void onConnectionFailed(ConnectionResult result) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Disconnected from Google Api Service"); + } + if (null != Wearable.NodeApi) { + Wearable.NodeApi.removeListener(mGoogleApiClient, this); + } + if (mResolvingError) { + // Already attempting to resolve an error. + return; + } else if (result.hasResolution()) { + try { + mResolvingError = true; + result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR); + } catch (IntentSender.SendIntentException e) { + // There was an error with the resolution intent. Try again. + mGoogleApiClient.connect(); + } + } else { + mResolvingError = false; + } + } +} diff --git a/samples/wearable/AgendaData/Application/src/main/res/drawable-hdpi/ic_launcher.png b/samples/wearable/AgendaData/Application/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..0564717bb Binary files /dev/null and b/samples/wearable/AgendaData/Application/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/wearable/AgendaData/Application/src/main/res/drawable-mdpi/ic_launcher.png b/samples/wearable/AgendaData/Application/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..0f4034743 Binary files /dev/null and b/samples/wearable/AgendaData/Application/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/wearable/AgendaData/Application/src/main/res/drawable-nodpi/nobody.png b/samples/wearable/AgendaData/Application/src/main/res/drawable-nodpi/nobody.png new file mode 100644 index 000000000..5a33d60e0 Binary files /dev/null and b/samples/wearable/AgendaData/Application/src/main/res/drawable-nodpi/nobody.png differ diff --git a/samples/wearable/AgendaData/Application/src/main/res/drawable-xhdpi/ic_launcher.png b/samples/wearable/AgendaData/Application/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d7705cfb0 Binary files /dev/null and b/samples/wearable/AgendaData/Application/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/wearable/AgendaData/Application/src/main/res/drawable-xxhdpi/ic_launcher.png b/samples/wearable/AgendaData/Application/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..f07299fd2 Binary files /dev/null and b/samples/wearable/AgendaData/Application/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/samples/wearable/AgendaData/Application/src/main/res/layout/main.xml b/samples/wearable/AgendaData/Application/src/main/res/layout/main.xml new file mode 100644 index 000000000..acd634024 --- /dev/null +++ b/samples/wearable/AgendaData/Application/src/main/res/layout/main.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + diff --git a/samples/wearable/Quiz/Application/src/main/res/layout/question_status_element.xml b/samples/wearable/Quiz/Application/src/main/res/layout/question_status_element.xml new file mode 100644 index 000000000..3cf9ec214 --- /dev/null +++ b/samples/wearable/Quiz/Application/src/main/res/layout/question_status_element.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/samples/wearable/Quiz/Application/src/main/res/values/dimens.xml b/samples/wearable/Quiz/Application/src/main/res/values/dimens.xml new file mode 100644 index 000000000..8f4947f3a --- /dev/null +++ b/samples/wearable/Quiz/Application/src/main/res/values/dimens.xml @@ -0,0 +1,13 @@ + + + 16dp + 16dp + + 12dp + 8dp + 8dp + 50dp + + 48dp + + diff --git a/samples/wearable/Quiz/Application/src/main/res/values/strings.xml b/samples/wearable/Quiz/Application/src/main/res/values/strings.xml new file mode 100644 index 000000000..5d3a44895 --- /dev/null +++ b/samples/wearable/Quiz/Application/src/main/res/values/strings.xml @@ -0,0 +1,25 @@ + + + + Quiz + + Read quiz from file + + Question + Choice A + Choice B + Choice C + Choice D + Add Question + + Quiz Status + Question + This question has not yet been answered. + This question has been answered incorrectly. + This question has been answered correctly! + This question was left blank. + + Reset Quiz + New Quiz + + diff --git a/samples/wearable/Quiz/Application/src/main/res/values/styles.xml b/samples/wearable/Quiz/Application/src/main/res/values/styles.xml new file mode 100644 index 000000000..7247b39b9 --- /dev/null +++ b/samples/wearable/Quiz/Application/src/main/res/values/styles.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/samples/wearable/Quiz/Wearable/build.gradle b/samples/wearable/Quiz/Wearable/build.gradle new file mode 100644 index 000000000..35b2f1f3d --- /dev/null +++ b/samples/wearable/Quiz/Wearable/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'android' + +android { + compileSdkVersion 20 + buildToolsVersion '20' + + defaultConfig { + minSdkVersion 20 + targetSdkVersion 20 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} + +dependencies { + compile 'com.google.android.gms:play-services:5.0.+@aar' + compile "com.android.support:support-v13:20.0.+" + compile "com.google.android.support:wearable:1.0.+" +} diff --git a/samples/wearable/Quiz/Wearable/proguard-rules.txt b/samples/wearable/Quiz/Wearable/proguard-rules.txt new file mode 100644 index 000000000..f2fe1559a --- /dev/null +++ b/samples/wearable/Quiz/Wearable/proguard-rules.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/samples/wearable/Quiz/Wearable/src/main/AndroidManifest.xml b/samples/wearable/Quiz/Wearable/src/main/AndroidManifest.xml new file mode 100644 index 000000000..3427de509 --- /dev/null +++ b/samples/wearable/Quiz/Wearable/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/Constants.java b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/Constants.java new file mode 100644 index 000000000..2038817c9 --- /dev/null +++ b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/Constants.java @@ -0,0 +1,25 @@ +package com.example.android.wearable.quiz; + +/** Constants used in the wearable app. */ +public final class Constants { + private Constants() { + } + + public static final String ANSWERS = "answers"; + public static final String CHOSEN_ANSWER_CORRECT = "chosen_answer_correct"; + public static final String CORRECT_ANSWER_INDEX = "correct_answer_index"; + public static final String QUESTION = "question"; + public static final String QUESTION_INDEX = "question_index"; + public static final String QUESTION_WAS_ANSWERED = "question_was_answered"; + public static final String QUESTION_WAS_DELETED = "question_was_deleted"; + + public static final String NUM_CORRECT = "num_correct"; + public static final String NUM_INCORRECT = "num_incorrect"; + public static final String NUM_SKIPPED = "num_skipped"; + + public static final String QUIZ_ENDED_PATH = "/quiz_ended"; + public static final String QUIZ_EXITED_PATH = "/quiz_exited"; + public static final String RESET_QUIZ_PATH = "/reset_quiz"; + + public static final int CONNECT_TIMEOUT_MS = 100; +} diff --git a/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/DeleteQuestionService.java b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/DeleteQuestionService.java new file mode 100644 index 000000000..6c5e09f15 --- /dev/null +++ b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/DeleteQuestionService.java @@ -0,0 +1,78 @@ +package com.example.android.wearable.quiz; + +import android.app.IntentService; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.DataApi; +import com.google.android.gms.wearable.DataMap; +import com.google.android.gms.wearable.DataMapItem; +import com.google.android.gms.wearable.PutDataMapRequest; +import com.google.android.gms.wearable.PutDataRequest; +import com.google.android.gms.wearable.Wearable; + +import java.util.concurrent.TimeUnit; + +import static com.example.android.wearable.quiz.Constants.CONNECT_TIMEOUT_MS; +import static com.example.android.wearable.quiz.Constants.QUESTION_WAS_DELETED; + +/** + * Used to update quiz status on the phone when user dismisses a question on the watch. + */ +public class DeleteQuestionService extends IntentService + implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { + + private static final String TAG = "DeleteQuestionReceiver"; + + private GoogleApiClient mGoogleApiClient; + + public DeleteQuestionService() { + super(DeleteQuestionService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + @Override + public void onHandleIntent(Intent intent) { + mGoogleApiClient.blockingConnect(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS); + Uri dataItemUri = intent.getData(); + if (!mGoogleApiClient.isConnected()) { + Log.e(TAG, "Failed to update data item " + dataItemUri + + " because client is disconnected from Google Play Services"); + return; + } + DataApi.DataItemResult dataItemResult = Wearable.DataApi.getDataItem( + mGoogleApiClient, dataItemUri).await(); + PutDataMapRequest putDataMapRequest = PutDataMapRequest + .createFromDataMapItem(DataMapItem.fromDataItem(dataItemResult.getDataItem())); + DataMap dataMap = putDataMapRequest.getDataMap(); + dataMap.putBoolean(QUESTION_WAS_DELETED, true); + PutDataRequest request = putDataMapRequest.asPutDataRequest(); + Wearable.DataApi.putDataItem(mGoogleApiClient, request).await(); + mGoogleApiClient.disconnect(); + } + + @Override + public void onConnected(Bundle bundle) { + } + + @Override + public void onConnectionSuspended(int i) { + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + } +} diff --git a/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/QuizListenerService.java b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/QuizListenerService.java new file mode 100644 index 000000000..f8c5322bb --- /dev/null +++ b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/QuizListenerService.java @@ -0,0 +1,203 @@ +package com.example.android.wearable.quiz; + +import static com.example.android.wearable.quiz.Constants.ANSWERS; +import static com.example.android.wearable.quiz.Constants.CONNECT_TIMEOUT_MS; +import static com.example.android.wearable.quiz.Constants.CORRECT_ANSWER_INDEX; +import static com.example.android.wearable.quiz.Constants.NUM_CORRECT; +import static com.example.android.wearable.quiz.Constants.NUM_INCORRECT; +import static com.example.android.wearable.quiz.Constants.NUM_SKIPPED; +import static com.example.android.wearable.quiz.Constants.QUESTION; +import static com.example.android.wearable.quiz.Constants.QUESTION_INDEX; +import static com.example.android.wearable.quiz.Constants.QUESTION_WAS_ANSWERED; +import static com.example.android.wearable.quiz.Constants.QUESTION_WAS_DELETED; +import static com.example.android.wearable.quiz.Constants.QUIZ_ENDED_PATH; +import static com.example.android.wearable.quiz.Constants.QUIZ_EXITED_PATH; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; +import android.text.SpannableStringBuilder; +import android.text.style.ForegroundColorSpan; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.data.FreezableUtils; +import com.google.android.gms.wearable.DataEvent; +import com.google.android.gms.wearable.DataEventBuffer; +import com.google.android.gms.wearable.DataItem; +import com.google.android.gms.wearable.DataMap; +import com.google.android.gms.wearable.DataMapItem; +import com.google.android.gms.wearable.MessageEvent; +import com.google.android.gms.wearable.Wearable; +import com.google.android.gms.wearable.WearableListenerService; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Listens to changes in DataItems, which represent quiz questions. + * If a new question is created, this builds a new notification for it. + * Otherwise, if a question is deleted, this cancels the corresponding notification. + * + * When the quiz ends, this listener receives a message telling it to create an end-of-quiz report. + */ +public class QuizListenerService extends WearableListenerService { + private static final String TAG = "QuizSample"; + private static final int QUIZ_REPORT_NOTIF_ID = -1; // Never used by question notifications. + private static final Map questionNumToDrawableId; + + static { + Map temp = new HashMap(4); + temp.put(0, R.drawable.ic_choice_a); + temp.put(1, R.drawable.ic_choice_b); + temp.put(2, R.drawable.ic_choice_c); + temp.put(3, R.drawable.ic_choice_d); + questionNumToDrawableId = Collections.unmodifiableMap(temp); + } + + @Override + public void onDataChanged(DataEventBuffer dataEvents) { + final List events = FreezableUtils.freezeIterable(dataEvents); + dataEvents.close(); + + GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .build(); + + ConnectionResult connectionResult = googleApiClient.blockingConnect(CONNECT_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + if (!connectionResult.isSuccess()) { + Log.e(TAG, "QuizListenerService failed to connect to GoogleApiClient."); + return; + } + + for (DataEvent event : events) { + if (event.getType() == DataEvent.TYPE_CHANGED) { + DataItem dataItem = event.getDataItem(); + DataMap dataMap = DataMapItem.fromDataItem(dataItem).getDataMap(); + if (dataMap.getBoolean(QUESTION_WAS_ANSWERED) + || dataMap.getBoolean(QUESTION_WAS_DELETED)) { + // Ignore the change in data; it is used in MainActivity to update + // the question's status (i.e. was the answer right or wrong or left blank). + continue; + } + String question = dataMap.getString(QUESTION); + int questionIndex = dataMap.getInt(QUESTION_INDEX); + int questionNum = questionIndex + 1; + String[] answers = dataMap.getStringArray(ANSWERS); + int correctAnswerIndex = dataMap.getInt(CORRECT_ANSWER_INDEX); + Intent deleteOperation = new Intent(this, DeleteQuestionService.class); + deleteOperation.setData(dataItem.getUri()); + PendingIntent deleteIntent = PendingIntent.getService(this, 0, + deleteOperation, PendingIntent.FLAG_UPDATE_CURRENT); + // First page of notification contains question as Big Text. + Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle() + .setBigContentTitle(getString(R.string.question, questionNum)) + .bigText(question); + Notification.Builder builder = new Notification.Builder(this) + .setStyle(bigTextStyle) + .setSmallIcon(R.drawable.ic_launcher) + .setLocalOnly(true) + .setDeleteIntent(deleteIntent); + + // Add answers as actions. + Notification.WearableExtender wearableOptions = new Notification.WearableExtender(); + for (int i = 0; i < answers.length; i++) { + Notification answerPage = new Notification.Builder(this) + .setContentTitle(question) + .setContentText(answers[i]) + .extend(new Notification.WearableExtender() + .setContentAction(i)) + .build(); + + boolean correct = (i == correctAnswerIndex); + Intent updateOperation = new Intent(this, UpdateQuestionService.class); + // Give each intent a unique action. + updateOperation.setAction("question_" + questionIndex + "_answer_" + i); + updateOperation.setData(dataItem.getUri()); + updateOperation.putExtra(UpdateQuestionService.EXTRA_QUESTION_INDEX, + questionIndex); + updateOperation.putExtra(UpdateQuestionService.EXTRA_QUESTION_CORRECT, correct); + PendingIntent updateIntent = PendingIntent.getService(this, 0, updateOperation, + PendingIntent.FLAG_UPDATE_CURRENT); + Notification.Action action = new Notification.Action.Builder( + questionNumToDrawableId.get(i), null, updateIntent) + .build(); + wearableOptions.addAction(action).addPage(answerPage); + } + builder.extend(wearableOptions); + Notification notification = builder.build(); + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) + .notify(questionIndex, notification); + } else if (event.getType() == DataEvent.TYPE_DELETED) { + Uri uri = event.getDataItem().getUri(); + // URI's are of the form "/question/0", "/question/1" etc. + // We use the question index as the notification id. + int notificationId = Integer.parseInt(uri.getLastPathSegment()); + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) + .cancel(notificationId); + } + // Delete the quiz report, if it exists. + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) + .cancel(QUIZ_REPORT_NOTIF_ID); + } + googleApiClient.disconnect(); + } + + @Override + public void onMessageReceived(MessageEvent messageEvent) { + String path = messageEvent.getPath(); + if (path.equals(QUIZ_EXITED_PATH)) { + // Remove any lingering question notifications. + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancelAll(); + } + if (path.equals(QUIZ_ENDED_PATH) || path.equals(QUIZ_EXITED_PATH)) { + // Quiz ended - display overall results. + DataMap dataMap = DataMap.fromByteArray(messageEvent.getData()); + int numCorrect = dataMap.getInt(NUM_CORRECT); + int numIncorrect = dataMap.getInt(NUM_INCORRECT); + int numSkipped = dataMap.getInt(NUM_SKIPPED); + + Notification.Builder builder = new Notification.Builder(this) + .setContentTitle(getString(R.string.quiz_report)) + .setSmallIcon(R.drawable.ic_launcher) + .setLocalOnly(true); + SpannableStringBuilder quizReportText = new SpannableStringBuilder(); + appendColored(quizReportText, String.valueOf(numCorrect), R.color.dark_green); + quizReportText.append(" " + getString(R.string.correct) + "\n"); + appendColored(quizReportText, String.valueOf(numIncorrect), R.color.dark_red); + quizReportText.append(" " + getString(R.string.incorrect) + "\n"); + appendColored(quizReportText, String.valueOf(numSkipped), R.color.dark_yellow); + quizReportText.append(" " + getString(R.string.skipped) + "\n"); + + builder.setContentText(quizReportText); + if (!path.equals(QUIZ_EXITED_PATH)) { + // Don't add reset option if user exited quiz (there might not be a quiz to reset!). + builder.addAction(R.drawable.ic_launcher, + getString(R.string.reset_quiz), getResetQuizPendingIntent()); + } + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) + .notify(QUIZ_REPORT_NOTIF_ID, builder.build()); + } + } + + private void appendColored(SpannableStringBuilder builder, String text, int colorResId) { + builder.append(text).setSpan(new ForegroundColorSpan(getResources().getColor(colorResId)), + builder.length() - text.length(), builder.length(), 0); + } + + /** + * Returns a PendingIntent that will send a message to the phone to reset the quiz when fired. + */ + private PendingIntent getResetQuizPendingIntent() { + Intent intent = new Intent(QuizReportActionService.ACTION_RESET_QUIZ) + .setClass(this, QuizReportActionService.class); + return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } +} diff --git a/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/QuizReportActionService.java b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/QuizReportActionService.java new file mode 100644 index 000000000..4bb5398fc --- /dev/null +++ b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/QuizReportActionService.java @@ -0,0 +1,51 @@ +package com.example.android.wearable.quiz; + +import android.app.IntentService; +import android.content.Intent; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.Node; +import com.google.android.gms.wearable.NodeApi; +import com.google.android.gms.wearable.Wearable; + +import java.util.concurrent.TimeUnit; + +import static com.example.android.wearable.quiz.Constants.CONNECT_TIMEOUT_MS; +import static com.example.android.wearable.quiz.Constants.RESET_QUIZ_PATH; + +/** + * Service to reset the quiz (by sending a message to the phone) when the Reset Quiz + * action on the Quiz Report is selected. + */ +public class QuizReportActionService extends IntentService { + public static final String ACTION_RESET_QUIZ = "com.example.android.wearable.quiz.RESET_QUIZ"; + + private static final String TAG = "QuizReportActionReceiver"; + + public QuizReportActionService() { + super(QuizReportActionService.class.getSimpleName()); + } + + @Override + public void onHandleIntent(Intent intent) { + if (intent.getAction().equals(ACTION_RESET_QUIZ)) { + GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .build(); + ConnectionResult result = googleApiClient.blockingConnect(CONNECT_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + if (!result.isSuccess()) { + Log.e(TAG, "QuizListenerService failed to connect to GoogleApiClient."); + return; + } + NodeApi.GetConnectedNodesResult nodes = + Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); + for (Node node : nodes.getNodes()) { + Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), RESET_QUIZ_PATH, + new byte[0]); + } + } + } +} diff --git a/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/UpdateQuestionService.java b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/UpdateQuestionService.java new file mode 100644 index 000000000..0668c97e0 --- /dev/null +++ b/samples/wearable/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/UpdateQuestionService.java @@ -0,0 +1,92 @@ +package com.example.android.wearable.quiz; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.DataApi; +import com.google.android.gms.wearable.DataMap; +import com.google.android.gms.wearable.DataMapItem; +import com.google.android.gms.wearable.PutDataMapRequest; +import com.google.android.gms.wearable.PutDataRequest; +import com.google.android.gms.wearable.Wearable; + +import java.util.concurrent.TimeUnit; + +import static com.example.android.wearable.quiz.Constants.CHOSEN_ANSWER_CORRECT; +import static com.example.android.wearable.quiz.Constants.QUESTION_INDEX; +import static com.example.android.wearable.quiz.Constants.QUESTION_WAS_ANSWERED; + +/** + * Updates quiz status on the phone when user selects an answer to a question on the watch. + */ +public class UpdateQuestionService extends IntentService + implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { + public static final String EXTRA_QUESTION_CORRECT = "extra_question_correct"; + public static final String EXTRA_QUESTION_INDEX = "extra_question_index"; + + private static final long TIME_OUT_MS = 100; + private static final String TAG = "UpdateQuestionService"; + + private GoogleApiClient mGoogleApiClient; + + public UpdateQuestionService() { + super(UpdateQuestionService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + @Override + protected void onHandleIntent(Intent intent) { + mGoogleApiClient.blockingConnect(TIME_OUT_MS, TimeUnit.MILLISECONDS); + Uri dataItemUri = intent.getData(); + if (!mGoogleApiClient.isConnected()) { + Log.e(TAG, "Failed to update data item " + dataItemUri + + " because client is disconnected from Google Play Services"); + return; + } + DataApi.DataItemResult dataItemResult = Wearable.DataApi.getDataItem( + mGoogleApiClient, dataItemUri).await(); + PutDataMapRequest putDataMapRequest = PutDataMapRequest + .createFromDataMapItem(DataMapItem.fromDataItem(dataItemResult.getDataItem())); + DataMap dataMap = putDataMapRequest.getDataMap(); + + // Update quiz status variables, which will be reflected on the phone. + int questionIndex = intent.getIntExtra(EXTRA_QUESTION_INDEX, -1); + boolean chosenAnswerCorrect = intent.getBooleanExtra(EXTRA_QUESTION_CORRECT, false); + dataMap.putInt(QUESTION_INDEX, questionIndex); + dataMap.putBoolean(CHOSEN_ANSWER_CORRECT, chosenAnswerCorrect); + dataMap.putBoolean(QUESTION_WAS_ANSWERED, true); + PutDataRequest request = putDataMapRequest.asPutDataRequest(); + Wearable.DataApi.putDataItem(mGoogleApiClient, request).await(); + + // Remove this question notification. + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancel(questionIndex); + mGoogleApiClient.disconnect(); + } + + @Override + public void onConnected(Bundle connectionHint) { + } + + @Override + public void onConnectionSuspended(int cause) { + } + + @Override + public void onConnectionFailed(ConnectionResult result) { + } +} diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_a.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_a.png new file mode 100644 index 000000000..de18ce11a Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_a.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_b.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_b.png new file mode 100644 index 000000000..3cdfe9750 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_b.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_c.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_c.png new file mode 100644 index 000000000..f0ed2ef99 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_c.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_d.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_d.png new file mode 100644 index 000000000..c158d2953 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_choice_d.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_launcher.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100755 index 000000000..91a8cff95 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_unknown_choice.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_unknown_choice.png new file mode 100644 index 000000000..9aed51728 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-hdpi/ic_unknown_choice.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_a.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_a.png new file mode 100644 index 000000000..57459365c Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_a.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_b.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_b.png new file mode 100644 index 000000000..958b92e31 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_b.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_c.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_c.png new file mode 100644 index 000000000..9fcfab754 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_c.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_d.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_d.png new file mode 100644 index 000000000..821cadb78 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_choice_d.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_launcher.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100755 index 000000000..728ee6d73 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_unknown_choice.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_unknown_choice.png new file mode 100644 index 000000000..b8030ef08 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-mdpi/ic_unknown_choice.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_a.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_a.png new file mode 100644 index 000000000..3dba96f32 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_a.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_b.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_b.png new file mode 100644 index 000000000..9ca3c85f2 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_b.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_c.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_c.png new file mode 100644 index 000000000..b84b3b76d Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_c.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_d.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_d.png new file mode 100644 index 000000000..185e91ec8 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_choice_d.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100755 index 000000000..64a585453 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_unknown_choice.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_unknown_choice.png new file mode 100644 index 000000000..57838d152 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xhdpi/ic_unknown_choice.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100755 index 000000000..86a395bb6 Binary files /dev/null and b/samples/wearable/Quiz/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/samples/wearable/Quiz/Wearable/src/main/res/values/colors.xml b/samples/wearable/Quiz/Wearable/src/main/res/values/colors.xml new file mode 100644 index 000000000..e19c9e7d7 --- /dev/null +++ b/samples/wearable/Quiz/Wearable/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #009900 + #800000 + #FF9900 + \ No newline at end of file diff --git a/samples/wearable/Quiz/Wearable/src/main/res/values/strings.xml b/samples/wearable/Quiz/Wearable/src/main/res/values/strings.xml new file mode 100644 index 000000000..2136073cc --- /dev/null +++ b/samples/wearable/Quiz/Wearable/src/main/res/values/strings.xml @@ -0,0 +1,14 @@ + + + + Quiz Sample Wearable App + + Question %d + + Quiz Report + correct + incorrect + skipped + Reset Quiz + + diff --git a/samples/wearable/Quiz/build.gradle b/samples/wearable/Quiz/build.gradle new file mode 100644 index 000000000..4c8f6d8b9 --- /dev/null +++ b/samples/wearable/Quiz/build.gradle @@ -0,0 +1,16 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.11.+' + } +} + +allprojects { + repositories { + mavenCentral() + } +} diff --git a/samples/wearable/Quiz/gradle.properties b/samples/wearable/Quiz/gradle.properties new file mode 100644 index 000000000..5d08ba75b --- /dev/null +++ b/samples/wearable/Quiz/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Settings specified in this file will override any Gradle settings +# configured through the IDE. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true \ No newline at end of file diff --git a/samples/wearable/Quiz/gradle/wrapper/gradle-wrapper.jar b/samples/wearable/Quiz/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..583859812 Binary files /dev/null and b/samples/wearable/Quiz/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/wearable/Quiz/gradle/wrapper/gradle-wrapper.properties b/samples/wearable/Quiz/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e83e46081 --- /dev/null +++ b/samples/wearable/Quiz/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Mar 14 14:02:22 PDT 2014 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip diff --git a/samples/wearable/Quiz/gradlew b/samples/wearable/Quiz/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/samples/wearable/Quiz/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/wearable/Quiz/gradlew.bat b/samples/wearable/Quiz/gradlew.bat new file mode 100755 index 000000000..aec99730b --- /dev/null +++ b/samples/wearable/Quiz/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/wearable/Quiz/settings.gradle b/samples/wearable/Quiz/settings.gradle new file mode 100644 index 000000000..d6b72d158 --- /dev/null +++ b/samples/wearable/Quiz/settings.gradle @@ -0,0 +1 @@ +include 'Application', 'Wearable' diff --git a/samples/wearable/RecipeAssistant/Application/build.gradle b/samples/wearable/RecipeAssistant/Application/build.gradle new file mode 100644 index 000000000..03f9be847 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'android' + +android { + compileSdkVersion 19 + buildToolsVersion '20' + + defaultConfig { + minSdkVersion 18 + targetSdkVersion 19 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} + +dependencies { + compile "com.android.support:support-v4:20.0.+" +} diff --git a/samples/wearable/RecipeAssistant/Application/proguard-rules.txt b/samples/wearable/RecipeAssistant/Application/proguard-rules.txt new file mode 100644 index 000000000..08e5bdc8d --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/proguard-rules.txt @@ -0,0 +1,22 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} +-dontwarn android.support.wearable.view.DelayedConfirmationView +-dontwarn android.support.wearable.view.CircledImageView diff --git a/samples/wearable/RecipeAssistant/Application/src/main/AndroidManifest.xml b/samples/wearable/RecipeAssistant/Application/src/main/AndroidManifest.xml new file mode 100644 index 000000000..7a1a04a3f --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/beef-brisket-chili.json b/samples/wearable/RecipeAssistant/Application/src/main/assets/beef-brisket-chili.json new file mode 100644 index 000000000..19598f9e5 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/assets/beef-brisket-chili.json @@ -0,0 +1,91 @@ +{ +name: "beef-brisket-chili.json", +steps: [ +{ +text: "Place the cinnamon stick, cumin, paprika and oregano in a pestle and mortar. Bash and grind until the cinnamon is in very small flakes. ", +img: "step-1-grind-spices.jpg" +}, +{ +text: "Score the beef and rub the spice mix into the cuts. ", +img: "step-2-score-beef.jpg" +}, +{ +text: "Season the beef and drizzle with olive oil. Brown in a hot pan. ", +img: "step-3-brown-beef.jpg" +}, +{ +text: "Finely slice the onion and fry in a little olive oil together with the chillies, finely sliced. You can control the heat of your chili by deseeding some of the chillies. ", +img: "step-4-fry-onion.jpg" +}, +{ +text: "Add the browned brisket to the pan with the onions and chillies and fry on a low heat." +}, +{ +text: "Place the bell peppers, roughly sliced, the bay leaves and chopped tomatoes in a large covered pot. Bring to the boil and then add the beef, onions and chillies. ", +img: "step-6-combine.jpg" +}, +{ +text: "Bring back to the boil, cover and allow to simmer on a low heat for 4 hours." +}, +{ +text: "Remove the brisket from the pot, place on a large plate and use two forks to pull the beef apart into individual pieces. ", +img: "step-8-pull.jpg" +}, +{ +text: "Remove the bay leaves and add the pulled beef and coriander back to the pot, together with the kidney beans." +}, +{ +text: "Bring back to the boil and simmer gently for a further 15 - 20 mins." +} +], +summary: "", +title: "Beef brisket chili", +img: "chili.jpg", +serving: [ +"Serve with rice, yoghurt and fresh guacamole. Garnish with the remaining coriander." +], +ingredients: [ +{ +text: "3 lbs beef brisket" +}, +{ +text: "2 red onions" +}, +{ +text: "4 jalapeno chillies" +}, +{ +text: "1 large cinnamon stick" +}, +{ +text: "1 tbsp ground cumin" +}, +{ +text: "1 tbsp paprika" +}, +{ +text: "1 heaped tbsp dried oregano" +}, +{ +text: "2 fresh bay leaves" +}, +{ +text: "2 red bell peppers" +}, +{ +text: "2 green bell peppers" +}, +{ +text: "800g tin chopped tomatoes" +}, +{ +text: "400g tin kidney beans" +}, +{ +text: "400ml beef stock" +}, +{ +text: "1/2 bunch coriander" +} +] +} \ No newline at end of file diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/chili.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/chili.jpg new file mode 100644 index 000000000..ca5f7510f Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/chili.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/guacamole.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/guacamole.jpg new file mode 100644 index 000000000..77175eb88 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/guacamole.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/guacamole.json b/samples/wearable/RecipeAssistant/Application/src/main/assets/guacamole.json new file mode 100644 index 000000000..5fae53fe1 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/assets/guacamole.json @@ -0,0 +1,36 @@ +{ +name: "guacamole.json", +steps: [ +{ +text: "Use a spoon to scoop the flesh of the avocados into a bowl." +}, +{ +text: "Mash with a fork until fairly smooth and creamy. Preserve some small solid chunks to add texture." +}, +{ +text: "Add the juice of the lime. ", +img: "step-3-lime.jpg" +}, +{ +text: "Add the cilantro." +}, +{ +text: "Mix thoroughly." +} +], +summary: "Some guacamole recipes call for many ingredients and can be a pain to prepare. This super simple guac can be thrown together in a couple of minutes and tastes great.", +title: "Super simple guacamole", +img: "guacamole.jpg", +serving: "", +ingredients: [ +{ +text: "2 ripe avocados" +}, +{ +text: "1 lime" +}, +{ +text: "2 tbsp cilantro" +} +] +} \ No newline at end of file diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/irish-stew.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/irish-stew.jpg new file mode 100644 index 000000000..0c20766fd Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/irish-stew.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/northern-irish-vegetable-soup.json b/samples/wearable/RecipeAssistant/Application/src/main/assets/northern-irish-vegetable-soup.json new file mode 100644 index 000000000..55d259190 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/assets/northern-irish-vegetable-soup.json @@ -0,0 +1,46 @@ +{ +name: "northern-irish-vegetable-soup.json", +steps: [ +{ +text: "Place the beef in a large stock pot, cover with water and stew for 1 - 2 hours." +}, +{ +text: "Allow the stock to cool, skim off any fat." +}, +{ +text: "Add the soup mix to the stock, bring to the boil and simmer for 1 hour." +}, +{ +text: "Roughly chop the leeks (green and white parts), onion, carrots, celery and parsley. Add to the soup. Season well and simmer until the vegetables are soft." +} +], +summary: "This recipe is apparently unique to Northern Ireland and uses soup/herb celery which is hard to find outside the area, but regular table celery can be substituted (including the leaves).", +title: "Northern irish vegetable soup", +img: "irish-stew.jpg", +serving: [ +"Whole boiled potatoes are traditionally placed in the soup at time of serving." +], +ingredients: [ +{ +text: "2 lbs beef shin or similar beef on bone" +}, +{ +text: "60g soup mix (30g barley, 15g red lentils, 15g split peas)" +}, +{ +text: "3 carrots" +}, +{ +text: "1 white onion" +}, +{ +text: "field celery or 1 stalk celery, plus any leaves on the bunch" +}, +{ +text: "2 leeks" +}, +{ +text: "1 bunch parsley" +} +] +} \ No newline at end of file diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/recipelist.json b/samples/wearable/RecipeAssistant/Application/src/main/assets/recipelist.json new file mode 100644 index 000000000..efd787d35 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/assets/recipelist.json @@ -0,0 +1,23 @@ +{ +recipe_list: +[ +{ +name: "guacamole.json", +summary: "Some guacamole recipes call for many ingredients and can be a pain to prepare. This super simple guac can be thrown together in a couple of minutes and tastes great.", +title: "Super simple guacamole", +img: "guacamole.jpg" +}, +{ +name: "northern-irish-vegetable-soup.json", +summary: "This recipe is apparently unique to Northern Ireland and uses soup/herb celery which is hard to find outside the area, but regular table celery can be substituted (including the leaves).", +title: "Northern irish vegetable soup", +img: "irish-stew.jpg" +}, +{ +name: "beef-brisket-chili.json", +summary: "", +title: "Beef brisket chili", +img: "chili.jpg" +} +] +} \ No newline at end of file diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/step-1-grind-spices.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-1-grind-spices.jpg new file mode 100644 index 000000000..9b5f2b978 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-1-grind-spices.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/step-2-score-beef.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-2-score-beef.jpg new file mode 100644 index 000000000..66c549dfa Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-2-score-beef.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/step-3-brown-beef.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-3-brown-beef.jpg new file mode 100644 index 000000000..1bb801e6e Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-3-brown-beef.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/step-3-lime.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-3-lime.jpg new file mode 100644 index 000000000..b615d5587 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-3-lime.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/step-4-fry-onion.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-4-fry-onion.jpg new file mode 100644 index 000000000..b5d2a1875 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-4-fry-onion.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/step-6-combine.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-6-combine.jpg new file mode 100644 index 000000000..fe2940578 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-6-combine.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/assets/step-8-pull.jpg b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-8-pull.jpg new file mode 100644 index 000000000..f8288a849 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/assets/step-8-pull.jpg differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/AssetUtils.java b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/AssetUtils.java new file mode 100644 index 000000000..c7991bbb0 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/AssetUtils.java @@ -0,0 +1,64 @@ +package com.example.android.wearable.recipeassistant; + + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; + +final class AssetUtils { + private static final String TAG = "RecipeAssistant"; + + public static byte[] loadAsset(Context context, String asset) { + byte[] buffer = null; + try { + InputStream is = context.getAssets().open(asset); + int size = is.available(); + buffer = new byte[size]; + is.read(buffer); + is.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to load asset " + asset + ": " + e); + } + return buffer; + } + + public static JSONObject loadJSONAsset(Context context, String asset) { + String jsonString = new String(loadAsset(context, asset)); + JSONObject jsonObject = null; + try { + jsonObject = new JSONObject(jsonString); + } catch (JSONException e) { + Log.e(TAG, "Failed to parse JSON asset " + asset + ": " + e); + } + return jsonObject; + } + + public static Bitmap loadBitmapAsset(Context context, String asset) { + InputStream is = null; + Bitmap bitmap = null; + try { + is = context.getAssets().open(asset); + if (is != null) { + bitmap = BitmapFactory.decodeStream(is); + } + } catch (IOException e) { + Log.e(TAG, e.toString()); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + Log.e(TAG, "Cannot close InputStream: ", e); + } + } + } + return bitmap; + } +} diff --git a/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/Constants.java b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/Constants.java new file mode 100644 index 000000000..375444efb --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/Constants.java @@ -0,0 +1,27 @@ +package com.example.android.wearable.recipeassistant; + +public final class Constants { + private Constants() { + } + public static final String RECIPE_LIST_FILE = "recipelist.json"; + public static final String RECIPE_NAME_TO_LOAD = "recipe_name"; + + public static final String RECIPE_FIELD_LIST = "recipe_list"; + public static final String RECIPE_FIELD_IMAGE = "img"; + public static final String RECIPE_FIELD_INGREDIENTS = "ingredients"; + public static final String RECIPE_FIELD_NAME = "name"; + public static final String RECIPE_FIELD_SUMMARY = "summary"; + public static final String RECIPE_FIELD_STEPS = "steps"; + public static final String RECIPE_FIELD_TEXT = "text"; + public static final String RECIPE_FIELD_TITLE = "title"; + public static final String RECIPE_FIELD_STEP_TEXT = "step_text"; + public static final String RECIPE_FIELD_STEP_IMAGE = "step_image"; + + static final String ACTION_START_COOKING = + "com.example.android.wearable.recipeassistant.START_COOKING"; + public static final String EXTRA_RECIPE = "recipe"; + + public static final int NOTIFICATION_ID = 0; + public static final int NOTIFICATION_IMAGE_WIDTH = 280; + public static final int NOTIFICATION_IMAGE_HEIGHT = 280; +} diff --git a/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/MainActivity.java b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/MainActivity.java new file mode 100644 index 000000000..a456190f2 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/MainActivity.java @@ -0,0 +1,35 @@ + +package com.example.android.wearable.recipeassistant; + +import android.app.ListActivity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.ListView; + +public class MainActivity extends ListActivity { + + private static final String TAG = "RecipeAssistant"; + private RecipeListAdapter mAdapter; + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG , "onListItemClick " + position); + } + String itemName = mAdapter.getItemName(position); + Intent intent = new Intent(getApplicationContext(), RecipeActivity.class); + intent.putExtra(Constants.RECIPE_NAME_TO_LOAD, itemName); + startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(android.R.layout.list_content); + + mAdapter = new RecipeListAdapter(this); + setListAdapter(mAdapter); + } +} diff --git a/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/Recipe.java b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/Recipe.java new file mode 100644 index 000000000..24356b61e --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/Recipe.java @@ -0,0 +1,110 @@ +package com.example.android.wearable.recipeassistant; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +public class Recipe { + private static final String TAG = "RecipeAssistant"; + + public String titleText; + public String summaryText; + public String recipeImage; + public String ingredientsText; + + public static class RecipeStep { + RecipeStep() { } + public String stepImage; + public String stepText; + + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putString(Constants.RECIPE_FIELD_STEP_TEXT, stepText); + bundle.putString(Constants.RECIPE_FIELD_STEP_IMAGE, stepImage); + return bundle; + } + + public static RecipeStep fromBundle(Bundle bundle) { + RecipeStep recipeStep = new RecipeStep(); + recipeStep.stepText = bundle.getString(Constants.RECIPE_FIELD_STEP_TEXT); + recipeStep.stepImage = bundle.getString(Constants.RECIPE_FIELD_STEP_IMAGE); + return recipeStep; + } + } + ArrayList recipeSteps; + + public Recipe() { + recipeSteps = new ArrayList(); + } + + public static Recipe fromJson(Context context, JSONObject json) { + Recipe recipe = new Recipe(); + try { + recipe.titleText = json.getString(Constants.RECIPE_FIELD_TITLE); + recipe.summaryText = json.getString(Constants.RECIPE_FIELD_SUMMARY); + if (json.has(Constants.RECIPE_FIELD_IMAGE)) { + recipe.recipeImage = json.getString(Constants.RECIPE_FIELD_IMAGE); + } + JSONArray ingredients = json.getJSONArray(Constants.RECIPE_FIELD_INGREDIENTS); + recipe.ingredientsText = ""; + for (int i = 0; i < ingredients.length(); i++) { + recipe.ingredientsText += " - " + + ingredients.getJSONObject(i).getString(Constants.RECIPE_FIELD_TEXT) + "\n"; + } + + JSONArray steps = json.getJSONArray(Constants.RECIPE_FIELD_STEPS); + for (int i = 0; i < steps.length(); i++) { + JSONObject step = steps.getJSONObject(i); + RecipeStep recipeStep = new RecipeStep(); + recipeStep.stepText = step.getString(Constants.RECIPE_FIELD_TEXT); + if (step.has(Constants.RECIPE_FIELD_IMAGE)) { + recipeStep.stepImage = step.getString(Constants.RECIPE_FIELD_IMAGE); + } + recipe.recipeSteps.add(recipeStep); + } + } catch (JSONException e) { + Log.e(TAG, "Error loading recipe: " + e); + return null; + } + return recipe; + } + + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putString(Constants.RECIPE_FIELD_TITLE, titleText); + bundle.putString(Constants.RECIPE_FIELD_SUMMARY, summaryText); + bundle.putString(Constants.RECIPE_FIELD_IMAGE, recipeImage); + bundle.putString(Constants.RECIPE_FIELD_INGREDIENTS, ingredientsText); + if (recipeSteps != null) { + ArrayList stepBundles = new ArrayList(recipeSteps.size()); + for (RecipeStep recipeStep : recipeSteps) { + stepBundles.add(recipeStep.toBundle()); + } + bundle.putParcelableArrayList(Constants.RECIPE_FIELD_STEPS, stepBundles); + } + return bundle; + } + + public static Recipe fromBundle(Bundle bundle) { + Recipe recipe = new Recipe(); + recipe.titleText = bundle.getString(Constants.RECIPE_FIELD_TITLE); + recipe.summaryText = bundle.getString(Constants.RECIPE_FIELD_SUMMARY); + recipe.recipeImage = bundle.getString(Constants.RECIPE_FIELD_IMAGE); + recipe.ingredientsText = bundle.getString(Constants.RECIPE_FIELD_INGREDIENTS); + ArrayList stepBundles = + bundle.getParcelableArrayList(Constants.RECIPE_FIELD_STEPS); + if (stepBundles != null) { + for (Parcelable stepBundle : stepBundles) { + recipe.recipeSteps.add(RecipeStep.fromBundle((Bundle) stepBundle)); + } + } + return recipe; + } +} diff --git a/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/RecipeActivity.java b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/RecipeActivity.java new file mode 100644 index 000000000..1637745ab --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/RecipeActivity.java @@ -0,0 +1,122 @@ +package com.example.android.wearable.recipeassistant; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.json.JSONObject; + +public class RecipeActivity extends Activity { + private static final String TAG = "RecipeAssistant"; + private String mRecipeName; + private Recipe mRecipe; + private ImageView mImageView; + private TextView mTitleTextView; + private TextView mSummaryTextView; + private TextView mIngredientsTextView; + private LinearLayout mStepsLayout; + + @Override + protected void onStart() { + super.onStart(); + Intent intent = getIntent(); + mRecipeName = intent.getStringExtra(Constants.RECIPE_NAME_TO_LOAD); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Intent: " + intent.toString() + " " + mRecipeName); + } + loadRecipe(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.recipe); + mTitleTextView = (TextView) findViewById(R.id.recipeTextTitle); + mSummaryTextView = (TextView) findViewById(R.id.recipeTextSummary); + mImageView = (ImageView) findViewById(R.id.recipeImageView); + mIngredientsTextView = (TextView) findViewById(R.id.textIngredients); + mStepsLayout = (LinearLayout) findViewById(R.id.layoutSteps); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.action_cook: + startCooking(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void loadRecipe() { + JSONObject jsonObject = AssetUtils.loadJSONAsset(this, mRecipeName); + if (jsonObject != null) { + mRecipe = Recipe.fromJson(this, jsonObject); + if (mRecipe != null) { + displayRecipe(mRecipe); + } + } + } + + private void displayRecipe(Recipe recipe) { + Animation fadeIn = AnimationUtils.loadAnimation(this, android.R.anim.fade_in); + mTitleTextView.setAnimation(fadeIn); + mTitleTextView.setText(recipe.titleText); + mSummaryTextView.setText(recipe.summaryText); + if (recipe.recipeImage != null) { + mImageView.setAnimation(fadeIn); + Bitmap recipeImage = AssetUtils.loadBitmapAsset(this, recipe.recipeImage); + mImageView.setImageBitmap(recipeImage); + } + mIngredientsTextView.setText(recipe.ingredientsText); + + findViewById(R.id.ingredientsHeader).setAnimation(fadeIn); + findViewById(R.id.ingredientsHeader).setVisibility(View.VISIBLE); + findViewById(R.id.stepsHeader).setAnimation(fadeIn); + + findViewById(R.id.stepsHeader).setVisibility(View.VISIBLE); + + LayoutInflater inf = LayoutInflater.from(this); + mStepsLayout.removeAllViews(); + int stepNumber = 1; + for (Recipe.RecipeStep step : recipe.recipeSteps) { + View view = inf.inflate(R.layout.step_item, null); + ImageView iv = (ImageView) view.findViewById(R.id.stepImageView); + if (step.stepImage == null) { + iv.setVisibility(View.GONE); + } else { + Bitmap stepImage = AssetUtils.loadBitmapAsset(this, step.stepImage); + iv.setImageBitmap(stepImage); + } + ((TextView) view.findViewById(R.id.textStep)).setText( + (stepNumber++) + ". " + step.stepText); + mStepsLayout.addView(view); + } + } + + private void startCooking() { + Intent intent = new Intent(this, RecipeService.class); + intent.setAction(Constants.ACTION_START_COOKING); + intent.putExtra(Constants.EXTRA_RECIPE, mRecipe.toBundle()); + startService(intent); + } +} diff --git a/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/RecipeListAdapter.java b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/RecipeListAdapter.java new file mode 100644 index 000000000..9602bc6ed --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/RecipeListAdapter.java @@ -0,0 +1,157 @@ +package com.example.android.wearable.recipeassistant; + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Bitmap; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class RecipeListAdapter implements ListAdapter { + private String TAG = "RecipeListAdapter"; + + private class Item { + String title; + String name; + String summary; + Bitmap image; + } + + private List mItems = new ArrayList(); + private Context mContext; + private DataSetObserver mObserver; + + public RecipeListAdapter(Context context) { + mContext = context; + loadRecipeList(); + } + + private void loadRecipeList() { + JSONObject jsonObject = AssetUtils.loadJSONAsset(mContext, Constants.RECIPE_LIST_FILE); + if (jsonObject != null) { + List items = parseJson(jsonObject); + appendItemsToList(items); + } + } + + private List parseJson(JSONObject json) { + List result = new ArrayList(); + try { + JSONArray items = json.getJSONArray(Constants.RECIPE_FIELD_LIST); + for (int i = 0; i < items.length(); i++) { + JSONObject item = items.getJSONObject(i); + Item parsed = new Item(); + parsed.name = item.getString(Constants.RECIPE_FIELD_NAME); + parsed.title = item.getString(Constants.RECIPE_FIELD_TITLE); + if (item.has(Constants.RECIPE_FIELD_IMAGE)) { + String imageFile = item.getString(Constants.RECIPE_FIELD_IMAGE); + parsed.image = AssetUtils.loadBitmapAsset(mContext, imageFile); + } + parsed.summary = item.getString(Constants.RECIPE_FIELD_SUMMARY); + result.add(parsed); + } + } catch (JSONException e) { + Log.e(TAG, "Failed to parse recipe list: " + e); + } + return result; + } + + private void appendItemsToList(List items) { + mItems.addAll(items); + if (mObserver != null) { + mObserver.onChanged(); + } + } + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mItems.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + LayoutInflater inf = LayoutInflater.from(mContext); + view = inf.inflate(R.layout.list_item, null); + } + Item item = (Item) getItem(position); + TextView titleView = (TextView) view.findViewById(R.id.textTitle); + TextView summaryView = (TextView) view.findViewById(R.id.textSummary); + ImageView iv = (ImageView) view.findViewById(R.id.imageView); + + titleView.setText(item.title); + summaryView.setText(item.summary); + if (item.image != null) { + iv.setImageBitmap(item.image); + } else { + iv.setImageDrawable(mContext.getResources().getDrawable(R.drawable.ic_noimage)); + } + return view; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public boolean isEmpty() { + return mItems.isEmpty(); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mObserver = observer; + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mObserver = null; + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + public String getItemName(int position) { + return mItems.get(position).name; + } +} diff --git a/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/RecipeService.java b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/RecipeService.java new file mode 100644 index 000000000..607dd2818 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/java/com/example/android/wearable/recipeassistant/RecipeService.java @@ -0,0 +1,80 @@ +package com.example.android.wearable.recipeassistant; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Binder; +import android.os.IBinder; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.app.NotificationCompat; + +import java.util.ArrayList; + +public class RecipeService extends Service { + private NotificationManagerCompat mNotificationManager; + private Binder mBinder = new LocalBinder(); + private Recipe mRecipe; + + public class LocalBinder extends Binder { + RecipeService getService() { + return RecipeService.this; + } + } + + @Override + public void onCreate() { + mNotificationManager = NotificationManagerCompat.from(this); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent.getAction().equals(Constants.ACTION_START_COOKING)) { + createNotification(intent); + return START_STICKY; + } + return START_NOT_STICKY; + } + + private void createNotification(Intent intent) { + mRecipe = Recipe.fromBundle(intent.getBundleExtra(Constants.EXTRA_RECIPE)); + ArrayList notificationPages = new ArrayList(); + + int stepCount = mRecipe.recipeSteps.size(); + + for (int i = 0; i < stepCount; ++i) { + Recipe.RecipeStep recipeStep = mRecipe.recipeSteps.get(i); + NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle(); + style.bigText(recipeStep.stepText); + style.setBigContentTitle(String.format( + getResources().getString(R.string.step_count), i + 1, stepCount)); + style.setSummaryText(""); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setStyle(style); + notificationPages.add(builder.build()); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + + if (mRecipe.recipeImage != null) { + Bitmap recipeImage = Bitmap.createScaledBitmap( + AssetUtils.loadBitmapAsset(this, mRecipe.recipeImage), + Constants.NOTIFICATION_IMAGE_WIDTH, Constants.NOTIFICATION_IMAGE_HEIGHT, false); + builder.setLargeIcon(recipeImage); + } + builder.setContentTitle(mRecipe.titleText); + builder.setContentText(mRecipe.summaryText); + builder.setSmallIcon(R.mipmap.ic_notification_recipe); + + Notification notification = builder + .extend(new NotificationCompat.WearableExtender() + .addPages(notificationPages)) + .build(); + mNotificationManager.notify(Constants.NOTIFICATION_ID, notification); + } +} diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/drawable-hdpi/ic_noimage.png b/samples/wearable/RecipeAssistant/Application/src/main/res/drawable-hdpi/ic_noimage.png new file mode 100644 index 000000000..7bba7ab76 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/drawable-hdpi/ic_noimage.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/drawable-mdpi/ic_noimage.png b/samples/wearable/RecipeAssistant/Application/src/main/res/drawable-mdpi/ic_noimage.png new file mode 100644 index 000000000..a5ad26f64 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/drawable-mdpi/ic_noimage.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/drawable-xhdpi/ic_noimage.png b/samples/wearable/RecipeAssistant/Application/src/main/res/drawable-xhdpi/ic_noimage.png new file mode 100644 index 000000000..8b631d121 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/drawable-xhdpi/ic_noimage.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/layout/list_item.xml b/samples/wearable/RecipeAssistant/Application/src/main/res/layout/list_item.xml new file mode 100644 index 000000000..421d057d0 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/res/layout/list_item.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/layout/recipe.xml b/samples/wearable/RecipeAssistant/Application/src/main/res/layout/recipe.xml new file mode 100644 index 000000000..220aac2d8 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/res/layout/recipe.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/layout/step_item.xml b/samples/wearable/RecipeAssistant/Application/src/main/res/layout/step_item.xml new file mode 100644 index 000000000..2eee9192b --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/res/layout/step_item.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/menu/main.xml b/samples/wearable/RecipeAssistant/Application/src/main/res/menu/main.xml new file mode 100644 index 000000000..fd1ead7f8 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/res/menu/main.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-hdpi/ic_app_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-hdpi/ic_app_recipe.png new file mode 100644 index 000000000..8ceb8696d Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-hdpi/ic_app_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-hdpi/ic_notification_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-hdpi/ic_notification_recipe.png new file mode 100644 index 000000000..844d8ede8 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-hdpi/ic_notification_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-mdpi/ic_app_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-mdpi/ic_app_recipe.png new file mode 100644 index 000000000..b884789c5 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-mdpi/ic_app_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-mdpi/ic_notification_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-mdpi/ic_notification_recipe.png new file mode 100644 index 000000000..3f3f58cde Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-mdpi/ic_notification_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xhdpi/ic_app_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xhdpi/ic_app_recipe.png new file mode 100644 index 000000000..2a27c3283 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xhdpi/ic_app_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xhdpi/ic_notification_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xhdpi/ic_notification_recipe.png new file mode 100644 index 000000000..5a99b7c71 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xhdpi/ic_notification_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxhdpi/ic_app_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxhdpi/ic_app_recipe.png new file mode 100644 index 000000000..b10c77068 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxhdpi/ic_app_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxhdpi/ic_notification_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxhdpi/ic_notification_recipe.png new file mode 100644 index 000000000..799726d05 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxhdpi/ic_notification_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxxhdpi/ic_app_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxxhdpi/ic_app_recipe.png new file mode 100644 index 000000000..606f07f77 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxxhdpi/ic_app_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxxhdpi/ic_notification_recipe.png b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxxhdpi/ic_notification_recipe.png new file mode 100644 index 000000000..30e28a884 Binary files /dev/null and b/samples/wearable/RecipeAssistant/Application/src/main/res/mipmap-xxxhdpi/ic_notification_recipe.png differ diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/values/colors.xml b/samples/wearable/RecipeAssistant/Application/src/main/res/values/colors.xml new file mode 100644 index 000000000..6e4689ae1 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #dfff + #6aaa + diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/values/strings.xml b/samples/wearable/RecipeAssistant/Application/src/main/res/values/strings.xml new file mode 100644 index 000000000..e99a61202 --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + Recipe Assistant + Settings + Start + Steps + Ingredients + Step %1$d of %2$d + diff --git a/samples/wearable/RecipeAssistant/Application/src/main/res/values/styles.xml b/samples/wearable/RecipeAssistant/Application/src/main/res/values/styles.xml new file mode 100644 index 000000000..93071e24e --- /dev/null +++ b/samples/wearable/RecipeAssistant/Application/src/main/res/values/styles.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/samples/wearable/RecipeAssistant/_index.html b/samples/wearable/RecipeAssistant/_index.html new file mode 100644 index 000000000..3dce7d3df --- /dev/null +++ b/samples/wearable/RecipeAssistant/_index.html @@ -0,0 +1,8 @@ +

This phone application uses the enhanced notifications API to display +recipe instructions using paged notifications. After starting the application +on your phone, you can browse from a short list of recipes and select one to +view. Each recipe is broken down into a number of steps; when ready, you can +click on the START action in the action bar to send the steps to the wearable. +On the wearable device, the steps are displayed as a multi-page notification, +with one page for each step in the recipe.

+ diff --git a/samples/wearable/RecipeAssistant/build.gradle b/samples/wearable/RecipeAssistant/build.gradle new file mode 100644 index 000000000..4c8f6d8b9 --- /dev/null +++ b/samples/wearable/RecipeAssistant/build.gradle @@ -0,0 +1,16 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.11.+' + } +} + +allprojects { + repositories { + mavenCentral() + } +} diff --git a/samples/wearable/RecipeAssistant/gradle/wrapper/gradle-wrapper.jar b/samples/wearable/RecipeAssistant/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..583859812 Binary files /dev/null and b/samples/wearable/RecipeAssistant/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/wearable/RecipeAssistant/gradle/wrapper/gradle-wrapper.properties b/samples/wearable/RecipeAssistant/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e83e46081 --- /dev/null +++ b/samples/wearable/RecipeAssistant/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Mar 14 14:02:22 PDT 2014 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip diff --git a/samples/wearable/RecipeAssistant/gradlew b/samples/wearable/RecipeAssistant/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/samples/wearable/RecipeAssistant/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/wearable/RecipeAssistant/gradlew.bat b/samples/wearable/RecipeAssistant/gradlew.bat new file mode 100644 index 000000000..aec99730b --- /dev/null +++ b/samples/wearable/RecipeAssistant/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/wearable/RecipeAssistant/settings.gradle b/samples/wearable/RecipeAssistant/settings.gradle new file mode 100644 index 000000000..aa94981a3 --- /dev/null +++ b/samples/wearable/RecipeAssistant/settings.gradle @@ -0,0 +1 @@ +include 'Application' \ No newline at end of file diff --git a/samples/wearable/SkeletonWearableApp/Wearable/build.gradle b/samples/wearable/SkeletonWearableApp/Wearable/build.gradle new file mode 100644 index 000000000..2a520308a --- /dev/null +++ b/samples/wearable/SkeletonWearableApp/Wearable/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'android' + +android { + compileSdkVersion 20 + buildToolsVersion '20' + + defaultConfig { + minSdkVersion 20 + targetSdkVersion 20 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} + +dependencies { + compile "com.android.support:support-v13:20.0.+" + compile "com.google.android.support:wearable:1.0.+" +} diff --git a/samples/wearable/SkeletonWearableApp/Wearable/proguard-rules.txt b/samples/wearable/SkeletonWearableApp/Wearable/proguard-rules.txt new file mode 100644 index 000000000..2ddbcfcdd --- /dev/null +++ b/samples/wearable/SkeletonWearableApp/Wearable/proguard-rules.txt @@ -0,0 +1,21 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} +-dontwarn android.support.wearable.view.DelayedConfirmationView diff --git a/samples/wearable/SkeletonWearableApp/Wearable/src/main/AndroidManifest.xml b/samples/wearable/SkeletonWearableApp/Wearable/src/main/AndroidManifest.xml new file mode 100644 index 000000000..cd684061a --- /dev/null +++ b/samples/wearable/SkeletonWearableApp/Wearable/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/wearable/SkeletonWearableApp/Wearable/src/main/java/com/example/android/google/wearable/app/GridExampleActivity.java b/samples/wearable/SkeletonWearableApp/Wearable/src/main/java/com/example/android/google/wearable/app/GridExampleActivity.java new file mode 100644 index 000000000..0a88c04d0 --- /dev/null +++ b/samples/wearable/SkeletonWearableApp/Wearable/src/main/java/com/example/android/google/wearable/app/GridExampleActivity.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.google.wearable.app; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.wearable.view.CardFragment; +import android.support.wearable.view.FragmentGridPagerAdapter; +import android.support.wearable.view.GridViewPager; +import android.support.wearable.view.ImageReference; +import java.util.HashMap; +import java.util.Map; + +public class GridExampleActivity extends Activity { + private static final int NUM_ROWS = 10; + private static final int NUM_COLS = 3; + + MainAdapter mAdapter; + GridViewPager mPager; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.grid_activity); + mPager = (GridViewPager) findViewById(R.id.fragment_container); + mAdapter = new MainAdapter(getFragmentManager()); + mPager.setAdapter(mAdapter); + + } + + private static class MainAdapter extends FragmentGridPagerAdapter{ + Map mBackgrounds = new HashMap(); + + public MainAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public int getRowCount() { + return NUM_ROWS; + } + + @Override + public int getColumnCount(int rowNum) { + return NUM_COLS; + } + + @Override + public Fragment getFragment(int rowNum, int colNum) { + return MainFragment.newInstance(rowNum, colNum); + } + + @Override + public ImageReference getBackground(int row, int column) { + Point pt = new Point(column, row); + ImageReference ref = mBackgrounds.get(pt); + if (ref == null) { + Bitmap bm = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bm); + Paint p = new Paint(); + // Clear previous image. + c.drawRect(0, 0, 200, 200, p); + p.setAntiAlias(true); + p.setTypeface(Typeface.DEFAULT); + p.setTextSize(64); + p.setColor(Color.LTGRAY); + c.drawText(column+ "-" + row, 20, 100, p); + ref = ImageReference.forBitmap(bm); + mBackgrounds.put(pt, ref); + } + return ref; + } + } + + public static class MainFragment extends CardFragment { + private static MainFragment newInstance(int rowNum, int colNum) { + Bundle args = new Bundle(); + args.putString(CardFragment.KEY_TITLE, "Row:" + rowNum); + args.putString(CardFragment.KEY_TEXT, "Col:" + colNum); + MainFragment f = new MainFragment(); + f.setArguments(args); + return f; + } + } +} diff --git a/samples/wearable/SkeletonWearableApp/Wearable/src/main/java/com/example/android/google/wearable/app/MainActivity.java b/samples/wearable/SkeletonWearableApp/Wearable/src/main/java/com/example/android/google/wearable/app/MainActivity.java new file mode 100644 index 000000000..11bcee752 --- /dev/null +++ b/samples/wearable/SkeletonWearableApp/Wearable/src/main/java/com/example/android/google/wearable/app/MainActivity.java @@ -0,0 +1,114 @@ +package com.example.android.google.wearable.app; + +import android.app.Activity; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.view.GestureDetectorCompat; +import android.support.wearable.view.DelayedConfirmationView; +import android.support.wearable.view.DismissOverlayView; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ScrollView; + + +public class MainActivity extends Activity + implements DelayedConfirmationView.DelayedConfirmationListener { + private static final String TAG = "MainActivity"; + + private static final int NOTIFICATION_ID = 1; + private static final int NOTIFICATION_REQUEST_CODE = 1; + private static final int NUM_SECONDS = 5; + + private GestureDetectorCompat mGestureDetector; + private DismissOverlayView mDismissOverlayView; + + @Override + public void onCreate(Bundle b) { + super.onCreate(b); + setContentView(R.layout.main_activity); + + mDismissOverlayView = (DismissOverlayView) findViewById(R.id.dismiss_overlay); + mDismissOverlayView.setIntroText(R.string.intro_text); + mDismissOverlayView.showIntroIfNecessary(); + mGestureDetector = new GestureDetectorCompat(this, new LongPressListener()); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event) || super.dispatchTouchEvent(event); + } + + private class LongPressListener extends GestureDetector.SimpleOnGestureListener { + @Override + public void onLongPress(MotionEvent event) { + mDismissOverlayView.show(); + } + } + + /** + * Handles the button to launch a notification. + */ + public void showNotification(View view) { + Notification notification = new NotificationCompat.Builder(this) + .setContentTitle(getString(R.string.notification_title)) + .setContentText(getString(R.string.notification_title)) + .setSmallIcon(R.drawable.ic_launcher) + .addAction(R.drawable.ic_launcher, + getText(R.string.action_launch_activity), + PendingIntent.getActivity(this, NOTIFICATION_REQUEST_CODE, + new Intent(this, GridExampleActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT)) + .build(); + NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, notification); + finish(); + } + + + /** + * Handles the button press to finish this activity and take the user back to the Home. + */ + public void onFinishActivity(View view) { + setResult(RESULT_OK); + finish(); + } + + /** + * Handles the button to start a DelayedConfirmationView timer. + */ + public void onStartTimer(View view) { + DelayedConfirmationView delayedConfirmationView = (DelayedConfirmationView) + findViewById(R.id.timer); + delayedConfirmationView.setTotalTimeMs(NUM_SECONDS * 1000); + delayedConfirmationView.setListener(this); + delayedConfirmationView.start(); + scroll(View.FOCUS_DOWN); + } + + @Override + public void onTimerFinished(View v) { + Log.d(TAG, "onTimerFinished is called."); + scroll(View.FOCUS_UP); + } + + @Override + public void onTimerSelected(View v) { + Log.d(TAG, "onTimerSelected is called."); + scroll(View.FOCUS_UP); + } + + private void scroll(final int scrollDirection) { + final ScrollView scrollView = (ScrollView) findViewById(R.id.scroll); + scrollView.post(new Runnable() { + @Override + public void run() { + scrollView.fullScroll(scrollDirection); + } + }); + } +} \ No newline at end of file diff --git a/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-hdpi/ic_launcher.png b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100755 index 000000000..589f229d1 Binary files /dev/null and b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-mdpi/ic_launcher.png b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100755 index 000000000..77dd57139 Binary files /dev/null and b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100755 index 000000000..fe34ebe13 Binary files /dev/null and b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100755 index 000000000..ab80bcd13 Binary files /dev/null and b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/layout/grid_activity.xml b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/layout/grid_activity.xml new file mode 100644 index 000000000..3ee687c73 --- /dev/null +++ b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/layout/grid_activity.xml @@ -0,0 +1,6 @@ + + diff --git a/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/layout/main_activity.xml b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/layout/main_activity.xml new file mode 100644 index 000000000..7b92da27c --- /dev/null +++ b/samples/wearable/SkeletonWearableApp/Wearable/src/main/res/layout/main_activity.xml @@ -0,0 +1,70 @@ + + + + + + + + + + +