diff --git a/samples/browseable/AgendaData/Wearable/AndroidManifest.xml b/samples/browseable/AgendaData/Wearable/AndroidManifest.xml
index dcab6227a..e6dbab705 100644
--- a/samples/browseable/AgendaData/Wearable/AndroidManifest.xml
+++ b/samples/browseable/AgendaData/Wearable/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.agendadata" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_app_restriction_enforcer.xml b/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_app_restriction_enforcer.xml
index 56c9133ab..b68398974 100644
--- a/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_app_restriction_enforcer.xml
+++ b/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_app_restriction_enforcer.xml
@@ -120,6 +120,7 @@
@@ -147,6 +148,7 @@
diff --git a/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/AppRestrictionEnforcerFragment.java b/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/AppRestrictionEnforcerFragment.java
index e30a9a469..361c4ac33 100644
--- a/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/AppRestrictionEnforcerFragment.java
+++ b/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/AppRestrictionEnforcerFragment.java
@@ -22,6 +22,7 @@ import android.content.Context;
import android.content.RestrictionEntry;
import android.content.RestrictionsManager;
import android.content.SharedPreferences;
+import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
@@ -105,6 +106,8 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
private static final String DELIMETER = ",";
private static final String SEPARATOR = ":";
+ private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
+
/**
* Current status of the restrictions.
*/
@@ -138,6 +141,15 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
mEditProfileAge = (EditText) view.findViewById(R.id.profile_age);
mLayoutItems = (LinearLayout) view.findViewById(R.id.items);
view.findViewById(R.id.item_add).setOnClickListener(this);
+ View bundleLayout = view.findViewById(R.id.bundle_layout);
+ View bundleArrayLayout = view.findViewById(R.id.bundle_array_layout);
+ if (BUNDLE_SUPPORTED) {
+ bundleLayout.setVisibility(View.VISIBLE);
+ bundleArrayLayout.setVisibility(View.VISIBLE);
+ } else {
+ bundleLayout.setVisibility(View.GONE);
+ bundleArrayLayout.setVisibility(View.GONE);
+ }
}
@Override
@@ -280,7 +292,7 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
TextUtils.join(DELIMETER,
restriction.getAllSelectedStrings())),
DELIMETER));
- } else if (RESTRICTION_KEY_PROFILE.equals(key)) {
+ } else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_PROFILE.equals(key)) {
String name = null;
int age = 0;
for (RestrictionEntry entry : restriction.getRestrictions()) {
@@ -294,7 +306,7 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
name = prefs.getString(RESTRICTION_KEY_PROFILE_NAME, name);
age = prefs.getInt(RESTRICTION_KEY_PROFILE_AGE, age);
updateProfile(name, age);
- } else if (RESTRICTION_KEY_ITEMS.equals(key)) {
+ } else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_ITEMS.equals(key)) {
String itemsString = prefs.getString(RESTRICTION_KEY_ITEMS, "");
HashMap items = new HashMap<>();
for (String itemString : TextUtils.split(itemsString, DELIMETER)) {
@@ -351,6 +363,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
}
private void updateProfile(String name, int age) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
Bundle profile = new Bundle();
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
@@ -364,6 +379,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
}
private void updateItems(Context context, Map items) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
LayoutInflater inflater = LayoutInflater.from(context);
mLayoutItems.removeAllViews();
@@ -500,6 +518,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
* @param age The value to be set for the "age" field.
*/
private void saveProfile(Activity activity, String name, int age) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
Bundle profile = new Bundle();
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
@@ -515,6 +536,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
* @param items The values.
*/
private void saveItems(Activity activity, Map items) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
saveRestrictions(activity);
StringBuilder builder = new StringBuilder();
diff --git a/samples/browseable/AppRestrictionSchema/res/layout/fragment_app_restriction_schema.xml b/samples/browseable/AppRestrictionSchema/res/layout/fragment_app_restriction_schema.xml
index 85708691b..02d83e615 100644
--- a/samples/browseable/AppRestrictionSchema/res/layout/fragment_app_restriction_schema.xml
+++ b/samples/browseable/AppRestrictionSchema/res/layout/fragment_app_restriction_schema.xml
@@ -59,7 +59,7 @@ limitations under the License.
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="@string/your_rank"/>
-
+
-
+ = 23;
+
// Message to show when the button is clicked (String restriction)
private String mMessage;
@@ -82,9 +85,22 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
mTextNumber = (TextView) view.findViewById(R.id.your_number);
mTextRank = (TextView) view.findViewById(R.id.your_rank);
mTextApprovals = (TextView) view.findViewById(R.id.approvals_you_have);
+ View bundleSeparator = view.findViewById(R.id.bundle_separator);
mTextProfile = (TextView) view.findViewById(R.id.your_profile);
+ View bundleArraySeparator = view.findViewById(R.id.bundle_array_separator);
mTextItems = (TextView) view.findViewById(R.id.your_items);
mButtonSayHello.setOnClickListener(this);
+ if (BUNDLE_SUPPORTED) {
+ bundleSeparator.setVisibility(View.VISIBLE);
+ mTextProfile.setVisibility(View.VISIBLE);
+ bundleArraySeparator.setVisibility(View.VISIBLE);
+ mTextItems.setVisibility(View.VISIBLE);
+ } else {
+ bundleSeparator.setVisibility(View.GONE);
+ mTextProfile.setVisibility(View.GONE);
+ bundleArraySeparator.setVisibility(View.GONE);
+ mTextItems.setVisibility(View.GONE);
+ }
}
@Override
@@ -178,6 +194,9 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
}
private void updateProfile(RestrictionEntry entry, Bundle restrictions) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
String name = null;
int age = 0;
if (restrictions == null || !restrictions.containsKey(KEY_PROFILE)) {
@@ -201,6 +220,9 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
}
private void updateItems(RestrictionEntry entry, Bundle restrictions) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
StringBuilder builder = new StringBuilder();
if (restrictions != null) {
Parcelable[] parcelables = restrictions.getParcelableArray(KEY_ITEMS);
diff --git a/samples/browseable/AsymmetricFingerprintDialog/_index.jd b/samples/browseable/AsymmetricFingerprintDialog/_index.jd
index 858ebaa9b..4e5987e7e 100644
--- a/samples/browseable/AsymmetricFingerprintDialog/_index.jd
+++ b/samples/browseable/AsymmetricFingerprintDialog/_index.jd
@@ -1,5 +1,5 @@
-page.tags="Asymmetric Fingerprint Dialog Sample"
+page.tags="AsymmetricFingerprintDialog"
sample.group=Security
@jd:body
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values/base-strings.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values/base-strings.xml
index b40ebdbf5..72dabc1d2 100644
--- a/samples/browseable/AsymmetricFingerprintDialog/res/values/base-strings.xml
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values/base-strings.xml
@@ -16,7 +16,7 @@
-->
- Asymmetric Fingerprint Dialog Sample
+ AsymmetricFingerprintDialog
Security -> Fingerprint' to set up a fingerprint",
- Toast.LENGTH_LONG).show();
- purchaseButton.setEnabled(false);
- return;
- }
- if (!mFingerprintManager.hasEnrolledFingerprints()) {
- purchaseButton.setEnabled(false);
- // This happens when no fingerprints are registered.
- Toast.makeText(this,
- "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
- Toast.LENGTH_LONG).show();
- return;
- }
- createKeyPair();
- purchaseButton.setEnabled(true);
- purchaseButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- findViewById(R.id.confirmation_message).setVisibility(View.GONE);
- findViewById(R.id.encrypted_message).setVisibility(View.GONE);
-
- // Set up the crypto object for later. The object will be authenticated by use
- // of the fingerprint.
- if (initSignature()) {
-
- // Show the fingerprint dialog. The user has the option to use the fingerprint with
- // crypto, or you can fall back to using a server-side verified password.
- mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature));
- boolean useFingerprintPreference = mSharedPreferences
- .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
- true);
- if (useFingerprintPreference) {
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
- } else {
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
- }
- mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
- } else {
- // This happens if the lock screen has been disabled or or a fingerprint got
- // enrolled. Thus show the dialog to authenticate with their password first
- // and ask the user if they want to authenticate with fingerprints in the
- // future
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
- mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
- }
- }
- });
+ setContentView(R.layout.activity_main);
+ Button purchaseButton = (Button) findViewById(R.id.purchase_button);
+ if (!mKeyguardManager.isKeyguardSecure()) {
+ // Show a message that the user hasn't set up a fingerprint or lock screen.
+ Toast.makeText(this,
+ "Secure lock screen hasn't set up.\n"
+ + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
+ Toast.LENGTH_LONG).show();
+ purchaseButton.setEnabled(false);
+ return;
}
+ //noinspection ResourceType
+ if (!mFingerprintManager.hasEnrolledFingerprints()) {
+ purchaseButton.setEnabled(false);
+ // This happens when no fingerprints are registered.
+ Toast.makeText(this,
+ "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ createKeyPair();
+ purchaseButton.setEnabled(true);
+ purchaseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ findViewById(R.id.confirmation_message).setVisibility(View.GONE);
+ findViewById(R.id.encrypted_message).setVisibility(View.GONE);
+
+ // Set up the crypto object for later. The object will be authenticated by use
+ // of the fingerprint.
+ if (initSignature()) {
+
+ // Show the fingerprint dialog. The user has the option to use the fingerprint with
+ // crypto, or you can fall back to using a server-side verified password.
+ mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature));
+ boolean useFingerprintPreference = mSharedPreferences
+ .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
+ true);
+ if (useFingerprintPreference) {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
+ } else {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
+ }
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ } else {
+ // This happens if the lock screen has been disabled or or a fingerprint got
+ // enrolled. Thus show the dialog to authenticate with their password first
+ // and ask the user if they want to authenticate with fingerprints in the
+ // future
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ }
+ }
+ });
}
/**
diff --git a/samples/browseable/BasicAndroidKeyStore/src/com.example.android.basicandroidkeystore/BasicAndroidKeyStoreFragment.java b/samples/browseable/BasicAndroidKeyStore/src/com.example.android.basicandroidkeystore/BasicAndroidKeyStoreFragment.java
index 12873e847..e6244bfb6 100644
--- a/samples/browseable/BasicAndroidKeyStore/src/com.example.android.basicandroidkeystore/BasicAndroidKeyStoreFragment.java
+++ b/samples/browseable/BasicAndroidKeyStore/src/com.example.android.basicandroidkeystore/BasicAndroidKeyStoreFragment.java
@@ -156,7 +156,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment {
// generated.
Calendar start = new GregorianCalendar();
Calendar end = new GregorianCalendar();
- end.add(1, Calendar.YEAR);
+ end.add(Calendar.YEAR, 1);
//END_INCLUDE(create_valid_dates)
@@ -316,8 +316,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment {
// Verify the data.
s.initVerify(((KeyStore.PrivateKeyEntry) entry).getCertificate());
s.update(data);
- boolean valid = s.verify(signature);
- return valid;
+ return s.verify(signature);
// END_INCLUDE(verify_data)
}
diff --git a/samples/browseable/Camera2Basic/src/com.example.android.camera2basic/Camera2BasicFragment.java b/samples/browseable/Camera2Basic/src/com.example.android.camera2basic/Camera2BasicFragment.java
index 4bf853433..c2b99bc4f 100644
--- a/samples/browseable/Camera2Basic/src/com.example.android.camera2basic/Camera2BasicFragment.java
+++ b/samples/browseable/Camera2Basic/src/com.example.android.camera2basic/Camera2BasicFragment.java
@@ -28,6 +28,7 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
+import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
@@ -116,6 +117,16 @@ public class Camera2BasicFragment extends Fragment
*/
private static final int STATE_PICTURE_TAKEN = 4;
+ /**
+ * Max preview width that is guaranteed by Camera2 API
+ */
+ private static final int MAX_PREVIEW_WIDTH = 1920;
+
+ /**
+ * Max preview height that is guaranteed by Camera2 API
+ */
+ private static final int MAX_PREVIEW_HEIGHT = 1080;
+
/**
* {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a
* {@link TextureView}.
@@ -344,31 +355,48 @@ public class Camera2BasicFragment extends Fragment
}
/**
- * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
- * width and height are at least as large as the respective requested values, and whose aspect
- * ratio matches with the specified value.
+ * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
+ * is at least as large as the respective texture view size, and that is at most as large as the
+ * respective max size, and whose aspect ratio matches with the specified value. If such size
+ * doesn't exist, choose the largest one that is at most as large as the respective max size,
+ * and whose aspect ratio matches with the specified value.
*
- * @param choices The list of sizes that the camera supports for the intended output class
- * @param width The minimum desired width
- * @param height The minimum desired height
- * @param aspectRatio The aspect ratio
+ * @param choices The list of sizes that the camera supports for the intended output
+ * class
+ * @param textureViewWidth The width of the texture view relative to sensor coordinate
+ * @param textureViewHeight The height of the texture view relative to sensor coordinate
+ * @param maxWidth The maximum width that can be chosen
+ * @param maxHeight The maximum height that can be chosen
+ * @param aspectRatio The aspect ratio
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
*/
- private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
+ private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
+ int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
+
// Collect the supported resolutions that are at least as big as the preview Surface
List bigEnough = new ArrayList<>();
+ // Collect the supported resolutions that are smaller than the preview Surface
+ List notBigEnough = new ArrayList<>();
int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight();
for (Size option : choices) {
- if (option.getHeight() == option.getWidth() * h / w &&
- option.getWidth() >= width && option.getHeight() >= height) {
- bigEnough.add(option);
+ if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
+ option.getHeight() == option.getWidth() * h / w) {
+ if (option.getWidth() >= textureViewWidth &&
+ option.getHeight() >= textureViewHeight) {
+ bigEnough.add(option);
+ } else {
+ notBigEnough.add(option);
+ }
}
}
- // Pick the smallest of those, assuming we found any
+ // Pick the smallest of those big enough. If there is no one big enough, pick the
+ // largest of those not big enough.
if (bigEnough.size() > 0) {
return Collections.min(bigEnough, new CompareSizesByArea());
+ } else if (notBigEnough.size() > 0) {
+ return Collections.max(notBigEnough, new CompareSizesByArea());
} else {
Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0];
@@ -478,11 +506,57 @@ public class Camera2BasicFragment extends Fragment
mImageReader.setOnImageAvailableListener(
mOnImageAvailableListener, mBackgroundHandler);
+ // Find out if we need to swap dimension to get the preview size relative to sensor
+ // coordinate.
+ int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+ int sensorOrientation =
+ characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ boolean swappedDimensions = false;
+ switch (displayRotation) {
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_180:
+ if (sensorOrientation == 90 || sensorOrientation == 270) {
+ swappedDimensions = true;
+ }
+ break;
+ case Surface.ROTATION_90:
+ case Surface.ROTATION_270:
+ if (sensorOrientation == 0 || sensorOrientation == 180) {
+ swappedDimensions = true;
+ }
+ break;
+ default:
+ Log.e(TAG, "Display rotation is invalid: " + displayRotation);
+ }
+
+ Point displaySize = new Point();
+ activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
+ int rotatedPreviewWidth = width;
+ int rotatedPreviewHeight = height;
+ int maxPreviewWidth = displaySize.x;
+ int maxPreviewHeight = displaySize.y;
+
+ if (swappedDimensions) {
+ rotatedPreviewWidth = height;
+ rotatedPreviewHeight = width;
+ maxPreviewWidth = displaySize.y;
+ maxPreviewHeight = displaySize.x;
+ }
+
+ if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
+ maxPreviewWidth = MAX_PREVIEW_WIDTH;
+ }
+
+ if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
+ maxPreviewHeight = MAX_PREVIEW_HEIGHT;
+ }
+
// Danger, W.R.! Attempting to use too large a preview size could exceed the camera
// bus' bandwidth limitation, resulting in gorgeous previews but the storage of
// garbage capture data.
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
- width, height, largest);
+ rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
+ maxPreviewHeight, largest);
// We fit the aspect ratio of TextureView to the size of preview we picked.
int orientation = getResources().getConfiguration().orientation;
diff --git a/samples/browseable/Camera2Raw/src/com.example.android.camera2raw/Camera2RawFragment.java b/samples/browseable/Camera2Raw/src/com.example.android.camera2raw/Camera2RawFragment.java
index 47cce388a..bf5efe588 100644
--- a/samples/browseable/Camera2Raw/src/com.example.android.camera2raw/Camera2RawFragment.java
+++ b/samples/browseable/Camera2Raw/src/com.example.android.camera2raw/Camera2RawFragment.java
@@ -27,6 +27,7 @@ import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
+import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
import android.hardware.SensorManager;
@@ -156,6 +157,16 @@ public class Camera2RawFragment extends Fragment
*/
private static final double ASPECT_RATIO_TOLERANCE = 0.005;
+ /**
+ * Max preview width that is guaranteed by Camera2 API
+ */
+ private static final int MAX_PREVIEW_WIDTH = 1920;
+
+ /**
+ * Max preview height that is guaranteed by Camera2 API
+ */
+ private static final int MAX_PREVIEW_HEIGHT = 1080;
+
/**
* Tag for the {@link Log}.
*/
@@ -1033,6 +1044,8 @@ public class Camera2RawFragment extends Fragment
// Find the rotation of the device relative to the native device orientation.
int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+ Point displaySize = new Point();
+ activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
// Find the rotation of the device relative to the camera sensor's orientation.
int totalRotation = sensorToDeviceRotation(mCharacteristics, deviceRotation);
@@ -1042,14 +1055,29 @@ public class Camera2RawFragment extends Fragment
boolean swappedDimensions = totalRotation == 90 || totalRotation == 270;
int rotatedViewWidth = viewWidth;
int rotatedViewHeight = viewHeight;
+ int maxPreviewWidth = displaySize.x;
+ int maxPreviewHeight = displaySize.y;
+
if (swappedDimensions) {
rotatedViewWidth = viewHeight;
rotatedViewHeight = viewWidth;
+ maxPreviewWidth = displaySize.y;
+ maxPreviewHeight = displaySize.x;
+ }
+
+ // Preview should not be larger than display size and 1080p.
+ if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
+ maxPreviewWidth = MAX_PREVIEW_WIDTH;
+ }
+
+ if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
+ maxPreviewHeight = MAX_PREVIEW_HEIGHT;
}
// Find the best preview size for these view dimensions and configured JPEG size.
Size previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
- rotatedViewWidth, rotatedViewHeight, largestJpeg);
+ rotatedViewWidth, rotatedViewHeight, maxPreviewWidth, maxPreviewHeight,
+ largestJpeg);
if (swappedDimensions) {
mTextureView.setAspectRatio(
@@ -1580,31 +1608,47 @@ public class Camera2RawFragment extends Fragment
}
/**
- * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
- * width and height are at least as large as the respective requested values, and whose aspect
- * ratio matches with the specified value.
+ * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
+ * is at least as large as the respective texture view size, and that is at most as large as the
+ * respective max size, and whose aspect ratio matches with the specified value. If such size
+ * doesn't exist, choose the largest one that is at most as large as the respective max size,
+ * and whose aspect ratio matches with the specified value.
*
- * @param choices The list of sizes that the camera supports for the intended output class
- * @param width The minimum desired width
- * @param height The minimum desired height
- * @param aspectRatio The aspect ratio
+ * @param choices The list of sizes that the camera supports for the intended output
+ * class
+ * @param textureViewWidth The width of the texture view relative to sensor coordinate
+ * @param textureViewHeight The height of the texture view relative to sensor coordinate
+ * @param maxWidth The maximum width that can be chosen
+ * @param maxHeight The maximum height that can be chosen
+ * @param aspectRatio The aspect ratio
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
*/
- private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
+ private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
+ int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
// Collect the supported resolutions that are at least as big as the preview Surface
List bigEnough = new ArrayList<>();
+ // Collect the supported resolutions that are smaller than the preview Surface
+ List notBigEnough = new ArrayList<>();
int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight();
for (Size option : choices) {
- if (option.getHeight() == option.getWidth() * h / w &&
- option.getWidth() >= width && option.getHeight() >= height) {
- bigEnough.add(option);
+ if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
+ option.getHeight() == option.getWidth() * h / w) {
+ if (option.getWidth() >= textureViewWidth &&
+ option.getHeight() >= textureViewHeight) {
+ bigEnough.add(option);
+ } else {
+ notBigEnough.add(option);
+ }
}
}
- // Pick the smallest of those, assuming we found any
+ // Pick the smallest of those big enough. If there is no one big enough, pick the
+ // largest of those not big enough.
if (bigEnough.size() > 0) {
return Collections.min(bigEnough, new CompareSizesByArea());
+ } else if (notBigEnough.size() > 0) {
+ return Collections.max(notBigEnough, new CompareSizesByArea());
} else {
Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0];
diff --git a/samples/browseable/DataLayer/Application/AndroidManifest.xml b/samples/browseable/DataLayer/Application/AndroidManifest.xml
index e80846de1..ed1cec347 100644
--- a/samples/browseable/DataLayer/Application/AndroidManifest.xml
+++ b/samples/browseable/DataLayer/Application/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.datalayer" >
+ android:targetSdkVersion="23" />
diff --git a/samples/browseable/DataLayer/Wearable/src/com.example.android.wearable.datalayer/MainActivity.java b/samples/browseable/DataLayer/Wearable/src/com.example.android.wearable.datalayer/MainActivity.java
index 678e428ca..b3cb25304 100644
--- a/samples/browseable/DataLayer/Wearable/src/com.example.android.wearable.datalayer/MainActivity.java
+++ b/samples/browseable/DataLayer/Wearable/src/com.example.android.wearable.datalayer/MainActivity.java
@@ -23,6 +23,7 @@ import android.app.Fragment;
import android.app.FragmentManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.wearable.view.DotsPageIndicator;
@@ -41,6 +42,7 @@ import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
+import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.Asset;
import com.google.android.gms.wearable.CapabilityApi;
@@ -85,7 +87,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
private static final String CAPABILITY_2_NAME = "capability_2";
private GoogleApiClient mGoogleApiClient;
- private Handler mHandler;
private GridViewPager mPager;
private DataFragment mDataFragment;
private AssetFragment mAssetFragment;
@@ -93,7 +94,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
@Override
public void onCreate(Bundle b) {
super.onCreate(b);
- mHandler = new Handler();
setContentView(R.layout.main_activity);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setupViews();
@@ -137,15 +137,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
Log.e(TAG, "onConnectionFailed(): Failed to connect, with result: " + result);
}
- private void generateEvent(final String title, final String text) {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- mDataFragment.appendItem(title, text);
- }
- });
- }
-
@Override
public void onDataChanged(DataEventBuffer dataEvents) {
LOGD(TAG, "onDataChanged(): " + dataEvents);
@@ -155,29 +146,22 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
String path = event.getDataItem().getUri().getPath();
if (DataLayerListenerService.IMAGE_PATH.equals(path)) {
DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
- Asset photo = dataMapItem.getDataMap()
+ Asset photoAsset = dataMapItem.getDataMap()
.getAsset(DataLayerListenerService.IMAGE_KEY);
- final Bitmap bitmap = loadBitmapFromAsset(mGoogleApiClient, photo);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- Log.d(TAG, "Setting background image on second page..");
- moveToPage(1);
- mAssetFragment.setBackgroundImage(bitmap);
- }
- });
+ // Loads image on background thread.
+ new LoadBitmapAsyncTask().execute(photoAsset);
} else if (DataLayerListenerService.COUNT_PATH.equals(path)) {
LOGD(TAG, "Data Changed for COUNT_PATH");
- generateEvent("DataItem Changed", event.getDataItem().toString());
+ mDataFragment.appendItem("DataItem Changed", event.getDataItem().toString());
} else {
LOGD(TAG, "Unrecognized path: " + path);
}
} else if (event.getType() == DataEvent.TYPE_DELETED) {
- generateEvent("DataItem Deleted", event.getDataItem().toString());
+ mDataFragment.appendItem("DataItem Deleted", event.getDataItem().toString());
} else {
- generateEvent("Unknown data event type", "Type = " + event.getType());
+ mDataFragment.appendItem("Unknown data event type", "Type = " + event.getType());
}
}
}
@@ -199,20 +183,27 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
* Find the connected nodes that provide at least one of the given capabilities
*/
private void showNodes(final String... capabilityNames) {
- Wearable.CapabilityApi.getAllCapabilities(mGoogleApiClient,
- CapabilityApi.FILTER_REACHABLE).setResultCallback(
+ PendingResult pendingCapabilityResult =
+ Wearable.CapabilityApi.getAllCapabilities(
+ mGoogleApiClient,
+ CapabilityApi.FILTER_REACHABLE);
+
+ pendingCapabilityResult.setResultCallback(
new ResultCallback() {
@Override
public void onResult(
CapabilityApi.GetAllCapabilitiesResult getAllCapabilitiesResult) {
+
if (!getAllCapabilitiesResult.getStatus().isSuccess()) {
Log.e(TAG, "Failed to get capabilities");
return;
}
- Map
- capabilitiesMap = getAllCapabilitiesResult.getAllCapabilities();
+
+ Map capabilitiesMap =
+ getAllCapabilitiesResult.getAllCapabilities();
Set nodes = new HashSet<>();
+
if (capabilitiesMap.isEmpty()) {
showDiscoveredNodes(nodes);
return;
@@ -231,7 +222,7 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
for (Node node : nodes) {
nodesList.add(node.getDisplayName());
}
- Log.d(TAG, "Connected Nodes: " + (nodesList.isEmpty()
+ LOGD(TAG, "Connected Nodes: " + (nodesList.isEmpty()
? "No connected device was found for the given capabilities"
: TextUtils.join(",", nodesList)));
String msg;
@@ -246,39 +237,20 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
});
}
- /**
- * Extracts {@link android.graphics.Bitmap} data from the
- * {@link com.google.android.gms.wearable.Asset}
- */
- private Bitmap loadBitmapFromAsset(GoogleApiClient apiClient, Asset asset) {
- if (asset == null) {
- throw new IllegalArgumentException("Asset must be non-null");
- }
-
- InputStream assetInputStream = Wearable.DataApi.getFdForAsset(
- apiClient, asset).await().getInputStream();
-
- if (assetInputStream == null) {
- Log.w(TAG, "Requested an unknown Asset.");
- return null;
- }
- return BitmapFactory.decodeStream(assetInputStream);
- }
-
@Override
public void onMessageReceived(MessageEvent event) {
LOGD(TAG, "onMessageReceived: " + event);
- generateEvent("Message", event.toString());
+ mDataFragment.appendItem("Message", event.toString());
}
@Override
public void onPeerConnected(Node node) {
- generateEvent("Node Connected", node.getId());
+ mDataFragment.appendItem("Node Connected", node.getId());
}
@Override
public void onPeerDisconnected(Node node) {
- generateEvent("Node Disconnected", node.getId());
+ mDataFragment.appendItem("Node Disconnected", node.getId());
}
private void setupViews() {
@@ -330,4 +302,43 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
}
}
+
+ /*
+ * Extracts {@link android.graphics.Bitmap} data from the
+ * {@link com.google.android.gms.wearable.Asset}
+ */
+ private class LoadBitmapAsyncTask extends AsyncTask {
+
+ @Override
+ protected Bitmap doInBackground(Asset... params) {
+
+ if(params.length > 0) {
+
+ Asset asset = params[0];
+
+ InputStream assetInputStream = Wearable.DataApi.getFdForAsset(
+ mGoogleApiClient, asset).await().getInputStream();
+
+ if (assetInputStream == null) {
+ Log.w(TAG, "Requested an unknown Asset.");
+ return null;
+ }
+ return BitmapFactory.decodeStream(assetInputStream);
+
+ } else {
+ Log.e(TAG, "Asset must be non-null");
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+
+ if(bitmap != null) {
+ LOGD(TAG, "Setting background image on second page..");
+ moveToPage(1);
+ mAssetFragment.setBackgroundImage(bitmap);
+ }
+ }
+ }
}
diff --git a/samples/browseable/DelayedConfirmation/Application/AndroidManifest.xml b/samples/browseable/DelayedConfirmation/Application/AndroidManifest.xml
index d8060a8d4..e3e6de174 100644
--- a/samples/browseable/DelayedConfirmation/Application/AndroidManifest.xml
+++ b/samples/browseable/DelayedConfirmation/Application/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.delayedconfirmation" >
+ android:targetSdkVersion="23" />
+ android:targetSdkVersion="23" />
+ android:targetSdkVersion="23" />
- Fingerprint Dialog Sample
+ FingerprintDialog
Security -> Fingerprint' to set up a fingerprint",
- Toast.LENGTH_LONG).show();
- purchaseButton.setEnabled(false);
- return;
- }
- if (!mFingerprintManager.hasEnrolledFingerprints()) {
- purchaseButton.setEnabled(false);
- // This happens when no fingerprints are registered.
- Toast.makeText(this,
- "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
- Toast.LENGTH_LONG).show();
- return;
- }
- createKey();
- purchaseButton.setEnabled(true);
- purchaseButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- findViewById(R.id.confirmation_message).setVisibility(View.GONE);
- findViewById(R.id.encrypted_message).setVisibility(View.GONE);
-
- // Set up the crypto object for later. The object will be authenticated by use
- // of the fingerprint.
- if (initCipher()) {
-
- // Show the fingerprint dialog. The user has the option to use the fingerprint with
- // crypto, or you can fall back to using a server-side verified password.
- mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
- boolean useFingerprintPreference = mSharedPreferences
- .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
- true);
- if (useFingerprintPreference) {
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
- } else {
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
- }
- mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
- } else {
- // This happens if the lock screen has been disabled or or a fingerprint got
- // enrolled. Thus show the dialog to authenticate with their password first
- // and ask the user if they want to authenticate with fingerprints in the
- // future
- mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
- mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
- }
- }
- });
+ setContentView(R.layout.activity_main);
+ Button purchaseButton = (Button) findViewById(R.id.purchase_button);
+ if (!mKeyguardManager.isKeyguardSecure()) {
+ // Show a message that the user hasn't set up a fingerprint or lock screen.
+ Toast.makeText(this,
+ "Secure lock screen hasn't set up.\n"
+ + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
+ Toast.LENGTH_LONG).show();
+ purchaseButton.setEnabled(false);
+ return;
}
+
+ //noinspection ResourceType
+ if (!mFingerprintManager.hasEnrolledFingerprints()) {
+ purchaseButton.setEnabled(false);
+ // This happens when no fingerprints are registered.
+ Toast.makeText(this,
+ "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ createKey();
+ purchaseButton.setEnabled(true);
+ purchaseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ findViewById(R.id.confirmation_message).setVisibility(View.GONE);
+ findViewById(R.id.encrypted_message).setVisibility(View.GONE);
+
+ // Set up the crypto object for later. The object will be authenticated by use
+ // of the fingerprint.
+ if (initCipher()) {
+
+ // Show the fingerprint dialog. The user has the option to use the fingerprint with
+ // crypto, or you can fall back to using a server-side verified password.
+ mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
+ boolean useFingerprintPreference = mSharedPreferences
+ .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
+ true);
+ if (useFingerprintPreference) {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
+ } else {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
+ }
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ } else {
+ // This happens if the lock screen has been disabled or or a fingerprint got
+ // enrolled. Thus show the dialog to authenticate with their password first
+ // and ask the user if they want to authenticate with fingerprints in the
+ // future
+ mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ }
+ }
+ });
}
/**
diff --git a/samples/browseable/Flashlight/AndroidManifest.xml b/samples/browseable/Flashlight/AndroidManifest.xml
index 738ba9d37..1eb15d072 100644
--- a/samples/browseable/Flashlight/AndroidManifest.xml
+++ b/samples/browseable/Flashlight/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.flashlight" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/Flashlight/_index.jd b/samples/browseable/Flashlight/_index.jd
index 1858b3ec4..15a65fc78 100644
--- a/samples/browseable/Flashlight/_index.jd
+++ b/samples/browseable/Flashlight/_index.jd
@@ -6,6 +6,6 @@ sample.group=Wearable
Wearable activity that uses your wearable screen as a flashlight. There is also
- a party-mode option, if you want to make things interesting.
+ a party-mode option (swipe left), if you want to make things interesting.
diff --git a/samples/browseable/Geofencing/Application/AndroidManifest.xml b/samples/browseable/Geofencing/Application/AndroidManifest.xml
index d07a2659f..d1eabc3d7 100644
--- a/samples/browseable/Geofencing/Application/AndroidManifest.xml
+++ b/samples/browseable/Geofencing/Application/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.geofencing">
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/Geofencing/Wearable/AndroidManifest.xml b/samples/browseable/Geofencing/Wearable/AndroidManifest.xml
index 082f396b6..f25cc4472 100644
--- a/samples/browseable/Geofencing/Wearable/AndroidManifest.xml
+++ b/samples/browseable/Geofencing/Wearable/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.geofencing" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/GridViewPager/AndroidManifest.xml b/samples/browseable/GridViewPager/AndroidManifest.xml
index 5c362dcb5..e25cd6373 100644
--- a/samples/browseable/GridViewPager/AndroidManifest.xml
+++ b/samples/browseable/GridViewPager/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.gridviewpager" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/JumpingJack/AndroidManifest.xml b/samples/browseable/JumpingJack/AndroidManifest.xml
index 02b7a4fff..f6cf22047 100644
--- a/samples/browseable/JumpingJack/AndroidManifest.xml
+++ b/samples/browseable/JumpingJack/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.jumpingjack">
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/Notifications/Application/AndroidManifest.xml b/samples/browseable/Notifications/Application/AndroidManifest.xml
index 3f1274d87..6a17ad8c3 100644
--- a/samples/browseable/Notifications/Application/AndroidManifest.xml
+++ b/samples/browseable/Notifications/Application/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.support.wearable.notifications" >
+ android:targetSdkVersion="23" />
diff --git a/samples/browseable/Notifications/Wearable/AndroidManifest.xml b/samples/browseable/Notifications/Wearable/AndroidManifest.xml
index 34a29ff66..a446fd9bb 100644
--- a/samples/browseable/Notifications/Wearable/AndroidManifest.xml
+++ b/samples/browseable/Notifications/Wearable/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.support.wearable.notifications" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/Quiz/Application/AndroidManifest.xml b/samples/browseable/Quiz/Application/AndroidManifest.xml
index 801a47323..8fabd42de 100644
--- a/samples/browseable/Quiz/Application/AndroidManifest.xml
+++ b/samples/browseable/Quiz/Application/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.quiz" >
+ android:targetSdkVersion="23" />
+ android:targetSdkVersion="23" />
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/SpeedTracker/Application/AndroidManifest.xml b/samples/browseable/SpeedTracker/Application/AndroidManifest.xml
index 44284d4fd..be88f6d68 100644
--- a/samples/browseable/SpeedTracker/Application/AndroidManifest.xml
+++ b/samples/browseable/SpeedTracker/Application/AndroidManifest.xml
@@ -2,25 +2,35 @@
+
+
+
+
+
+
-
-
+
+
+
-
+
+
-
+ android:theme="@style/Theme.AppCompat.Light" >
diff --git a/samples/browseable/SpeedTracker/Application/res/layout/main_activity.xml b/samples/browseable/SpeedTracker/Application/res/layout/main_activity.xml
index a18c64484..17a8f6a9d 100644
--- a/samples/browseable/SpeedTracker/Application/res/layout/main_activity.xml
+++ b/samples/browseable/SpeedTracker/Application/res/layout/main_activity.xml
@@ -21,7 +21,8 @@
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp">
- \
+
+ android:targetSdkVersion="23" />
+
+
+
+
-
@@ -48,12 +51,6 @@
-
-
-
-
-
diff --git a/samples/browseable/SpeedTracker/Wearable/res/layout/main_activity.xml b/samples/browseable/SpeedTracker/Wearable/res/layout/main_activity.xml
index a1b9081a0..a2b678eb8 100644
--- a/samples/browseable/SpeedTracker/Wearable/res/layout/main_activity.xml
+++ b/samples/browseable/SpeedTracker/Wearable/res/layout/main_activity.xml
@@ -29,11 +29,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
+ android:paddingLeft="16dp"
android:fontFamily="sans-serif-light"
+ android:textAlignment="center"
android:textSize="17sp"
android:textStyle="italic"
- android:id="@+id/acquiring_gps"
- android:text="@string/acquiring_gps"/>
+ android:id="@+id/gps_issue_text"
+ android:text=""/>
+ android:layout_marginLeft="50dp" />
+ android:layout_alignBottom="@+id/gps_permission"
+ android:layout_marginRight="50dp"/>
diff --git a/samples/browseable/SpeedTracker/Wearable/res/layout/saving_activity.xml b/samples/browseable/SpeedTracker/Wearable/res/layout/saving_activity.xml
deleted file mode 100644
index c37d95930..000000000
--- a/samples/browseable/SpeedTracker/Wearable/res/layout/saving_activity.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/samples/browseable/SpeedTracker/Wearable/res/values/strings.xml b/samples/browseable/SpeedTracker/Wearable/res/values/strings.xml
index dda3ecd6b..b0c37478d 100644
--- a/samples/browseable/SpeedTracker/Wearable/res/values/strings.xml
+++ b/samples/browseable/SpeedTracker/Wearable/res/values/strings.xml
@@ -25,11 +25,16 @@
Limit: %1$d mphAcquiring GPS Fix ...%1$d mph
- Start Recording GPS?
- Stop Recording GPS?
+
+ Enable Location Permission?
+
mphSpeed Limit
- GPS not available.
+ No GPS on device. Will use phone GPS when available.OK%.0f
+
+ App requires location permission to function, tap GPS icon.
+
+
diff --git a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/SpeedPickerActivity.java b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/SpeedPickerActivity.java
index d55d7dfb6..d178891f8 100644
--- a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/SpeedPickerActivity.java
+++ b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/SpeedPickerActivity.java
@@ -17,9 +17,8 @@
package com.example.android.wearable.speedtracker;
import android.app.Activity;
-import android.content.SharedPreferences;
+import android.content.Intent;
import android.os.Bundle;
-import android.preference.PreferenceManager;
import android.support.wearable.view.WearableListView;
import android.widget.TextView;
@@ -31,6 +30,9 @@ import com.example.android.wearable.speedtracker.ui.SpeedPickerListAdapter;
*/
public class SpeedPickerActivity extends Activity implements WearableListView.ClickListener {
+ public static final String EXTRA_NEW_SPEED_LIMIT =
+ "com.example.android.wearable.speedtracker.extra.NEW_SPEED_LIMIT";
+
/* Speeds, in mph, that will be shown on the list */
private int[] speeds = {25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75};
@@ -75,9 +77,13 @@ public class SpeedPickerActivity extends Activity implements WearableListView.Cl
@Override
public void onClick(WearableListView.ViewHolder viewHolder) {
- SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
- pref.edit().putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY,
- speeds[viewHolder.getPosition()]).apply();
+
+ int newSpeedLimit = speeds[viewHolder.getPosition()];
+
+ Intent resultIntent = new Intent(Intent.ACTION_PICK);
+ resultIntent.putExtra(EXTRA_NEW_SPEED_LIMIT, newSpeedLimit);
+ setResult(RESULT_OK, resultIntent);
+
finish();
}
diff --git a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/WearableMainActivity.java b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/WearableMainActivity.java
index f3015bf87..ee3c3ef95 100644
--- a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/WearableMainActivity.java
+++ b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/WearableMainActivity.java
@@ -28,7 +28,7 @@ import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.PutDataRequest;
import com.google.android.gms.wearable.Wearable;
-import android.app.Activity;
+import android.Manifest;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
@@ -38,18 +38,19 @@ import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.wearable.activity.WearableActivity;
import android.util.Log;
import android.view.View;
-import android.view.WindowManager;
-import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.example.android.wearable.speedtracker.common.Constants;
import com.example.android.wearable.speedtracker.common.LocationEntry;
-import com.example.android.wearable.speedtracker.ui.LocationSettingActivity;
import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
/**
* The main activity for the wearable app. User can pick a speed limit, and after this activity
@@ -58,33 +59,54 @@ import java.util.Calendar;
* and if the user exceeds the speed limit, it will turn red. In order to show the user that GPS
* location data is coming in, a small green dot keeps on blinking while GPS data is available.
*/
-public class WearableMainActivity extends Activity implements GoogleApiClient.ConnectionCallbacks,
- GoogleApiClient.OnConnectionFailedListener, LocationListener {
+public class WearableMainActivity extends WearableActivity implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ ActivityCompat.OnRequestPermissionsResultCallback,
+ LocationListener {
private static final String TAG = "WearableActivity";
- private static final long UPDATE_INTERVAL_MS = 5 * 1000;
- private static final long FASTEST_INTERVAL_MS = 5 * 1000;
+ private static final long UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
+ private static final long FASTEST_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
- public static final float MPH_IN_METERS_PER_SECOND = 2.23694f;
+ private static final float MPH_IN_METERS_PER_SECOND = 2.23694f;
+
+ private static final int SPEED_LIMIT_DEFAULT_MPH = 45;
- public static final String PREFS_SPEED_LIMIT_KEY = "speed_limit";
- public static final int SPEED_LIMIT_DEFAULT_MPH = 45;
private static final long INDICATOR_DOT_FADE_AWAY_MS = 500L;
- private GoogleApiClient mGoogleApiClient;
- private TextView mSpeedLimitText;
- private TextView mCurrentSpeedText;
- private ImageView mSaveImageView;
- private TextView mAcquiringGps;
- private TextView mCurrentSpeedMphText;
+ // Request codes for changing speed limit and location permissions.
+ private static final int REQUEST_PICK_SPEED_LIMIT = 0;
+
+ // Id to identify Location permission request.
+ private static final int REQUEST_GPS_PERMISSION = 1;
+
+ // Shared Preferences for saving speed limit and location permission between app launches.
+ private static final String PREFS_SPEED_LIMIT_KEY = "SpeedLimit";
- private int mCurrentSpeedLimit;
- private float mCurrentSpeed;
- private View mDot;
- private Handler mHandler = new Handler();
private Calendar mCalendar;
- private boolean mSaveGpsLocation;
+
+ private TextView mSpeedLimitTextView;
+ private TextView mSpeedTextView;
+ private ImageView mGpsPermissionImageView;
+ private TextView mCurrentSpeedMphTextView;
+ private TextView mGpsIssueTextView;
+ private View mBlinkingGpsStatusDotView;
+
+ private String mGpsPermissionNeededMessage;
+ private String mAcquiringGpsMessage;
+
+ private int mSpeedLimit;
+ private float mSpeed;
+
+ private boolean mGpsPermissionApproved;
+
+ private boolean mWaitingForGpsSignal;
+
+ private GoogleApiClient mGoogleApiClient;
+
+ private Handler mHandler = new Handler();
private enum SpeedState {
BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above);
@@ -104,20 +126,53 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ Log.d(TAG, "onCreate()");
+
+
setContentView(R.layout.main_activity);
+
+ /*
+ * Enables Always-on, so our app doesn't shut down when the watch goes into ambient mode.
+ * Best practice is to override onEnterAmbient(), onUpdateAmbient(), and onExitAmbient() to
+ * optimize the display for ambient mode. However, for brevity, we aren't doing that here
+ * to focus on learning location and permissions. For more information on best practices
+ * in ambient mode, check this page:
+ * https://developer.android.com/training/wearables/apps/always-on.html
+ */
+ setAmbientEnabled();
+
+ mCalendar = Calendar.getInstance();
+
+ // Enables app to handle 23+ (M+) style permissions.
+ mGpsPermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED;
+
+ mGpsPermissionNeededMessage = getString(R.string.permission_rationale);
+ mAcquiringGpsMessage = getString(R.string.acquiring_gps);
+
+
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ mSpeedLimit = sharedPreferences.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH);
+
+ mSpeed = 0;
+
+ mWaitingForGpsSignal = true;
+
+
+ /*
+ * If this hardware doesn't support GPS, we warn the user. Note that when such device is
+ * connected to a phone with GPS capabilities, the framework automatically routes the
+ * location requests from the phone. However, if the phone becomes disconnected and the
+ * wearable doesn't support GPS, no location is recorded until the phone is reconnected.
+ */
if (!hasGps()) {
- // If this hardware doesn't support GPS, we prefer to exit.
- // Note that when such device is connected to a phone with GPS capabilities, the
- // framework automatically routes the location requests to the phone. For this
- // application, this would not be desirable so we exit the app but for some other
- // applications, that might be a valid scenario.
- Log.w(TAG, "This hardware doesn't have GPS, so we exit");
+ Log.w(TAG, "This hardware doesn't have GPS, so we warn user.");
new AlertDialog.Builder(this)
.setMessage(getString(R.string.gps_not_available))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
- finish();
dialog.cancel();
}
})
@@ -125,7 +180,6 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
@Override
public void onDismiss(DialogInterface dialog) {
dialog.cancel();
- finish();
}
})
.setCancelable(false)
@@ -133,164 +187,216 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
.show();
}
+
setupViews();
- updateSpeedVisibility(false);
- setSpeedLimit();
+
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
- mGoogleApiClient.connect();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected()) &&
+ (mGoogleApiClient.isConnecting())) {
+ LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
+ mGoogleApiClient.disconnect();
+ }
+
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mGoogleApiClient != null) {
+ mGoogleApiClient.connect();
+ }
}
private void setupViews() {
- mSpeedLimitText = (TextView) findViewById(R.id.max_speed_text);
- mCurrentSpeedText = (TextView) findViewById(R.id.current_speed_text);
- mSaveImageView = (ImageView) findViewById(R.id.saving);
- ImageButton settingButton = (ImageButton) findViewById(R.id.settings);
- mAcquiringGps = (TextView) findViewById(R.id.acquiring_gps);
- mCurrentSpeedMphText = (TextView) findViewById(R.id.current_speed_mph);
- mDot = findViewById(R.id.dot);
+ mSpeedLimitTextView = (TextView) findViewById(R.id.max_speed_text);
+ mSpeedTextView = (TextView) findViewById(R.id.current_speed_text);
+ mCurrentSpeedMphTextView = (TextView) findViewById(R.id.current_speed_mph);
- settingButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent speedIntent = new Intent(WearableMainActivity.this,
- SpeedPickerActivity.class);
- startActivity(speedIntent);
- }
- });
+ mGpsPermissionImageView = (ImageView) findViewById(R.id.gps_permission);
+ mGpsIssueTextView = (TextView) findViewById(R.id.gps_issue_text);
+ mBlinkingGpsStatusDotView = findViewById(R.id.dot);
- mSaveImageView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent savingIntent = new Intent(WearableMainActivity.this,
- LocationSettingActivity.class);
- startActivity(savingIntent);
- }
- });
+ updateActivityViewsBasedOnLocationPermissions();
}
- private void setSpeedLimit(int speedLimit) {
- mSpeedLimitText.setText(getString(R.string.speed_limit, speedLimit));
+ public void onSpeedLimitClick(View view) {
+ Intent speedIntent = new Intent(WearableMainActivity.this,
+ SpeedPickerActivity.class);
+ startActivityForResult(speedIntent, REQUEST_PICK_SPEED_LIMIT);
}
- private void setSpeedLimit() {
- SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
- mCurrentSpeedLimit = pref.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH);
- setSpeedLimit(mCurrentSpeedLimit);
- }
+ public void onGpsPermissionClick(View view) {
- private void setCurrentSpeed(float speed) {
- mCurrentSpeed = speed;
- mCurrentSpeedText.setText(String.format(getString(R.string.speed_format), speed));
- adjustColor();
+ if (!mGpsPermissionApproved) {
+
+ Log.i(TAG, "Location permission has NOT been granted. Requesting permission.");
+
+ // On 23+ (M+) devices, GPS permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
+ REQUEST_GPS_PERMISSION);
+ }
}
/**
- * Adjusts the color of the speed based on its value relative to the speed limit.
+ * Adjusts the visibility of views based on location permissions.
*/
- private void adjustColor() {
- SpeedState state = SpeedState.ABOVE;
- if (mCurrentSpeed <= mCurrentSpeedLimit - 5) {
- state = SpeedState.BELOW;
- } else if (mCurrentSpeed <= mCurrentSpeedLimit) {
- state = SpeedState.CLOSE;
- }
+ private void updateActivityViewsBasedOnLocationPermissions() {
- mCurrentSpeedText.setTextColor(getResources().getColor(state.getColor()));
+ /*
+ * If the user has approved location but we don't have a signal yet, we let the user know
+ * we are waiting on the GPS signal (this sometimes takes a little while). Otherwise, the
+ * user might think something is wrong.
+ */
+ if (mGpsPermissionApproved && mWaitingForGpsSignal) {
+
+ // We are getting a GPS signal w/ user permission.
+ mGpsIssueTextView.setText(mAcquiringGpsMessage);
+ mGpsIssueTextView.setVisibility(View.VISIBLE);
+ mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
+
+ mSpeedTextView.setVisibility(View.GONE);
+ mSpeedLimitTextView.setVisibility(View.GONE);
+ mCurrentSpeedMphTextView.setVisibility(View.GONE);
+
+ } else if (mGpsPermissionApproved) {
+
+ mGpsIssueTextView.setVisibility(View.GONE);
+
+ mSpeedTextView.setVisibility(View.VISIBLE);
+ mSpeedLimitTextView.setVisibility(View.VISIBLE);
+ mCurrentSpeedMphTextView.setVisibility(View.VISIBLE);
+ mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
+
+ } else {
+
+ // User needs to enable location for the app to work.
+ mGpsIssueTextView.setVisibility(View.VISIBLE);
+ mGpsIssueTextView.setText(mGpsPermissionNeededMessage);
+ mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_not_saving_grey600_96dp);
+
+ mSpeedTextView.setVisibility(View.GONE);
+ mSpeedLimitTextView.setVisibility(View.GONE);
+ mCurrentSpeedMphTextView.setVisibility(View.GONE);
+ }
+ }
+
+ private void updateSpeedInViews() {
+
+ if (mGpsPermissionApproved) {
+
+ mSpeedLimitTextView.setText(getString(R.string.speed_limit, mSpeedLimit));
+ mSpeedTextView.setText(String.format(getString(R.string.speed_format), mSpeed));
+
+ // Adjusts the color of the speed based on its value relative to the speed limit.
+ SpeedState state = SpeedState.ABOVE;
+ if (mSpeed <= mSpeedLimit - 5) {
+ state = SpeedState.BELOW;
+ } else if (mSpeed <= mSpeedLimit) {
+ state = SpeedState.CLOSE;
+ }
+
+ mSpeedTextView.setTextColor(getResources().getColor(state.getColor()));
+
+ // Causes the (green) dot blinks when new GPS location data is acquired.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
+ }
+ });
+ mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mBlinkingGpsStatusDotView.setVisibility(View.INVISIBLE);
+ }
+ }, INDICATOR_DOT_FADE_AWAY_MS);
+ }
}
@Override
public void onConnected(Bundle bundle) {
- LocationRequest locationRequest = LocationRequest.create()
- .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
- .setInterval(UPDATE_INTERVAL_MS)
- .setFastestInterval(FASTEST_INTERVAL_MS);
- LocationServices.FusedLocationApi
- .requestLocationUpdates(mGoogleApiClient, locationRequest, this)
- .setResultCallback(new ResultCallback() {
+ Log.d(TAG, "onConnected()");
- @Override
- public void onResult(Status status) {
- if (status.getStatus().isSuccess()) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Successfully requested location updates");
+ /*
+ * mGpsPermissionApproved covers 23+ (M+) style permissions. If that is already approved or
+ * the device is pre-23, the app uses mSaveGpsLocation to save the user's location
+ * preference.
+ */
+ if (mGpsPermissionApproved) {
+
+ LocationRequest locationRequest = LocationRequest.create()
+ .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
+ .setInterval(UPDATE_INTERVAL_MS)
+ .setFastestInterval(FASTEST_INTERVAL_MS);
+
+ LocationServices.FusedLocationApi
+ .requestLocationUpdates(mGoogleApiClient, locationRequest, this)
+ .setResultCallback(new ResultCallback() {
+
+ @Override
+ public void onResult(Status status) {
+ if (status.getStatus().isSuccess()) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Successfully requested location updates");
+ }
+ } else {
+ Log.e(TAG,
+ "Failed in requesting location updates, "
+ + "status code: "
+ + status.getStatusCode() + ", message: " + status
+ .getStatusMessage());
}
- } else {
- Log.e(TAG,
- "Failed in requesting location updates, "
- + "status code: "
- + status.getStatusCode() + ", message: " + status
- .getStatusMessage());
}
- }
- });
+ });
+ }
}
@Override
public void onConnectionSuspended(int i) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
- }
+ Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
+
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
- Log.e(TAG, "onConnectionFailed(): connection to location client failed");
+ Log.e(TAG, "onConnectionFailed(): " + connectionResult.getErrorMessage());
}
@Override
public void onLocationChanged(Location location) {
- updateSpeedVisibility(true);
- setCurrentSpeed(location.getSpeed() * MPH_IN_METERS_PER_SECOND);
- flashDot();
+ Log.d(TAG, "onLocationChanged() : " + location);
+
+
+ if (mWaitingForGpsSignal) {
+ mWaitingForGpsSignal = false;
+ updateActivityViewsBasedOnLocationPermissions();
+ }
+
+ mSpeed = location.getSpeed() * MPH_IN_METERS_PER_SECOND;
+ updateSpeedInViews();
addLocationEntry(location.getLatitude(), location.getLongitude());
}
- /**
- * Causes the (green) dot blinks when new GPS location data is acquired.
- */
- private void flashDot() {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mDot.setVisibility(View.VISIBLE);
- }
- });
- mDot.setVisibility(View.VISIBLE);
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- mDot.setVisibility(View.INVISIBLE);
- }
- }, INDICATOR_DOT_FADE_AWAY_MS);
- }
-
- /**
- * Adjusts the visibility of speed indicator based on the arrival of GPS data.
- */
- private void updateSpeedVisibility(boolean speedVisible) {
- if (speedVisible) {
- mAcquiringGps.setVisibility(View.GONE);
- mCurrentSpeedText.setVisibility(View.VISIBLE);
- mCurrentSpeedMphText.setVisibility(View.VISIBLE);
- } else {
- mAcquiringGps.setVisibility(View.VISIBLE);
- mCurrentSpeedText.setVisibility(View.GONE);
- mCurrentSpeedMphText.setVisibility(View.GONE);
- }
- }
-
- /**
- * Adds a data item to the data Layer storage
+ /*
+ * Adds a data item to the data Layer storage.
*/
private void addLocationEntry(double latitude, double longitude) {
- if (!mSaveGpsLocation || !mGoogleApiClient.isConnected()) {
+ if (!mGpsPermissionApproved || !mGoogleApiClient.isConnected()) {
return;
}
mCalendar.setTimeInMillis(System.currentTimeMillis());
@@ -315,29 +421,56 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
});
}
+ /**
+ * Handles user choices for both speed limit and location permissions (GPS tracking).
+ */
@Override
- protected void onStop() {
- super.onStop();
- if (mGoogleApiClient.isConnected()) {
- LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+
+ if (requestCode == REQUEST_PICK_SPEED_LIMIT) {
+ if (resultCode == RESULT_OK) {
+ // The user updated the speed limit.
+ int newSpeedLimit =
+ data.getIntExtra(SpeedPickerActivity.EXTRA_NEW_SPEED_LIMIT, mSpeedLimit);
+
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(this);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY, newSpeedLimit);
+ editor.apply();
+
+ mSpeedLimit = newSpeedLimit;
+
+ updateSpeedInViews();
+ }
}
- mGoogleApiClient.disconnect();
}
+ /**
+ * Callback received when a permissions request has been completed.
+ */
@Override
- protected void onResume() {
- super.onResume();
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- mCalendar = Calendar.getInstance();
- setSpeedLimit();
- adjustColor();
- updateRecordingIcon();
- }
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- private void updateRecordingIcon() {
- mSaveGpsLocation = LocationSettingActivity.getGpsRecordingStatusFromPreferences(this);
- mSaveImageView.setImageResource(mSaveGpsLocation ? R.drawable.ic_gps_saving_grey600_96dp
- : R.drawable.ic_gps_not_saving_grey600_96dp);
+ Log.d(TAG, "onRequestPermissionsResult(): " + permissions);
+
+
+ if (requestCode == REQUEST_GPS_PERMISSION) {
+ Log.i(TAG, "Received response for GPS permission request.");
+
+ if ((grantResults.length == 1)
+ && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
+ Log.i(TAG, "GPS permission granted.");
+ mGpsPermissionApproved = true;
+ } else {
+ Log.i(TAG, "GPS permission NOT granted.");
+ mGpsPermissionApproved = false;
+ }
+
+ updateActivityViewsBasedOnLocationPermissions();
+
+ }
}
/**
@@ -346,4 +479,4 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
private boolean hasGps() {
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS);
}
-}
+}
\ No newline at end of file
diff --git a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/LocationSettingActivity.java b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/LocationSettingActivity.java
deleted file mode 100644
index 1f8be71c2..000000000
--- a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/LocationSettingActivity.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2014 Google Inc. All Rights Reserved.
- *
- * 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.speedtracker.ui;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.view.View;
-import android.widget.TextView;
-
-import com.example.android.wearable.speedtracker.R;
-
-/**
- * A simple activity that allows the user to start or stop recording of GPS location data.
- */
-public class LocationSettingActivity extends Activity {
-
- private static final String PREFS_KEY_SAVE_GPS = "save-gps";
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.saving_activity);
- TextView textView = (TextView) findViewById(R.id.textView);
- textView.setText(getGpsRecordingStatusFromPreferences(this) ? R.string.stop_saving_gps
- : R.string.start_saving_gps);
-
- }
-
- public void onClick(View view) {
- switch (view.getId()) {
- case R.id.submitBtn:
- saveGpsRecordingStatusToPreferences(LocationSettingActivity.this,
- !getGpsRecordingStatusFromPreferences(this));
- break;
- case R.id.cancelBtn:
- break;
- }
- finish();
- }
-
- /**
- * Get the persisted value for whether the app should record the GPS location data or not. If
- * there is no prior value persisted, it returns {@code false}.
- */
- public static boolean getGpsRecordingStatusFromPreferences(Context context) {
- SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
- return pref.getBoolean(PREFS_KEY_SAVE_GPS, false);
- }
-
- /**
- * Persists the user selection to whether save the GPS location data or not.
- */
- public static void saveGpsRecordingStatusToPreferences(Context context, boolean value) {
- SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
- pref.edit().putBoolean(PREFS_KEY_SAVE_GPS, value).apply();
-
- }
-}
diff --git a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/SpeedPickerListAdapter.java b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/SpeedPickerListAdapter.java
index e3b284bfa..df25a6a89 100644
--- a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/SpeedPickerListAdapter.java
+++ b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/SpeedPickerListAdapter.java
@@ -41,6 +41,9 @@ public class SpeedPickerListAdapter extends WearableListView.Adapter {
mDataSet = dataset;
}
+ /**
+ * Displays all possible speed limit choices.
+ */
public static class ItemViewHolder extends WearableListView.ViewHolder {
private TextView mTextView;
diff --git a/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudProvider.java b/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudProvider.java
index d8be8138b..9f9249a33 100644
--- a/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudProvider.java
+++ b/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudProvider.java
@@ -51,7 +51,7 @@ import java.util.Set;
* Manages documents and exposes them to the Android system for sharing.
*/
public class MyCloudProvider extends DocumentsProvider {
- private static final String TAG = MyCloudProvider.class.getSimpleName();
+ private static final String TAG = "MyCloudProvider";
// Use these as the default columns to return information about a root if no specific
// columns are requested in a query.
diff --git a/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudFragment.java b/samples/browseable/StorageProvider/src/com.example.android.storageprovider/StorageProviderFragment.java
similarity index 96%
rename from samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudFragment.java
rename to samples/browseable/StorageProvider/src/com.example.android.storageprovider/StorageProviderFragment.java
index f624e908a..80d0296d5 100644
--- a/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudFragment.java
+++ b/samples/browseable/StorageProvider/src/com.example.android.storageprovider/StorageProviderFragment.java
@@ -31,9 +31,9 @@ import com.example.android.common.logger.Log;
* Toggles the user's login status via a login menu option, and enables/disables the cloud storage
* content provider.
*/
-public class MyCloudFragment extends Fragment {
+public class StorageProviderFragment extends Fragment {
- private static final String TAG = "MyCloudFragment";
+ private static final String TAG = "StorageProviderFragment";
private static final String AUTHORITY = "com.example.android.storageprovider.documents";
private boolean mLoggedIn = false;
diff --git a/samples/browseable/SynchronizedNotifications/Application/AndroidManifest.xml b/samples/browseable/SynchronizedNotifications/Application/AndroidManifest.xml
index 1737c7da0..04a69e01b 100644
--- a/samples/browseable/SynchronizedNotifications/Application/AndroidManifest.xml
+++ b/samples/browseable/SynchronizedNotifications/Application/AndroidManifest.xml
@@ -23,7 +23,7 @@
android:versionName="1.0">
+ android:targetSdkVersion="23" />
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/Timer/AndroidManifest.xml b/samples/browseable/Timer/AndroidManifest.xml
index 364fb5a68..59634fc49 100644
--- a/samples/browseable/Timer/AndroidManifest.xml
+++ b/samples/browseable/Timer/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.timer" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/WatchFace/Application/AndroidManifest.xml b/samples/browseable/WatchFace/Application/AndroidManifest.xml
index 5433c94f9..723e4e573 100644
--- a/samples/browseable/WatchFace/Application/AndroidManifest.xml
+++ b/samples/browseable/WatchFace/Application/AndroidManifest.xml
@@ -18,13 +18,19 @@
package="com.example.android.wearable.watchface" >
+ android:targetSdkVersion="23" />
+
+
+
+
+
@@ -55,6 +61,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/WatchFace/Application/res/layout/activity_fit_watch_face_config.xml b/samples/browseable/WatchFace/Application/res/layout/activity_fit_watch_face_config.xml
new file mode 100644
index 000000000..73d14891b
--- /dev/null
+++ b/samples/browseable/WatchFace/Application/res/layout/activity_fit_watch_face_config.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/WatchFace/Application/res/values-w820dp/dimens.xml b/samples/browseable/WatchFace/Application/res/values-w820dp/dimens.xml
new file mode 100644
index 000000000..63fc81644
--- /dev/null
+++ b/samples/browseable/WatchFace/Application/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/samples/browseable/WatchFace/Application/res/values/base-strings.xml b/samples/browseable/WatchFace/Application/res/values/base-strings.xml
index 1b346c9b8..49e710098 100644
--- a/samples/browseable/WatchFace/Application/res/values/base-strings.xml
+++ b/samples/browseable/WatchFace/Application/res/values/base-strings.xml
@@ -23,8 +23,13 @@
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, interactive, etc. It also includes a watch-side configuration example.
+opengl, calendar, steps, interactive, etc. It also includes a watch-side configuration example.
The phone app includes a phone-side configuration example.
+
+Additional note on Steps WatchFace Sample, if the user has not installed or setup the Google Fit app
+on their phone and their Wear device has not configured the Google Fit Wear App, then you may get
+zero steps until one of the two is setup. Please note, many Wear devices configure the Google Fit
+Wear App beforehand.
]]>
diff --git a/samples/browseable/WatchFace/Application/res/values/dimens.xml b/samples/browseable/WatchFace/Application/res/values/dimens.xml
new file mode 100644
index 000000000..56dca871d
--- /dev/null
+++ b/samples/browseable/WatchFace/Application/res/values/dimens.xml
@@ -0,0 +1,8 @@
+
+
+
+ 64dp
+ 10dp
+
diff --git a/samples/browseable/WatchFace/Application/res/values/strings.xml b/samples/browseable/WatchFace/Application/res/values/strings.xml
index 6c6834f06..275dcd3f0 100644
--- a/samples/browseable/WatchFace/Application/res/values/strings.xml
+++ b/samples/browseable/WatchFace/Application/res/values/strings.xml
@@ -22,6 +22,8 @@
MinutesSeconds
+ Google Fit
+
No wearable device is currently connected.OK
diff --git a/samples/browseable/WatchFace/Application/src/com.example.android.wearable.watchface/FitDistanceWatchFaceConfigActivity.java b/samples/browseable/WatchFace/Application/src/com.example.android.wearable.watchface/FitDistanceWatchFaceConfigActivity.java
new file mode 100644
index 000000000..1d8e4c9be
--- /dev/null
+++ b/samples/browseable/WatchFace/Application/src/com.example.android.wearable.watchface/FitDistanceWatchFaceConfigActivity.java
@@ -0,0 +1,255 @@
+package com.example.android.wearable.watchface;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+import com.google.android.gms.common.Scopes;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Scope;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.fitness.Fitness;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Switch;
+import android.widget.Toast;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Allows users of the Fit WatchFace to tie their Google Fit account to the WatchFace.
+ */
+public class FitDistanceWatchFaceConfigActivity extends Activity implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener {
+
+ private static final String TAG = "FitDistanceConfig";
+
+ // Request code for launching the Intent to resolve authorization.
+ private static final int REQUEST_OAUTH = 1;
+
+ // Shared Preference used to record if the user has enabled Google Fit previously.
+ private static final String PREFS_FIT_ENABLED_BY_USER =
+ "com.example.android.wearable.watchface.preferences.FIT_ENABLED_BY_USER";
+
+ /* Tracks whether an authorization activity is stacking over the current activity, i.e., when
+ * a known auth error is being resolved, such as showing the account chooser or presenting a
+ * consent dialog. This avoids common duplications as might happen on screen rotations, etc.
+ */
+ private static final String EXTRA_AUTH_STATE_PENDING =
+ "com.example.android.wearable.watchface.extra.AUTH_STATE_PENDING";
+
+ private static final long FIT_DISABLE_TIMEOUT_SECS = TimeUnit.SECONDS.toMillis(5);;
+
+ private boolean mResolvingAuthorization;
+
+ private boolean mFitEnabled;
+
+ private GoogleApiClient mGoogleApiClient;
+
+ private Switch mFitAuthSwitch;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_fit_watch_face_config);
+
+ mFitAuthSwitch = (Switch) findViewById(R.id.fit_auth_switch);
+
+ if (savedInstanceState != null) {
+ mResolvingAuthorization =
+ savedInstanceState.getBoolean(EXTRA_AUTH_STATE_PENDING, false);
+ } else {
+ mResolvingAuthorization = false;
+ }
+
+ // Checks if user previously enabled/approved Google Fit.
+ SharedPreferences sharedPreferences = getPreferences(Context.MODE_PRIVATE);
+ mFitEnabled =
+ sharedPreferences.getBoolean(PREFS_FIT_ENABLED_BY_USER, false);
+
+ mGoogleApiClient = new GoogleApiClient.Builder(this)
+ .addApi(Fitness.HISTORY_API)
+ .addApi(Fitness.RECORDING_API)
+ .addApi(Fitness.CONFIG_API)
+ .addScope(new Scope(Scopes.FITNESS_LOCATION_READ_WRITE))
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ if ((mFitEnabled) && (mGoogleApiClient != null)) {
+
+ mFitAuthSwitch.setChecked(true);
+ mFitAuthSwitch.setEnabled(true);
+
+ mGoogleApiClient.connect();
+
+ } else {
+
+ mFitAuthSwitch.setChecked(false);
+ mFitAuthSwitch.setEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) {
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ bundle.putBoolean(EXTRA_AUTH_STATE_PENDING, mResolvingAuthorization);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ mResolvingAuthorization =
+ savedInstanceState.getBoolean(EXTRA_AUTH_STATE_PENDING, false);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.d(TAG, "onActivityResult()");
+
+ if (requestCode == REQUEST_OAUTH) {
+ mResolvingAuthorization = false;
+
+ if (resultCode == RESULT_OK) {
+ setUserFitPreferences(true);
+
+ if (!mGoogleApiClient.isConnecting() && !mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.connect();
+ }
+ } else {
+ // User cancelled authorization, reset the switch.
+ setUserFitPreferences(false);
+ }
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle connectionHint) {
+ Log.d(TAG, "onConnected: " + connectionHint);
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+
+ if (cause == GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST) {
+ Log.i(TAG, "Connection lost. Cause: Network Lost.");
+ } else if (cause == GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED) {
+ Log.i(TAG, "Connection lost. Reason: Service Disconnected");
+ } else {
+ Log.i(TAG, "onConnectionSuspended: " + cause);
+ }
+
+ mFitAuthSwitch.setChecked(false);
+ mFitAuthSwitch.setEnabled(true);
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ Log.d(TAG, "Connection to Google Fit failed. Cause: " + result.toString());
+
+ if (!result.hasResolution()) {
+ // User cancelled authorization, reset the switch.
+ mFitAuthSwitch.setChecked(false);
+ mFitAuthSwitch.setEnabled(true);
+ // Show the localized error dialog
+ GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), this, 0).show();
+ return;
+ }
+
+ // Resolve failure if not already trying/authorizing.
+ if (!mResolvingAuthorization) {
+ try {
+ Log.i(TAG, "Attempting to resolve failed GoogleApiClient connection");
+ mResolvingAuthorization = true;
+ result.startResolutionForResult(this, REQUEST_OAUTH);
+ } catch (IntentSender.SendIntentException e) {
+ Log.e(TAG, "Exception while starting resolution activity", e);
+ }
+ }
+ }
+
+ public void onSwitchClicked(View view) {
+
+ boolean userWantsToEnableFit = mFitAuthSwitch.isChecked();
+
+ if (userWantsToEnableFit) {
+
+ Log.d(TAG, "User wants to enable Fit.");
+ if ((mGoogleApiClient != null) && (!mGoogleApiClient.isConnected())) {
+ mGoogleApiClient.connect();
+ }
+
+ } else {
+ Log.d(TAG, "User wants to disable Fit.");
+
+ // Disable switch until disconnect request is finished.
+ mFitAuthSwitch.setEnabled(false);
+
+ PendingResult pendingResult = Fitness.ConfigApi.disableFit(mGoogleApiClient);
+
+ pendingResult.setResultCallback(new ResultCallback() {
+ @Override
+ public void onResult(Status status) {
+
+ if (status.isSuccess()) {
+ Toast.makeText(
+ FitDistanceWatchFaceConfigActivity.this,
+ "Disconnected from Google Fit.",
+ Toast.LENGTH_LONG).show();
+
+ setUserFitPreferences(false);
+
+ mGoogleApiClient.disconnect();
+
+
+ } else {
+ Toast.makeText(
+ FitDistanceWatchFaceConfigActivity.this,
+ "Unable to disconnect from Google Fit. See logcat for details.",
+ Toast.LENGTH_LONG).show();
+
+ // Re-set the switch since auth failed.
+ setUserFitPreferences(true);
+ }
+ }
+ }, FIT_DISABLE_TIMEOUT_SECS, TimeUnit.SECONDS);
+ }
+ }
+
+ private void setUserFitPreferences(boolean userFitPreferences) {
+
+ mFitEnabled = userFitPreferences;
+ SharedPreferences sharedPreferences = getPreferences(Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putBoolean(PREFS_FIT_ENABLED_BY_USER, userFitPreferences);
+ editor.commit();
+
+ mFitAuthSwitch.setChecked(userFitPreferences);
+ mFitAuthSwitch.setEnabled(true);
+ }
+}
diff --git a/samples/browseable/WatchFace/Wearable/AndroidManifest.xml b/samples/browseable/WatchFace/Wearable/AndroidManifest.xml
index c96d73059..026107e74 100644
--- a/samples/browseable/WatchFace/Wearable/AndroidManifest.xml
+++ b/samples/browseable/WatchFace/Wearable/AndroidManifest.xml
@@ -1,5 +1,6 @@
-
-
+ package="com.example.android.wearable.watchface" >
-
+
@@ -29,102 +30,113 @@
+
+
+
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name" >
+
+
+
+
+ android:name=".AnalogWatchFaceService"
+ android:label="@string/analog_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_analog" />
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_analog_circular" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_ANALOG" />
+
-
+ android:name=".SweepWatchFaceService"
+ android:label="@string/sweep_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_analog" />
-
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_analog_circular" />
+
+
-
-
+ android:name=".OpenGLWatchFaceService"
+ android:label="@string/opengl_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -134,81 +146,130 @@
+
-
+
+
-
+ android:name=".DigitalWatchFaceService"
+ android:label="@string/digital_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_digital" />
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_digital_circular" />
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
+ android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
+
+
-
+ and android.intent.category.DEFAULT.
+ -->
+ android:name=".DigitalWatchFaceWearableConfigActivity"
+ android:label="@string/digital_config_name" >
+
+ android:name=".CalendarWatchFaceService"
+ android:label="@string/calendar_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_calendar" />
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_calendar_circular" />
+
+
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/ic_lock_open_white_24dp.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/ic_lock_open_white_24dp.png
new file mode 100644
index 000000000..6bae68f56
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/ic_lock_open_white_24dp.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance.png
new file mode 100644
index 000000000..a96f35579
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance_circular.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance_circular.png
new file mode 100644
index 000000000..912d85bbb
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance_circular.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit.png
new file mode 100644
index 000000000..04b8b5e95
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit_circular.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit_circular.png
new file mode 100644
index 000000000..b421e2898
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit_circular.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-mdpi/ic_lock_open_white_24dp.png b/samples/browseable/WatchFace/Wearable/res/drawable-mdpi/ic_lock_open_white_24dp.png
new file mode 100644
index 000000000..3f47b54cf
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-mdpi/ic_lock_open_white_24dp.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-xhdpi/ic_lock_open_white_24dp.png b/samples/browseable/WatchFace/Wearable/res/drawable-xhdpi/ic_lock_open_white_24dp.png
new file mode 100644
index 000000000..cbe9e1cd0
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-xhdpi/ic_lock_open_white_24dp.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-xxhdpi/ic_lock_open_white_24dp.png b/samples/browseable/WatchFace/Wearable/res/drawable-xxhdpi/ic_lock_open_white_24dp.png
new file mode 100644
index 000000000..1d1b0f4d3
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-xxhdpi/ic_lock_open_white_24dp.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/layout/activity_calendar_watch_face_permission.xml b/samples/browseable/WatchFace/Wearable/res/layout/activity_calendar_watch_face_permission.xml
new file mode 100644
index 000000000..bf0e3f670
--- /dev/null
+++ b/samples/browseable/WatchFace/Wearable/res/layout/activity_calendar_watch_face_permission.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/WatchFace/Wearable/res/values/dimens.xml b/samples/browseable/WatchFace/Wearable/res/values/dimens.xml
index 4973466ec..0b0672b66 100644
--- a/samples/browseable/WatchFace/Wearable/res/values/dimens.xml
+++ b/samples/browseable/WatchFace/Wearable/res/values/dimens.xml
@@ -32,4 +32,15 @@
72dp84dp25dp
+ 40dp
+ 45dp
+ 20dp
+ 25dp
+ 30dp
+ 15dp
+ 25dp
+ 20dp
+ 30dp
+ 80dp
+ 25dp
diff --git a/samples/browseable/WatchFace/Wearable/res/values/strings.xml b/samples/browseable/WatchFace/Wearable/res/values/strings.xml
index 19bc3e7fe..4090995de 100644
--- a/samples/browseable/WatchFace/Wearable/res/values/strings.xml
+++ b/samples/browseable/WatchFace/Wearable/res/values/strings.xml
@@ -25,11 +25,22 @@
Digital watch face configurationAMPM
+
+ Sample Fit Steps
+ Sample Fit Distance
+ AM
+ PM
+ %1$d steps
+ %1$,.2f meters
+
Sample Calendar
+ <br><br><br>WatchFace requires Calendar permission. Click on this WatchFace or visit Settings > Permissions to approve.<br><br><br>You have <b>%1$d</b> meeting in the next 24 hours.<br><br><br>You have <b>%1$d</b> meetings in the next 24 hours.
+ Calendar Permission Activity
+ WatchFace requires Calendar access.Black
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFacePermissionActivity.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFacePermissionActivity.java
new file mode 100644
index 000000000..7effd33dd
--- /dev/null
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFacePermissionActivity.java
@@ -0,0 +1,56 @@
+package com.example.android.wearable.watchface;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.wearable.activity.WearableActivity;
+import android.util.Log;
+import android.view.View;
+
+/**
+ * Simple Activity for displaying Calendar Permission Rationale to user.
+ */
+public class CalendarWatchFacePermissionActivity extends WearableActivity {
+
+ private static final String TAG = "PermissionActivity";
+
+ /* Id to identify permission request for calendar. */
+ private static final int PERMISSION_REQUEST_READ_CALENDAR = 1;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_calendar_watch_face_permission);
+ setAmbientEnabled();
+ }
+
+ public void onClickEnablePermission(View view) {
+ Log.d(TAG, "onClickEnablePermission()");
+
+ // On 23+ (M+) devices, GPS permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{Manifest.permission.READ_CALENDAR},
+ PERMISSION_REQUEST_READ_CALENDAR);
+
+ }
+
+ /*
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+
+ Log.d(TAG, "onRequestPermissionsResult()");
+
+ if (requestCode == PERMISSION_REQUEST_READ_CALENDAR) {
+ if ((grantResults.length == 1)
+ && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
+ finish();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFaceService.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFaceService.java
index a8ab95568..98a251cd1 100644
--- a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFaceService.java
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFaceService.java
@@ -16,11 +16,13 @@
package com.example.android.wearable.watchface;
+import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -30,8 +32,10 @@ import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
+import android.support.v4.app.ActivityCompat;
import android.support.wearable.provider.WearableCalendarContract;
import android.support.wearable.watchface.CanvasWatchFaceService;
+import android.support.wearable.watchface.WatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.text.DynamicLayout;
import android.text.Editable;
@@ -74,31 +78,37 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
final TextPaint mTextPaint = new TextPaint();
int mNumMeetings;
+ private boolean mCalendarPermissionApproved;
+ private String mCalendarNotApprovedMessage;
private AsyncTask mLoadMeetingsTask;
+ private boolean mIsReceiverRegistered;
+
/** 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();
+
+ // Loads meetings.
+ if (mCalendarPermissionApproved) {
+ mLoadMeetingsTask = new LoadMeetingsTask();
+ mLoadMeetingsTask.execute();
+ }
break;
}
}
};
- private boolean mIsReceiverRegistered;
-
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_PROVIDER_CHANGED.equals(intent.getAction())
&& WearableCalendarContract.CONTENT_URI.equals(intent.getData())) {
- cancelLoadMeetingTask();
mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
}
}
@@ -106,29 +116,59 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
@Override
public void onCreate(SurfaceHolder holder) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCreate");
- }
super.onCreate(holder);
+ Log.d(TAG, "onCreate");
+
+ mCalendarNotApprovedMessage =
+ getResources().getString(R.string.calendar_permission_not_approved);
+
+ /* Accepts tap events to allow permission changes by user. */
setWatchFaceStyle(new WatchFaceStyle.Builder(CalendarWatchFaceService.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
+ .setAcceptsTapEvents(true)
.build());
mTextPaint.setColor(FOREGROUND_COLOR);
mTextPaint.setTextSize(TEXT_SIZE);
- mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
+ // Enables app to handle 23+ (M+) style permissions.
+ mCalendarPermissionApproved =
+ ActivityCompat.checkSelfPermission(
+ getApplicationContext(),
+ Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED;
+
+ if (mCalendarPermissionApproved) {
+ mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
+ }
}
@Override
public void onDestroy() {
mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);
- cancelLoadMeetingTask();
super.onDestroy();
}
+ /*
+ * Captures tap event (and tap type) and increments correct tap type total.
+ */
+ @Override
+ public void onTapCommand(int tapType, int x, int y, long eventTime) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Tap Command: " + tapType);
+ }
+
+ // Ignore lint error (fixed in wearable support library 1.4)
+ if (tapType == WatchFaceService.TAP_TYPE_TAP && !mCalendarPermissionApproved) {
+ Intent permissionIntent = new Intent(
+ getApplicationContext(),
+ CalendarWatchFacePermissionActivity.class);
+ permissionIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(permissionIntent);
+ }
+ }
+
@Override
public void onDraw(Canvas canvas, Rect bounds) {
// Create or update mLayout if necessary.
@@ -141,8 +181,13 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
// Update the contents of mEditable.
mEditable.clear();
- mEditable.append(Html.fromHtml(getResources().getQuantityString(
- R.plurals.calendar_meetings, mNumMeetings, mNumMeetings)));
+
+ if (mCalendarPermissionApproved) {
+ mEditable.append(Html.fromHtml(getResources().getQuantityString(
+ R.plurals.calendar_meetings, mNumMeetings, mNumMeetings)));
+ } else {
+ mEditable.append(Html.fromHtml(mCalendarNotApprovedMessage));
+ }
// Draw the text on a solid background.
canvas.drawColor(BACKGROUND_COLOR);
@@ -151,15 +196,24 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
@Override
public void onVisibilityChanged(boolean visible) {
+ Log.d(TAG, "onVisibilityChanged()");
super.onVisibilityChanged(visible);
if (visible) {
- IntentFilter filter = new IntentFilter(Intent.ACTION_PROVIDER_CHANGED);
- filter.addDataScheme("content");
- filter.addDataAuthority(WearableCalendarContract.AUTHORITY, null);
- registerReceiver(mBroadcastReceiver, filter);
- mIsReceiverRegistered = true;
- mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
+ // Enables app to handle 23+ (M+) style permissions.
+ mCalendarPermissionApproved = ActivityCompat.checkSelfPermission(
+ getApplicationContext(),
+ Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED;
+
+ if (mCalendarPermissionApproved) {
+ IntentFilter filter = new IntentFilter(Intent.ACTION_PROVIDER_CHANGED);
+ filter.addDataScheme("content");
+ filter.addDataAuthority(WearableCalendarContract.AUTHORITY, null);
+ registerReceiver(mBroadcastReceiver, filter);
+ mIsReceiverRegistered = true;
+
+ mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
+ }
} else {
if (mIsReceiverRegistered) {
unregisterReceiver(mBroadcastReceiver);
@@ -204,9 +258,9 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
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);
- }
+
+ Log.d(TAG, "Num meetings: " + numMeetings);
+
return numMeetings;
}
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitDistanceWatchFaceService.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitDistanceWatchFaceService.java
new file mode 100644
index 000000000..b29a1902d
--- /dev/null
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitDistanceWatchFaceService.java
@@ -0,0 +1,533 @@
+/*
+ * 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.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+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.WatchFaceStyle;
+import android.text.format.DateFormat;
+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.Scopes;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Scope;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.fitness.Fitness;
+import com.google.android.gms.fitness.FitnessStatusCodes;
+import com.google.android.gms.fitness.data.DataPoint;
+import com.google.android.gms.fitness.data.DataType;
+import com.google.android.gms.fitness.data.Field;
+import com.google.android.gms.fitness.result.DailyTotalResult;
+
+import java.util.Calendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Displays the user's daily distance total via Google Fit. Distance is polled initially when the
+ * Google API Client successfully connects and once a minute after that via the onTimeTick callback.
+ * If you want more frequent updates, you will want to add your own Handler.
+ *
+ * Authentication IS a requirement to request distance from Google Fit on Wear. Otherwise, distance
+ * will always come back as zero (or stay at whatever the distance was prior to you
+ * de-authorizing watchface).
+ *
+ * In ambient mode, the seconds are replaced with an AM/PM indicator.
+ *
+ * On devices with low-bit ambient mode, the text is drawn without anti-aliasing. On devices which
+ * require burn-in protection, the hours are drawn in normal rather than bold.
+ *
+ */
+public class FitDistanceWatchFaceService extends CanvasWatchFaceService {
+
+ private static final String TAG = "DistanceWatchFace";
+
+ 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 active mode (non-ambient).
+ */
+ private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends CanvasWatchFaceService.Engine implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ ResultCallback {
+
+ private static final int BACKGROUND_COLOR = Color.BLACK;
+ private static final int TEXT_HOURS_MINS_COLOR = Color.WHITE;
+ private static final int TEXT_SECONDS_COLOR = Color.GRAY;
+ private static final int TEXT_AM_PM_COLOR = Color.GRAY;
+ private static final int TEXT_COLON_COLOR = Color.GRAY;
+ private static final int TEXT_DISTANCE_COUNT_COLOR = Color.GRAY;
+
+ private static final String COLON_STRING = ":";
+
+ private static final int MSG_UPDATE_TIME = 0;
+
+ /* Handler to update the time periodically in interactive mode. */
+ private final Handler mUpdateTimeHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_UPDATE_TIME:
+ Log.v(TAG, "updating time");
+ invalidate();
+ if (shouldUpdateTimeHandlerBeRunning()) {
+ long timeMs = System.currentTimeMillis();
+ long delayMs =
+ ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
+ mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
+ }
+ break;
+ }
+ }
+ };
+
+ /**
+ * Handles time zone and locale changes.
+ */
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mCalendar.setTimeZone(TimeZone.getDefault());
+ invalidate();
+ }
+ };
+
+ /**
+ * Unregistering an unregistered receiver throws an exception. Keep track of the
+ * registration state to prevent that.
+ */
+ private boolean mRegisteredReceiver = false;
+
+ private Paint mHourPaint;
+ private Paint mMinutePaint;
+ private Paint mSecondPaint;
+ private Paint mAmPmPaint;
+ private Paint mColonPaint;
+ private Paint mDistanceCountPaint;
+
+ private float mColonWidth;
+
+ private Calendar mCalendar;
+
+ private float mXOffset;
+ private float mXDistanceOffset;
+ private float mYOffset;
+ private float mLineHeight;
+
+ private String mAmString;
+ private String mPmString;
+
+
+ /**
+ * Whether the display supports fewer bits for each color in ambient mode. When true, we
+ * disable anti-aliasing in ambient mode.
+ */
+ private boolean mLowBitAmbient;
+
+ /*
+ * Google API Client used to make Google Fit requests for step data.
+ */
+ private GoogleApiClient mGoogleApiClient;
+
+ private boolean mDistanceRequested;
+
+ private float mDistanceTotal = 0;
+
+ @Override
+ public void onCreate(SurfaceHolder holder) {
+ Log.d(TAG, "onCreate");
+
+ super.onCreate(holder);
+
+ mDistanceRequested = false;
+ mGoogleApiClient = new GoogleApiClient.Builder(FitDistanceWatchFaceService.this)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .addApi(Fitness.HISTORY_API)
+ .addApi(Fitness.RECORDING_API)
+ .addScope(new Scope(Scopes.FITNESS_LOCATION_READ))
+ // When user has multiple accounts, useDefaultAccount() allows Google Fit to
+ // associated with the main account for steps. It also replaces the need for
+ // a scope request.
+ .useDefaultAccount()
+ .build();
+
+ setWatchFaceStyle(new WatchFaceStyle.Builder(FitDistanceWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setShowSystemUiTime(false)
+ .build());
+
+ Resources resources = getResources();
+
+ mYOffset = resources.getDimension(R.dimen.fit_y_offset);
+ mLineHeight = resources.getDimension(R.dimen.fit_line_height);
+ mAmString = resources.getString(R.string.fit_am);
+ mPmString = resources.getString(R.string.fit_pm);
+
+ mHourPaint = createTextPaint(TEXT_HOURS_MINS_COLOR, BOLD_TYPEFACE);
+ mMinutePaint = createTextPaint(TEXT_HOURS_MINS_COLOR);
+ mSecondPaint = createTextPaint(TEXT_SECONDS_COLOR);
+ mAmPmPaint = createTextPaint(TEXT_AM_PM_COLOR);
+ mColonPaint = createTextPaint(TEXT_COLON_COLOR);
+ mDistanceCountPaint = createTextPaint(TEXT_DISTANCE_COUNT_COLOR);
+
+ mCalendar = Calendar.getInstance();
+
+ }
+
+ @Override
+ public void onDestroy() {
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ super.onDestroy();
+ }
+
+ private Paint createTextPaint(int color) {
+ return createTextPaint(color, NORMAL_TYPEFACE);
+ }
+
+ private Paint createTextPaint(int color, Typeface typeface) {
+ Paint paint = new Paint();
+ paint.setColor(color);
+ paint.setTypeface(typeface);
+ paint.setAntiAlias(true);
+ return paint;
+ }
+
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ Log.d(TAG, "onVisibilityChanged: " + visible);
+
+ super.onVisibilityChanged(visible);
+
+ if (visible) {
+ mGoogleApiClient.connect();
+
+ registerReceiver();
+
+ // Update time zone and date formats, in case they changed while we weren't visible.
+ mCalendar.setTimeZone(TimeZone.getDefault());
+ } else {
+ unregisterReceiver();
+
+ if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ // Whether the timer should be running depends on whether we're visible (as well as
+ // whether we're in ambient mode), so we may need to start or stop the timer.
+ updateTimer();
+ }
+
+
+ private void registerReceiver() {
+ if (mRegisteredReceiver) {
+ return;
+ }
+ mRegisteredReceiver = true;
+ IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+ FitDistanceWatchFaceService.this.registerReceiver(mReceiver, filter);
+ }
+
+ private void unregisterReceiver() {
+ if (!mRegisteredReceiver) {
+ return;
+ }
+ mRegisteredReceiver = false;
+ FitDistanceWatchFaceService.this.unregisterReceiver(mReceiver);
+ }
+
+ @Override
+ public void onApplyWindowInsets(WindowInsets insets) {
+ Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square"));
+
+ super.onApplyWindowInsets(insets);
+
+ // Load resources that have alternate values for round watches.
+ Resources resources = FitDistanceWatchFaceService.this.getResources();
+ boolean isRound = insets.isRound();
+ mXOffset = resources.getDimension(isRound
+ ? R.dimen.fit_x_offset_round : R.dimen.fit_x_offset);
+ mXDistanceOffset =
+ resources.getDimension(
+ isRound ?
+ R.dimen.fit_steps_or_distance_x_offset_round :
+ R.dimen.fit_steps_or_distance_x_offset);
+ float textSize = resources.getDimension(isRound
+ ? R.dimen.fit_text_size_round : R.dimen.fit_text_size);
+ float amPmSize = resources.getDimension(isRound
+ ? R.dimen.fit_am_pm_size_round : R.dimen.fit_am_pm_size);
+
+ mHourPaint.setTextSize(textSize);
+ mMinutePaint.setTextSize(textSize);
+ mSecondPaint.setTextSize(textSize);
+ mAmPmPaint.setTextSize(amPmSize);
+ mColonPaint.setTextSize(textSize);
+ mDistanceCountPaint.setTextSize(
+ resources.getDimension(R.dimen.fit_steps_or_distance_text_size));
+
+ 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);
+
+ Log.d(TAG, "onPropertiesChanged: burn-in protection = " + burnInProtection
+ + ", low-bit ambient = " + mLowBitAmbient);
+
+ }
+
+ @Override
+ public void onTimeTick() {
+ super.onTimeTick();
+ Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
+ getTotalDistance();
+ invalidate();
+ }
+
+ @Override
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ super.onAmbientModeChanged(inAmbientMode);
+ Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
+
+ if (mLowBitAmbient) {
+ boolean antiAlias = !inAmbientMode;;
+ mHourPaint.setAntiAlias(antiAlias);
+ mMinutePaint.setAntiAlias(antiAlias);
+ mSecondPaint.setAntiAlias(antiAlias);
+ mAmPmPaint.setAntiAlias(antiAlias);
+ mColonPaint.setAntiAlias(antiAlias);
+ mDistanceCountPaint.setAntiAlias(antiAlias);
+ }
+ invalidate();
+
+ // Whether the timer should be running depends on whether we're in ambient mode (as well
+ // as whether we're visible), so we may need to start or stop the timer.
+ updateTimer();
+ }
+
+ private String formatTwoDigitNumber(int hour) {
+ return String.format("%02d", hour);
+ }
+
+ private String getAmPmString(int amPm) {
+ return amPm == Calendar.AM ? mAmString : mPmString;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas, Rect bounds) {
+ long now = System.currentTimeMillis();
+ mCalendar.setTimeInMillis(now);
+ boolean is24Hour = DateFormat.is24HourFormat(FitDistanceWatchFaceService.this);
+
+ // Draw the background.
+ canvas.drawColor(BACKGROUND_COLOR);
+
+ // Draw the hours.
+ float x = mXOffset;
+ String hourString;
+ if (is24Hour) {
+ hourString = formatTwoDigitNumber(mCalendar.get(Calendar.HOUR_OF_DAY));
+ } else {
+ int hour = mCalendar.get(Calendar.HOUR);
+ if (hour == 0) {
+ hour = 12;
+ }
+ hourString = String.valueOf(hour);
+ }
+ canvas.drawText(hourString, x, mYOffset, mHourPaint);
+ x += mHourPaint.measureText(hourString);
+
+ // Draw first colon (between hour and minute).
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+
+ x += mColonWidth;
+
+ // Draw the minutes.
+ String minuteString = formatTwoDigitNumber(mCalendar.get(Calendar.MINUTE));
+ canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
+ x += mMinutePaint.measureText(minuteString);
+
+ // In interactive mode, draw a second colon followed by the seconds.
+ // Otherwise, if we're in 12-hour mode, draw AM/PM
+ if (!isInAmbientMode()) {
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+
+ x += mColonWidth;
+ canvas.drawText(formatTwoDigitNumber(
+ mCalendar.get(Calendar.SECOND)), x, mYOffset, mSecondPaint);
+ } else if (!is24Hour) {
+ x += mColonWidth;
+ canvas.drawText(getAmPmString(
+ mCalendar.get(Calendar.AM_PM)), x, mYOffset, mAmPmPaint);
+ }
+
+ // Only render distance if there is no peek card, so they do not bleed into each other
+ // in ambient mode.
+ if (getPeekCardPosition().isEmpty()) {
+ canvas.drawText(
+ getString(R.string.fit_distance, mDistanceTotal),
+ mXDistanceOffset,
+ mYOffset + mLineHeight,
+ mDistanceCountPaint);
+ }
+ }
+
+ /**
+ * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
+ * or stops it if it shouldn't be running but currently is.
+ */
+ private void updateTimer() {
+ Log.d(TAG, "updateTimer");
+
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ if (shouldUpdateTimeHandlerBeRunning()) {
+ mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
+ }
+ }
+
+ /**
+ * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
+ * only run when we're visible and in interactive mode.
+ */
+ private boolean shouldUpdateTimeHandlerBeRunning() {
+ return isVisible() && !isInAmbientMode();
+ }
+
+ private void getTotalDistance() {
+
+ Log.d(TAG, "getTotalDistance()");
+
+ if ((mGoogleApiClient != null)
+ && (mGoogleApiClient.isConnected())
+ && (!mDistanceRequested)) {
+
+ mDistanceRequested = true;
+
+ PendingResult distanceResult =
+ Fitness.HistoryApi.readDailyTotal(
+ mGoogleApiClient,
+ DataType.TYPE_DISTANCE_DELTA);
+
+ distanceResult.setResultCallback(this);
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle connectionHint) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnected: " + connectionHint);
+
+ mDistanceRequested = false;
+
+ // Subscribe covers devices that do not have Google Fit installed.
+ subscribeToDistance();
+
+ getTotalDistance();
+ }
+
+ /*
+ * Subscribes to distance.
+ */
+ private void subscribeToDistance() {
+
+ if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnecting())) {
+
+ Fitness.RecordingApi.subscribe(mGoogleApiClient, DataType.TYPE_DISTANCE_DELTA)
+ .setResultCallback(new ResultCallback() {
+ @Override
+ public void onResult(Status status) {
+ if (status.isSuccess()) {
+ if (status.getStatusCode()
+ == FitnessStatusCodes.SUCCESS_ALREADY_SUBSCRIBED) {
+ Log.i(TAG, "Existing subscription for activity detected.");
+ } else {
+ Log.i(TAG, "Successfully subscribed!");
+ }
+ } else {
+ Log.i(TAG, "There was a problem subscribing.");
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionSuspended: " + cause);
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionFailed: " + result);
+ }
+
+ @Override
+ public void onResult(DailyTotalResult dailyTotalResult) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onResult(): " + dailyTotalResult);
+
+ mDistanceRequested = false;
+
+ if (dailyTotalResult.getStatus().isSuccess()) {
+
+ List points = dailyTotalResult.getTotal().getDataPoints();
+
+ if (!points.isEmpty()) {
+ mDistanceTotal = points.get(0).getValue(Field.FIELD_DISTANCE).asFloat();
+ Log.d(TAG, "distance updated: " + mDistanceTotal);
+ }
+ } else {
+ Log.e(TAG, "onResult() failed! " + dailyTotalResult.getStatus().getStatusMessage());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitStepsWatchFaceService.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitStepsWatchFaceService.java
new file mode 100644
index 000000000..1f7b298f2
--- /dev/null
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitStepsWatchFaceService.java
@@ -0,0 +1,542 @@
+/*
+ * 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 com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.fitness.Fitness;
+import com.google.android.gms.fitness.FitnessStatusCodes;
+import com.google.android.gms.fitness.data.DataPoint;
+import com.google.android.gms.fitness.data.DataType;
+import com.google.android.gms.fitness.data.Field;
+import com.google.android.gms.fitness.result.DailyTotalResult;
+
+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.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+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.WatchFaceStyle;
+import android.text.format.DateFormat;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.WindowInsets;
+
+import java.util.Calendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The step count watch face shows user's daily step total via Google Fit (matches Google Fit app).
+ * Steps are polled initially when the Google API Client successfully connects and once a minute
+ * after that via the onTimeTick callback. If you want more frequent updates, you will want to add
+ * your own Handler.
+ *
+ * Authentication is not a requirement to request steps from Google Fit on Wear.
+ *
+ * In ambient mode, the seconds are replaced with an AM/PM indicator.
+ *
+ * On devices with low-bit ambient mode, the text is drawn without anti-aliasing. On devices which
+ * require burn-in protection, the hours are drawn in normal rather than bold.
+ *
+ */
+public class FitStepsWatchFaceService extends CanvasWatchFaceService {
+
+ private static final String TAG = "StepCountWatchFace";
+
+ 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 active mode (non-ambient).
+ */
+ private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends CanvasWatchFaceService.Engine implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ ResultCallback {
+
+ private static final int BACKGROUND_COLOR = Color.BLACK;
+ private static final int TEXT_HOURS_MINS_COLOR = Color.WHITE;
+ private static final int TEXT_SECONDS_COLOR = Color.GRAY;
+ private static final int TEXT_AM_PM_COLOR = Color.GRAY;
+ private static final int TEXT_COLON_COLOR = Color.GRAY;
+ private static final int TEXT_STEP_COUNT_COLOR = Color.GRAY;
+
+ private static final String COLON_STRING = ":";
+
+ private static final int MSG_UPDATE_TIME = 0;
+
+ /* Handler to update the time periodically in interactive mode. */
+ private 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 (shouldUpdateTimeHandlerBeRunning()) {
+ long timeMs = System.currentTimeMillis();
+ long delayMs =
+ ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
+ mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
+ }
+ break;
+ }
+ }
+ };
+
+ /**
+ * Handles time zone and locale changes.
+ */
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mCalendar.setTimeZone(TimeZone.getDefault());
+ invalidate();
+ }
+ };
+
+ /**
+ * Unregistering an unregistered receiver throws an exception. Keep track of the
+ * registration state to prevent that.
+ */
+ private boolean mRegisteredReceiver = false;
+
+ private Paint mHourPaint;
+ private Paint mMinutePaint;
+ private Paint mSecondPaint;
+ private Paint mAmPmPaint;
+ private Paint mColonPaint;
+ private Paint mStepCountPaint;
+
+ private float mColonWidth;
+
+ private Calendar mCalendar;
+
+ private float mXOffset;
+ private float mXStepsOffset;
+ private float mYOffset;
+ private float mLineHeight;
+
+ private String mAmString;
+ private String mPmString;
+
+
+ /**
+ * Whether the display supports fewer bits for each color in ambient mode. When true, we
+ * disable anti-aliasing in ambient mode.
+ */
+ private boolean mLowBitAmbient;
+
+ /*
+ * Google API Client used to make Google Fit requests for step data.
+ */
+ private GoogleApiClient mGoogleApiClient;
+
+ private boolean mStepsRequested;
+
+ private int mStepsTotal = 0;
+
+ @Override
+ public void onCreate(SurfaceHolder holder) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate");
+ }
+
+ super.onCreate(holder);
+
+ mStepsRequested = false;
+ mGoogleApiClient = new GoogleApiClient.Builder(FitStepsWatchFaceService.this)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .addApi(Fitness.HISTORY_API)
+ .addApi(Fitness.RECORDING_API)
+ // When user has multiple accounts, useDefaultAccount() allows Google Fit to
+ // associated with the main account for steps. It also replaces the need for
+ // a scope request.
+ .useDefaultAccount()
+ .build();
+
+ setWatchFaceStyle(new WatchFaceStyle.Builder(FitStepsWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setShowSystemUiTime(false)
+ .build());
+
+ Resources resources = getResources();
+
+ mYOffset = resources.getDimension(R.dimen.fit_y_offset);
+ mLineHeight = resources.getDimension(R.dimen.fit_line_height);
+ mAmString = resources.getString(R.string.fit_am);
+ mPmString = resources.getString(R.string.fit_pm);
+
+ mHourPaint = createTextPaint(TEXT_HOURS_MINS_COLOR, BOLD_TYPEFACE);
+ mMinutePaint = createTextPaint(TEXT_HOURS_MINS_COLOR);
+ mSecondPaint = createTextPaint(TEXT_SECONDS_COLOR);
+ mAmPmPaint = createTextPaint(TEXT_AM_PM_COLOR);
+ mColonPaint = createTextPaint(TEXT_COLON_COLOR);
+ mStepCountPaint = createTextPaint(TEXT_STEP_COUNT_COLOR);
+
+ mCalendar = Calendar.getInstance();
+
+ }
+
+ @Override
+ public void onDestroy() {
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ super.onDestroy();
+ }
+
+ private Paint createTextPaint(int color) {
+ return createTextPaint(color, NORMAL_TYPEFACE);
+ }
+
+ private Paint createTextPaint(int color, Typeface typeface) {
+ Paint paint = new Paint();
+ paint.setColor(color);
+ paint.setTypeface(typeface);
+ paint.setAntiAlias(true);
+ return paint;
+ }
+
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onVisibilityChanged: " + visible);
+ }
+ super.onVisibilityChanged(visible);
+
+ if (visible) {
+ mGoogleApiClient.connect();
+
+ registerReceiver();
+
+ // Update time zone and date formats, in case they changed while we weren't visible.
+ mCalendar.setTimeZone(TimeZone.getDefault());
+ } else {
+ unregisterReceiver();
+
+ if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ // Whether the timer should be running depends on whether we're visible (as well as
+ // whether we're in ambient mode), so we may need to start or stop the timer.
+ updateTimer();
+ }
+
+
+ private void registerReceiver() {
+ if (mRegisteredReceiver) {
+ return;
+ }
+ mRegisteredReceiver = true;
+ IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+ FitStepsWatchFaceService.this.registerReceiver(mReceiver, filter);
+ }
+
+ private void unregisterReceiver() {
+ if (!mRegisteredReceiver) {
+ return;
+ }
+ mRegisteredReceiver = false;
+ FitStepsWatchFaceService.this.unregisterReceiver(mReceiver);
+ }
+
+ @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 = FitStepsWatchFaceService.this.getResources();
+ boolean isRound = insets.isRound();
+ mXOffset = resources.getDimension(isRound
+ ? R.dimen.fit_x_offset_round : R.dimen.fit_x_offset);
+ mXStepsOffset = resources.getDimension(isRound
+ ? R.dimen.fit_steps_or_distance_x_offset_round : R.dimen.fit_steps_or_distance_x_offset);
+ float textSize = resources.getDimension(isRound
+ ? R.dimen.fit_text_size_round : R.dimen.fit_text_size);
+ float amPmSize = resources.getDimension(isRound
+ ? R.dimen.fit_am_pm_size_round : R.dimen.fit_am_pm_size);
+
+ mHourPaint.setTextSize(textSize);
+ mMinutePaint.setTextSize(textSize);
+ mSecondPaint.setTextSize(textSize);
+ mAmPmPaint.setTextSize(amPmSize);
+ mColonPaint.setTextSize(textSize);
+ mStepCountPaint.setTextSize(resources.getDimension(R.dimen.fit_steps_or_distance_text_size));
+
+ 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());
+ }
+
+ getTotalSteps();
+ invalidate();
+ }
+
+ @Override
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ super.onAmbientModeChanged(inAmbientMode);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
+ }
+
+ if (mLowBitAmbient) {
+ boolean antiAlias = !inAmbientMode;;
+ mHourPaint.setAntiAlias(antiAlias);
+ mMinutePaint.setAntiAlias(antiAlias);
+ mSecondPaint.setAntiAlias(antiAlias);
+ mAmPmPaint.setAntiAlias(antiAlias);
+ mColonPaint.setAntiAlias(antiAlias);
+ mStepCountPaint.setAntiAlias(antiAlias);
+ }
+ invalidate();
+
+ // Whether the timer should be running depends on whether we're in ambient mode (as well
+ // as whether we're visible), so we may need to start or stop the timer.
+ updateTimer();
+ }
+
+ private String formatTwoDigitNumber(int hour) {
+ return String.format("%02d", hour);
+ }
+
+ private String getAmPmString(int amPm) {
+ return amPm == Calendar.AM ? mAmString : mPmString;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas, Rect bounds) {
+ long now = System.currentTimeMillis();
+ mCalendar.setTimeInMillis(now);
+ boolean is24Hour = DateFormat.is24HourFormat(FitStepsWatchFaceService.this);
+
+ // Draw the background.
+ canvas.drawColor(BACKGROUND_COLOR);
+
+ // Draw the hours.
+ float x = mXOffset;
+ String hourString;
+ if (is24Hour) {
+ hourString = formatTwoDigitNumber(mCalendar.get(Calendar.HOUR_OF_DAY));
+ } else {
+ int hour = mCalendar.get(Calendar.HOUR);
+ if (hour == 0) {
+ hour = 12;
+ }
+ hourString = String.valueOf(hour);
+ }
+ canvas.drawText(hourString, x, mYOffset, mHourPaint);
+ x += mHourPaint.measureText(hourString);
+
+ // Draw first colon (between hour and minute).
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+
+ x += mColonWidth;
+
+ // Draw the minutes.
+ String minuteString = formatTwoDigitNumber(mCalendar.get(Calendar.MINUTE));
+ canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
+ x += mMinutePaint.measureText(minuteString);
+
+ // In interactive mode, draw a second colon followed by the seconds.
+ // Otherwise, if we're in 12-hour mode, draw AM/PM
+ if (!isInAmbientMode()) {
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+
+ x += mColonWidth;
+ canvas.drawText(formatTwoDigitNumber(
+ mCalendar.get(Calendar.SECOND)), x, mYOffset, mSecondPaint);
+ } else if (!is24Hour) {
+ x += mColonWidth;
+ canvas.drawText(getAmPmString(
+ mCalendar.get(Calendar.AM_PM)), x, mYOffset, mAmPmPaint);
+ }
+
+ // Only render steps if there is no peek card, so they do not bleed into each other
+ // in ambient mode.
+ if (getPeekCardPosition().isEmpty()) {
+ canvas.drawText(
+ getString(R.string.fit_steps, mStepsTotal),
+ mXStepsOffset,
+ mYOffset + mLineHeight,
+ mStepCountPaint);
+ }
+ }
+
+ /**
+ * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
+ * or stops it if it shouldn't be running but currently is.
+ */
+ private void updateTimer() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "updateTimer");
+ }
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ if (shouldUpdateTimeHandlerBeRunning()) {
+ mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
+ }
+ }
+
+ /**
+ * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
+ * only run when we're visible and in interactive mode.
+ */
+ private boolean shouldUpdateTimeHandlerBeRunning() {
+ return isVisible() && !isInAmbientMode();
+ }
+
+ private void getTotalSteps() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "getTotalSteps()");
+ }
+
+ if ((mGoogleApiClient != null)
+ && (mGoogleApiClient.isConnected())
+ && (!mStepsRequested)) {
+
+ mStepsRequested = true;
+
+ PendingResult stepsResult =
+ Fitness.HistoryApi.readDailyTotal(
+ mGoogleApiClient,
+ DataType.TYPE_STEP_COUNT_DELTA);
+
+ stepsResult.setResultCallback(this);
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle connectionHint) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnected: " + connectionHint);
+ }
+ mStepsRequested = false;
+
+ // The subscribe step covers devices that do not have Google Fit installed.
+ subscribeToSteps();
+
+ getTotalSteps();
+ }
+
+ /*
+ * Subscribes to step count (for phones that don't have Google Fit app).
+ */
+ private void subscribeToSteps() {
+ Fitness.RecordingApi.subscribe(mGoogleApiClient, DataType.TYPE_STEP_COUNT_DELTA)
+ .setResultCallback(new ResultCallback() {
+ @Override
+ public void onResult(Status status) {
+ if (status.isSuccess()) {
+ if (status.getStatusCode()
+ == FitnessStatusCodes.SUCCESS_ALREADY_SUBSCRIBED) {
+ Log.i(TAG, "Existing subscription for activity detected.");
+ } else {
+ Log.i(TAG, "Successfully subscribed!");
+ }
+ } else {
+ Log.i(TAG, "There was a problem subscribing.");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionSuspended: " + cause);
+ }
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionFailed: " + result);
+ }
+ }
+
+ @Override
+ public void onResult(DailyTotalResult dailyTotalResult) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onResult(): " + dailyTotalResult);
+ }
+
+ mStepsRequested = false;
+
+ if (dailyTotalResult.getStatus().isSuccess()) {
+
+ List points = dailyTotalResult.getTotal().getDataPoints();;
+
+ if (!points.isEmpty()) {
+ mStepsTotal = points.get(0).getValue(Field.FIELD_STEPS).asInt();
+ Log.d(TAG, "steps updated: " + mStepsTotal);
+ }
+ } else {
+ Log.e(TAG, "onResult() failed! " + dailyTotalResult.getStatus().getStatusMessage());
+ }
+ }
+ }
+}
diff --git a/samples/browseable/WatchFace/_index.jd b/samples/browseable/WatchFace/_index.jd
index 8b779d0cd..4367419fa 100644
--- a/samples/browseable/WatchFace/_index.jd
+++ b/samples/browseable/WatchFace/_index.jd
@@ -7,7 +7,12 @@ sample.group=Wearable
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, interactive, etc. It also includes a watch-side configuration example.
+opengl, calendar, steps, interactive, etc. It also includes a watch-side configuration example.
The phone app includes a phone-side configuration example.
+
+Additional note on Steps WatchFace Sample, if the user has not installed or setup the Google Fit app
+on their phone and their Wear device has not configured the Google Fit Wear App, then you may get
+zero steps until one of the two is setup. Please note, many Wear devices configure the Google Fit
+Wear App beforehand.
+
+A sample that shows how you can record voice using the microphone on a wearable and
+play the recorded voice or an mp3 file, if the wearable device has a built-in speaker.
+
+This sample doesn't have any companion phone app so you need to install this directly
+on your watch (using "adb").
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/circle.xml b/samples/browseable/WearSpeakerSample/res/drawable/circle.xml
new file mode 100644
index 000000000..df4abe529
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/circle.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_120dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_120dp.xml
new file mode 100644
index 000000000..0971d96de
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_120dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_32dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_32dp.xml
new file mode 100644
index 000000000..70de799c6
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_32dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_120dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_120dp.xml
new file mode 100644
index 000000000..15e798a29
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_120dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_32dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_32dp.xml
new file mode 100644
index 000000000..c9417dd2b
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_32dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_120dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_120dp.xml
new file mode 100644
index 000000000..e87660d2f
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_120dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_32dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_32dp.xml
new file mode 100644
index 000000000..9dd86787d
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_32dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/layout/main_activity.xml b/samples/browseable/WearSpeakerSample/res/layout/main_activity.xml
new file mode 100644
index 000000000..7e004ad42
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/layout/main_activity.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/WearSpeakerSample/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/WearSpeakerSample/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..cde69bccc
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/browseable/WearSpeakerSample/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/WearSpeakerSample/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c133a0cbd
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/browseable/WearSpeakerSample/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/WearSpeakerSample/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bfa42f0e7
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/browseable/WearSpeakerSample/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/WearSpeakerSample/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..324e72cdd
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/WearSpeakerSample/res/raw/sound.mp3 b/samples/browseable/WearSpeakerSample/res/raw/sound.mp3
new file mode 100644
index 000000000..94e3d0e53
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/raw/sound.mp3 differ
diff --git a/samples/browseable/WearSpeakerSample/res/values/colors.xml b/samples/browseable/WearSpeakerSample/res/values/colors.xml
new file mode 100644
index 000000000..e9b8605f2
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/values/colors.xml
@@ -0,0 +1,21 @@
+
+
+
+ #FFF3E0
+ #FFF3E0
+ #FF9100
+ #E65100
+ #FFD180
+ #E65100
+
\ No newline at end of file
diff --git a/samples/browseable/WearSpeakerSample/res/values/strings.xml b/samples/browseable/WearSpeakerSample/res/values/strings.xml
new file mode 100644
index 000000000..cc342b5cb
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/values/strings.xml
@@ -0,0 +1,18 @@
+
+
+
+ Wear Speaker Sample
+ Recording Audio permission is required, exiting now!
+ Speaker is not supported
+
diff --git a/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/MainActivity.java b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/MainActivity.java
new file mode 100644
index 000000000..e7a4870fe
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/MainActivity.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * 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.speaker;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.wearable.activity.WearableActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * We first get the required permission to use the MIC. If it is granted, then we continue with
+ * the application and present the UI with three icons: a MIC icon (if pressed, user can record up
+ * to 10 seconds), a Play icon (if clicked, it wil playback the recorded audio file) and a music
+ * note icon (if clicked, it plays an MP3 file that is included in the app).
+ */
+public class MainActivity extends WearableActivity implements UIAnimation.UIStateListener,
+ SoundRecorder.OnVoicePlaybackStateChangedListener {
+
+ private static final String TAG = "MainActivity";
+ private static final int PERMISSIONS_REQUEST_CODE = 100;
+ private static final long COUNT_DOWN_MS = TimeUnit.SECONDS.toMillis(10);
+ private static final long MILLIS_IN_SECOND = TimeUnit.SECONDS.toMillis(1);
+ private static final String VOICE_FILE_NAME = "audiorecord.pcm";
+ private MediaPlayer mMediaPlayer;
+ private AppState mState = AppState.READY;
+ private UIAnimation.UIState mUiState = UIAnimation.UIState.HOME;
+ private SoundRecorder mSoundRecorder;
+
+ private UIAnimation mUIAnimation;
+ private ProgressBar mProgressBar;
+ private CountDownTimer mCountDownTimer;
+
+ enum AppState {
+ READY, PLAYING_VOICE, PLAYING_MUSIC, RECORDING
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+ mProgressBar = (ProgressBar) findViewById(R.id.progress);
+ mProgressBar.setMax((int) (COUNT_DOWN_MS / MILLIS_IN_SECOND));
+ setAmbientEnabled();
+ }
+
+ private void setProgressBar(long progressInMillis) {
+ mProgressBar.setProgress((int) (progressInMillis / MILLIS_IN_SECOND));
+ }
+
+ @Override
+ public void onUIStateChanged(UIAnimation.UIState state) {
+ Log.d(TAG, "UI State is: " + state);
+ if (mUiState == state) {
+ return;
+ }
+ switch (state) {
+ case MUSIC_UP:
+ mState = AppState.PLAYING_MUSIC;
+ mUiState = state;
+ playMusic();
+ break;
+ case MIC_UP:
+ mState = AppState.RECORDING;
+ mUiState = state;
+ mSoundRecorder.startRecording();
+ setProgressBar(COUNT_DOWN_MS);
+ mCountDownTimer = new CountDownTimer(COUNT_DOWN_MS, MILLIS_IN_SECOND) {
+ @Override
+ public void onTick(long millisUntilFinished) {
+ mProgressBar.setVisibility(View.VISIBLE);
+ setProgressBar(millisUntilFinished);
+ Log.d(TAG, "Time Left: " + millisUntilFinished / MILLIS_IN_SECOND);
+ }
+
+ @Override
+ public void onFinish() {
+ mProgressBar.setProgress(0);
+ mProgressBar.setVisibility(View.INVISIBLE);
+ mSoundRecorder.stopRecording();
+ mUIAnimation.transitionToHome();
+ mUiState = UIAnimation.UIState.HOME;
+ mState = AppState.READY;
+ mCountDownTimer = null;
+ }
+ };
+ mCountDownTimer.start();
+ break;
+ case SOUND_UP:
+ mState = AppState.PLAYING_VOICE;
+ mUiState = state;
+ mSoundRecorder.startPlay();
+ break;
+ case HOME:
+ switch (mState) {
+ case PLAYING_MUSIC:
+ mState = AppState.READY;
+ mUiState = state;
+ stopMusic();
+ break;
+ case PLAYING_VOICE:
+ mState = AppState.READY;
+ mUiState = state;
+ mSoundRecorder.stopPlaying();
+ break;
+ case RECORDING:
+ mState = AppState.READY;
+ mUiState = state;
+ mSoundRecorder.stopRecording();
+ if (mCountDownTimer != null) {
+ mCountDownTimer.cancel();
+ mCountDownTimer = null;
+ }
+ mProgressBar.setVisibility(View.INVISIBLE);
+ setProgressBar(COUNT_DOWN_MS);
+ break;
+ }
+ break;
+ }
+ }
+
+ /**
+ * Plays back the MP3 file embedded in the application
+ */
+ private void playMusic() {
+ if (mMediaPlayer == null) {
+ mMediaPlayer = MediaPlayer.create(this, R.raw.sound);
+ mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ // we need to transition to the READY/Home state
+ Log.d(TAG, "Music Finished");
+ mUIAnimation.transitionToHome();
+ }
+ });
+ }
+ mMediaPlayer.start();
+ }
+
+ /**
+ * Stops the playback of the MP3 file.
+ */
+ private void stopMusic() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+ }
+
+ /**
+ * Checks the permission that this app needs and if it has not been granted, it will
+ * prompt the user to grant it, otherwise it shuts down the app.
+ */
+ private void checkPermissions() {
+ boolean recordAudioPermissionGranted =
+ ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (recordAudioPermissionGranted) {
+ start();
+ } else {
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.RECORD_AUDIO},
+ PERMISSIONS_REQUEST_CODE);
+ }
+
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ String permissions[], int[] grantResults) {
+ if (requestCode == PERMISSIONS_REQUEST_CODE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ start();
+ } else {
+ // Permission has been denied before. At this point we should show a dialog to
+ // user and explain why this permission is needed and direct him to go to the
+ // Permissions settings for the app in the System settings. For this sample, we
+ // simply exit to get to the important part.
+ Toast.makeText(this, R.string.exiting_for_permissions, Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }
+ }
+
+ /**
+ * Starts the main flow of the application.
+ */
+ private void start() {
+ mSoundRecorder = new SoundRecorder(this, VOICE_FILE_NAME, this);
+ int[] thumbResources = new int[] {R.id.mic, R.id.play, R.id.music};
+ ImageView[] thumbs = new ImageView[3];
+ for(int i=0; i < 3; i++) {
+ thumbs[i] = (ImageView) findViewById(thumbResources[i]);
+ }
+ View containerView = findViewById(R.id.container);
+ ImageView expandedView = (ImageView) findViewById(R.id.expanded);
+ int animationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
+ mUIAnimation = new UIAnimation(containerView, thumbs, expandedView, animationDuration,
+ this);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (speakerIsSupported()) {
+ checkPermissions();
+ } else {
+ findViewById(R.id.container2).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Toast.makeText(MainActivity.this, R.string.no_speaker_supported,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (mSoundRecorder != null) {
+ mSoundRecorder.cleanup();
+ mSoundRecorder = null;
+ }
+ if (mCountDownTimer != null) {
+ mCountDownTimer.cancel();
+ }
+
+ if (mMediaPlayer != null) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onPlaybackStopped() {
+ mUIAnimation.transitionToHome();
+ mUiState = UIAnimation.UIState.HOME;
+ mState = AppState.READY;
+ }
+
+ /**
+ * Determines if the wear device has a built-in speaker and if it is supported. Speaker, even if
+ * physically present, is only supported in Android M+ on a wear device..
+ */
+ public final boolean speakerIsSupported() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PackageManager packageManager = getPackageManager();
+ // The results from AudioManager.getDevices can't be trusted unless the device
+ // advertises FEATURE_AUDIO_OUTPUT.
+ if (!packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) {
+ return false;
+ }
+ AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device : devices) {
+ if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/SoundRecorder.java b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/SoundRecorder.java
new file mode 100644
index 000000000..a45bdd27c
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/SoundRecorder.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * 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.speaker;
+
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioTrack;
+import android.media.MediaRecorder;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * A helper class to provide methods to record audio input from the MIC to the internal storage
+ * and to playback the same recorded audio file.
+ */
+public class SoundRecorder {
+
+ private static final String TAG = "SoundRecorder";
+ private static final int RECORDING_RATE = 8000; // can go up to 44K, if needed
+ private static final int CHANNEL_IN = AudioFormat.CHANNEL_IN_MONO;
+ private static final int CHANNELS_OUT = AudioFormat.CHANNEL_OUT_MONO;
+ private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
+ private static int BUFFER_SIZE = AudioRecord
+ .getMinBufferSize(RECORDING_RATE, CHANNEL_IN, FORMAT);
+
+ private final String mOutputFileName;
+ private final AudioManager mAudioManager;
+ private final Handler mHandler;
+ private final Context mContext;
+ private State mState = State.IDLE;
+
+ private OnVoicePlaybackStateChangedListener mListener;
+ private AsyncTask mRecordingAsyncTask;
+ private AsyncTask mPlayingAsyncTask;
+
+ enum State {
+ IDLE, RECORDING, PLAYING
+ }
+
+ public SoundRecorder(Context context, String outputFileName,
+ OnVoicePlaybackStateChangedListener listener) {
+ mOutputFileName = outputFileName;
+ mListener = listener;
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mHandler = new Handler(Looper.getMainLooper());
+ mContext = context;
+ }
+
+ /**
+ * Starts recording from the MIC.
+ */
+ public void startRecording() {
+ if (mState != State.IDLE) {
+ Log.w(TAG, "Requesting to start recording while state was not IDLE");
+ return;
+ }
+
+ mRecordingAsyncTask = new AsyncTask() {
+
+ private AudioRecord mAudioRecord;
+
+ @Override
+ protected void onPreExecute() {
+ mState = State.RECORDING;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
+ RECORDING_RATE, CHANNEL_IN, FORMAT, BUFFER_SIZE * 3);
+ BufferedOutputStream bufferedOutputStream = null;
+ try {
+ bufferedOutputStream = new BufferedOutputStream(
+ mContext.openFileOutput(mOutputFileName, Context.MODE_PRIVATE));
+ byte[] buffer = new byte[BUFFER_SIZE];
+ mAudioRecord.startRecording();
+ while (!isCancelled()) {
+ int read = mAudioRecord.read(buffer, 0, buffer.length);
+ bufferedOutputStream.write(buffer, 0, read);
+ }
+ } catch (IOException | NullPointerException | IndexOutOfBoundsException e) {
+ Log.e(TAG, "Failed to record data: " + e);
+ } finally {
+ if (bufferedOutputStream != null) {
+ try {
+ bufferedOutputStream.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ mAudioRecord.release();
+ mAudioRecord = null;
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ mState = State.IDLE;
+ mRecordingAsyncTask = null;
+ }
+
+ @Override
+ protected void onCancelled() {
+ if (mState == State.RECORDING) {
+ Log.d(TAG, "Stopping the recording ...");
+ mState = State.IDLE;
+ } else {
+ Log.w(TAG, "Requesting to stop recording while state was not RECORDING");
+ }
+ mRecordingAsyncTask = null;
+ }
+ };
+
+ mRecordingAsyncTask.execute();
+ }
+
+ public void stopRecording() {
+ if (mRecordingAsyncTask != null) {
+ mRecordingAsyncTask.cancel(true);
+ }
+ }
+
+ public void stopPlaying() {
+ if (mPlayingAsyncTask != null) {
+ mPlayingAsyncTask.cancel(true);
+ }
+ }
+
+ /**
+ * Starts playback of the recorded audio file.
+ */
+ public void startPlay() {
+ if (mState != State.IDLE) {
+ Log.w(TAG, "Requesting to play while state was not IDLE");
+ return;
+ }
+
+ if (!new File(mContext.getFilesDir(), mOutputFileName).exists()) {
+ // there is no recording to play
+ if (mListener != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onPlaybackStopped();
+ }
+ });
+ }
+ return;
+ }
+ final int intSize = AudioTrack.getMinBufferSize(RECORDING_RATE, CHANNELS_OUT, FORMAT);
+
+ mPlayingAsyncTask = new AsyncTask() {
+
+ private AudioTrack mAudioTrack;
+
+ @Override
+ protected void onPreExecute() {
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
+ mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC), 0 /* flags */);
+ mState = State.PLAYING;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, RECORDING_RATE,
+ CHANNELS_OUT, FORMAT, intSize, AudioTrack.MODE_STREAM);
+ byte[] buffer = new byte[intSize * 2];
+ FileInputStream in = null;
+ BufferedInputStream bis = null;
+ mAudioTrack.setVolume(AudioTrack.getMaxVolume());
+ mAudioTrack.play();
+ try {
+ in = mContext.openFileInput(mOutputFileName);
+ bis = new BufferedInputStream(in);
+ int read;
+ while (!isCancelled() && (read = bis.read(buffer, 0, buffer.length)) > 0) {
+ mAudioTrack.write(buffer, 0, read);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read the sound file into a byte array", e);
+ } finally {
+ try {
+ if (in != null) {
+ in.close();
+ }
+ if (bis != null) {
+ bis.close();
+ }
+ } catch (IOException e) { /* ignore */}
+
+ mAudioTrack.release();
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to start playback", e);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ cleanup();
+ }
+
+ @Override
+ protected void onCancelled() {
+ cleanup();
+ }
+
+ private void cleanup() {
+ if (mListener != null) {
+ mListener.onPlaybackStopped();
+ }
+ mState = State.IDLE;
+ mPlayingAsyncTask = null;
+ }
+ };
+
+ mPlayingAsyncTask.execute();
+ }
+
+ public interface OnVoicePlaybackStateChangedListener {
+
+ /**
+ * Called when the playback of the audio file ends. This should be called on the UI thread.
+ */
+ void onPlaybackStopped();
+ }
+
+ /**
+ * Cleans up some resources related to {@link AudioTrack} and {@link AudioRecord}
+ */
+ public void cleanup() {
+ Log.d(TAG, "cleanup() is called");
+ stopPlaying();
+ stopRecording();
+ }
+}
diff --git a/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/UIAnimation.java b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/UIAnimation.java
new file mode 100644
index 000000000..7ce2fd534
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/UIAnimation.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * 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.speaker;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+
+/**
+ * A helper class to provide a simple animation when user selects any of the three icons on the
+ * main UI.
+ */
+public class UIAnimation {
+
+ private AnimatorSet mCurrentAnimator;
+ private final int[] mLargeDrawables = new int[]{R.drawable.ic_mic_120dp,
+ R.drawable.ic_play_arrow_120dp, R.drawable.ic_audiotrack_120dp};
+ private final ImageView[] mThumbs;
+ private ImageView expandedImageView;
+ private final View mContainerView;
+ private final int mAnimationDurationTime;
+
+ private UIStateListener mListener;
+ private UIState mState = UIState.HOME;
+
+ public UIAnimation(View containerView, ImageView[] thumbs, ImageView expandedView,
+ int animationDuration, UIStateListener listener) {
+ mContainerView = containerView;
+ mThumbs = thumbs;
+ expandedImageView = expandedView;
+ mAnimationDurationTime = animationDuration;
+ mListener = listener;
+
+ mThumbs[0].setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ zoomImageFromThumb(0);
+ }
+ });
+
+ mThumbs[1].setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ zoomImageFromThumb(1);
+ }
+ });
+
+ mThumbs[2].setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ zoomImageFromThumb(2);
+ }
+ });
+ }
+
+ private void zoomImageFromThumb(final int index) {
+ int imageResId = mLargeDrawables[index];
+ final ImageView thumbView = mThumbs[index];
+ if (mCurrentAnimator != null) {
+ return;
+ }
+
+ expandedImageView.setImageResource(imageResId);
+
+ final Rect startBounds = new Rect();
+ final Rect finalBounds = new Rect();
+ final Point globalOffset = new Point();
+ thumbView.getGlobalVisibleRect(startBounds);
+ mContainerView.getGlobalVisibleRect(finalBounds, globalOffset);
+ startBounds.offset(-globalOffset.x, -globalOffset.y);
+ finalBounds.offset(-globalOffset.x, -globalOffset.y);
+ float startScale;
+ if ((float) finalBounds.width() / finalBounds.height()
+ > (float) startBounds.width() / startBounds.height()) {
+ startScale = (float) startBounds.height() / finalBounds.height();
+ float startWidth = startScale * finalBounds.width();
+ float deltaWidth = (startWidth - startBounds.width()) / 2;
+ startBounds.left -= deltaWidth;
+ startBounds.right += deltaWidth;
+ } else {
+ startScale = (float) startBounds.width() / finalBounds.width();
+ float startHeight = startScale * finalBounds.height();
+ float deltaHeight = (startHeight - startBounds.height()) / 2;
+ startBounds.top -= deltaHeight;
+ startBounds.bottom += deltaHeight;
+ }
+
+ for(int k=0; k < 3; k++) {
+ mThumbs[k].setAlpha(0f);
+ }
+ expandedImageView.setVisibility(View.VISIBLE);
+
+ expandedImageView.setPivotX(0f);
+ expandedImageView.setPivotY(0f);
+
+ AnimatorSet zommInAnimator = new AnimatorSet();
+ zommInAnimator.play(ObjectAnimator
+ .ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left)).with(
+ ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds
+ .top)).with(
+ ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
+ .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
+ zommInAnimator.setDuration(mAnimationDurationTime);
+ zommInAnimator.setInterpolator(new DecelerateInterpolator());
+ zommInAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentAnimator = null;
+ if (mListener != null) {
+ mState = UIState.getUIState(index);
+ mListener.onUIStateChanged(mState);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCurrentAnimator = null;
+ }
+ });
+ zommInAnimator.start();
+ mCurrentAnimator = zommInAnimator;
+
+ final float startScaleFinal = startScale;
+ expandedImageView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mCurrentAnimator != null) {
+ return;
+ }
+ AnimatorSet zoomOutAnimator = new AnimatorSet();
+ zoomOutAnimator.play(ObjectAnimator
+ .ofFloat(expandedImageView, View.X, startBounds.left))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.Y, startBounds.top))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.SCALE_X, startScaleFinal))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.SCALE_Y, startScaleFinal));
+ zoomOutAnimator.setDuration(mAnimationDurationTime);
+ zoomOutAnimator.setInterpolator(new DecelerateInterpolator());
+ zoomOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ for (int k = 0; k < 3; k++) {
+ mThumbs[k].setAlpha(1f);
+ }
+ expandedImageView.setVisibility(View.GONE);
+ mCurrentAnimator = null;
+ if (mListener != null) {
+ mState = UIState.HOME;
+ mListener.onUIStateChanged(mState);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ thumbView.setAlpha(1f);
+ expandedImageView.setVisibility(View.GONE);
+ mCurrentAnimator = null;
+ }
+ });
+ zoomOutAnimator.start();
+ mCurrentAnimator = zoomOutAnimator;
+ }
+ });
+ }
+
+ public enum UIState {
+ MIC_UP(0), SOUND_UP(1), MUSIC_UP(2), HOME(3);
+ private int mState;
+
+ UIState(int state) {
+ mState = state;
+ }
+
+ static UIState getUIState(int state) {
+ for(UIState uiState : values()) {
+ if (uiState.mState == state) {
+ return uiState;
+ }
+ }
+ return null;
+ }
+ }
+
+ public interface UIStateListener {
+ void onUIStateChanged(UIState state);
+ }
+
+ public void transitionToHome() {
+ if (mState == UIState.HOME) {
+ return;
+ }
+ expandedImageView.callOnClick();
+
+ }
+}