diff --git a/build/sdk.atree b/build/sdk.atree index a8b68888f..14d5fc835 100644 --- a/build/sdk.atree +++ b/build/sdk.atree @@ -270,21 +270,22 @@ developers/build/prebuilts/gradle/SwipeRefreshMultipleViews sam developers/build/prebuilts/gradle/MediaRouter samples/${PLATFORM_NAME}/media/MediaRouter # Wearable sample tree -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/GridViewPager samples/${PLATFORM_NAME}/wearable/GridViewPager -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 +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/GridViewPager samples/${PLATFORM_NAME}/wearable/GridViewPager +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/SynchronizedNotifications samples/${PLATFORM_NAME}/wearable/SynchronizedNotifications +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/SynchronizedNotifications/Application/build.gradle b/samples/wearable/SynchronizedNotifications/Application/build.gradle new file mode 100644 index 000000000..a850a8d61 --- /dev/null +++ b/samples/wearable/SynchronizedNotifications/Application/build.gradle @@ -0,0 +1,28 @@ +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' + } + } + lintOptions { + abortOnError false + } +} + +dependencies { + compile 'com.google.android.gms:play-services:5.0.+@aar' + compile 'com.android.support:support-v13:20.0.+' + compile project(':Common') + wearApp project(':Wearable') +} diff --git a/samples/wearable/SynchronizedNotifications/Application/proguard-rules.txt b/samples/wearable/SynchronizedNotifications/Application/proguard-rules.txt new file mode 100644 index 000000000..5b86c0858 --- /dev/null +++ b/samples/wearable/SynchronizedNotifications/Application/proguard-rules.txt @@ -0,0 +1,17 @@ +# 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 proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/samples/wearable/SynchronizedNotifications/Application/src/main/AndroidManifest.xml b/samples/wearable/SynchronizedNotifications/Application/src/main/AndroidManifest.xml new file mode 100644 index 000000000..60960a8a8 --- /dev/null +++ b/samples/wearable/SynchronizedNotifications/Application/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/wearable/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/DismissListener.java b/samples/wearable/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/DismissListener.java new file mode 100644 index 000000000..98747f55b --- /dev/null +++ b/samples/wearable/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/DismissListener.java @@ -0,0 +1,125 @@ +/* + * 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.wearable.synchronizednotifications; + +import static com.google.android.gms.wearable.PutDataRequest.WEAR_URI_SCHEME; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.NotificationManagerCompat; +import android.util.Log; + +import com.example.android.wearable.synchronizednotifications.common.Constants; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.wearable.DataApi; +import com.google.android.gms.wearable.DataEvent; +import com.google.android.gms.wearable.DataEventBuffer; +import com.google.android.gms.wearable.PutDataMapRequest; +import com.google.android.gms.wearable.Wearable; +import com.google.android.gms.wearable.WearableListenerService; + +/** + * A {@link com.google.android.gms.wearable.WearableListenerService} that is invoked when certain + * notifications are dismissed from either the phone or watch. + */ +public class DismissListener extends WearableListenerService + implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, + ResultCallback { + + private static final String TAG = "DismissListener"; + private GoogleApiClient mGoogleApiClient; + + @Override + public void onCreate() { + super.onCreate(); + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + @Override + public void onDataChanged(DataEventBuffer dataEvents) { + for (DataEvent dataEvent : dataEvents) { + if (dataEvent.getType() == DataEvent.TYPE_DELETED) { + if (Constants.BOTH_PATH.equals(dataEvent.getDataItem().getUri().getPath())) { + // notification on the phone should be dismissed + NotificationManagerCompat.from(this).cancel(Constants.BOTH_ID); + } + } + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (null != intent) { + String action = intent.getAction(); + if (Constants.ACTION_DISMISS.equals(action)) { + // We need to dismiss the wearable notification. We delete the DataItem that + // created the notification to inform the wearable. + int notificationId = intent.getIntExtra(Constants.KEY_NOTIFICATION_ID, -1); + if (notificationId == Constants.BOTH_ID) { + dismissWearableNotification(notificationId); + } + } + } + return super.onStartCommand(intent, flags, startId); + } + + /** + * Removes the DataItem that was used to create a notification on the watch. By deleting the + * data item, a {@link com.google.android.gms.wearable.WearableListenerService} on the watch + * will be notified and the notification on the watch will be removed. + * + * @param id The ID of the notification that should be removed + */ + private void dismissWearableNotification(final int id) { + mGoogleApiClient.connect(); + } + + @Override // ConnectionCallbacks + public void onConnected(Bundle bundle) { + final Uri dataItemUri = + new Uri.Builder().scheme(WEAR_URI_SCHEME).path(Constants.BOTH_PATH).build(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Deleting Uri: " + dataItemUri.toString()); + } + Wearable.DataApi.deleteDataItems( + mGoogleApiClient, dataItemUri).setResultCallback(this); + } + + @Override // ConnectionCallbacks + public void onConnectionSuspended(int i) { + } + + @Override // OnConnectionFailedListener + public void onConnectionFailed(ConnectionResult connectionResult) { + Log.e(TAG, "Failed to connect to the Google API client"); + } + + @Override // ResultCallback + public void onResult(DataApi.DeleteDataItemsResult deleteDataItemsResult) { + if (!deleteDataItemsResult.getStatus().isSuccess()) { + Log.e(TAG, "dismissWearableNotification(): failed to delete DataItem"); + } + mGoogleApiClient.disconnect(); + } +} diff --git a/samples/wearable/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/PhoneActivity.java b/samples/wearable/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/PhoneActivity.java new file mode 100644 index 000000000..2cac13751 --- /dev/null +++ b/samples/wearable/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/PhoneActivity.java @@ -0,0 +1,191 @@ +/* + * 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.wearable.synchronizednotifications; + +import android.app.Activity; +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.util.Log; +import android.view.View; + +import com.example.android.wearable.synchronizednotifications.common.Constants; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.wearable.DataApi; +import com.google.android.gms.wearable.PutDataMapRequest; +import com.google.android.gms.wearable.PutDataRequest; +import com.google.android.gms.wearable.Wearable; + +import java.text.DateFormat; +import java.util.Date; + +/** + * A simple activity that presents three buttons that would trigger three different combinations of + * notifications on the handset and the watch: + * + */ +public class PhoneActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener { + + private static final String TAG = "PhoneActivity"; + private GoogleApiClient mGoogleApiClient; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_phone); + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + /** + * Builds a local-only notification for the handset. This is achieved by using + * setLocalOnly(true). If withDismissal is set to true, a + * {@link android.app.PendingIntent} will be added to handle the dismissal of notification to + * be able to remove the mirrored notification on the wearable. + */ + private void buildLocalOnlyNotification(String title, String content, int notificationId, + boolean withDismissal) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setContentTitle(title) + .setContentText(content) + .setLocalOnly(true) + .setSmallIcon(R.drawable.ic_launcher); + + if (withDismissal) { + Intent dismissIntent = new Intent(Constants.ACTION_DISMISS); + dismissIntent.putExtra(Constants.KEY_NOTIFICATION_ID, Constants.BOTH_ID); + PendingIntent pendingIntent = PendingIntent + .getService(this, 0, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setDeleteIntent(pendingIntent); + } + NotificationManagerCompat.from(this).notify(notificationId, builder.build()); + } + + /** + * Builds a DataItem that on the wearable will be interpreted as a request to show a + * notification. The result will be a notification that only shows up on the wearable. + */ + private void buildWearableOnlyNotification(String title, String content, String path) { + if (mGoogleApiClient.isConnected()) { + PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(path); + putDataMapRequest.getDataMap().putString(Constants.KEY_CONTENT, content); + putDataMapRequest.getDataMap().putString(Constants.KEY_TITLE, title); + PutDataRequest request = putDataMapRequest.asPutDataRequest(); + Wearable.DataApi.putDataItem(mGoogleApiClient, request) + .setResultCallback(new ResultCallback() { + @Override + public void onResult(DataApi.DataItemResult dataItemResult) { + if (!dataItemResult.getStatus().isSuccess()) { + Log.e(TAG, "buildWatchOnlyNotification(): Failed to set the data, " + + "status: " + dataItemResult.getStatus().getStatusCode()); + } + } + }); + } else { + Log.e(TAG, "buildWearableOnlyNotification(): no Google API Client connection"); + } + } + + /** + * Builds a local notification and sets a DataItem that will be interpreted by the wearable as + * a request to build a notification on the wearable as as well. The two notifications show + * different messages. + * Dismissing either of the notifications will result in dismissal of the other; this is + * achieved by creating a {@link android.app.PendingIntent} that results in removal of + * the DataItem that created the watch notification. The deletion of the DataItem is observed on + * both sides, using WearableListenerService callbacks, and is interpreted on each side as a + * request to dismiss the corresponding notification. + */ + private void buildMirroredNotifications(String phoneTitle, String watchTitle, String content) { + if (mGoogleApiClient.isConnected()) { + // Wearable notification + buildWearableOnlyNotification(watchTitle, content, Constants.BOTH_PATH); + + // Local notification, with a pending intent for dismissal + buildLocalOnlyNotification(phoneTitle, content, Constants.BOTH_ID, true); + } + } + + @Override + protected void onStart() { + super.onStart(); + mGoogleApiClient.connect(); + } + + @Override + protected void onStop() { + mGoogleApiClient.disconnect(); + super.onStop(); + } + + @Override + public void onConnected(Bundle bundle) { + } + + @Override + public void onConnectionSuspended(int i) { + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + Log.e(TAG, "Failed to connect to Google API Client"); + } + + /** + * Returns a string built from the current time + */ + private String now() { + DateFormat dateFormat = android.text.format.DateFormat.getTimeFormat(this); + return dateFormat.format(new Date()); + } + + /** + * Handles button clicks in the UI. + */ + public void onClick(View view) { + int id = view.getId(); + switch (id) { + case R.id.phone_only: + buildLocalOnlyNotification(getString(R.string.phone_only), now(), + Constants.PHONE_ONLY_ID, false); + break; + case R.id.wear_only: + buildWearableOnlyNotification(getString(R.string.wear_only), now(), + Constants.WATCH_ONLY_PATH); + break; + case R.id.different_notifications: + buildMirroredNotifications(getString(R.string.phone_both), getString(R.string.watch_both), now()); + break; + } + } +} diff --git a/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-hdpi/ic_launcher.png b/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..f7dd9a0bd Binary files /dev/null and b/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-mdpi/ic_launcher.png b/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..fef2da78f Binary files /dev/null and b/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-xhdpi/ic_launcher.png b/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..f2fb03cdd Binary files /dev/null and b/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-xxhdpi/ic_launcher.png b/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..944042ca8 Binary files /dev/null and b/samples/wearable/SynchronizedNotifications/Application/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/samples/wearable/SynchronizedNotifications/Application/src/main/res/layout/activity_phone.xml b/samples/wearable/SynchronizedNotifications/Application/src/main/res/layout/activity_phone.xml new file mode 100644 index 000000000..140c4e8c3 --- /dev/null +++ b/samples/wearable/SynchronizedNotifications/Application/src/main/res/layout/activity_phone.xml @@ -0,0 +1,41 @@ + + +