Merge "WatchFace sample" into lmp-docs
diff --git a/wearable/wear/WatchFace/Application/.gitignore b/wearable/wear/WatchFace/Application/.gitignore
new file mode 100644
index 0000000..8fd2e1c
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/.gitignore
@@ -0,0 +1,16 @@
+# Copyright 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.
+src/template/
+src/common/
+build.gradle
diff --git a/wearable/wear/WatchFace/Application/proguard-project.txt b/wearable/wear/WatchFace/Application/proguard-project.txt
new file mode 100644
index 0000000..0d8f171
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/proguard-project.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/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml b/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..732e306
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.wearable.watchface" >
+
+ <uses-sdk android:minSdkVersion="18"
+ android:targetSdkVersion="21" />
+
+ <!-- Permissions required by the wearable app -->
+ <uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.READ_CALENDAR" />
+
+ <!-- All intent-filters for config actions must include the categories
+ com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION and
+ android.intent.category.DEFAULT. -->
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+
+ <activity
+ android:name=".AnalogAndCardBoundsWatchFaceConfigActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="com.example.android.wearable.watchface.CONFIG_ANALOG" />
+ <action android:name="com.example.android.wearable.watchface.CONFIG_CARD_BOUNDS" />
+ <category android:name="com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".DigitalWatchFaceCompanionConfigActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
+ <category android:name="com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".TiltWatchFaceConfigActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="com.example.android.wearable.watchface.CONFIG_TILT" />
+ <category android:name="com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version" />
+
+ </application>
+
+</manifest>
diff --git a/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/AnalogAndCardBoundsWatchFaceConfigActivity.java b/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/AnalogAndCardBoundsWatchFaceConfigActivity.java
new file mode 100644
index 0000000..5943e6b
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/AnalogAndCardBoundsWatchFaceConfigActivity.java
@@ -0,0 +1,37 @@
+/*
+ * 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.watchface;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.support.wearable.companion.WatchFaceCompanion;
+import android.widget.TextView;
+
+public class AnalogAndCardBoundsWatchFaceConfigActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_analog_watch_face_config);
+
+ ComponentName name =
+ getIntent().getParcelableExtra(WatchFaceCompanion.EXTRA_WATCH_FACE_COMPONENT);
+ TextView label = (TextView) findViewById(R.id.label);
+ label.setText(label.getText() + " (" + name.getClassName() + ")");
+ }
+}
diff --git a/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceCompanionConfigActivity.java b/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceCompanionConfigActivity.java
new file mode 100644
index 0000000..b04f11e
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceCompanionConfigActivity.java
@@ -0,0 +1,217 @@
+/*
+ * 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.watchface;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.wearable.companion.WatchFaceCompanion;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Spinner;
+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.ResultCallback;
+import com.google.android.gms.wearable.DataApi;
+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.Wearable;
+
+/**
+ * The phone-side config activity for {@code DigitalWatchFaceService}. Like the watch-side config
+ * activity ({@code DigitalWatchFaceWearableConfigActivity}), allows for setting the background
+ * color. Additionally, enables setting the color for hour, minute and second digits.
+ */
+public class DigitalWatchFaceCompanionConfigActivity extends Activity
+ implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener,
+ ResultCallback<DataApi.DataItemResult> {
+ private static final String TAG = "DigitalWatchFaceConfig";
+
+ // TODO: use the shared constants (needs covering all the samples with Gradle build model)
+ private static final String KEY_BACKGROUND_COLOR = "BACKGROUND_COLOR";
+ private static final String KEY_HOURS_COLOR = "HOURS_COLOR";
+ private static final String KEY_MINUTES_COLOR = "MINUTES_COLOR";
+ private static final String KEY_SECONDS_COLOR = "SECONDS_COLOR";
+ private static final String PATH_WITH_FEATURE = "/watch_face_config/Digital";
+
+ private GoogleApiClient mGoogleApiClient;
+ private String mPeerId;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_digital_watch_face_config);
+
+ mPeerId = getIntent().getStringExtra(WatchFaceCompanion.EXTRA_PEER_ID);
+ mGoogleApiClient = new GoogleApiClient.Builder(this)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .addApi(Wearable.API)
+ .build();
+
+ ComponentName name = getIntent().getParcelableExtra(
+ WatchFaceCompanion.EXTRA_WATCH_FACE_COMPONENT);
+ TextView label = (TextView)findViewById(R.id.label);
+ label.setText(label.getText() + " (" + name.getClassName() + ")");
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mGoogleApiClient.connect();
+ }
+
+ @Override
+ protected void onStop() {
+ if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.disconnect();
+ }
+ super.onStop();
+ }
+
+ @Override // GoogleApiClient.ConnectionCallbacks
+ public void onConnected(Bundle connectionHint) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnected: " + connectionHint);
+ }
+
+ if (mPeerId != null) {
+ Uri.Builder builder = new Uri.Builder();
+ Uri uri = builder.scheme("wear").path(PATH_WITH_FEATURE).authority(mPeerId).build();
+ Wearable.DataApi.getDataItem(mGoogleApiClient, uri).setResultCallback(this);
+ } else {
+ displayNoConnectedDeviceDialog();
+ }
+ }
+
+ @Override // ResultCallback<DataApi.DataItemResult>
+ public void onResult(DataApi.DataItemResult dataItemResult) {
+ if (dataItemResult.getStatus().isSuccess() && dataItemResult.getDataItem() != null) {
+ DataItem configDataItem = dataItemResult.getDataItem();
+ DataMapItem dataMapItem = DataMapItem.fromDataItem(configDataItem);
+ DataMap config = dataMapItem.getDataMap();
+ setUpAllPickers(config);
+ } else {
+ // If DataItem with the current config can't be retrieved, select the default items on
+ // each picker.
+ setUpAllPickers(null);
+ }
+ }
+
+ @Override // GoogleApiClient.ConnectionCallbacks
+ public void onConnectionSuspended(int cause) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnectionSuspended: " + cause);
+ }
+ }
+
+ @Override // GoogleApiClient.OnConnectionFailedListener
+ public void onConnectionFailed(ConnectionResult result) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnectionFailed: " + result);
+ }
+ }
+
+ private void displayNoConnectedDeviceDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ String messageText = getResources().getString(R.string.title_no_device_connected);
+ String okText = getResources().getString(R.string.ok_no_device_connected);
+ builder.setMessage(messageText)
+ .setCancelable(false)
+ .setPositiveButton(okText, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) { }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ /**
+ * Sets up selected items for all pickers according to given {@code config} and sets up their
+ * item selection listeners.
+ *
+ * @param config the {@code DigitalWatchFaceService} config {@link DataMap}. If null, the
+ * default items are selected.
+ */
+ private void setUpAllPickers(DataMap config) {
+ setUpColorPickerSelection(R.id.background, KEY_BACKGROUND_COLOR, config,
+ R.string.color_black);
+ setUpColorPickerSelection(R.id.hours, KEY_HOURS_COLOR, config, R.string.color_white);
+ setUpColorPickerSelection(R.id.minutes, KEY_MINUTES_COLOR, config, R.string.color_white);
+ setUpColorPickerSelection(R.id.seconds, KEY_SECONDS_COLOR, config, R.string.color_gray);
+
+ setUpColorPickerListener(R.id.background, KEY_BACKGROUND_COLOR);
+ setUpColorPickerListener(R.id.hours, KEY_HOURS_COLOR);
+ setUpColorPickerListener(R.id.minutes, KEY_MINUTES_COLOR);
+ setUpColorPickerListener(R.id.seconds, KEY_SECONDS_COLOR);
+ }
+
+ private void setUpColorPickerSelection(int spinnerId, final String configKey, DataMap config,
+ int defaultColorNameResId) {
+ String defaultColorName = getString(defaultColorNameResId);
+ int defaultColor = Color.parseColor(defaultColorName);
+ int color;
+ if (config != null) {
+ color = config.getInt(configKey, defaultColor);
+ } else {
+ color = defaultColor;
+ }
+ Spinner spinner = (Spinner) findViewById(spinnerId);
+ String[] colorNames = getResources().getStringArray(R.array.color_array);
+ for (int i = 0; i < colorNames.length; i++) {
+ if (Color.parseColor(colorNames[i]) == color) {
+ spinner.setSelection(i);
+ break;
+ }
+ }
+ }
+
+ private void setUpColorPickerListener(int spinnerId, final String configKey) {
+ Spinner spinner = (Spinner) findViewById(spinnerId);
+ spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long id) {
+ final String colorName = (String) adapterView.getItemAtPosition(pos);
+ sendConfigUpdateMessage(configKey, Color.parseColor(colorName));
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> adapterView) { }
+ });
+ }
+
+ private void sendConfigUpdateMessage(String configKey, int color) {
+ if (mPeerId != null) {
+ DataMap config = new DataMap();
+ config.putInt(configKey, color);
+ byte[] rawData = config.toByteArray();
+ Wearable.MessageApi.sendMessage(mGoogleApiClient, mPeerId, PATH_WITH_FEATURE, rawData);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Sent watch face config message: " + configKey + " -> "
+ + Integer.toHexString(color));
+ }
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/TiltWatchFaceConfigActivity.java b/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/TiltWatchFaceConfigActivity.java
new file mode 100644
index 0000000..303e72e
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/TiltWatchFaceConfigActivity.java
@@ -0,0 +1,37 @@
+/*
+ * 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.watchface;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.support.wearable.companion.WatchFaceCompanion;
+import android.widget.TextView;
+
+public class TiltWatchFaceConfigActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_tilt_watch_face_config);
+
+ ComponentName name =
+ getIntent().getParcelableExtra(WatchFaceCompanion.EXTRA_WATCH_FACE_COMPONENT);
+ TextView label = (TextView)findViewById(R.id.label);
+ label.setText(label.getText() + " (" + name.getClassName() + ")");
+ }
+}
diff --git a/wearable/wear/WatchFace/Application/src/main/res/drawable-hdpi/ic_launcher.png b/wearable/wear/WatchFace/Application/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100755
index 0000000..589f229
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Application/src/main/res/drawable-mdpi/ic_launcher.png b/wearable/wear/WatchFace/Application/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100755
index 0000000..77dd571
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Application/src/main/res/drawable-xhdpi/ic_launcher.png b/wearable/wear/WatchFace/Application/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100755
index 0000000..fe34ebe
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Application/src/main/res/drawable-xxhdpi/ic_launcher.png b/wearable/wear/WatchFace/Application/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100755
index 0000000..ab80bcd
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Application/src/main/res/layout/activity_analog_watch_face_config.xml b/wearable/wear/WatchFace/Application/src/main/res/layout/activity_analog_watch_face_config.xml
new file mode 100644
index 0000000..ec816c2
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/res/layout/activity_analog_watch_face_config.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<FrameLayout 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">
+
+ <TextView
+ android:id="@+id/label"
+ android:text="@string/analog_config_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+</FrameLayout>
diff --git a/wearable/wear/WatchFace/Application/src/main/res/layout/activity_digital_watch_face_config.xml b/wearable/wear/WatchFace/Application/src/main/res/layout/activity_digital_watch_face_config.xml
new file mode 100644
index 0000000..204d523
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/res/layout/activity_digital_watch_face_config.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/label"
+ android:text="@string/digital_config_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:text="@string/digital_config_background"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+ <Spinner
+ android:id="@+id/background"
+ android:entries="@array/color_array"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="3" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:text="@string/digital_config_hours"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+ <Spinner
+ android:id="@+id/hours"
+ android:entries="@array/color_array"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="3" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:text="@string/digital_config_minutes"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+ <Spinner
+ android:id="@+id/minutes"
+ android:entries="@array/color_array"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="3" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:text="@string/digital_config_seconds"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+ <Spinner
+ android:id="@+id/seconds"
+ android:entries="@array/color_array"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="3" />
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/wearable/wear/WatchFace/Application/src/main/res/layout/activity_tilt_watch_face_config.xml b/wearable/wear/WatchFace/Application/src/main/res/layout/activity_tilt_watch_face_config.xml
new file mode 100644
index 0000000..bda2d68
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/res/layout/activity_tilt_watch_face_config.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<FrameLayout 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">
+
+ <TextView
+ android:id="@+id/label"
+ android:text="@string/tilt_config_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+</FrameLayout>
diff --git a/wearable/wear/WatchFace/Application/src/main/res/values/strings.xml b/wearable/wear/WatchFace/Application/src/main/res/values/strings.xml
new file mode 100644
index 0000000..aacb108
--- /dev/null
+++ b/wearable/wear/WatchFace/Application/src/main/res/values/strings.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <string name="analog_config_text">This is the config activity for the Analog and Card Bounds watch faces</string>
+ <string name="digital_config_text">Digital watch face configuration</string>
+ <string name="tilt_config_text">Tilt watch face configuration</string>
+ <string name="digital_config_background">Background</string>
+ <string name="digital_config_hours">Hours</string>
+ <string name="digital_config_minutes">Minutes</string>
+ <string name="digital_config_seconds">Seconds</string>
+
+ <string name="title_no_device_connected">No wearable device is currently connected.</string>
+ <string name="ok_no_device_connected">OK</string>
+
+ <string name="color_black">Black</string>
+ <string name="color_blue">Blue</string>
+ <string name="color_gray">Gray</string>
+ <string name="color_green">Green</string>
+ <string name="color_navy">Navy</string>
+ <string name="color_red">Red</string>
+ <string name="color_white">White</string>
+
+ <string-array name="color_array">
+ <item>@string/color_black</item>
+ <item>@string/color_blue</item>
+ <item>@string/color_gray</item>
+ <item>@string/color_green</item>
+ <item>@string/color_navy</item>
+ <item>@string/color_red</item>
+ <item>@string/color_white</item>
+ </string-array>
+</resources>
diff --git a/wearable/wear/WatchFace/CONTRIB.md b/wearable/wear/WatchFace/CONTRIB.md
new file mode 100644
index 0000000..14a4fcf
--- /dev/null
+++ b/wearable/wear/WatchFace/CONTRIB.md
@@ -0,0 +1,35 @@
+# How to become a contributor and submit your own code
+
+## Contributor License Agreements
+
+We'd love to accept your sample apps and patches! Before we can take them, we
+have to jump a couple of legal hurdles.
+
+Please fill out either the individual or corporate Contributor License Agreement (CLA).
+
+ * If you are an individual writing original source code and you're sure you
+ own the intellectual property, then you'll need to sign an [individual CLA]
+ (https://developers.google.com/open-source/cla/individual).
+ * If you work for a company that wants to allow you to contribute your work,
+ then you'll need to sign a [corporate CLA]
+ (https://developers.google.com/open-source/cla/corporate).
+
+Follow either of the two links above to access the appropriate CLA and
+instructions for how to sign and return it. Once we receive it, we'll be able to
+accept your pull requests.
+
+## Contributing A Patch
+
+1. Submit an issue describing your proposed change to the repo in question.
+1. The repo owner will respond to your issue promptly.
+1. If your proposed change is accepted, and you haven't already done so, sign a
+ Contributor License Agreement (see details above).
+1. Fork the desired repo, develop and test your code changes.
+1. Ensure that your code adheres to the existing style in the sample to which
+ you are contributing. Refer to the
+ [Android Code Style Guide]
+ (https://source.android.com/source/code-style.html) for the
+ recommended coding standards for this organization.
+1. Ensure that your code has an appropriate set of unit tests which all pass.
+1. Submit a pull request.
+
diff --git a/wearable/wear/WatchFace/LICENSE b/wearable/wear/WatchFace/LICENSE
new file mode 100644
index 0000000..1af981f
--- /dev/null
+++ b/wearable/wear/WatchFace/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 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.
diff --git a/wearable/wear/WatchFace/Wearable/.gitignore b/wearable/wear/WatchFace/Wearable/.gitignore
new file mode 100644
index 0000000..8fd2e1c
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/.gitignore
@@ -0,0 +1,16 @@
+# Copyright 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.
+src/template/
+src/common/
+build.gradle
diff --git a/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml b/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..6e0aac2
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.wearable.watchface" >
+
+ <uses-sdk android:minSdkVersion="21"
+ android:targetSdkVersion="21" />
+
+ <uses-feature android:name="android.hardware.type.watch" />
+
+ <!-- Required to act as a custom watch face. -->
+ <uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+ <!-- Calendar permission used by CalendarWatchFaceService -->
+ <uses-permission android:name="android.permission.READ_CALENDAR" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name" >
+
+ <service
+ android:name=".AnalogWatchFaceService"
+ android:label="@string/analog_name"
+ android:allowEmbedded="true"
+ android:taskAffinity=""
+ android:permission="android.permission.BIND_WALLPAPER" >
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_analog" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_ANALOG" />
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
+ </service>
+
+ <service
+ android:name=".SweepWatchFaceService"
+ android:label="@string/sweep_name"
+ android:allowEmbedded="true"
+ android:taskAffinity=""
+ android:permission="android.permission.BIND_WALLPAPER" >
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_analog" />
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
+ </service>
+
+ <service
+ android:name=".TiltWatchFaceService"
+ android:label="@string/tilt_name"
+ android:allowEmbedded="true"
+ android:taskAffinity=""
+ android:permission="android.permission.BIND_WALLPAPER" >
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_tilt" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_TILT" />
+
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
+ </service>
+
+ <service
+ android:name=".CardBoundsWatchFaceService"
+ android:label="@string/card_bounds_name"
+ android:allowEmbedded="true"
+ android:taskAffinity=""
+ android:permission="android.permission.BIND_WALLPAPER" >
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_card_bounds" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_CARD_BOUNDS" />
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
+ </service>
+
+ <service
+ android:name=".DigitalWatchFaceService"
+ android:label="@string/digital_name"
+ android:allowEmbedded="true"
+ android:taskAffinity=""
+ android:permission="android.permission.BIND_WALLPAPER" >
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_digital" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
+ </service>
+
+ <!-- All intent-filters for config actions must include the categories
+ com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION
+ and android.intent.category.DEFAULT. -->
+
+ <activity
+ android:name=".DigitalWatchFaceWearableConfigActivity"
+ android:label="@string/digital_config_name">
+ <intent-filter>
+ <action android:name="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
+ <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <service
+ android:name=".CalendarWatchFaceService"
+ android:label="@string/calendar_name"
+ android:allowEmbedded="true"
+ android:taskAffinity=""
+ android:permission="android.permission.BIND_WALLPAPER" >
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_calendar" />
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
+ </service>
+
+ <service android:name=".DigitalWatchFaceConfigListenerService">
+ <intent-filter>
+ <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
+ </intent-filter>
+ </service>
+
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version" />
+
+ </application>
+
+</manifest>
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/AnalogWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/AnalogWatchFaceService.java
new file mode 100644
index 0000000..19c8d2b
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/AnalogWatchFaceService.java
@@ -0,0 +1,299 @@
+/*
+ * 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.watchface;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.wearable.watchface.CanvasWatchFaceService;
+import android.support.wearable.watchface.WatchFaceService;
+import android.support.wearable.watchface.WatchFaceStyle;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Sample analog watch face with a ticking second hand. In ambient mode, the second hand isn't
+ * shown. On devices with low-bit ambient mode, the hands are drawn without anti-aliasing in ambient
+ * mode. The watch face is drawn with less contrast in mute mode.
+ *
+ * {@link SweepWatchFaceService} is similar but has a sweep second hand.
+ */
+public class AnalogWatchFaceService extends CanvasWatchFaceService {
+ private static final String TAG = "AnalogWatchFaceService";
+
+ /**
+ * Update rate in milliseconds for interactive mode. We update once a second to advance the
+ * second hand.
+ */
+ private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends CanvasWatchFaceService.Engine {
+ static final int MSG_UPDATE_TIME = 0;
+
+ Paint mHourPaint;
+ Paint mMinutePaint;
+ Paint mSecondPaint;
+ Paint mTickPaint;
+ boolean mMute;
+ Time mTime;
+
+ /** Handler to update the time once a second in interactive mode. */
+ final Handler mUpdateTimeHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_UPDATE_TIME:
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "updating time");
+ }
+ invalidate();
+ if (isVisible()) {
+ long timeMs = System.currentTimeMillis();
+ long delayMs = INTERACTIVE_UPDATE_RATE_MS
+ - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
+ mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
+ }
+ break;
+ }
+ }
+ };
+
+ final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mTime.clear(intent.getStringExtra("time-zone"));
+ mTime.setToNow();
+ }
+ };
+ boolean mRegisteredTimeZoneReceiver = false;
+
+ /**
+ * Whether the display supports fewer bits for each color in ambient mode. When true, we
+ * disable anti-aliasing in ambient mode.
+ */
+ boolean mLowBitAmbient;
+
+ Bitmap mBackgroundBitmap;
+ Bitmap mBackgroundScaledBitmap;
+
+ @Override
+ public void onCreate(SurfaceHolder holder) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate");
+ }
+ super.onCreate(holder);
+
+ setWatchFaceStyle(new WatchFaceStyle.Builder(AnalogWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setShowSystemUiTime(false)
+ .build());
+
+ Resources resources = AnalogWatchFaceService.this.getResources();
+ Drawable backgroundDrawable = resources.getDrawable(R.drawable.bg);
+ mBackgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();
+
+ mHourPaint = new Paint();
+ mHourPaint.setARGB(255, 200, 200, 200);
+ mHourPaint.setStrokeWidth(5.f);
+ mHourPaint.setAntiAlias(true);
+ mHourPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mMinutePaint = new Paint();
+ mMinutePaint.setARGB(255, 200, 200, 200);
+ mMinutePaint.setStrokeWidth(3.f);
+ mMinutePaint.setAntiAlias(true);
+ mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mSecondPaint = new Paint();
+ mSecondPaint.setARGB(255, 255, 0, 0);
+ mSecondPaint.setStrokeWidth(2.f);
+ mSecondPaint.setAntiAlias(true);
+ mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mTickPaint = new Paint();
+ mTickPaint.setARGB(100, 255, 255, 255);
+ mTickPaint.setStrokeWidth(2.f);
+ mTickPaint.setAntiAlias(true);
+
+ mTime = new Time();
+ }
+
+ @Override
+ public void onDestroy() {
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onPropertiesChanged(Bundle properties) {
+ super.onPropertiesChanged(properties);
+ mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
+ }
+ }
+
+ @Override
+ public void onTimeTick() {
+ super.onTimeTick();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
+ }
+ invalidate();
+ }
+
+ @Override
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ super.onAmbientModeChanged(inAmbientMode);
+ if (mLowBitAmbient) {
+ boolean antiAlias = !inAmbientMode;
+ mHourPaint.setAntiAlias(antiAlias);
+ mMinutePaint.setAntiAlias(antiAlias);
+ mSecondPaint.setAntiAlias(antiAlias);
+ mTickPaint.setAntiAlias(antiAlias);
+ }
+ invalidate();
+ }
+
+ @Override
+ public void onInterruptionFilterChanged(int interruptionFilter) {
+ super.onInterruptionFilterChanged(interruptionFilter);
+ boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
+ if (mMute != inMuteMode) {
+ mMute = inMuteMode;
+ mHourPaint.setAlpha(inMuteMode ? 100 : 255);
+ mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
+ mSecondPaint.setAlpha(inMuteMode ? 80 : 255);
+ invalidate();
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ mTime.setToNow();
+
+ int width = canvas.getWidth();
+ int height = canvas.getHeight();
+
+ // Draw the background, scaled to fit.
+ if (mBackgroundScaledBitmap == null
+ || mBackgroundScaledBitmap.getWidth() != width
+ || mBackgroundScaledBitmap.getHeight() != height) {
+ mBackgroundScaledBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
+ width, height, true /* filter */);
+ }
+ canvas.drawBitmap(mBackgroundScaledBitmap, 0, 0, null);
+
+ // Find the center. Ignore the window insets so that, on round watches with a
+ // "chin", the watch face is centered on the entire screen, not just the usable
+ // portion.
+ float centerX = width / 2f;
+ float centerY = height / 2f;
+
+ // Draw the ticks.
+ float innerTickRadius = centerX - 10;
+ float outerTickRadius = centerX;
+ for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
+ float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
+ float innerX = (float) Math.sin(tickRot) * innerTickRadius;
+ float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
+ float outerX = (float) Math.sin(tickRot) * outerTickRadius;
+ float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
+ canvas.drawLine(centerX + innerX, centerY + innerY,
+ centerX + outerX, centerY + outerY, mTickPaint);
+ }
+
+ float secRot = mTime.second / 30f * (float) Math.PI;
+ int minutes = mTime.minute;
+ float minRot = minutes / 30f * (float) Math.PI;
+ float hrRot = ((mTime.hour + (minutes / 60f)) / 6f ) * (float) Math.PI;
+
+ float secLength = centerX - 20;
+ float minLength = centerX - 40;
+ float hrLength = centerX - 80;
+
+ if (!isInAmbientMode()) {
+ float secX = (float) Math.sin(secRot) * secLength;
+ float secY = (float) -Math.cos(secRot) * secLength;
+ canvas.drawLine(centerX, centerY, centerX + secX, centerY + secY, mSecondPaint);
+ }
+
+ float minX = (float) Math.sin(minRot) * minLength;
+ float minY = (float) -Math.cos(minRot) * minLength;
+ canvas.drawLine(centerX, centerY, centerX + minX, centerY + minY, mMinutePaint);
+
+ float hrX = (float) Math.sin(hrRot) * hrLength;
+ float hrY = (float) -Math.cos(hrRot) * hrLength;
+ canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY, mHourPaint);
+ }
+
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ super.onVisibilityChanged(visible);
+
+ if (visible) {
+ registerReceiver();
+
+ // Update time zone in case it changed while we weren't visible.
+ mTime.clear(TimeZone.getDefault().getID());
+ mTime.setToNow();
+
+ mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
+ } else {
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ unregisterReceiver();
+ }
+ }
+
+ private void registerReceiver() {
+ if (mRegisteredTimeZoneReceiver) {
+ return;
+ }
+ mRegisteredTimeZoneReceiver = true;
+ IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+ AnalogWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
+ }
+
+ private void unregisterReceiver() {
+ if (!mRegisteredTimeZoneReceiver) {
+ return;
+ }
+ mRegisteredTimeZoneReceiver = false;
+ AnalogWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFaceService.java
new file mode 100644
index 0000000..929651d
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFaceService.java
@@ -0,0 +1,194 @@
+/*
+ * 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.watchface;
+
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.support.wearable.provider.WearableCalendarContract;
+import android.support.wearable.watchface.CanvasWatchFaceService;
+import android.support.wearable.watchface.WatchFaceStyle;
+import android.text.DynamicLayout;
+import android.text.Editable;
+import android.text.Html;
+import android.text.Layout;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Proof of concept sample watch face that demonstrates how a watch face can load calendar data.
+ */
+public class CalendarWatchFaceService extends CanvasWatchFaceService {
+ private static final String TAG = "CalendarWatchFace";
+
+ /**
+ * How long to wait after loading meetings before loading them again. Meetings are only loaded
+ * in interactive mode.
+ */
+ private static final long LOAD_MEETINGS_DELAY_MS = TimeUnit.MINUTES.toMillis(1);
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends CanvasWatchFaceService.Engine {
+
+ static final int BACKGROUND_COLOR = Color.BLACK;
+ static final int FOREGROUND_COLOR = Color.WHITE;
+ static final int TEXT_SIZE = 25;
+ static final int MSG_LOAD_MEETINGS = 0;
+
+ /** Editable string containing the text to draw with the number of meetings in bold. */
+ final Editable mEditable = new SpannableStringBuilder();
+
+ /** Width specified when {@link #mLayout} was created. */
+ int mLayoutWidth;
+
+ /** Layout to wrap {@link #mEditable} onto multiple lines. */
+ DynamicLayout mLayout;
+
+ /** Paint used to draw text. */
+ final TextPaint mTextPaint = new TextPaint();
+
+ int mNumMeetings;
+
+ private AsyncTask<Void, Void, Integer> mLoadMeetingsTask;
+
+ /** Handler to load the meetings once a minute in interactive mode. */
+ final Handler mLoadMeetingsHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_LOAD_MEETINGS:
+ cancelLoadMeetingTask();
+ mLoadMeetingsTask = new LoadMeetingsTask();
+ mLoadMeetingsTask.execute();
+ break;
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(SurfaceHolder holder) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate");
+ }
+ super.onCreate(holder);
+ setWatchFaceStyle(new WatchFaceStyle.Builder(CalendarWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setShowSystemUiTime(false)
+ .build());
+
+ mTextPaint.setColor(FOREGROUND_COLOR);
+ mTextPaint.setTextSize(TEXT_SIZE);
+ }
+
+ @Override
+ public void onDestroy() {
+ mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);
+ cancelLoadMeetingTask();
+ super.onDestroy();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ // Create or update mLayout if necessary.
+ if (mLayout == null || mLayoutWidth != canvas.getWidth()) {
+ mLayoutWidth = canvas.getWidth();
+ mLayout = new DynamicLayout(mEditable, mTextPaint, mLayoutWidth,
+ Layout.Alignment.ALIGN_NORMAL, 1 /* spacingMult */, 0 /* spacingAdd */,
+ false /* includePad */);
+ }
+
+ // Update the contents of mEditable.
+ mEditable.clear();
+ mEditable.append(Html.fromHtml(getResources().getQuantityString(
+ R.plurals.calendar_meetings, mNumMeetings, mNumMeetings)));
+
+ // Draw the text on a solid background.
+ canvas.drawColor(BACKGROUND_COLOR);
+ mLayout.draw(canvas);
+ }
+
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ super.onVisibilityChanged(visible);
+ if (visible) {
+ mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
+ } else {
+ mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);
+ cancelLoadMeetingTask();
+ }
+ }
+
+ private void onMeetingsLoaded(Integer result) {
+ if (result != null) {
+ mNumMeetings = result;
+ invalidate();
+ }
+ if (isVisible()) {
+ mLoadMeetingsHandler.sendEmptyMessageDelayed(
+ MSG_LOAD_MEETINGS, LOAD_MEETINGS_DELAY_MS);
+ }
+ }
+
+ private void cancelLoadMeetingTask() {
+ if (mLoadMeetingsTask != null) {
+ mLoadMeetingsTask.cancel(true);
+ }
+ }
+
+ /**
+ * Asynchronous task to load the meetings from the content provider and report the number of
+ * meetings back via {@link #onMeetingsLoaded}.
+ */
+ private class LoadMeetingsTask extends AsyncTask<Void, Void, Integer> {
+ @Override
+ protected Integer doInBackground(Void... voids) {
+ long begin = System.currentTimeMillis();
+ Uri.Builder builder =
+ WearableCalendarContract.Instances.CONTENT_URI.buildUpon();
+ ContentUris.appendId(builder, begin);
+ ContentUris.appendId(builder, begin + DateUtils.DAY_IN_MILLIS);
+ final Cursor cursor = getContentResolver() .query(builder.build(),
+ null, null, null, null);
+ int numMeetings = cursor.getCount();
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Num meetings: " + numMeetings);
+ }
+ return numMeetings;
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ onMeetingsLoaded(result);
+ }
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CardBoundsWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CardBoundsWatchFaceService.java
new file mode 100644
index 0000000..c0d4437
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CardBoundsWatchFaceService.java
@@ -0,0 +1,103 @@
+/*
+ * 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.watchface;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.support.wearable.watchface.CanvasWatchFaceService;
+import android.support.wearable.watchface.WatchFaceStyle;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+/**
+ * Proof of concept sample watch face that demonstrates how a watch face can detect where the peek
+ * card is. This watch face draws a border around the area where the peeking card is.
+ */
+public class CardBoundsWatchFaceService extends CanvasWatchFaceService {
+
+ private static final String TAG = "CardBoundsWatchFace";
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends CanvasWatchFaceService.Engine {
+
+ static final int BORDER_WIDTH_PX = 5;
+
+ final Rect mCardBounds = new Rect();
+ final Paint mPaint = new Paint();
+
+ @Override
+ public void onCreate(SurfaceHolder holder) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate");
+ }
+ super.onCreate(holder);
+ setWatchFaceStyle(new WatchFaceStyle.Builder(CardBoundsWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setShowSystemUiTime(true)
+ .setPeekOpacityMode(WatchFaceStyle.PEEK_OPACITY_MODE_TRANSLUCENT)
+ .build());
+ }
+
+ @Override
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
+ }
+ super.onAmbientModeChanged(inAmbientMode);
+ invalidate();
+ }
+
+ @Override
+ public void onPeekCardPositionUpdate(Rect bounds) {
+ super.onPeekCardPositionUpdate(bounds);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onPeekCardPositionUpdate: " + bounds);
+ }
+ super.onPeekCardPositionUpdate(bounds);
+ if (!bounds.equals(mCardBounds)) {
+ mCardBounds.set(bounds);
+ invalidate();
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ // Clear screen.
+ canvas.drawColor(isInAmbientMode() ? Color.BLACK : Color.BLUE);
+
+ // Draw border around card in interactive mode.
+ if (!isInAmbientMode()) {
+ mPaint.setColor(Color.MAGENTA);
+ canvas.drawRect(mCardBounds.left - BORDER_WIDTH_PX,
+ mCardBounds.top - BORDER_WIDTH_PX,
+ mCardBounds.right + BORDER_WIDTH_PX,
+ mCardBounds.bottom + BORDER_WIDTH_PX, mPaint);
+ }
+
+ // Fill area under card.
+ mPaint.setColor(isInAmbientMode() ? Color.RED : Color.GREEN);
+ canvas.drawRect(mCardBounds, mPaint);
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceConfigListenerService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceConfigListenerService.java
new file mode 100644
index 0000000..725c51a
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceConfigListenerService.java
@@ -0,0 +1,91 @@
+/*
+ * 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.watchface;
+
+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.DataMap;
+import com.google.android.gms.wearable.MessageEvent;
+import com.google.android.gms.wearable.Wearable;
+import com.google.android.gms.wearable.WearableListenerService;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link WearableListenerService} listening for {@link DigitalWatchFaceService} config messages
+ * and updating the config {@link com.google.android.gms.wearable.DataItem} accordingly.
+ */
+public class DigitalWatchFaceConfigListenerService extends WearableListenerService
+ implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
+ private static final String TAG = "DigitalListenerService";
+
+ private GoogleApiClient mGoogleApiClient;
+
+ @Override // WearableListenerService
+ public void onMessageReceived(MessageEvent messageEvent) {
+ if (!messageEvent.getPath().equals(DigitalWatchFaceUtil.PATH_WITH_FEATURE)) {
+ return;
+ }
+ byte[] rawData = messageEvent.getData();
+ // It's allowed that the message carries only some of the keys used in the config DataItem
+ // and skips the ones that we don't want to change.
+ DataMap configKeysToOverwrite = DataMap.fromByteArray(rawData);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Received watch face config message: " + configKeysToOverwrite);
+ }
+
+ if (mGoogleApiClient == null) {
+ mGoogleApiClient = new GoogleApiClient.Builder(this).addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this).addApi(Wearable.API).build();
+ }
+ if (!mGoogleApiClient.isConnected()) {
+ ConnectionResult connectionResult =
+ mGoogleApiClient.blockingConnect(30, TimeUnit.SECONDS);
+
+ if (!connectionResult.isSuccess()) {
+ Log.e(TAG, "Failed to connect to GoogleApiClient.");
+ return;
+ }
+ }
+
+ DigitalWatchFaceUtil.overwriteKeysInConfigDataMap(mGoogleApiClient, configKeysToOverwrite);
+ }
+
+ @Override // GoogleApiClient.ConnectionCallbacks
+ public void onConnected(Bundle connectionHint) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnected: " + connectionHint);
+ }
+ }
+
+ @Override // GoogleApiClient.ConnectionCallbacks
+ public void onConnectionSuspended(int cause) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnectionSuspended: " + cause);
+ }
+ }
+
+ @Override // GoogleApiClient.OnConnectionFailedListener
+ public void onConnectionFailed(ConnectionResult result) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnectionFailed: " + result);
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceService.java
new file mode 100644
index 0000000..7620d1a
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceService.java
@@ -0,0 +1,574 @@
+/*
+ * 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.watchface;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.wearable.watchface.CanvasWatchFaceService;
+import android.support.wearable.watchface.WatchFaceService;
+import android.support.wearable.watchface.WatchFaceStyle;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.WindowInsets;
+
+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.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.Wearable;
+
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Sample digital watch face with blinking colons and seconds. In ambient mode, the seconds are
+ * replaced with an AM/PM indicator and the colons don't blink. On devices with low-bit ambient
+ * mode, the text is drawn without anti-aliasing in ambient mode. On devices which require burn-in
+ * protection, the hours are drawn in normal rather than bold. The time is drawn with less contrast
+ * and without seconds in mute mode.
+ */
+public class DigitalWatchFaceService extends CanvasWatchFaceService {
+ private static final String TAG = "DigitalWatchFaceService";
+
+ private static final Typeface BOLD_TYPEFACE =
+ Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
+ private static final Typeface NORMAL_TYPEFACE =
+ Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
+
+ /**
+ * Update rate in milliseconds for normal (not ambient and not mute) mode. We update twice
+ * a second to blink the colons.
+ */
+ private static final long NORMAL_UPDATE_RATE_MS = 500;
+
+ /**
+ * Update rate in milliseconds for mute mode. We update every minute, like in ambient mode.
+ */
+ private static final long MUTE_UPDATE_RATE_MS = TimeUnit.MINUTES.toMillis(1);
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends CanvasWatchFaceService.Engine implements DataApi.DataListener,
+ GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
+ static final String COLON_STRING = ":";
+
+ /** Alpha value for drawing time when in mute mode. */
+ static final int MUTE_ALPHA = 100;
+
+ /** Alpha value for drawing time when not in mute mode. */
+ static final int NORMAL_ALPHA = 255;
+
+ static final int MSG_UPDATE_TIME = 0;
+
+ /** How often {@link #mUpdateTimeHandler} ticks in milliseconds. */
+ long mInteractiveUpdateRateMs = NORMAL_UPDATE_RATE_MS;
+
+ /** Handler to update the time periodically in interactive mode. */
+ final Handler mUpdateTimeHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_UPDATE_TIME:
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "updating time");
+ }
+ invalidate();
+ if (isVisible()) {
+ long timeMs = System.currentTimeMillis();
+ long delayMs = mInteractiveUpdateRateMs
+ - (timeMs % mInteractiveUpdateRateMs);
+ mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
+ }
+ break;
+ }
+ }
+ };
+
+ GoogleApiClient mGoogleApiClient = new GoogleApiClient.Builder(DigitalWatchFaceService.this)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .addApi(Wearable.API)
+ .build();
+
+ final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mTime.clear(intent.getStringExtra("time-zone"));
+ mTime.setToNow();
+ }
+ };
+ boolean mRegisteredTimeZoneReceiver = false;
+
+ Paint mBackgroundPaint;
+ Paint mHourPaint;
+ Paint mMinutePaint;
+ Paint mSecondPaint;
+ Paint mAmPmPaint;
+ Paint mColonPaint;
+ float mColonWidth;
+ boolean mMute;
+ Time mTime;
+ boolean mShouldDrawColons;
+ float mXOffset;
+ float mYOffset;
+ String mAmString;
+ String mPmString;
+ int mInteractiveBackgroundColor =
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND;
+ int mInteractiveHourDigitsColor =
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS;
+ int mInteractiveMinuteDigitsColor =
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS;
+ int mInteractiveSecondDigitsColor =
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS;
+
+ /**
+ * Whether the display supports fewer bits for each color in ambient mode. When true, we
+ * disable anti-aliasing in ambient mode.
+ */
+ boolean mLowBitAmbient;
+
+ @Override
+ public void onCreate(SurfaceHolder holder) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate");
+ }
+ super.onCreate(holder);
+
+ setWatchFaceStyle(new WatchFaceStyle.Builder(DigitalWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setShowSystemUiTime(false)
+ .build());
+ Resources resources = DigitalWatchFaceService.this.getResources();
+ mYOffset = resources.getDimension(R.dimen.digital_y_offset);
+ mAmString = resources.getString(R.string.digital_am);
+ mPmString = resources.getString(R.string.digital_pm);
+
+ mBackgroundPaint = new Paint();
+ mBackgroundPaint.setColor(mInteractiveBackgroundColor);
+ mHourPaint = createTextPaint(mInteractiveHourDigitsColor, BOLD_TYPEFACE);
+ mMinutePaint = createTextPaint(mInteractiveMinuteDigitsColor);
+ mSecondPaint = createTextPaint(mInteractiveSecondDigitsColor);
+ mAmPmPaint = createTextPaint(resources.getColor(R.color.digital_am_pm));
+ mColonPaint = createTextPaint(resources.getColor(R.color.digital_colons));
+
+ mTime = new Time();
+ }
+
+ @Override
+ public void onDestroy() {
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ super.onDestroy();
+ }
+
+ private Paint createTextPaint(int defaultInteractiveColor) {
+ return createTextPaint(defaultInteractiveColor, NORMAL_TYPEFACE);
+ }
+
+ private Paint createTextPaint(int defaultInteractiveColor, Typeface typeface) {
+ Paint paint = new Paint();
+ paint.setColor(defaultInteractiveColor);
+ paint.setTypeface(typeface);
+ paint.setAntiAlias(true);
+ return paint;
+ }
+
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ super.onVisibilityChanged(visible);
+
+ if (visible) {
+ mGoogleApiClient.connect();
+
+ registerReceiver();
+
+ // Update time zone in case it changed while we weren't visible.
+ mTime.clear(TimeZone.getDefault().getID());
+ mTime.setToNow();
+
+ mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
+ } else {
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+
+ unregisterReceiver();
+
+ if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
+ Wearable.DataApi.removeListener(mGoogleApiClient, this);
+ mGoogleApiClient.disconnect();
+ }
+ }
+ }
+
+ private void registerReceiver() {
+ if (mRegisteredTimeZoneReceiver) {
+ return;
+ }
+ mRegisteredTimeZoneReceiver = true;
+ IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+ DigitalWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
+ }
+
+ private void unregisterReceiver() {
+ if (!mRegisteredTimeZoneReceiver) {
+ return;
+ }
+ mRegisteredTimeZoneReceiver = false;
+ DigitalWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
+ }
+
+ @Override
+ public void onApplyWindowInsets(WindowInsets insets) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square"));
+ }
+ super.onApplyWindowInsets(insets);
+
+ // Load resources that have alternate values for round watches.
+ Resources resources = DigitalWatchFaceService.this.getResources();
+ boolean isRound = insets.isRound();
+ mXOffset = resources.getDimension(isRound
+ ? R.dimen.digital_x_offset_round : R.dimen.digital_x_offset);
+ float textSize = resources.getDimension(isRound
+ ? R.dimen.digital_text_size_round : R.dimen.digital_text_size);
+ float amPmSize = resources.getDimension(isRound
+ ? R.dimen.digital_am_pm_size_round : R.dimen.digital_am_pm_size);
+
+ mHourPaint.setTextSize(textSize);
+ mMinutePaint.setTextSize(textSize);
+ mSecondPaint.setTextSize(textSize);
+ mAmPmPaint.setTextSize(amPmSize);
+ mColonPaint.setTextSize(textSize);
+
+ mColonWidth = mColonPaint.measureText(COLON_STRING);
+ }
+
+ @Override
+ public void onPropertiesChanged(Bundle properties) {
+ super.onPropertiesChanged(properties);
+
+ boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
+ mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : BOLD_TYPEFACE);
+
+ mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onPropertiesChanged: burn-in protection = " + burnInProtection
+ + ", low-bit ambient = " + mLowBitAmbient);
+ }
+ }
+
+ @Override
+ public void onTimeTick() {
+ super.onTimeTick();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
+ }
+ invalidate();
+ }
+
+ @Override
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
+ }
+ super.onAmbientModeChanged(inAmbientMode);
+ adjustPaintColorToCurrentMode(mBackgroundPaint, mInteractiveBackgroundColor,
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND);
+ adjustPaintColorToCurrentMode(mHourPaint, mInteractiveHourDigitsColor,
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS);
+ adjustPaintColorToCurrentMode(mMinutePaint, mInteractiveMinuteDigitsColor,
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS);
+ // Actually, the seconds are not rendered in the ambient mode, so we could pass just
+ // any value as ambientColor here.
+ adjustPaintColorToCurrentMode(mSecondPaint, mInteractiveSecondDigitsColor,
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS);
+
+ if (mLowBitAmbient) {
+ boolean antiAlias = !inAmbientMode;
+ mHourPaint.setAntiAlias(antiAlias);
+ mMinutePaint.setAntiAlias(antiAlias);
+ mSecondPaint.setAntiAlias(antiAlias);
+ mAmPmPaint.setAntiAlias(antiAlias);
+ mColonPaint.setAntiAlias(antiAlias);
+ }
+ invalidate();
+ }
+
+ private void adjustPaintColorToCurrentMode(Paint paint, int interactiveColor,
+ int ambientColor) {
+ paint.setColor(isInAmbientMode() ? ambientColor : interactiveColor);
+ }
+
+ @Override
+ public void onInterruptionFilterChanged(int interruptionFilter) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onInterruptionFilterChanged: " + interruptionFilter);
+ }
+ super.onInterruptionFilterChanged(interruptionFilter);
+
+ boolean inMuteMode = interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE;
+ // We only need to update once a minute in mute mode.
+ setInteractiveUpdateRateMs(inMuteMode ? MUTE_UPDATE_RATE_MS : NORMAL_UPDATE_RATE_MS);
+
+ if (mMute != inMuteMode) {
+ mMute = inMuteMode;
+ int alpha = inMuteMode ? MUTE_ALPHA : NORMAL_ALPHA;
+ mHourPaint.setAlpha(alpha);
+ mMinutePaint.setAlpha(alpha);
+ mColonPaint.setAlpha(alpha);
+ mAmPmPaint.setAlpha(alpha);
+ invalidate();
+ }
+ }
+
+ public void setInteractiveUpdateRateMs(long updateRateMs) {
+ if (updateRateMs == mInteractiveUpdateRateMs) {
+ return;
+ }
+ mInteractiveUpdateRateMs = updateRateMs;
+ if (isVisible()) {
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
+ }
+ }
+
+ private void updatePaintIfInteractive(Paint paint, int interactiveColor) {
+ if (!isInAmbientMode() && paint != null) {
+ paint.setColor(interactiveColor);
+ }
+ }
+
+ private void setInteractiveBackgroundColor(int color) {
+ mInteractiveBackgroundColor = color;
+ updatePaintIfInteractive(mBackgroundPaint, color);
+ }
+
+ private void setInteractiveHourDigitsColor(int color) {
+ mInteractiveHourDigitsColor = color;
+ updatePaintIfInteractive(mHourPaint, color);
+ }
+
+ private void setInteractiveMinuteDigitsColor(int color) {
+ mInteractiveMinuteDigitsColor = color;
+ updatePaintIfInteractive(mMinutePaint, color);
+ }
+
+ private void setInteractiveSecondDigitsColor(int color) {
+ mInteractiveSecondDigitsColor = color;
+ updatePaintIfInteractive(mSecondPaint, color);
+ }
+
+ private String formatTwoDigitNumber(int hour) {
+ return String.format("%02d", hour);
+ }
+
+ private int convertTo12Hour(int hour) {
+ int result = hour % 12;
+ return (result == 0) ? 12 : result;
+ }
+
+ private String getAmPmString(int hour) {
+ return (hour < 12) ? mAmString : mPmString;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ mTime.setToNow();
+
+ // Show colons for the first half of each second so the colons blink on when the time
+ // updates.
+ mShouldDrawColons = (System.currentTimeMillis() % 1000) < 500;
+
+ // Draw the background.
+ canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mBackgroundPaint);
+
+ // Draw the hours.
+ float x = mXOffset;
+ String hourString = String.valueOf(convertTo12Hour(mTime.hour));
+ canvas.drawText(hourString, x, mYOffset, mHourPaint);
+ x += mHourPaint.measureText(hourString);
+
+ // In ambient and mute modes, always draw the first colon. Otherwise, draw the
+ // first colon for the first half of each second.
+ if (isInAmbientMode() || mMute || mShouldDrawColons) {
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+ }
+ x += mColonWidth;
+
+ // Draw the minutes.
+ String minuteString = formatTwoDigitNumber(mTime.minute);
+ canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
+ x += mMinutePaint.measureText(minuteString);
+
+ // In ambient and mute modes, draw AM/PM. Otherwise, draw a second blinking
+ // colon followed by the seconds.
+ if (isInAmbientMode() || mMute) {
+ x += mColonWidth;
+ canvas.drawText(getAmPmString(mTime.hour), x, mYOffset, mAmPmPaint);
+ } else {
+ if (mShouldDrawColons) {
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+ }
+ x += mColonWidth;
+ canvas.drawText(formatTwoDigitNumber(mTime.second), x, mYOffset,
+ mSecondPaint);
+ }
+ }
+
+ private void updateConfigDataItemAndUiOnStartup() {
+ DigitalWatchFaceUtil.fetchConfigDataMap(mGoogleApiClient,
+ new DigitalWatchFaceUtil.FetchConfigDataMapCallback() {
+ @Override
+ public void onConfigDataMapFetched(DataMap startupConfig) {
+ // If the DataItem hasn't been created yet or some keys are missing,
+ // use the default values.
+ setDefaultValuesForMissingConfigKeys(startupConfig);
+ DigitalWatchFaceUtil.putConfigDataItem(mGoogleApiClient, startupConfig);
+
+ updateUiForConfigDataMap(startupConfig);
+ }
+ }
+ );
+ }
+
+ private void setDefaultValuesForMissingConfigKeys(DataMap config) {
+ addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_BACKGROUND_COLOR,
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND);
+ addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_HOURS_COLOR,
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS);
+ addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_MINUTES_COLOR,
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS);
+ addIntKeyIfMissing(config, DigitalWatchFaceUtil.KEY_SECONDS_COLOR,
+ DigitalWatchFaceUtil.COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS);
+ }
+
+ private void addIntKeyIfMissing(DataMap config, String key, int color) {
+ if (!config.containsKey(key)) {
+ config.putInt(key, color);
+ }
+ }
+
+ @Override // DataApi.DataListener
+ public void onDataChanged(DataEventBuffer dataEvents) {
+ try {
+ for (DataEvent dataEvent : dataEvents) {
+ if (dataEvent.getType() != DataEvent.TYPE_CHANGED) {
+ continue;
+ }
+
+ DataItem dataItem = dataEvent.getDataItem();
+ if (!dataItem.getUri().getPath().equals(
+ DigitalWatchFaceUtil.PATH_WITH_FEATURE)) {
+ continue;
+ }
+
+ DataMapItem dataMapItem = DataMapItem.fromDataItem(dataItem);
+ DataMap config = dataMapItem.getDataMap();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Config DataItem updated:" + config);
+ }
+ updateUiForConfigDataMap(config);
+ }
+ } finally {
+ dataEvents.close();
+ }
+ }
+
+ private void updateUiForConfigDataMap(final DataMap config) {
+ boolean uiUpdated = false;
+ for (String configKey : config.keySet()) {
+ if (!config.containsKey(configKey)) {
+ continue;
+ }
+ int color = config.getInt(configKey);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Found watch face config key: " + configKey + " -> "
+ + Integer.toHexString(color));
+ }
+ if (updateUiForKey(configKey, color)) {
+ uiUpdated = true;
+ }
+ }
+ if (uiUpdated) {
+ invalidate();
+ }
+ }
+
+ /**
+ * Updates the color of a UI item according to the given {@code configKey}. Does nothing if
+ * {@code configKey} isn't recognized.
+ *
+ * @return whether UI has been updated
+ */
+ private boolean updateUiForKey(String configKey, int color) {
+ if (configKey.equals(DigitalWatchFaceUtil.KEY_BACKGROUND_COLOR)) {
+ setInteractiveBackgroundColor(color);
+ } else if (configKey.equals(DigitalWatchFaceUtil.KEY_HOURS_COLOR)) {
+ setInteractiveHourDigitsColor(color);
+ } else if (configKey.equals(DigitalWatchFaceUtil.KEY_MINUTES_COLOR)) {
+ setInteractiveMinuteDigitsColor(color);
+ } else if (configKey.equals(DigitalWatchFaceUtil.KEY_SECONDS_COLOR)) {
+ setInteractiveSecondDigitsColor(color);
+ } else {
+ Log.w(TAG, "Ignoring unknown config key: " + configKey);
+ return false;
+ }
+ return true;
+ }
+
+ @Override // GoogleApiClient.ConnectionCallbacks
+ public void onConnected(Bundle connectionHint) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnected: " + connectionHint);
+ }
+ Wearable.DataApi.addListener(mGoogleApiClient, Engine.this);
+ updateConfigDataItemAndUiOnStartup();
+ }
+
+ @Override // GoogleApiClient.ConnectionCallbacks
+ public void onConnectionSuspended(int cause) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnectionSuspended: " + cause);
+ }
+ }
+
+ @Override // GoogleApiClient.OnConnectionFailedListener
+ public void onConnectionFailed(ConnectionResult result) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnectionFailed: " + result);
+ }
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceUtil.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceUtil.java
new file mode 100644
index 0000000..1c4af70
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceUtil.java
@@ -0,0 +1,205 @@
+/*
+ * 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.watchface;
+
+import android.graphics.Color;
+import android.net.Uri;
+import android.util.Log;
+
+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.DataItem;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.DataMapItem;
+import com.google.android.gms.wearable.NodeApi;
+import com.google.android.gms.wearable.PutDataMapRequest;
+import com.google.android.gms.wearable.Wearable;
+
+public final class DigitalWatchFaceUtil {
+ private static final String TAG = "DigitalWatchFaceUtil";
+
+ /**
+ * The {@link DataMap} key for {@link DigitalWatchFaceService} background color name.
+ * The color name must be a {@link String} recognized by {@link Color#parseColor}.
+ */
+ public static final String KEY_BACKGROUND_COLOR = "BACKGROUND_COLOR";
+
+ /**
+ * The {@link DataMap} key for {@link DigitalWatchFaceService} hour digits color name.
+ * The color name must be a {@link String} recognized by {@link Color#parseColor}.
+ */
+ public static final String KEY_HOURS_COLOR = "HOURS_COLOR";
+
+ /**
+ * The {@link DataMap} key for {@link DigitalWatchFaceService} minute digits color name.
+ * The color name must be a {@link String} recognized by {@link Color#parseColor}.
+ */
+ public static final String KEY_MINUTES_COLOR = "MINUTES_COLOR";
+
+ /**
+ * The {@link DataMap} key for {@link DigitalWatchFaceService} second digits color name.
+ * The color name must be a {@link String} recognized by {@link Color#parseColor}.
+ */
+ public static final String KEY_SECONDS_COLOR = "SECONDS_COLOR";
+
+ /**
+ * The path for the {@link DataItem} containing {@link DigitalWatchFaceService} configuration.
+ */
+ public static final String PATH_WITH_FEATURE = "/watch_face_config/Digital";
+
+ /**
+ * Name of the default interactive mode background color and the ambient mode background color.
+ */
+ public static final String COLOR_NAME_DEFAULT_AND_AMBIENT_BACKGROUND = "Black";
+ public static final int COLOR_VALUE_DEFAULT_AND_AMBIENT_BACKGROUND =
+ parseColor(COLOR_NAME_DEFAULT_AND_AMBIENT_BACKGROUND);
+
+ /**
+ * Name of the default interactive mode hour digits color and the ambient mode hour digits
+ * color.
+ */
+ public static final String COLOR_NAME_DEFAULT_AND_AMBIENT_HOUR_DIGITS = "White";
+ public static final int COLOR_VALUE_DEFAULT_AND_AMBIENT_HOUR_DIGITS =
+ parseColor(COLOR_NAME_DEFAULT_AND_AMBIENT_HOUR_DIGITS);
+
+ /**
+ * Name of the default interactive mode minute digits color and the ambient mode minute digits
+ * color.
+ */
+ public static final String COLOR_NAME_DEFAULT_AND_AMBIENT_MINUTE_DIGITS = "White";
+ public static final int COLOR_VALUE_DEFAULT_AND_AMBIENT_MINUTE_DIGITS =
+ parseColor(COLOR_NAME_DEFAULT_AND_AMBIENT_MINUTE_DIGITS);
+
+ /**
+ * Name of the default interactive mode second digits color and the ambient mode second digits
+ * color.
+ */
+ public static final String COLOR_NAME_DEFAULT_AND_AMBIENT_SECOND_DIGITS = "Gray";
+ public static final int COLOR_VALUE_DEFAULT_AND_AMBIENT_SECOND_DIGITS =
+ parseColor(COLOR_NAME_DEFAULT_AND_AMBIENT_SECOND_DIGITS);
+
+ /**
+ * Callback interface to perform an action with the current config {@link DataMap} for
+ * {@link DigitalWatchFaceService}.
+ */
+ public interface FetchConfigDataMapCallback {
+ /**
+ * Callback invoked with the current config {@link DataMap} for
+ * {@link DigitalWatchFaceService}.
+ */
+ void onConfigDataMapFetched(DataMap config);
+ }
+
+ private static int parseColor(String colorName) {
+ return Color.parseColor(colorName.toLowerCase());
+ }
+
+ /**
+ * Asynchronously fetches the current config {@link DataMap} for {@link DigitalWatchFaceService}
+ * and passes it to the given callback.
+ * <p>
+ * If the current config {@link DataItem} doesn't exist, it isn't created and the callback
+ * receives an empty DataMap.
+ */
+ public static void fetchConfigDataMap(final GoogleApiClient client,
+ final FetchConfigDataMapCallback callback) {
+ Wearable.NodeApi.getLocalNode(client).setResultCallback(
+ new ResultCallback<NodeApi.GetLocalNodeResult>() {
+ @Override
+ public void onResult(NodeApi.GetLocalNodeResult getLocalNodeResult) {
+ String localNode = getLocalNodeResult.getNode().getId();
+ Uri uri = new Uri.Builder()
+ .scheme("wear")
+ .path(DigitalWatchFaceUtil.PATH_WITH_FEATURE)
+ .authority(localNode)
+ .build();
+ Wearable.DataApi.getDataItem(client, uri)
+ .setResultCallback(new DataItemResultCallback(callback));
+ }
+ }
+ );
+ }
+
+ /**
+ * Overwrites (or sets, if not present) the keys in the current config {@link DataItem} with
+ * the ones appearing in the given {@link DataMap}. If the config DataItem doesn't exist,
+ * it's created.
+ * <p>
+ * It is allowed that only some of the keys used in the config DataItem appear in
+ * {@code configKeysToOverwrite}. The rest of the keys remains unmodified in this case.
+ */
+ public static void overwriteKeysInConfigDataMap(final GoogleApiClient googleApiClient,
+ final DataMap configKeysToOverwrite) {
+
+ DigitalWatchFaceUtil.fetchConfigDataMap(googleApiClient,
+ new FetchConfigDataMapCallback() {
+ @Override
+ public void onConfigDataMapFetched(DataMap currentConfig) {
+ DataMap overwrittenConfig = new DataMap();
+ overwrittenConfig.putAll(currentConfig);
+ overwrittenConfig.putAll(configKeysToOverwrite);
+ DigitalWatchFaceUtil.putConfigDataItem(googleApiClient, overwrittenConfig);
+ }
+ }
+ );
+ }
+
+ /**
+ * Overwrites the current config {@link DataItem}'s {@link DataMap} with {@code newConfig}.
+ * If the config DataItem doesn't exist, it's created.
+ */
+ public static void putConfigDataItem(GoogleApiClient googleApiClient, DataMap newConfig) {
+ PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(PATH_WITH_FEATURE);
+ DataMap configToPut = putDataMapRequest.getDataMap();
+ configToPut.putAll(newConfig);
+ Wearable.DataApi.putDataItem(googleApiClient, putDataMapRequest.asPutDataRequest())
+ .setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
+ @Override
+ public void onResult(DataApi.DataItemResult dataItemResult) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "putDataItem result status: " + dataItemResult.getStatus());
+ }
+ }
+ });
+ }
+
+ private static class DataItemResultCallback implements ResultCallback<DataApi.DataItemResult> {
+
+ private final FetchConfigDataMapCallback mCallback;
+
+ public DataItemResultCallback(FetchConfigDataMapCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onResult(DataApi.DataItemResult dataItemResult) {
+ if (dataItemResult.getStatus().isSuccess()) {
+ if (dataItemResult.getDataItem() != null) {
+ DataItem configDataItem = dataItemResult.getDataItem();
+ DataMapItem dataMapItem = DataMapItem.fromDataItem(configDataItem);
+ DataMap config = dataMapItem.getDataMap();
+ mCallback.onConfigDataMapFetched(config);
+ } else {
+ mCallback.onConfigDataMapFetched(new DataMap());
+ }
+ }
+ }
+ }
+
+ private DigitalWatchFaceUtil() { }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceWearableConfigActivity.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceWearableConfigActivity.java
new file mode 100644
index 0000000..a2e5251
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceWearableConfigActivity.java
@@ -0,0 +1,174 @@
+/*
+ * 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.watchface;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.wearable.view.WearableListView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.Wearable;
+
+/**
+ * The watch-side config activity for {@link DigitalWatchFaceService}, which allows for setting the
+ * background color.
+ */
+public class DigitalWatchFaceWearableConfigActivity extends Activity
+ implements WearableListView.ClickListener {
+ private static final String TAG = "DigitalWatchFaceConfig";
+
+ private GoogleApiClient mGoogleApiClient;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_digital_config);
+
+ WearableListView listView = (WearableListView) findViewById(R.id.color_picker);
+
+ listView.setHasFixedSize(true);
+ listView.setClickListener(this);
+
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
+ listView.setLayoutManager(layoutManager);
+
+ String[] colors = getResources().getStringArray(R.array.color_array);
+ listView.setAdapter(new ColorListAdapter(colors));
+
+ mGoogleApiClient = new GoogleApiClient.Builder(this)
+ .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
+ @Override
+ public void onConnected(Bundle connectionHint) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnected: " + connectionHint);
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnectionSuspended: " + cause);
+ }
+ }
+ })
+ .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onConnectionFailed: " + result);
+ }
+ }
+ })
+ .addApi(Wearable.API)
+ .build();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mGoogleApiClient.connect();
+ }
+
+ @Override
+ protected void onStop() {
+ if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.disconnect();
+ }
+ super.onStop();
+ }
+
+ @Override // WearableListView.ClickListener
+ public void onClick(WearableListView.ViewHolder viewHolder) {
+ ColorItemViewHolder colorItemViewHolder = (ColorItemViewHolder) viewHolder;
+ String colorName = colorItemViewHolder.mLabel.getText().toString();
+ updateConfigDataItem(Color.parseColor(colorName));
+ finish();
+ }
+
+ private void updateConfigDataItem(final int backgroundColor) {
+ DataMap configKeysToOverwrite = new DataMap();
+ configKeysToOverwrite.putInt(DigitalWatchFaceUtil.KEY_BACKGROUND_COLOR,
+ backgroundColor);
+ DigitalWatchFaceUtil.overwriteKeysInConfigDataMap(mGoogleApiClient, configKeysToOverwrite);
+ }
+
+ @Override // WearableListView.ClickListener
+ public void onTopEmptyRegionClick() { }
+
+ private class ColorListAdapter extends RecyclerView.Adapter<ColorItemViewHolder> {
+
+ private final String[] mColors;
+
+ public ColorListAdapter(String[] colors) {
+ mColors = colors;
+ }
+
+ @Override
+ public ColorItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View root = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.color_picker_item, parent, false /* attachToRoot */);
+ TextView label = (TextView) root.findViewById(R.id.label);
+ return new ColorItemViewHolder(root, label);
+ }
+
+ @Override
+ public void onBindViewHolder(ColorItemViewHolder holder, int position) {
+ String colorName = mColors[position];
+ holder.mLabel.setText(colorName);
+
+ RecyclerView.LayoutParams layoutParams =
+ new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ int colorPickerItemMargin = (int) getResources()
+ .getDimension(R.dimen.digital_config_color_picker_item_margin);
+ // Add margins to first and last item to make it possible for user to tap on them.
+ if (position == 0) {
+ layoutParams.setMargins(0, colorPickerItemMargin, 0, 0);
+ } else if (position == mColors.length - 1) {
+ layoutParams.setMargins(0, 0, 0, colorPickerItemMargin);
+ } else {
+ layoutParams.setMargins(0, 0, 0, 0);
+ }
+ holder.itemView.setLayoutParams(layoutParams);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mColors.length;
+ }
+ }
+
+ private static class ColorItemViewHolder extends WearableListView.ViewHolder {
+ private final TextView mLabel;
+
+ public ColorItemViewHolder(View root, TextView label) {
+ super(root);
+ this.mLabel = label;
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/Gles2ColoredTriangleList.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/Gles2ColoredTriangleList.java
new file mode 100644
index 0000000..2441c65
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/Gles2ColoredTriangleList.java
@@ -0,0 +1,278 @@
+/*
+ * 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.watchface;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+import android.opengl.GLES20;
+import android.opengl.GLU;
+import android.opengl.GLUtils;
+import android.util.Log;
+
+/**
+ * A list of triangles drawn in a single solid color using OpenGL ES 2.0.
+ */
+public class Gles2ColoredTriangleList {
+ private static final String TAG = "GlColoredTriangleList";
+
+ /** Whether to check for GL errors. This is slow, so not appropriate for production builds. */
+ private static final boolean CHECK_GL_ERRORS = false;
+
+ /** Number of coordinates per vertex in this array: one for each of x, y, and z. */
+ private static final int COORDS_PER_VERTEX = 3;
+
+ /** Number of bytes to store a float in GL. */
+ public static final int BYTES_PER_FLOAT = 4;
+
+ /** Number of bytes per vertex. */
+ private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT;
+
+ /** Triangles have three vertices. */
+ private static final int VERTICE_PER_TRIANGLE = 3;
+
+ /**
+ * Number of components in an OpenGL color. The components are:<ol>
+ * <li>red
+ * <li>green
+ * <li>blue
+ * <li>alpha
+ * </ol>
+ */
+ private static final int NUM_COLOR_COMPONENTS = 4;
+
+ /** Shaders to render this triangle list. */
+ private final Program mProgram;
+
+ /** The VBO containing the vertex coordinates. */
+ private final FloatBuffer mVertexBuffer;
+
+ /**
+ * Color of this triangle list represented as an array of floats in the range [0, 1] in RGBA
+ * order.
+ */
+ private final float mColor[];
+
+ /** Number of coordinates in this triangle list. */
+ private final int mNumCoords;
+
+ /**
+ * Creates a Gles2ColoredTriangleList to draw a triangle list with the given vertices and color.
+ *
+ * @param program program for drawing triangles
+ * @param triangleCoords flat array of 3D coordinates of triangle vertices in counterclockwise
+ * order
+ * @param color color in RGBA order, each in the range [0, 1]
+ */
+ public Gles2ColoredTriangleList(Program program, float[] triangleCoords, float[] color) {
+ if (triangleCoords.length % (VERTICE_PER_TRIANGLE * COORDS_PER_VERTEX) != 0) {
+ throw new IllegalArgumentException("must be multiple"
+ + " of VERTICE_PER_TRIANGLE * COORDS_PER_VERTEX coordinates");
+ }
+ if (color.length != NUM_COLOR_COMPONENTS) {
+ throw new IllegalArgumentException("wrong number of color components");
+ }
+ mProgram = program;
+ mColor = color;
+
+ ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT);
+
+ // Use the device hardware's native byte order.
+ bb.order(ByteOrder.nativeOrder());
+
+ // Create a FloatBuffer that wraps the ByteBuffer.
+ mVertexBuffer = bb.asFloatBuffer();
+
+ // Add the coordinates to the FloatBuffer.
+ mVertexBuffer.put(triangleCoords);
+
+ // Go back to the start for reading.
+ mVertexBuffer.position(0);
+
+ mNumCoords = triangleCoords.length / COORDS_PER_VERTEX;
+ }
+
+ /**
+ * Draws this triangle list using OpenGL commands.
+ *
+ * @param mvpMatrix the Model View Project matrix to draw this triangle list
+ */
+ public void draw(float[] mvpMatrix) {
+ // Pass the MVP matrix, vertex data, and color to OpenGL.
+ mProgram.bind(mvpMatrix, mVertexBuffer, mColor);
+
+ // Draw the triangle list.
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mNumCoords);
+ if (CHECK_GL_ERRORS) checkGlError("glDrawArrays");
+ }
+
+ /**
+ * Checks if any of the GL calls since the last time this method was called set an error
+ * condition. Call this method immediately after calling a GL method. Pass the name of the GL
+ * operation. For example:
+ *
+ * <pre>
+ * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
+ * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
+ *
+ * If the operation is not successful, the check throws an exception.
+ *
+ * <p><em>Note</em> This is quite slow so it's best to use it sparingly in production builds.
+ *
+ * @param glOperation name of the OpenGL call to check
+ */
+ private static void checkGlError(String glOperation) {
+ int error = GLES20.glGetError();
+ if (error != GLES20.GL_NO_ERROR) {
+ String errorString = GLU.gluErrorString(error);
+ if (errorString == null) {
+ errorString = GLUtils.getEGLErrorString(error);
+ }
+ String message = glOperation + " caused GL error 0x" + Integer.toHexString(error) +
+ ": " + errorString;
+ Log.e(TAG, message);
+ throw new RuntimeException(message);
+ }
+ }
+
+ /**
+ * Compiles an OpenGL shader.
+ *
+ * @param type {@link GLES20#GL_VERTEX_SHADER} or {@link GLES20#GL_FRAGMENT_SHADER}
+ * @param shaderCode string containing the shader source code
+ * @return ID for the shader
+ */
+ private static int loadShader(int type, String shaderCode){
+ // Create a vertex or fragment shader.
+ int shader = GLES20.glCreateShader(type);
+ if (CHECK_GL_ERRORS) checkGlError("glCreateShader");
+ if (shader == 0) {
+ throw new IllegalStateException("glCreateShader failed");
+ }
+
+ // Add the source code to the shader and compile it.
+ GLES20.glShaderSource(shader, shaderCode);
+ if (CHECK_GL_ERRORS) checkGlError("glShaderSource");
+ GLES20.glCompileShader(shader);
+ if (CHECK_GL_ERRORS) checkGlError("glCompileShader");
+
+ return shader;
+ }
+
+ /** OpenGL shaders for drawing solid colored triangle lists. */
+ public static class Program {
+ /** Trivial vertex shader that transforms the input vertex by the MVP matrix. */
+ private static final String VERTEX_SHADER_CODE = "" +
+ "uniform mat4 uMvpMatrix;\n" +
+ "attribute vec4 aPosition;\n" +
+ "void main() {\n" +
+ " gl_Position = uMvpMatrix * aPosition;\n" +
+ "}\n";
+
+ /** Trivial fragment shader that draws with a fixed color. */
+ private static final String FRAGMENT_SHADER_CODE = "" +
+ "precision mediump float;\n" +
+ "uniform vec4 uColor;\n" +
+ "void main() {\n" +
+ " gl_FragColor = uColor;\n" +
+ "}\n";
+
+ /** ID OpenGL uses to identify this program. */
+ private final int mProgramId;
+
+ /** Handle for uMvpMatrix uniform in vertex shader. */
+ private final int mMvpMatrixHandle;
+
+ /** Handle for aPosition attribute in vertex shader. */
+ private final int mPositionHandle;
+
+ /** Handle for uColor uniform in fragment shader. */
+ private final int mColorHandle;
+
+ /**
+ * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
+ * should be used for all triangle lists being drawn.
+ */
+ public Program() {
+ // Prepare shaders.
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER_CODE);
+ int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER_CODE);
+
+ // Create empty OpenGL Program.
+ mProgramId = GLES20.glCreateProgram();
+ if (CHECK_GL_ERRORS) checkGlError("glCreateProgram");
+ if (mProgramId == 0) {
+ throw new IllegalStateException("glCreateProgram failed");
+ }
+
+ // Add the shaders to the program.
+ GLES20.glAttachShader(mProgramId, vertexShader);
+ if (CHECK_GL_ERRORS) checkGlError("glAttachShader");
+ GLES20.glAttachShader(mProgramId, fragmentShader);
+ if (CHECK_GL_ERRORS) checkGlError("glAttachShader");
+
+ // Link the program so it can be executed.
+ GLES20.glLinkProgram(mProgramId);
+ if (CHECK_GL_ERRORS) checkGlError("glLinkProgram");
+
+ // Get a handle to the uMvpMatrix uniform in the vertex shader.
+ mMvpMatrixHandle = GLES20.glGetUniformLocation(mProgramId, "uMvpMatrix");
+ if (CHECK_GL_ERRORS) checkGlError("glGetUniformLocation");
+
+ // Get a handle to the vertex shader's aPosition attribute.
+ mPositionHandle = GLES20.glGetAttribLocation(mProgramId, "aPosition");
+ if (CHECK_GL_ERRORS) checkGlError("glGetAttribLocation");
+
+ // Enable vertex array (VBO).
+ GLES20.glEnableVertexAttribArray(mPositionHandle);
+ if (CHECK_GL_ERRORS) checkGlError("glEnableVertexAttribArray");
+
+ // Get a handle to fragment shader's uColor uniform.
+ mColorHandle = GLES20.glGetUniformLocation(mProgramId, "uColor");
+ if (CHECK_GL_ERRORS) checkGlError("glGetUniformLocation");
+ }
+
+ /**
+ * Tells OpenGL to use this program. Call this method before drawing a sequence of
+ * triangle lists.
+ */
+ public void use() {
+ GLES20.glUseProgram(mProgramId);
+ if (CHECK_GL_ERRORS) checkGlError("glUseProgram");
+ }
+
+ /** Sends the given MVP matrix, vertex data, and color to OpenGL. */
+ public void bind(float[] mvpMatrix, FloatBuffer vertexBuffer, float[] color) {
+ // Pass the MVP matrix to OpenGL.
+ GLES20.glUniformMatrix4fv(mMvpMatrixHandle, 1 /* count */, false /* transpose */,
+ mvpMatrix, 0 /* offset */);
+ if (CHECK_GL_ERRORS) checkGlError("glUniformMatrix4fv");
+
+ // Pass the VBO with the triangle list's vertices to OpenGL.
+ GLES20.glEnableVertexAttribArray(mPositionHandle);
+ if (CHECK_GL_ERRORS) checkGlError("glEnableVertexAttribArray");
+ GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
+ false /* normalized */, VERTEX_STRIDE, vertexBuffer);
+ if (CHECK_GL_ERRORS) checkGlError("glVertexAttribPointer");
+
+ // Pass the triangle list's color to OpenGL.
+ GLES20.glUniform4fv(mColorHandle, 1 /* count */, color, 0 /* offset */);
+ if (CHECK_GL_ERRORS) checkGlError("glUniform4fv");
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/SweepWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/SweepWatchFaceService.java
new file mode 100644
index 0000000..5fc738b
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/SweepWatchFaceService.java
@@ -0,0 +1,271 @@
+/*
+ * 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.watchface;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.wearable.watchface.CanvasWatchFaceService;
+import android.support.wearable.watchface.WatchFaceService;
+import android.support.wearable.watchface.WatchFaceStyle;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import java.util.TimeZone;
+
+/**
+ * Sample analog watch face with a sweep second hand. In ambient mode, the second hand isn't shown.
+ * On devices with low-bit ambient mode, the hands are drawn without anti-aliasing in ambient mode.
+ * The watch face is drawn with less contrast in mute mode.
+ *
+ * {@link AnalogWatchFaceService} is similar but has a ticking second hand.
+ */
+public class SweepWatchFaceService extends CanvasWatchFaceService {
+ private static final String TAG = "SweepWatchFaceService";
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends CanvasWatchFaceService.Engine {
+ Paint mHourPaint;
+ Paint mMinutePaint;
+ Paint mSecondPaint;
+ Paint mTickPaint;
+ boolean mMute;
+ Time mTime;
+
+ final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mTime.clear(intent.getStringExtra("time-zone"));
+ mTime.setToNow();
+ }
+ };
+ boolean mRegisteredTimeZoneReceiver = false;
+
+ /**
+ * Whether the display supports fewer bits for each color in ambient mode. When true, we
+ * disable anti-aliasing in ambient mode.
+ */
+ boolean mLowBitAmbient;
+
+ Bitmap mBackgroundBitmap;
+ Bitmap mBackgroundScaledBitmap;
+
+ @Override
+ public void onCreate(SurfaceHolder holder) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate");
+ }
+ super.onCreate(holder);
+
+ setWatchFaceStyle(new WatchFaceStyle.Builder(SweepWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setShowSystemUiTime(false)
+ .build());
+
+ Resources resources = SweepWatchFaceService.this.getResources();
+ Drawable backgroundDrawable = resources.getDrawable(R.drawable.bg);
+ mBackgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();
+
+ mHourPaint = new Paint();
+ mHourPaint.setARGB(255, 200, 200, 200);
+ mHourPaint.setStrokeWidth(5.f);
+ mHourPaint.setAntiAlias(true);
+ mHourPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mMinutePaint = new Paint();
+ mMinutePaint.setARGB(255, 200, 200, 200);
+ mMinutePaint.setStrokeWidth(3.f);
+ mMinutePaint.setAntiAlias(true);
+ mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mSecondPaint = new Paint();
+ mSecondPaint.setARGB(255, 255, 0, 0);
+ mSecondPaint.setStrokeWidth(2.f);
+ mSecondPaint.setAntiAlias(true);
+ mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mTickPaint = new Paint();
+ mTickPaint.setARGB(100, 255, 255, 255);
+ mTickPaint.setStrokeWidth(2.f);
+ mTickPaint.setAntiAlias(true);
+
+ mTime = new Time();
+ }
+
+ @Override
+ public void onPropertiesChanged(Bundle properties) {
+ super.onPropertiesChanged(properties);
+ mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
+ }
+ }
+
+ @Override
+ public void onTimeTick() {
+ super.onTimeTick();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
+ }
+ invalidate();
+ }
+
+ @Override
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ super.onAmbientModeChanged(inAmbientMode);
+ if (mLowBitAmbient) {
+ boolean antiAlias = !inAmbientMode;
+ mHourPaint.setAntiAlias(antiAlias);
+ mMinutePaint.setAntiAlias(antiAlias);
+ mSecondPaint.setAntiAlias(antiAlias);
+ mTickPaint.setAntiAlias(antiAlias);
+ }
+ invalidate();
+ }
+
+ @Override
+ public void onInterruptionFilterChanged(int interruptionFilter) {
+ super.onInterruptionFilterChanged(interruptionFilter);
+ boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
+ if (mMute != inMuteMode) {
+ mMute = inMuteMode;
+ mHourPaint.setAlpha(inMuteMode ? 100 : 255);
+ mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
+ mSecondPaint.setAlpha(inMuteMode ? 80 : 255);
+ invalidate();
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "onDraw");
+ }
+ long now = System.currentTimeMillis();
+ mTime.set(now);
+ int milliseconds = (int) (now % 1000);
+
+ int width = canvas.getWidth();
+ int height = canvas.getHeight();
+
+ // Draw the background, scaled to fit.
+ if (mBackgroundScaledBitmap == null
+ || mBackgroundScaledBitmap.getWidth() != width
+ || mBackgroundScaledBitmap.getHeight() != height) {
+ mBackgroundScaledBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
+ width, height, true /* filter */);
+ }
+ canvas.drawBitmap(mBackgroundScaledBitmap, 0, 0, null);
+
+ // Find the center. Ignore the window insets so that, on round watches with a
+ // "chin", the watch face is centered on the entire screen, not just the usable
+ // portion.
+ float centerX = width / 2f;
+ float centerY = height / 2f;
+
+ // Draw the ticks.
+ float innerTickRadius = centerX - 10;
+ float outerTickRadius = centerX;
+ for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
+ float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
+ float innerX = (float) Math.sin(tickRot) * innerTickRadius;
+ float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
+ float outerX = (float) Math.sin(tickRot) * outerTickRadius;
+ float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
+ canvas.drawLine(centerX + innerX, centerY + innerY,
+ centerX + outerX, centerY + outerY, mTickPaint);
+ }
+
+ float seconds = mTime.second + milliseconds / 1000f;
+ float secRot = seconds / 30f * (float) Math.PI;
+ int minutes = mTime.minute;
+ float minRot = minutes / 30f * (float) Math.PI;
+ float hrRot = ((mTime.hour + (minutes / 60f)) / 6f ) * (float) Math.PI;
+
+ float secLength = centerX - 20;
+ float minLength = centerX - 40;
+ float hrLength = centerX - 80;
+
+ if (!isInAmbientMode()) {
+ float secX = (float) Math.sin(secRot) * secLength;
+ float secY = (float) -Math.cos(secRot) * secLength;
+ canvas.drawLine(centerX, centerY, centerX + secX, centerY + secY, mSecondPaint);
+ }
+
+ float minX = (float) Math.sin(minRot) * minLength;
+ float minY = (float) -Math.cos(minRot) * minLength;
+ canvas.drawLine(centerX, centerY, centerX + minX, centerY + minY, mMinutePaint);
+
+ float hrX = (float) Math.sin(hrRot) * hrLength;
+ float hrY = (float) -Math.cos(hrRot) * hrLength;
+ canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY, mHourPaint);
+
+ // Draw every frame as long as we're visible.
+ if (isVisible()) {
+ invalidate();
+ }
+ }
+
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ super.onVisibilityChanged(visible);
+
+ if (visible) {
+ registerReceiver();
+
+ // Update time zone in case it changed while we weren't visible.
+ mTime.clear(TimeZone.getDefault().getID());
+ mTime.setToNow();
+
+ invalidate();
+ } else {
+ unregisterReceiver();
+ }
+ }
+
+ private void registerReceiver() {
+ if (mRegisteredTimeZoneReceiver) {
+ return;
+ }
+ mRegisteredTimeZoneReceiver = true;
+ IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+ SweepWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
+ }
+
+ private void unregisterReceiver() {
+ if (!mRegisteredTimeZoneReceiver) {
+ return;
+ }
+ mRegisteredTimeZoneReceiver = false;
+ SweepWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/TiltWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/TiltWatchFaceService.java
new file mode 100644
index 0000000..ce90d77
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/TiltWatchFaceService.java
@@ -0,0 +1,481 @@
+/*
+ * 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.watchface;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.opengl.GLES20;
+import android.opengl.Matrix;
+import android.support.wearable.watchface.Gles2WatchFaceService;
+import android.support.wearable.watchface.WatchFaceStyle;
+import android.text.format.Time;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.SurfaceHolder;
+
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Sample watch face using OpenGL. The watch face is rendered using
+ * {@link Gles2ColoredTriangleList}s. The camera moves around in interactive mode and stops moving
+ * when the watch enters ambient mode.
+ */
+public class TiltWatchFaceService extends Gles2WatchFaceService {
+
+ private static final String TAG = "TiltWatchFaceService";
+
+ /** Expected frame rate in interactive mode. */
+ private static final long FPS = 60;
+
+ /** How long each frame is displayed at expected frame rate. */
+ private static final long FRAME_PERIOD_MS = TimeUnit.SECONDS.toMillis(1) / FPS;
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends Gles2WatchFaceService.Engine {
+ /** Cycle time before the camera motion repeats. */
+ private static final long CYCLE_PERIOD_SECONDS = 5;
+
+ /** Number of camera angles to precompute. */
+ private final int mNumCameraAngles = (int) (CYCLE_PERIOD_SECONDS * FPS);
+
+ /** Projection transformation matrix. Converts from 3D to 2D. */
+ private final float[] mProjectionMatrix = new float[16];
+
+ /**
+ * View transformation matrices to use in interactive mode. Converts from world to camera-
+ * relative coordinates. One matrix per camera position.
+ */
+ private final float[][] mViewMatrices = new float[mNumCameraAngles][16];
+
+ /** The view transformation matrix to use in ambient mode */
+ private final float[] mAmbientViewMatrix = new float[16];
+
+ /**
+ * Model transformation matrices. Converts from model-relative coordinates to world
+ * coordinates. One matrix per degree of rotation.
+ */
+ private final float[][] mModelMatrices = new float[360][16];
+
+ /**
+ * Products of {@link #mViewMatrices} and {@link #mProjectionMatrix}. One matrix per camera
+ * position.
+ */
+ private final float[][] mVpMatrices = new float[mNumCameraAngles][16];
+
+ /** The product of {@link #mAmbientViewMatrix} and {@link #mProjectionMatrix} */
+ private final float[] mAmbientVpMatrix = new float[16];
+
+ /**
+ * Product of {@link #mModelMatrices}, {@link #mViewMatrices}, and
+ * {@link #mProjectionMatrix}.
+ */
+ private final float[] mMvpMatrix = new float[16];
+
+ /** Triangles for the 4 major ticks. These are grouped together to speed up rendering. */
+ private Gles2ColoredTriangleList mMajorTickTriangles;
+
+ /** Triangles for the 8 minor ticks. These are grouped together to speed up rendering. */
+ private Gles2ColoredTriangleList mMinorTickTriangles;
+
+ /** Triangle for the second hand. */
+ private Gles2ColoredTriangleList mSecondHandTriangle;
+
+ /** Triangle for the minute hand. */
+ private Gles2ColoredTriangleList mMinuteHandTriangle;
+
+ /** Triangle for the hour hand. */
+ private Gles2ColoredTriangleList mHourHandTriangle;
+
+ private Time mTime = new Time();
+
+ /** Whether we've registered {@link #mTimeZoneReceiver}. */
+ private boolean mRegisteredTimeZoneReceiver;
+
+ private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mTime.clear(intent.getStringExtra("time-zone"));
+ mTime.setToNow();
+ }
+ };
+
+ @Override
+ public void onCreate(SurfaceHolder surfaceHolder) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate");
+ }
+ super.onCreate(surfaceHolder);
+ setWatchFaceStyle(new WatchFaceStyle.Builder(TiltWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setStatusBarGravity(Gravity.RIGHT | Gravity.TOP)
+ .setHotwordIndicatorGravity(Gravity.LEFT | Gravity.TOP)
+ .setShowSystemUiTime(false)
+ .build());
+ }
+
+ @Override
+ public void onGlContextCreated() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onGlContextCreated");
+ }
+ super.onGlContextCreated();
+
+ // Create program for drawing triangles.
+ Gles2ColoredTriangleList.Program triangleProgram =
+ new Gles2ColoredTriangleList.Program();
+
+ // We only draw triangles which all use the same program so we don't need to switch
+ // programs mid-frame. This means we can tell OpenGL to use this program only once
+ // rather than having to do so for each frame. This makes OpenGL draw faster.
+ triangleProgram.use();
+
+ // Create triangles for the ticks.
+ mMajorTickTriangles = createMajorTicks(triangleProgram);
+ mMinorTickTriangles = createMinorTicks(triangleProgram);
+
+ // Create triangles for the hands.
+ mSecondHandTriangle = createHand(
+ triangleProgram,
+ 0.02f /* width */,
+ 1.0f /* height */,
+ new float[]{
+ 1.0f /* red */,
+ 0.0f /* green */,
+ 0.0f /* blue */,
+ 1.0f /* alpha */
+ }
+ );
+ mMinuteHandTriangle = createHand(
+ triangleProgram,
+ 0.06f /* width */,
+ 0.8f /* height */,
+ new float[]{
+ 0.7f /* red */,
+ 0.7f /* green */,
+ 0.7f /* blue */,
+ 1.0f /* alpha */
+ }
+ );
+ mHourHandTriangle = createHand(
+ triangleProgram,
+ 0.1f /* width */,
+ 0.5f /* height */,
+ new float[]{
+ 0.9f /* red */,
+ 0.9f /* green */,
+ 0.9f /* blue */,
+ 1.0f /* alpha */
+ }
+ );
+
+ // Precompute the clock angles.
+ for (int i = 0; i < mModelMatrices.length; ++i) {
+ Matrix.setRotateM(mModelMatrices[i], 0, i, 0, 0, 1);
+ }
+
+ // Precompute the camera angles.
+ for (int i = 0; i < mNumCameraAngles; ++i) {
+ // Set the camera position (View matrix). When active, move the eye around to show
+ // off that this is 3D.
+ final float cameraAngle = (float) (((float) i) / mNumCameraAngles * 2 * Math.PI);
+ final float eyeX = (float) Math.cos(cameraAngle);
+ final float eyeY = (float) Math.sin(cameraAngle);
+ Matrix.setLookAtM(mViewMatrices[i],
+ 0, // dest index
+ eyeX, eyeY, -3, // eye
+ 0, 0, 0, // center
+ 0, 1, 0); // up vector
+ }
+
+ Matrix.setLookAtM(mAmbientViewMatrix,
+ 0, // dest index
+ 0, 0, -3, // eye
+ 0, 0, 0, // center
+ 0, 1, 0); // up vector
+ }
+
+ @Override
+ public void onGlSurfaceCreated(int width, int height) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onGlSurfaceCreated");
+ }
+ super.onGlSurfaceCreated(width, height);
+
+ // Adjust the viewport based on geometry changes such as screen rotation.
+ GLES20.glViewport(0, 0, width, height);
+
+ // Update the projection matrix based on the new aspect ratio.
+ final float aspectRatio = (float) width / height;
+ Matrix.frustumM(mProjectionMatrix,
+ 0 /* offset */,
+ -aspectRatio /* left */,
+ aspectRatio /* right */,
+ -1 /* bottom */,
+ 1 /* top */,
+ 2 /* near */,
+ 7 /* far */);
+
+ // Precompute the products of Projection and View matrices for each camera angle.
+ for (int i = 0; i < mNumCameraAngles; ++i) {
+ Matrix.multiplyMM(mVpMatrices[i], 0, mProjectionMatrix, 0, mViewMatrices[i], 0);
+ }
+
+ Matrix.multiplyMM(mAmbientVpMatrix, 0, mProjectionMatrix, 0, mAmbientViewMatrix, 0);
+ }
+
+ /**
+ * Creates a triangle for a hand on the watch face.
+ *
+ * @param program program for drawing triangles
+ * @param width width of base of triangle
+ * @param length length of triangle
+ * @param color color in RGBA order, each in the range [0, 1]
+ */
+ private Gles2ColoredTriangleList createHand(Gles2ColoredTriangleList.Program program,
+ float width, float length, float[] color) {
+ // Create the data for the VBO.
+ float[] triangleCoords = new float[]{
+ // in counterclockwise order:
+ 0, length, 0, // top
+ -width / 2, 0, 0, // bottom left
+ width / 2, 0, 0 // bottom right
+ };
+ return new Gles2ColoredTriangleList(program, triangleCoords, color);
+ }
+
+ /**
+ * Creates a triangle list for the major ticks on the watch face.
+ *
+ * @param program program for drawing triangles
+ */
+ private Gles2ColoredTriangleList createMajorTicks(
+ Gles2ColoredTriangleList.Program program) {
+ // Create the data for the VBO.
+ float[] trianglesCoords = new float[9 * 4];
+ for (int i = 0; i < 4; i++) {
+ float[] triangleCoords = getMajorTickTriangleCoords(i);
+ System.arraycopy(triangleCoords, 0, trianglesCoords, i * 9, triangleCoords.length);
+ }
+
+ return new Gles2ColoredTriangleList(program, trianglesCoords,
+ new float[]{
+ 1.0f /* red */,
+ 1.0f /* green */,
+ 1.0f /* blue */,
+ 1.0f /* alpha */
+ }
+ );
+ }
+
+ /**
+ * Creates a triangle list for the minor ticks on the watch face.
+ *
+ * @param program program for drawing triangles
+ */
+ private Gles2ColoredTriangleList createMinorTicks(
+ Gles2ColoredTriangleList.Program program) {
+ // Create the data for the VBO.
+ float[] trianglesCoords = new float[9 * (12 - 4)];
+ int index = 0;
+ for (int i = 0; i < 12; i++) {
+ if (i % 3 == 0) {
+ // This is where a major tick goes, so skip it.
+ continue;
+ }
+ float[] triangleCoords = getMinorTickTriangleCoords(i);
+ System.arraycopy(triangleCoords, 0, trianglesCoords, index, triangleCoords.length);
+ index += 9;
+ }
+
+ return new Gles2ColoredTriangleList(program, trianglesCoords,
+ new float[]{
+ 0.5f /* red */,
+ 0.5f /* green */,
+ 0.5f /* blue */,
+ 1.0f /* alpha */
+ }
+ );
+ }
+
+ private float[] getMajorTickTriangleCoords(int index) {
+ return getTickTriangleCoords(0.03f /* width */, 0.09f /* length */,
+ index * 360 / 4 /* angleDegrees */);
+ }
+
+ private float[] getMinorTickTriangleCoords(int index) {
+ return getTickTriangleCoords(0.02f /* width */, 0.06f /* length */,
+ index * 360 / 12 /* angleDegrees */);
+ }
+
+ private float[] getTickTriangleCoords(float width, float length, int angleDegrees) {
+ // Create the data for the VBO.
+ float[] coords = new float[]{
+ // in counterclockwise order:
+ 0, 1, 0, // top
+ width / 2, length + 1, 0, // bottom left
+ -width / 2, length + 1, 0 // bottom right
+ };
+
+ rotateCoords(coords, angleDegrees);
+ return coords;
+ }
+
+ /**
+ * Destructively rotates the given coordinates in the XY plane about the origin by the given
+ * angle.
+ *
+ * @param coords flattened 3D coordinates
+ * @param angleDegrees angle in degrees clockwise when viewed from negative infinity on the
+ * Z axis
+ */
+ private void rotateCoords(float[] coords, int angleDegrees) {
+ double angleRadians = Math.toRadians(angleDegrees);
+ double cos = Math.cos(angleRadians);
+ double sin = Math.sin(angleRadians);
+ for (int i = 0; i < coords.length; i += 3) {
+ float x = coords[i];
+ float y = coords[i + 1];
+ coords[i] = (float) (cos * x - sin * y);
+ coords[i + 1] = (float) (sin * x + cos * y);
+ }
+ }
+
+ @Override
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
+ }
+ super.onAmbientModeChanged(inAmbientMode);
+ invalidate();
+ }
+
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onVisibilityChanged: " + visible);
+ }
+ super.onVisibilityChanged(visible);
+ if (visible) {
+ registerReceiver();
+
+ // Update time zone in case it changed while we were detached.
+ mTime.clear(TimeZone.getDefault().getID());
+ mTime.setToNow();
+
+ invalidate();
+ } else {
+ unregisterReceiver();
+ }
+ }
+
+ private void registerReceiver() {
+ if (mRegisteredTimeZoneReceiver) {
+ return;
+ }
+ mRegisteredTimeZoneReceiver = true;
+ IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+ TiltWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
+ }
+
+ private void unregisterReceiver() {
+ if (!mRegisteredTimeZoneReceiver) {
+ return;
+ }
+ mRegisteredTimeZoneReceiver = false;
+ TiltWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
+ }
+
+ @Override
+ public void onTimeTick() {
+ super.onTimeTick();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
+ }
+ invalidate();
+ }
+
+ @Override
+ public void onDraw() {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "onDraw");
+ }
+ super.onDraw();
+ final float[] vpMatrix;
+
+ // Draw background color and select the appropriate view projection matrix. The
+ // background should always be black in ambient mode. The view projection matrix used is
+ // overhead in ambient. In interactive mode, it's tilted depending on the current time.
+ if (isInAmbientMode()) {
+ GLES20.glClearColor(0, 0, 0, 1);
+ vpMatrix = mAmbientVpMatrix;
+ } else {
+ GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1);
+ final int cameraIndex =
+ (int) ((System.currentTimeMillis() / FRAME_PERIOD_MS) % mNumCameraAngles);
+ vpMatrix = mVpMatrices[cameraIndex];
+ }
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+
+ // Compute angle indices for the three hands.
+ mTime.setToNow();
+ final int secIndex = mTime.second * 360 / 60;
+ final int minIndex = mTime.minute * 360 / 60;
+ final int hoursIndex = (mTime.hour % 12) * 360 / 12 + mTime.minute * 360 / 60 / 12;
+
+ // Draw triangles from back to front. Don't draw the second hand in ambient mode.
+ {
+ // Combine the model matrix with the projection and camera view.
+ Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[hoursIndex], 0);
+
+ // Draw the triangle.
+ mHourHandTriangle.draw(mMvpMatrix);
+ }
+ {
+ // Combine the model matrix with the projection and camera view.
+ Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[minIndex], 0);
+
+ // Draw the triangle.
+ mMinuteHandTriangle.draw(mMvpMatrix);
+ }
+ if (!isInAmbientMode()) {
+ // Combine the model matrix with the projection and camera view.
+ Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[secIndex], 0);
+
+ // Draw the triangle.
+ mSecondHandTriangle.draw(mMvpMatrix);
+ }
+ {
+ // Draw the major and minor ticks.
+ mMajorTickTriangles.draw(vpMatrix);
+ mMinorTickTriangles.draw(vpMatrix);
+ }
+
+ // Draw every frame as long as we're visible.
+ if (isVisible()) {
+ invalidate();
+ }
+ }
+ }
+}
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/bg.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/bg.png
new file mode 100644
index 0000000..5199af2
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/bg.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/ic_launcher.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..589f229
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_analog.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_analog.png
new file mode 100644
index 0000000..ed6960d
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_analog.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_calendar.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_calendar.png
new file mode 100644
index 0000000..928aa1f
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_calendar.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_card_bounds.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_card_bounds.png
new file mode 100644
index 0000000..f87b6c5
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_card_bounds.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_digital.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_digital.png
new file mode 100644
index 0000000..4853a64
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_digital.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_tilt.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_tilt.png
new file mode 100644
index 0000000..aab5f18
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_tilt.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-mdpi/ic_launcher.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..77dd571
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..fe34ebe
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ab80bcd
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_digital_config.xml b/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_digital_config.xml
new file mode 100644
index 0000000..eef7aae
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_digital_config.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="#ffffff">
+ <TextView
+ android:id="@+id/header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dp"
+ android:layout_marginTop="14dp"
+ android:layout_gravity="center_horizontal"
+ android:textSize="22sp"
+ android:textColor="#a2a2a2"
+ android:text="@string/digital_background_color"
+ android:fontFamily="sans-serif-condensed-light"/>
+ <android.support.wearable.view.WearableListView
+ android:id="@+id/color_picker"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"/>
+</LinearLayout>
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/layout/color_picker_item.xml b/wearable/wear/WatchFace/Wearable/src/main/res/layout/color_picker_item.xml
new file mode 100644
index 0000000..e7edb39
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/layout/color_picker_item.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/label"
+ android:fontFamily="sans-serif-condensed-light"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:paddingTop="20dp"
+ android:paddingBottom="20dp"
+ android:textColor="#000000"
+ android:textSize="18sp"/>
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/values/color.xml b/wearable/wear/WatchFace/Wearable/src/main/res/values/color.xml
new file mode 100644
index 0000000..2ba1355
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/values/color.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <color name="digital_am_pm">#aaaaa0</color>
+ <color name="digital_colons">#aaaaa0</color>
+</resources>
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/values/dimens.xml b/wearable/wear/WatchFace/Wearable/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..b9c4c86
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/values/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <dimen name="digital_text_size">40dp</dimen>
+ <dimen name="digital_text_size_round">45dp</dimen>
+ <dimen name="digital_am_pm_size">25dp</dimen>
+ <dimen name="digital_am_pm_size_round">30dp</dimen>
+ <dimen name="digital_x_offset">15dp</dimen>
+ <dimen name="digital_x_offset_round">25dp</dimen>
+ <dimen name="digital_y_offset">90dp</dimen>
+ <dimen name="digital_config_color_picker_item_margin">32dp</dimen>
+</resources>
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml b/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e54591f
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <string name="app_name">WatchFace</string>
+ <string name="tilt_name">Sample Tilt</string>
+ <string name="analog_name">Sample Analog</string>
+ <string name="sweep_name">Sample Sweep</string>
+ <string name="card_bounds_name">Sample Card Bounds</string>
+ <string name="digital_name">Sample Digital</string>
+ <string name="digital_background_color">Background color</string>
+ <string name="digital_config_name">Digital watch face configuration</string>
+ <string name="digital_am">AM</string>
+ <string name="digital_pm">PM</string>
+ <string name="calendar_name">Sample Calendar</string>
+ <plurals name="calendar_meetings">
+ <item quantity="one"><br><br><br>You have <b>%1$d</b> meeting in the next 24 hours.</item>
+ <item quantity="other"><br><br><br>You have <b>%1$d</b> meetings in the next 24 hours.</item>
+ </plurals>
+
+ <!-- TODO: this should be shared (needs covering all the samples with Gradle build model) -->
+ <string name="color_black">Black</string>
+ <string name="color_blue">Blue</string>
+ <string name="color_gray">Gray</string>
+ <string name="color_green">Green</string>
+ <string name="color_navy">Navy</string>
+ <string name="color_red">Red</string>
+ <string name="color_white">White</string>
+
+ <string-array name="color_array">
+ <item>@string/color_black</item>
+ <item>@string/color_blue</item>
+ <item>@string/color_gray</item>
+ <item>@string/color_green</item>
+ <item>@string/color_navy</item>
+ <item>@string/color_red</item>
+ <item>@string/color_white</item>
+ </string-array>
+</resources>
diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/xml/watch_face.xml b/wearable/wear/WatchFace/Wearable/src/main/res/xml/watch_face.xml
new file mode 100644
index 0000000..aa2e343
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/xml/watch_face.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+-->
+<wallpaper xmlns:android="http://schemas.android.com/apk/res/android" />
diff --git a/wearable/wear/WatchFace/build.gradle b/wearable/wear/WatchFace/build.gradle
new file mode 100644
index 0000000..be1fa82
--- /dev/null
+++ b/wearable/wear/WatchFace/build.gradle
@@ -0,0 +1,14 @@
+
+
+
+
+// BEGIN_EXCLUDE
+import com.example.android.samples.build.SampleGenPlugin
+apply plugin: SampleGenPlugin
+
+samplegen {
+ pathToBuild "../../../../../build"
+ pathToSamplesCommon "../../../common"
+}
+apply from: "../../../../../build/build.gradle"
+// END_EXCLUDE
diff --git a/wearable/wear/WatchFace/buildSrc/build.gradle b/wearable/wear/WatchFace/buildSrc/build.gradle
new file mode 100644
index 0000000..e344a8c
--- /dev/null
+++ b/wearable/wear/WatchFace/buildSrc/build.gradle
@@ -0,0 +1,18 @@
+
+
+
+repositories {
+ mavenCentral()
+}
+dependencies {
+ compile 'org.freemarker:freemarker:2.3.20'
+}
+
+sourceSets {
+ main {
+ groovy {
+ srcDir new File(rootDir, "../../../../../../build/buildSrc/src/main/groovy")
+ }
+ }
+}
+
diff --git a/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.jar b/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
--- /dev/null
+++ b/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..d7f03cf
--- /dev/null
+++ b/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties
@@ -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-bin.zip
diff --git a/wearable/wear/WatchFace/gradlew b/wearable/wear/WatchFace/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/wearable/wear/WatchFace/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/wearable/wear/WatchFace/gradlew.bat b/wearable/wear/WatchFace/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/wearable/wear/WatchFace/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/wearable/wear/WatchFace/screenshots/analog_and_sweep_face.png b/wearable/wear/WatchFace/screenshots/analog_and_sweep_face.png
new file mode 100644
index 0000000..df0820e
--- /dev/null
+++ b/wearable/wear/WatchFace/screenshots/analog_and_sweep_face.png
Binary files differ
diff --git a/wearable/wear/WatchFace/screenshots/calendar_face.png b/wearable/wear/WatchFace/screenshots/calendar_face.png
new file mode 100644
index 0000000..94214c0
--- /dev/null
+++ b/wearable/wear/WatchFace/screenshots/calendar_face.png
Binary files differ
diff --git a/wearable/wear/WatchFace/screenshots/card_bounds_face.png b/wearable/wear/WatchFace/screenshots/card_bounds_face.png
new file mode 100644
index 0000000..0ee827e
--- /dev/null
+++ b/wearable/wear/WatchFace/screenshots/card_bounds_face.png
Binary files differ
diff --git a/wearable/wear/WatchFace/screenshots/card_bounds_face2.png b/wearable/wear/WatchFace/screenshots/card_bounds_face2.png
new file mode 100644
index 0000000..c9c107e
--- /dev/null
+++ b/wearable/wear/WatchFace/screenshots/card_bounds_face2.png
Binary files differ
diff --git a/wearable/wear/WatchFace/screenshots/digital_face.png b/wearable/wear/WatchFace/screenshots/digital_face.png
new file mode 100644
index 0000000..f2d1723
--- /dev/null
+++ b/wearable/wear/WatchFace/screenshots/digital_face.png
Binary files differ
diff --git a/wearable/wear/WatchFace/screenshots/digital_phoneside_config.png b/wearable/wear/WatchFace/screenshots/digital_phoneside_config.png
new file mode 100644
index 0000000..f614d10
--- /dev/null
+++ b/wearable/wear/WatchFace/screenshots/digital_phoneside_config.png
Binary files differ
diff --git a/wearable/wear/WatchFace/screenshots/digital_watchside_config.png b/wearable/wear/WatchFace/screenshots/digital_watchside_config.png
new file mode 100644
index 0000000..d2813c4
--- /dev/null
+++ b/wearable/wear/WatchFace/screenshots/digital_watchside_config.png
Binary files differ
diff --git a/wearable/wear/WatchFace/screenshots/tilt_face.png b/wearable/wear/WatchFace/screenshots/tilt_face.png
new file mode 100644
index 0000000..69601a7
--- /dev/null
+++ b/wearable/wear/WatchFace/screenshots/tilt_face.png
Binary files differ
diff --git a/wearable/wear/WatchFace/settings.gradle b/wearable/wear/WatchFace/settings.gradle
new file mode 100644
index 0000000..19d00ac
--- /dev/null
+++ b/wearable/wear/WatchFace/settings.gradle
@@ -0,0 +1 @@
+include ':Application', ':Wearable'
diff --git a/wearable/wear/WatchFace/template-params.xml b/wearable/wear/WatchFace/template-params.xml
new file mode 100644
index 0000000..0e6a7b3
--- /dev/null
+++ b/wearable/wear/WatchFace/template-params.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 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.
+-->
+
+
+
+<sample>
+ <name>WatchFace</name>
+ <group>Wearable</group>
+ <package>com.example.android.wearable.watchface</package>
+
+
+
+ <!-- change minSdk if needed-->
+ <minSdk>18</minSdk>
+
+ <dependency>com.google.android.support:wearable:1.1.+</dependency>
+
+
+ <strings>
+ <intro>
+ <![CDATA[
+This sample demonstrates how to create watch faces for android wear and includes a phone app
+and a wearable app. The wearable app has a variety of watch faces including analog, digital,
+opengl, calendar, etc. It also includes a watch-side configuration example. The phone app
+includes a phone-side configuration example.
+ ]]>
+ </intro>
+ </strings>
+
+ <template src="base"/>
+ <template src="Wear"/>
+
+</sample>