Add new SynchronizedNotifications sample.

This sample demonstrates how to use DataItems to construct linked
phone- and wearable-side notifications.

Change-Id: I6defa7fad671ba2ec0de25c8913abd66d07846a2
This commit is contained in:
Dan Aminzade
2014-06-22 16:46:26 -07:00
parent 6cb66cf80e
commit 9fd71d61f2
35 changed files with 1113 additions and 15 deletions

View File

@@ -283,6 +283,7 @@ development/samples/wearable/Notifications samples/${PLATFORM_NAME}/we
development/samples/wearable/Quiz samples/${PLATFORM_NAME}/wearable/Quiz development/samples/wearable/Quiz samples/${PLATFORM_NAME}/wearable/Quiz
development/samples/wearable/RecipeAssistant samples/${PLATFORM_NAME}/wearable/RecipeAssistant development/samples/wearable/RecipeAssistant samples/${PLATFORM_NAME}/wearable/RecipeAssistant
development/samples/wearable/SkeletonWearableApp samples/${PLATFORM_NAME}/wearable/SkeletonWearableApp 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/Timer samples/${PLATFORM_NAME}/wearable/Timer
development/samples/wearable/WatchViewStub samples/${PLATFORM_NAME}/wearable/WatchViewStub development/samples/wearable/WatchViewStub samples/${PLATFORM_NAME}/wearable/WatchViewStub

View File

@@ -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')
}

View File

@@ -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 *;
#}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.wearable.synchronizednotifications" >
<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="19" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.DeviceDefault.Light" >
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<activity
android:name=".PhoneActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".DismissListener">
<intent-filter>
<action
android:name="com.google.android.gms.wearable.BIND_LISTENER" />
</intent-filter>
<intent-filter>
<action
android:name="com.example.android.wearable.synchronizednotifications.DISMISS" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -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<DataApi.DeleteDataItemsResult> {
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<DataApi.DeleteDataItemsResult>
public void onResult(DataApi.DeleteDataItemsResult deleteDataItemsResult) {
if (!deleteDataItemsResult.getStatus().isSuccess()) {
Log.e(TAG, "dismissWearableNotification(): failed to delete DataItem");
}
mGoogleApiClient.disconnect();
}
}

View File

@@ -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:
* <ul>
* <li>The first button builds a simple local-only notification on the handset.</li>
* <li>The second one creates a wearable-only notification by putting a data item in the shared data
* store and having a {@link com.google.android.gms.wearable.WearableListenerService} listen for
* that on the wearable</li>
* <li>The third one creates a local notification and a wearable notification by combining the above
* two. It, however, demonstrates how one can set things up so that the dismissal of one
* notification results in the dismissal of the other one.</li>
* </ul>
*/
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
* <code>setLocalOnly(true)</code>. If <code>withDismissal</code> is set to <code>true</code>, 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<DataApi.DataItemResult>() {
@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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

View File

@@ -0,0 +1,41 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".PhoneActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/phone_only"
android:id="@+id/phone_only"
android:onClick="onClick"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/wear_only"
android:id="@+id/wear_only"
android:onClick="onClick"
android:layout_below="@+id/phone_only"
android:layout_alignLeft="@+id/phone_only"
android:layout_marginTop="30dp"
android:layout_alignRight="@+id/phone_only" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/different_notifications"
android:id="@+id/different_notifications"
android:layout_below="@+id/wear_only"
android:onClick="onClick"
android:layout_alignLeft="@+id/wear_only"
android:layout_marginTop="30dp"
android:layout_alignRight="@+id/wear_only" />
</RelativeLayout>

View File

@@ -0,0 +1,5 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Synchronized Notifications</string>
<string name="wear_only">Watch Only Notification</string>
<string name="phone_only">Phone Only Notification</string>
<string name="different_notifications">Different Notifications</string>
<string name="phone_both">Phone Notification</string>
<string name="watch_both">Watch Notification</string>
</resources>

View File

@@ -0,0 +1,20 @@
apply plugin: 'android-library'
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'
}
}
productFlavors {
}
}

View File

@@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/android-sdk/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 *;
#}

View File

@@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.wearable.synchronizednotifications.common">
<application android:allowBackup="true"
android:label="@string/app_name">
</application>
</manifest>

View File

@@ -0,0 +1,38 @@
/*
* 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.common;
/**
* Constants that are used in both the Application and the Wearable modules.
*/
public final class Constants {
private Constants() {};
public static final int WATCH_ONLY_ID = 2;
public static final int PHONE_ONLY_ID = 3;
public static final int BOTH_ID = 4;
public static final String BOTH_PATH = "/both";
public static final String WATCH_ONLY_PATH = "/watch-only";
public static final String KEY_NOTIFICATION_ID = "notification-id";
public static final String KEY_TITLE = "title";
public static final String KEY_CONTENT = "content";
public static final String ACTION_DISMISS
= "com.example.android.wearable.synchronizednotifications.DISMISS";
}

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Common</string>
</resources>

View File

@@ -0,0 +1,28 @@
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'
}
}
lintOptions {
abortOnError false
}
}
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.+'
compile project(':Common')
}

View File

@@ -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 *;
#}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.wearable.synchronizednotifications" >
<uses-sdk android:minSdkVersion="20" android:targetSdkVersion="20" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.DeviceDefault.Light" >
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<activity
android:name=".WearableActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".NotificationUpdateService">
<intent-filter>
<action
android:name="com.google.android.gms.wearable.BIND_LISTENER" />
</intent-filter>
<intent-filter>
<action
android:name="com.example.android.wearable.synchronizednotifications.DISMISS" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -0,0 +1,164 @@
/*
* 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.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
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.DataMap;
import com.google.android.gms.wearable.DataMapItem;
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 will be invoked when a
* DataItem is added or deleted. The creation of a new DataItem will be interpreted as a request to
* create a new notification and the removal of that DataItem is interpreted as a request to
* dismiss that notification.
*/
public class NotificationUpdateService extends WearableListenerService
implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener,
ResultCallback<DataApi.DeleteDataItemsResult> {
private static final String TAG = "NotificationUpdate";
private GoogleApiClient mGoogleApiClient;
@Override
public void onCreate() {
super.onCreate();
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
@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 data item that
// created the notification and that is how we inform the phone
int notificationId = intent.getIntExtra(Constants.KEY_NOTIFICATION_ID, -1);
if (notificationId == Constants.BOTH_ID) {
dismissPhoneNotification(notificationId);
}
}
}
return super.onStartCommand(intent, flags, startId);
}
/**
* Dismisses the phone notification, via a {@link android.app.PendingIntent} that is triggered
* when the user dismisses the local notification. Deleting the corresponding data item notifies
* the {@link com.google.android.gms.wearable.WearableListenerService} on the phone that the
* matching notification on the phone side should be removed.
*/
private void dismissPhoneNotification(int id) {
mGoogleApiClient.connect();
}
@Override
public void onDataChanged(DataEventBuffer dataEvents) {
for (DataEvent dataEvent : dataEvents) {
if (dataEvent.getType() == DataEvent.TYPE_CHANGED) {
DataMap dataMap = DataMapItem.fromDataItem(dataEvent.getDataItem()).getDataMap();
String content = dataMap.getString(Constants.KEY_CONTENT);
String title = dataMap.getString(Constants.KEY_TITLE);
if (Constants.WATCH_ONLY_PATH.equals(dataEvent.getDataItem().getUri().getPath())) {
buildWearableOnlyNotification(title, content, false);
} else if (Constants.BOTH_PATH.equals(dataEvent.getDataItem().getUri().getPath())) {
buildWearableOnlyNotification(title, content, true);
}
} else if (dataEvent.getType() == DataEvent.TYPE_DELETED) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "DataItem deleted: " + dataEvent.getDataItem().getUri().getPath());
}
if (Constants.BOTH_PATH.equals(dataEvent.getDataItem().getUri().getPath())) {
// Dismiss the corresponding notification
((NotificationManager) getSystemService(NOTIFICATION_SERVICE))
.cancel(Constants.BOTH_ID);
}
}
}
}
/**
* Builds a simple notification on the wearable.
*/
private void buildWearableOnlyNotification(String title, String content,
boolean withDismissal) {
Notification.Builder builder = new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(title)
.setContentText(content);
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);
}
((NotificationManager) getSystemService(NOTIFICATION_SERVICE))
.notify(Constants.WATCH_ONLY_ID, builder.build());
}
@Override
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
public void onConnectionSuspended(int i) {
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
}
@Override
public void onResult(DataApi.DeleteDataItemsResult deleteDataItemsResult) {
if (!deleteDataItemsResult.getStatus().isSuccess()) {
Log.e(TAG, "dismissWearableNotification(): failed to delete DataItem");
}
mGoogleApiClient.disconnect();
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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.os.Bundle;
public class WearableActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wearable);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

View File

@@ -0,0 +1,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".WearableActivity">
</RelativeLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Synchronized Notifications</string>
</resources>

View File

@@ -0,0 +1,14 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.+'
}
}
allprojects {
repositories {
mavenCentral()
}
}

View File

@@ -0,0 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip

View File

@@ -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 "$@"

View File

@@ -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

View File

@@ -0,0 +1 @@
include ':Application', ':Wearable', ':Common'

View File

@@ -22,6 +22,7 @@ List<String> samples = [
"Quiz", "Quiz",
"RecipeAssistant", "RecipeAssistant",
"SkeletonWearableApp", "SkeletonWearableApp",
"SynchronizedNotifications",
"Timer", "Timer",
"WatchViewStub", "WatchViewStub",
] ]