Sync sample browser prebuilts for mnc-docs

Synced to //developers/samples/android commit
44c699bf11bae6c9eef1f8c56bd405d547e179e6.

Change-Id: I61ceec197e6fa420fc5a6d16e703aa1aab2b0c4e
This commit is contained in:
Trevor Johns
2015-11-17 21:38:44 -08:00
parent 03d12cb64f
commit 8af0d723f4
92 changed files with 3630 additions and 715 deletions

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.agendadata" > package="com.example.android.wearable.agendadata" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -120,6 +120,7 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/bundle_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal">
@@ -147,6 +148,7 @@
</LinearLayout> </LinearLayout>
<RelativeLayout <RelativeLayout
android:id="@+id/bundle_array_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">

View File

@@ -22,6 +22,7 @@ import android.content.Context;
import android.content.RestrictionEntry; import android.content.RestrictionEntry;
import android.content.RestrictionsManager; import android.content.RestrictionsManager;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@@ -105,6 +106,8 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
private static final String DELIMETER = ","; private static final String DELIMETER = ",";
private static final String SEPARATOR = ":"; private static final String SEPARATOR = ":";
private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
/** /**
* Current status of the restrictions. * Current status of the restrictions.
*/ */
@@ -138,6 +141,15 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
mEditProfileAge = (EditText) view.findViewById(R.id.profile_age); mEditProfileAge = (EditText) view.findViewById(R.id.profile_age);
mLayoutItems = (LinearLayout) view.findViewById(R.id.items); mLayoutItems = (LinearLayout) view.findViewById(R.id.items);
view.findViewById(R.id.item_add).setOnClickListener(this); 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 @Override
@@ -280,7 +292,7 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
TextUtils.join(DELIMETER, TextUtils.join(DELIMETER,
restriction.getAllSelectedStrings())), restriction.getAllSelectedStrings())),
DELIMETER)); DELIMETER));
} else if (RESTRICTION_KEY_PROFILE.equals(key)) { } else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_PROFILE.equals(key)) {
String name = null; String name = null;
int age = 0; int age = 0;
for (RestrictionEntry entry : restriction.getRestrictions()) { for (RestrictionEntry entry : restriction.getRestrictions()) {
@@ -294,7 +306,7 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
name = prefs.getString(RESTRICTION_KEY_PROFILE_NAME, name); name = prefs.getString(RESTRICTION_KEY_PROFILE_NAME, name);
age = prefs.getInt(RESTRICTION_KEY_PROFILE_AGE, age); age = prefs.getInt(RESTRICTION_KEY_PROFILE_AGE, age);
updateProfile(name, 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, ""); String itemsString = prefs.getString(RESTRICTION_KEY_ITEMS, "");
HashMap<String, String> items = new HashMap<>(); HashMap<String, String> items = new HashMap<>();
for (String itemString : TextUtils.split(itemsString, DELIMETER)) { for (String itemString : TextUtils.split(itemsString, DELIMETER)) {
@@ -351,6 +363,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
} }
private void updateProfile(String name, int age) { private void updateProfile(String name, int age) {
if (!BUNDLE_SUPPORTED) {
return;
}
Bundle profile = new Bundle(); Bundle profile = new Bundle();
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name); profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age); profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
@@ -364,6 +379,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
} }
private void updateItems(Context context, Map<String, String> items) { private void updateItems(Context context, Map<String, String> items) {
if (!BUNDLE_SUPPORTED) {
return;
}
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items)); mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
LayoutInflater inflater = LayoutInflater.from(context); LayoutInflater inflater = LayoutInflater.from(context);
mLayoutItems.removeAllViews(); mLayoutItems.removeAllViews();
@@ -500,6 +518,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
* @param age The value to be set for the "age" field. * @param age The value to be set for the "age" field.
*/ */
private void saveProfile(Activity activity, String name, int age) { private void saveProfile(Activity activity, String name, int age) {
if (!BUNDLE_SUPPORTED) {
return;
}
Bundle profile = new Bundle(); Bundle profile = new Bundle();
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name); profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age); profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
@@ -515,6 +536,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
* @param items The values. * @param items The values.
*/ */
private void saveItems(Activity activity, Map<String, String> items) { private void saveItems(Activity activity, Map<String, String> items) {
if (!BUNDLE_SUPPORTED) {
return;
}
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items)); mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
saveRestrictions(activity); saveRestrictions(activity);
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();

View File

@@ -59,7 +59,7 @@ limitations under the License.
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="@string/your_rank"/> tools:text="@string/your_rank"/>
<include layout="@layout/separator"/> <include layout="@layout/separator" android:id="@+id/bundle_separator"/>
<TextView <TextView
android:id="@+id/approvals_you_have" android:id="@+id/approvals_you_have"
@@ -77,7 +77,7 @@ limitations under the License.
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="@string/your_profile"/> tools:text="@string/your_profile"/>
<include layout="@layout/separator"/> <include layout="@layout/separator" android:id="@+id/bundle_array_separator" />
<TextView <TextView
android:id="@+id/your_items" android:id="@+id/your_items"

View File

@@ -19,6 +19,7 @@ package com.example.android.apprestrictionschema;
import android.content.Context; import android.content.Context;
import android.content.RestrictionEntry; import android.content.RestrictionEntry;
import android.content.RestrictionsManager; import android.content.RestrictionsManager;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@@ -57,6 +58,8 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
private static final String KEY_ITEM_KEY = "key"; private static final String KEY_ITEM_KEY = "key";
private static final String KEY_ITEM_VALUE = "value"; private static final String KEY_ITEM_VALUE = "value";
private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
// Message to show when the button is clicked (String restriction) // Message to show when the button is clicked (String restriction)
private String mMessage; private String mMessage;
@@ -82,9 +85,22 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
mTextNumber = (TextView) view.findViewById(R.id.your_number); mTextNumber = (TextView) view.findViewById(R.id.your_number);
mTextRank = (TextView) view.findViewById(R.id.your_rank); mTextRank = (TextView) view.findViewById(R.id.your_rank);
mTextApprovals = (TextView) view.findViewById(R.id.approvals_you_have); 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); 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); mTextItems = (TextView) view.findViewById(R.id.your_items);
mButtonSayHello.setOnClickListener(this); 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 @Override
@@ -178,6 +194,9 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
} }
private void updateProfile(RestrictionEntry entry, Bundle restrictions) { private void updateProfile(RestrictionEntry entry, Bundle restrictions) {
if (!BUNDLE_SUPPORTED) {
return;
}
String name = null; String name = null;
int age = 0; int age = 0;
if (restrictions == null || !restrictions.containsKey(KEY_PROFILE)) { 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) { private void updateItems(RestrictionEntry entry, Bundle restrictions) {
if (!BUNDLE_SUPPORTED) {
return;
}
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
if (restrictions != null) { if (restrictions != null) {
Parcelable[] parcelables = restrictions.getParcelableArray(KEY_ITEMS); Parcelable[] parcelables = restrictions.getParcelableArray(KEY_ITEMS);

View File

@@ -1,5 +1,5 @@
page.tags="Asymmetric Fingerprint Dialog Sample" page.tags="AsymmetricFingerprintDialog"
sample.group=Security sample.group=Security
@jd:body @jd:body

View File

@@ -16,7 +16,7 @@
--> -->
<resources> <resources>
<string name="app_name">Asymmetric Fingerprint Dialog Sample</string> <string name="app_name">AsymmetricFingerprintDialog</string>
<string name="intro_message"> <string name="intro_message">
<![CDATA[ <![CDATA[

View File

@@ -16,12 +16,10 @@
package com.example.android.asymmetricfingerprintdialog; package com.example.android.asymmetricfingerprintdialog;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.app.KeyguardManager; import android.app.KeyguardManager;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle; import android.os.Bundle;
import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyGenParameterSpec;
@@ -59,8 +57,6 @@ public class MainActivity extends Activity {
/** Alias for our key in the Android Key Store */ /** Alias for our key in the Android Key Store */
public static final String KEY_NAME = "my_key"; public static final String KEY_NAME = "my_key";
private static final int FINGERPRINT_PERMISSION_REQUEST_CODE = 0;
@Inject KeyguardManager mKeyguardManager; @Inject KeyguardManager mKeyguardManager;
@Inject FingerprintManager mFingerprintManager; @Inject FingerprintManager mFingerprintManager;
@Inject FingerprintAuthenticationDialogFragment mFragment; @Inject FingerprintAuthenticationDialogFragment mFragment;
@@ -74,71 +70,63 @@ public class MainActivity extends Activity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
((InjectedApplication) getApplication()).inject(this); ((InjectedApplication) getApplication()).inject(this);
requestPermissions(new String[]{Manifest.permission.USE_FINGERPRINT}, setContentView(R.layout.activity_main);
FINGERPRINT_PERMISSION_REQUEST_CODE); 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.
@Override Toast.makeText(this,
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) { "Secure lock screen hasn't set up.\n"
if (requestCode == FINGERPRINT_PERMISSION_REQUEST_CODE + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
&& state[0] == PackageManager.PERMISSION_GRANTED) { Toast.LENGTH_LONG).show();
setContentView(R.layout.activity_main); purchaseButton.setEnabled(false);
Button purchaseButton = (Button) findViewById(R.id.purchase_button); return;
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;
}
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);
}
}
});
} }
//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);
}
}
});
} }
/** /**

View File

@@ -156,7 +156,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment {
// generated. // generated.
Calendar start = new GregorianCalendar(); Calendar start = new GregorianCalendar();
Calendar end = new GregorianCalendar(); Calendar end = new GregorianCalendar();
end.add(1, Calendar.YEAR); end.add(Calendar.YEAR, 1);
//END_INCLUDE(create_valid_dates) //END_INCLUDE(create_valid_dates)
@@ -316,8 +316,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment {
// Verify the data. // Verify the data.
s.initVerify(((KeyStore.PrivateKeyEntry) entry).getCertificate()); s.initVerify(((KeyStore.PrivateKeyEntry) entry).getCertificate());
s.update(data); s.update(data);
boolean valid = s.verify(signature); return s.verify(signature);
return valid;
// END_INCLUDE(verify_data) // END_INCLUDE(verify_data)
} }

View File

@@ -28,6 +28,7 @@ import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraAccessException;
@@ -116,6 +117,16 @@ public class Camera2BasicFragment extends Fragment
*/ */
private static final int STATE_PICTURE_TAKEN = 4; 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.SurfaceTextureListener} handles several lifecycle events on a
* {@link TextureView}. * {@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 * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
* width and height are at least as large as the respective requested values, and whose aspect * is at least as large as the respective texture view size, and that is at most as large as the
* ratio matches with the specified value. * 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 choices The list of sizes that the camera supports for the intended output
* @param width The minimum desired width * class
* @param height The minimum desired height * @param textureViewWidth The width of the texture view relative to sensor coordinate
* @param aspectRatio The aspect ratio * @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 * @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 // Collect the supported resolutions that are at least as big as the preview Surface
List<Size> bigEnough = new ArrayList<>(); List<Size> bigEnough = new ArrayList<>();
// Collect the supported resolutions that are smaller than the preview Surface
List<Size> notBigEnough = new ArrayList<>();
int w = aspectRatio.getWidth(); int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight(); int h = aspectRatio.getHeight();
for (Size option : choices) { for (Size option : choices) {
if (option.getHeight() == option.getWidth() * h / w && if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
option.getWidth() >= width && option.getHeight() >= height) { option.getHeight() == option.getWidth() * h / w) {
bigEnough.add(option); 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) { if (bigEnough.size() > 0) {
return Collections.min(bigEnough, new CompareSizesByArea()); return Collections.min(bigEnough, new CompareSizesByArea());
} else if (notBigEnough.size() > 0) {
return Collections.max(notBigEnough, new CompareSizesByArea());
} else { } else {
Log.e(TAG, "Couldn't find any suitable preview size"); Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0]; return choices[0];
@@ -478,11 +506,57 @@ public class Camera2BasicFragment extends Fragment
mImageReader.setOnImageAvailableListener( mImageReader.setOnImageAvailableListener(
mOnImageAvailableListener, mBackgroundHandler); 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 // 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 // bus' bandwidth limitation, resulting in gorgeous previews but the storage of
// garbage capture data. // garbage capture data.
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), 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. // We fit the aspect ratio of TextureView to the size of preview we picked.
int orientation = getResources().getConfiguration().orientation; int orientation = getResources().getConfiguration().orientation;

View File

@@ -27,6 +27,7 @@ import android.content.DialogInterface;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.hardware.SensorManager; import android.hardware.SensorManager;
@@ -156,6 +157,16 @@ public class Camera2RawFragment extends Fragment
*/ */
private static final double ASPECT_RATIO_TOLERANCE = 0.005; 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}. * 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. // Find the rotation of the device relative to the native device orientation.
int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); 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. // Find the rotation of the device relative to the camera sensor's orientation.
int totalRotation = sensorToDeviceRotation(mCharacteristics, deviceRotation); int totalRotation = sensorToDeviceRotation(mCharacteristics, deviceRotation);
@@ -1042,14 +1055,29 @@ public class Camera2RawFragment extends Fragment
boolean swappedDimensions = totalRotation == 90 || totalRotation == 270; boolean swappedDimensions = totalRotation == 90 || totalRotation == 270;
int rotatedViewWidth = viewWidth; int rotatedViewWidth = viewWidth;
int rotatedViewHeight = viewHeight; int rotatedViewHeight = viewHeight;
int maxPreviewWidth = displaySize.x;
int maxPreviewHeight = displaySize.y;
if (swappedDimensions) { if (swappedDimensions) {
rotatedViewWidth = viewHeight; rotatedViewWidth = viewHeight;
rotatedViewHeight = viewWidth; 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. // Find the best preview size for these view dimensions and configured JPEG size.
Size previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), Size previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
rotatedViewWidth, rotatedViewHeight, largestJpeg); rotatedViewWidth, rotatedViewHeight, maxPreviewWidth, maxPreviewHeight,
largestJpeg);
if (swappedDimensions) { if (swappedDimensions) {
mTextureView.setAspectRatio( 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 * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
* width and height are at least as large as the respective requested values, and whose aspect * is at least as large as the respective texture view size, and that is at most as large as the
* ratio matches with the specified value. * 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 choices The list of sizes that the camera supports for the intended output
* @param width The minimum desired width * class
* @param height The minimum desired height * @param textureViewWidth The width of the texture view relative to sensor coordinate
* @param aspectRatio The aspect ratio * @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 * @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 // Collect the supported resolutions that are at least as big as the preview Surface
List<Size> bigEnough = new ArrayList<>(); List<Size> bigEnough = new ArrayList<>();
// Collect the supported resolutions that are smaller than the preview Surface
List<Size> notBigEnough = new ArrayList<>();
int w = aspectRatio.getWidth(); int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight(); int h = aspectRatio.getHeight();
for (Size option : choices) { for (Size option : choices) {
if (option.getHeight() == option.getWidth() * h / w && if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
option.getWidth() >= width && option.getHeight() >= height) { option.getHeight() == option.getWidth() * h / w) {
bigEnough.add(option); 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) { if (bigEnough.size() > 0) {
return Collections.min(bigEnough, new CompareSizesByArea()); return Collections.min(bigEnough, new CompareSizesByArea());
} else if (notBigEnough.size() > 0) {
return Collections.max(notBigEnough, new CompareSizesByArea());
} else { } else {
Log.e(TAG, "Couldn't find any suitable preview size"); Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0]; return choices[0];

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.datalayer" > package="com.example.android.wearable.datalayer" >
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="22" /> android:targetSdkVersion="23" />
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />

View File

@@ -23,6 +23,7 @@ import android.app.Fragment;
import android.app.FragmentManager; import android.app.FragmentManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.support.wearable.view.DotsPageIndicator; 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;
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; 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.GoogleApiClient.OnConnectionFailedListener;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.Asset;
import com.google.android.gms.wearable.CapabilityApi; 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 static final String CAPABILITY_2_NAME = "capability_2";
private GoogleApiClient mGoogleApiClient; private GoogleApiClient mGoogleApiClient;
private Handler mHandler;
private GridViewPager mPager; private GridViewPager mPager;
private DataFragment mDataFragment; private DataFragment mDataFragment;
private AssetFragment mAssetFragment; private AssetFragment mAssetFragment;
@@ -93,7 +94,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
@Override @Override
public void onCreate(Bundle b) { public void onCreate(Bundle b) {
super.onCreate(b); super.onCreate(b);
mHandler = new Handler();
setContentView(R.layout.main_activity); setContentView(R.layout.main_activity);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setupViews(); setupViews();
@@ -137,15 +137,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
Log.e(TAG, "onConnectionFailed(): Failed to connect, with result: " + result); 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 @Override
public void onDataChanged(DataEventBuffer dataEvents) { public void onDataChanged(DataEventBuffer dataEvents) {
LOGD(TAG, "onDataChanged(): " + dataEvents); LOGD(TAG, "onDataChanged(): " + dataEvents);
@@ -155,29 +146,22 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
String path = event.getDataItem().getUri().getPath(); String path = event.getDataItem().getUri().getPath();
if (DataLayerListenerService.IMAGE_PATH.equals(path)) { if (DataLayerListenerService.IMAGE_PATH.equals(path)) {
DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem()); DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
Asset photo = dataMapItem.getDataMap() Asset photoAsset = dataMapItem.getDataMap()
.getAsset(DataLayerListenerService.IMAGE_KEY); .getAsset(DataLayerListenerService.IMAGE_KEY);
final Bitmap bitmap = loadBitmapFromAsset(mGoogleApiClient, photo); // Loads image on background thread.
mHandler.post(new Runnable() { new LoadBitmapAsyncTask().execute(photoAsset);
@Override
public void run() {
Log.d(TAG, "Setting background image on second page..");
moveToPage(1);
mAssetFragment.setBackgroundImage(bitmap);
}
});
} else if (DataLayerListenerService.COUNT_PATH.equals(path)) { } else if (DataLayerListenerService.COUNT_PATH.equals(path)) {
LOGD(TAG, "Data Changed for COUNT_PATH"); LOGD(TAG, "Data Changed for COUNT_PATH");
generateEvent("DataItem Changed", event.getDataItem().toString()); mDataFragment.appendItem("DataItem Changed", event.getDataItem().toString());
} else { } else {
LOGD(TAG, "Unrecognized path: " + path); LOGD(TAG, "Unrecognized path: " + path);
} }
} else if (event.getType() == DataEvent.TYPE_DELETED) { } else if (event.getType() == DataEvent.TYPE_DELETED) {
generateEvent("DataItem Deleted", event.getDataItem().toString()); mDataFragment.appendItem("DataItem Deleted", event.getDataItem().toString());
} else { } 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 * Find the connected nodes that provide at least one of the given capabilities
*/ */
private void showNodes(final String... capabilityNames) { private void showNodes(final String... capabilityNames) {
Wearable.CapabilityApi.getAllCapabilities(mGoogleApiClient,
CapabilityApi.FILTER_REACHABLE).setResultCallback(
PendingResult<CapabilityApi.GetAllCapabilitiesResult> pendingCapabilityResult =
Wearable.CapabilityApi.getAllCapabilities(
mGoogleApiClient,
CapabilityApi.FILTER_REACHABLE);
pendingCapabilityResult.setResultCallback(
new ResultCallback<CapabilityApi.GetAllCapabilitiesResult>() { new ResultCallback<CapabilityApi.GetAllCapabilitiesResult>() {
@Override @Override
public void onResult( public void onResult(
CapabilityApi.GetAllCapabilitiesResult getAllCapabilitiesResult) { CapabilityApi.GetAllCapabilitiesResult getAllCapabilitiesResult) {
if (!getAllCapabilitiesResult.getStatus().isSuccess()) { if (!getAllCapabilitiesResult.getStatus().isSuccess()) {
Log.e(TAG, "Failed to get capabilities"); Log.e(TAG, "Failed to get capabilities");
return; return;
} }
Map<String, CapabilityInfo>
capabilitiesMap = getAllCapabilitiesResult.getAllCapabilities(); Map<String, CapabilityInfo> capabilitiesMap =
getAllCapabilitiesResult.getAllCapabilities();
Set<Node> nodes = new HashSet<>(); Set<Node> nodes = new HashSet<>();
if (capabilitiesMap.isEmpty()) { if (capabilitiesMap.isEmpty()) {
showDiscoveredNodes(nodes); showDiscoveredNodes(nodes);
return; return;
@@ -231,7 +222,7 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
for (Node node : nodes) { for (Node node : nodes) {
nodesList.add(node.getDisplayName()); 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" ? "No connected device was found for the given capabilities"
: TextUtils.join(",", nodesList))); : TextUtils.join(",", nodesList)));
String msg; 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 @Override
public void onMessageReceived(MessageEvent event) { public void onMessageReceived(MessageEvent event) {
LOGD(TAG, "onMessageReceived: " + event); LOGD(TAG, "onMessageReceived: " + event);
generateEvent("Message", event.toString()); mDataFragment.appendItem("Message", event.toString());
} }
@Override @Override
public void onPeerConnected(Node node) { public void onPeerConnected(Node node) {
generateEvent("Node Connected", node.getId()); mDataFragment.appendItem("Node Connected", node.getId());
} }
@Override @Override
public void onPeerDisconnected(Node node) { public void onPeerDisconnected(Node node) {
generateEvent("Node Disconnected", node.getId()); mDataFragment.appendItem("Node Disconnected", node.getId());
} }
private void setupViews() { 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<Asset, Void, Bitmap> {
@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);
}
}
}
} }

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.delayedconfirmation" > package="com.example.android.wearable.delayedconfirmation" >
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="22" /> android:targetSdkVersion="23" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.elizachat" > package="com.example.android.wearable.elizachat" >
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="21" /> android:targetSdkVersion="23" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@@ -96,7 +96,7 @@ public class ResponderService extends Service {
.setContentText(mLastResponse) .setContentText(mLastResponse)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.bg_eliza)) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.bg_eliza))
.setSmallIcon(R.drawable.bg_eliza) .setSmallIcon(R.drawable.bg_eliza)
.setPriority(NotificationCompat.PRIORITY_MIN); .setPriority(NotificationCompat.PRIORITY_DEFAULT);
Intent intent = new Intent(ACTION_RESPONSE); Intent intent = new Intent(ACTION_RESPONSE);
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent,

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.findphone"> package="com.example.android.wearable.findphone">
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="22" /> android:targetSdkVersion="23" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<application <application

View File

@@ -1,5 +1,5 @@
page.tags="Fingerprint Dialog Sample" page.tags="FingerprintDialog"
sample.group=Security sample.group=Security
@jd:body @jd:body

View File

@@ -16,7 +16,7 @@
--> -->
<resources> <resources>
<string name="app_name">Fingerprint Dialog Sample</string> <string name="app_name">FingerprintDialog</string>
<string name="intro_message"> <string name="intro_message">
<![CDATA[ <![CDATA[

View File

@@ -16,12 +16,10 @@
package com.example.android.fingerprintdialog; package com.example.android.fingerprintdialog;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.app.KeyguardManager; import android.app.KeyguardManager;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle; import android.os.Bundle;
import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyGenParameterSpec;
@@ -64,8 +62,6 @@ public class MainActivity extends Activity {
/** Alias for our key in the Android Key Store */ /** Alias for our key in the Android Key Store */
private static final String KEY_NAME = "my_key"; private static final String KEY_NAME = "my_key";
private static final int FINGERPRINT_PERMISSION_REQUEST_CODE = 0;
@Inject KeyguardManager mKeyguardManager; @Inject KeyguardManager mKeyguardManager;
@Inject FingerprintManager mFingerprintManager; @Inject FingerprintManager mFingerprintManager;
@Inject FingerprintAuthenticationDialogFragment mFragment; @Inject FingerprintAuthenticationDialogFragment mFragment;
@@ -79,72 +75,65 @@ public class MainActivity extends Activity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
((InjectedApplication) getApplication()).inject(this); ((InjectedApplication) getApplication()).inject(this);
requestPermissions(new String[]{Manifest.permission.USE_FINGERPRINT}, setContentView(R.layout.activity_main);
FINGERPRINT_PERMISSION_REQUEST_CODE); 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.
@Override Toast.makeText(this,
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) { "Secure lock screen hasn't set up.\n"
if (requestCode == FINGERPRINT_PERMISSION_REQUEST_CODE + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
&& state[0] == PackageManager.PERMISSION_GRANTED) { Toast.LENGTH_LONG).show();
setContentView(R.layout.activity_main); purchaseButton.setEnabled(false);
Button purchaseButton = (Button) findViewById(R.id.purchase_button); return;
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;
}
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);
}
}
});
} }
//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);
}
}
});
} }
/** /**

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.flashlight" > package="com.example.android.wearable.flashlight" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -6,6 +6,6 @@ sample.group=Wearable
<p> <p>
Wearable activity that uses your wearable screen as a flashlight. There is also 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.
</p> </p>

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.geofencing"> package="com.example.android.wearable.geofencing">
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.geofencing" > package="com.example.android.wearable.geofencing" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.gridviewpager" > package="com.example.android.wearable.gridviewpager" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.jumpingjack"> package="com.example.android.wearable.jumpingjack">
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -18,7 +18,7 @@
package="com.example.android.support.wearable.notifications" > package="com.example.android.support.wearable.notifications" >
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="21" /> android:targetSdkVersion="23" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />

View File

@@ -18,7 +18,7 @@
package="com.example.android.support.wearable.notifications" > package="com.example.android.support.wearable.notifications" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.quiz" > package="com.example.android.wearable.quiz" >
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="22" /> android:targetSdkVersion="23" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.recipeassistant" > package="com.example.android.wearable.recipeassistant" >
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="21" /> android:targetSdkVersion="23" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@@ -18,7 +18,7 @@
package="com.example.android.google.wearable.app" > package="com.example.android.google.wearable.app" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -2,25 +2,35 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.wearable.speedtracker" > package="com.example.android.wearable.speedtracker" >
<uses-sdk
android:minSdkVersion="18"
android:targetSdkVersion="23" />
<!-- BEGIN_INCLUDE(manifest) -->
<!-- Note that all required permissions are declared here in the Android manifest.
On Android M and above, use of permissions not in the normal permission group are
requested at run time. -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- END_INCLUDE(manifest) -->
<uses-feature android:name="android.hardware.location.gps" android:required="true" /> <uses-feature android:name="android.hardware.location.gps" android:required="true" />
<uses-feature <uses-feature
android:glEsVersion="0x00020000" android:required="true"/> android:glEsVersion="0x00020000" android:required="true"/>
<uses-sdk
android:minSdkVersion="18"
android:targetSdkVersion="21" />
<application <application
android:name=".PhoneApplication" android:name=".PhoneApplication"
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/AppTheme" > android:theme="@style/Theme.AppCompat.Light" >
<meta-data <meta-data
android:name="com.google.android.maps.v2.API_KEY" android:name="com.google.android.maps.v2.API_KEY"
android:value="@string/map_v2_api_key"/> android:value="@string/map_v2_api_key"/>

View File

@@ -21,7 +21,8 @@
<RelativeLayout <RelativeLayout
android:id="@+id/top_container" android:id="@+id/top_container"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<Button <Button
android:id="@+id/date_picker" android:id="@+id/date_picker"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -23,10 +23,10 @@ import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.PolylineOptions; import com.google.android.gms.maps.model.PolylineOptions;
import android.app.Activity;
import android.app.DatePickerDialog; import android.app.DatePickerDialog;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
@@ -45,7 +45,8 @@ import java.util.List;
* a map. This data is then saved into an internal database and the corresponding data items are * a map. This data is then saved into an internal database and the corresponding data items are
* deleted. * deleted.
*/ */
public class PhoneMainActivity extends Activity implements DatePickerDialog.OnDateSetListener { public class PhoneMainActivity extends AppCompatActivity implements
DatePickerDialog.OnDateSetListener {
private static final String TAG = "PhoneMainActivity"; private static final String TAG = "PhoneMainActivity";
private static final int BOUNDING_BOX_PADDING_PX = 50; private static final int BOUNDING_BOX_PADDING_PX = 50;

View File

@@ -19,18 +19,22 @@
<uses-feature android:name="android.hardware.type.watch"/> <uses-feature android:name="android.hardware.type.watch"/>
<uses-feature android:name="android.hardware.location.gps" android:required="true" /> <uses-feature android:name="android.hardware.location.gps" android:required="true" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>\
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-sdk <uses-sdk
android:minSdkVersion="20" android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="23" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@android:style/Theme.DeviceDefault"> android:theme="@android:style/Theme.DeviceDefault">
<!--If you want your app to run on pre-22, then set required to false -->
<uses-library android:name="com.google.android.wearable" android:required="false" />
<meta-data android:name="com.google.android.gms.version" <meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/> android:value="@integer/google_play_services_version"/>
<activity <activity
@@ -38,7 +42,6 @@
android:label="@string/app_name"> android:label="@string/app_name">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
@@ -48,12 +51,6 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.LocationSettingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
</application> </application>
</manifest> </manifest>

View File

@@ -29,11 +29,13 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:paddingLeft="16dp"
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:textAlignment="center"
android:textSize="17sp" android:textSize="17sp"
android:textStyle="italic" android:textStyle="italic"
android:id="@+id/acquiring_gps" android:id="@+id/gps_issue_text"
android:text="@string/acquiring_gps"/> android:text=""/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -84,18 +86,20 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/ic_gps_not_saving_grey600_96dp" android:src="@drawable/ic_gps_not_saving_grey600_96dp"
android:id="@+id/saving" android:id="@+id/gps_permission"
android:onClick="onGpsPermissionClick"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_marginBottom="20dp" android:layout_marginBottom="20dp"
android:layout_marginLeft="60dp" /> android:layout_marginLeft="50dp" />
<ImageButton <ImageButton
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/settings" android:id="@+id/speed_limit_setting"
android:onClick="onSpeedLimitClick"
android:background="@drawable/settings" android:background="@drawable/settings"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignBottom="@+id/saving" android:layout_alignBottom="@+id/gps_permission"
android:layout_marginRight="60dp"/> android:layout_marginRight="50dp"/>
</RelativeLayout> </RelativeLayout>

View File

@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_height="match_parent">
<View
android:id="@+id/center"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_above="@id/center"
android:layout_marginBottom="18dp"
android:fontFamily="sans-serif-light"
android:textSize="18sp"
android:text="@string/start_saving_gps"/>
<android.support.wearable.view.CircledImageView
android:id="@+id/cancelBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_below="@id/center"
android:layout_toLeftOf="@id/center"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_cancel_80"
app:circle_color="@color/grey"
android:onClick="onClick"
app:circle_padding="@dimen/circle_padding"
app:circle_radius="@dimen/circle_radius"
app:circle_radius_pressed="@dimen/circle_radius_pressed" />
<android.support.wearable.view.CircledImageView
android:id="@+id/submitBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_below="@id/center"
android:layout_toRightOf="@id/center"
android:layout_marginStart="10dp"
android:src="@drawable/ic_confirmation_80"
app:circle_color="@color/blue"
android:onClick="onClick"
app:circle_padding="@dimen/circle_padding"
app:circle_radius="@dimen/circle_radius"
app:circle_radius_pressed="@dimen/circle_radius_pressed" />
</RelativeLayout>

View File

@@ -25,11 +25,16 @@
<string name="speed_limit">Limit: %1$d mph</string> <string name="speed_limit">Limit: %1$d mph</string>
<string name="acquiring_gps">Acquiring GPS Fix ...</string> <string name="acquiring_gps">Acquiring GPS Fix ...</string>
<string name="speed_for_list">%1$d mph</string> <string name="speed_for_list">%1$d mph</string>
<string name="start_saving_gps">Start Recording GPS?</string>
<string name="stop_saving_gps">Stop Recording GPS?</string> <string name="enable_disable_gps_label">Enable Location Permission?</string>
<string name="mph">mph</string> <string name="mph">mph</string>
<string name="speed_limit_header">Speed Limit</string> <string name="speed_limit_header">Speed Limit</string>
<string name="gps_not_available">GPS not available.</string> <string name="gps_not_available">No GPS on device. Will use phone GPS when available.</string>
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="speed_format">%.0f</string> <string name="speed_format">%.0f</string>
<string name="permission_rationale">App requires location permission to function, tap GPS icon.</string>
</resources> </resources>

View File

@@ -17,9 +17,8 @@
package com.example.android.wearable.speedtracker; package com.example.android.wearable.speedtracker;
import android.app.Activity; import android.app.Activity;
import android.content.SharedPreferences; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.wearable.view.WearableListView; import android.support.wearable.view.WearableListView;
import android.widget.TextView; 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 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 */ /* Speeds, in mph, that will be shown on the list */
private int[] speeds = {25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75}; 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 @Override
public void onClick(WearableListView.ViewHolder viewHolder) { public void onClick(WearableListView.ViewHolder viewHolder) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
pref.edit().putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY, int newSpeedLimit = speeds[viewHolder.getPosition()];
speeds[viewHolder.getPosition()]).apply();
Intent resultIntent = new Intent(Intent.ACTION_PICK);
resultIntent.putExtra(EXTRA_NEW_SPEED_LIMIT, newSpeedLimit);
setResult(RESULT_OK, resultIntent);
finish(); finish();
} }

View File

@@ -28,7 +28,7 @@ import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.PutDataRequest; import com.google.android.gms.wearable.PutDataRequest;
import com.google.android.gms.wearable.Wearable; import com.google.android.gms.wearable.Wearable;
import android.app.Activity; import android.Manifest;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
@@ -38,18 +38,19 @@ import android.location.Location;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.preference.PreferenceManager; 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.util.Log;
import android.view.View; import android.view.View;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.example.android.wearable.speedtracker.common.Constants; import com.example.android.wearable.speedtracker.common.Constants;
import com.example.android.wearable.speedtracker.common.LocationEntry; import com.example.android.wearable.speedtracker.common.LocationEntry;
import com.example.android.wearable.speedtracker.ui.LocationSettingActivity;
import java.util.Calendar; 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 * 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 * 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. * 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, public class WearableMainActivity extends WearableActivity implements
GoogleApiClient.OnConnectionFailedListener, LocationListener { GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
ActivityCompat.OnRequestPermissionsResultCallback,
LocationListener {
private static final String TAG = "WearableActivity"; private static final String TAG = "WearableActivity";
private static final long UPDATE_INTERVAL_MS = 5 * 1000; private static final long UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
private static final long FASTEST_INTERVAL_MS = 5 * 1000; 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 static final long INDICATOR_DOT_FADE_AWAY_MS = 500L;
private GoogleApiClient mGoogleApiClient; // Request codes for changing speed limit and location permissions.
private TextView mSpeedLimitText; private static final int REQUEST_PICK_SPEED_LIMIT = 0;
private TextView mCurrentSpeedText;
private ImageView mSaveImageView; // Id to identify Location permission request.
private TextView mAcquiringGps; private static final int REQUEST_GPS_PERMISSION = 1;
private TextView mCurrentSpeedMphText;
// 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 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 { private enum SpeedState {
BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above); 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) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate()");
setContentView(R.layout.main_activity); 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 (!hasGps()) {
// If this hardware doesn't support GPS, we prefer to exit. Log.w(TAG, "This hardware doesn't have GPS, so we warn user.");
// 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");
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setMessage(getString(R.string.gps_not_available)) .setMessage(getString(R.string.gps_not_available))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int id) { public void onClick(DialogInterface dialog, int id) {
finish();
dialog.cancel(); dialog.cancel();
} }
}) })
@@ -125,7 +180,6 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
@Override @Override
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
dialog.cancel(); dialog.cancel();
finish();
} }
}) })
.setCancelable(false) .setCancelable(false)
@@ -133,164 +187,216 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
.show(); .show();
} }
setupViews(); setupViews();
updateSpeedVisibility(false);
setSpeedLimit();
mGoogleApiClient = new GoogleApiClient.Builder(this) mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API) .addApi(LocationServices.API)
.addApi(Wearable.API) .addApi(Wearable.API)
.addConnectionCallbacks(this) .addConnectionCallbacks(this)
.addOnConnectionFailedListener(this) .addOnConnectionFailedListener(this)
.build(); .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() { private void setupViews() {
mSpeedLimitText = (TextView) findViewById(R.id.max_speed_text); mSpeedLimitTextView = (TextView) findViewById(R.id.max_speed_text);
mCurrentSpeedText = (TextView) findViewById(R.id.current_speed_text); mSpeedTextView = (TextView) findViewById(R.id.current_speed_text);
mSaveImageView = (ImageView) findViewById(R.id.saving); mCurrentSpeedMphTextView = (TextView) findViewById(R.id.current_speed_mph);
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);
settingButton.setOnClickListener(new View.OnClickListener() { mGpsPermissionImageView = (ImageView) findViewById(R.id.gps_permission);
@Override mGpsIssueTextView = (TextView) findViewById(R.id.gps_issue_text);
public void onClick(View v) { mBlinkingGpsStatusDotView = findViewById(R.id.dot);
Intent speedIntent = new Intent(WearableMainActivity.this,
SpeedPickerActivity.class);
startActivity(speedIntent);
}
});
mSaveImageView.setOnClickListener(new View.OnClickListener() { updateActivityViewsBasedOnLocationPermissions();
@Override
public void onClick(View v) {
Intent savingIntent = new Intent(WearableMainActivity.this,
LocationSettingActivity.class);
startActivity(savingIntent);
}
});
} }
private void setSpeedLimit(int speedLimit) { public void onSpeedLimitClick(View view) {
mSpeedLimitText.setText(getString(R.string.speed_limit, speedLimit)); Intent speedIntent = new Intent(WearableMainActivity.this,
SpeedPickerActivity.class);
startActivityForResult(speedIntent, REQUEST_PICK_SPEED_LIMIT);
} }
private void setSpeedLimit() { public void onGpsPermissionClick(View view) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
mCurrentSpeedLimit = pref.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH);
setSpeedLimit(mCurrentSpeedLimit);
}
private void setCurrentSpeed(float speed) { if (!mGpsPermissionApproved) {
mCurrentSpeed = speed;
mCurrentSpeedText.setText(String.format(getString(R.string.speed_format), speed)); Log.i(TAG, "Location permission has NOT been granted. Requesting permission.");
adjustColor();
// 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() { private void updateActivityViewsBasedOnLocationPermissions() {
SpeedState state = SpeedState.ABOVE;
if (mCurrentSpeed <= mCurrentSpeedLimit - 5) {
state = SpeedState.BELOW;
} else if (mCurrentSpeed <= mCurrentSpeedLimit) {
state = SpeedState.CLOSE;
}
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 @Override
public void onConnected(Bundle bundle) { public void onConnected(Bundle bundle) {
LocationRequest locationRequest = LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setInterval(UPDATE_INTERVAL_MS)
.setFastestInterval(FASTEST_INTERVAL_MS);
LocationServices.FusedLocationApi Log.d(TAG, "onConnected()");
.requestLocationUpdates(mGoogleApiClient, locationRequest, this)
.setResultCallback(new ResultCallback<Status>() {
@Override /*
public void onResult(Status status) { * mGpsPermissionApproved covers 23+ (M+) style permissions. If that is already approved or
if (status.getStatus().isSuccess()) { * the device is pre-23, the app uses mSaveGpsLocation to save the user's location
if (Log.isLoggable(TAG, Log.DEBUG)) { * preference.
Log.d(TAG, "Successfully requested location updates"); */
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<Status>() {
@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 @Override
public void onConnectionSuspended(int i) { 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); LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
} }
@Override @Override
public void onConnectionFailed(ConnectionResult connectionResult) { public void onConnectionFailed(ConnectionResult connectionResult) {
Log.e(TAG, "onConnectionFailed(): connection to location client failed"); Log.e(TAG, "onConnectionFailed(): " + connectionResult.getErrorMessage());
} }
@Override @Override
public void onLocationChanged(Location location) { public void onLocationChanged(Location location) {
updateSpeedVisibility(true); Log.d(TAG, "onLocationChanged() : " + location);
setCurrentSpeed(location.getSpeed() * MPH_IN_METERS_PER_SECOND);
flashDot();
if (mWaitingForGpsSignal) {
mWaitingForGpsSignal = false;
updateActivityViewsBasedOnLocationPermissions();
}
mSpeed = location.getSpeed() * MPH_IN_METERS_PER_SECOND;
updateSpeedInViews();
addLocationEntry(location.getLatitude(), location.getLongitude()); addLocationEntry(location.getLatitude(), location.getLongitude());
} }
/** /*
* Causes the (green) dot blinks when new GPS location data is acquired. * Adds a data item to the data Layer storage.
*/
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
*/ */
private void addLocationEntry(double latitude, double longitude) { private void addLocationEntry(double latitude, double longitude) {
if (!mSaveGpsLocation || !mGoogleApiClient.isConnected()) { if (!mGpsPermissionApproved || !mGoogleApiClient.isConnected()) {
return; return;
} }
mCalendar.setTimeInMillis(System.currentTimeMillis()); 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 @Override
protected void onStop() { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onStop();
if (mGoogleApiClient.isConnected()) { if (requestCode == REQUEST_PICK_SPEED_LIMIT) {
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); 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 @Override
protected void onResume() { public void onRequestPermissionsResult(
super.onResume(); int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
mCalendar = Calendar.getInstance();
setSpeedLimit();
adjustColor();
updateRecordingIcon();
}
private void updateRecordingIcon() { Log.d(TAG, "onRequestPermissionsResult(): " + permissions);
mSaveGpsLocation = LocationSettingActivity.getGpsRecordingStatusFromPreferences(this);
mSaveImageView.setImageResource(mSaveGpsLocation ? R.drawable.ic_gps_saving_grey600_96dp
: R.drawable.ic_gps_not_saving_grey600_96dp); 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() { private boolean hasGps() {
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS); return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS);
} }
} }

View File

@@ -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();
}
}

View File

@@ -41,6 +41,9 @@ public class SpeedPickerListAdapter extends WearableListView.Adapter {
mDataSet = dataset; mDataSet = dataset;
} }
/**
* Displays all possible speed limit choices.
*/
public static class ItemViewHolder extends WearableListView.ViewHolder { public static class ItemViewHolder extends WearableListView.ViewHolder {
private TextView mTextView; private TextView mTextView;

View File

@@ -51,7 +51,7 @@ import java.util.Set;
* Manages documents and exposes them to the Android system for sharing. * Manages documents and exposes them to the Android system for sharing.
*/ */
public class MyCloudProvider extends DocumentsProvider { 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 // Use these as the default columns to return information about a root if no specific
// columns are requested in a query. // columns are requested in a query.

View File

@@ -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 * Toggles the user's login status via a login menu option, and enables/disables the cloud storage
* content provider. * 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 static final String AUTHORITY = "com.example.android.storageprovider.documents";
private boolean mLoggedIn = false; private boolean mLoggedIn = false;

View File

@@ -23,7 +23,7 @@
android:versionName="1.0"> android:versionName="1.0">
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="21" /> android:targetSdkVersion="23" />
<application android:allowBackup="true" <application android:allowBackup="true"
android:label="@string/app_name" android:label="@string/app_name"

View File

@@ -20,7 +20,7 @@
package="com.example.android.wearable.synchronizednotifications"> package="com.example.android.wearable.synchronizednotifications">
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.timer" > package="com.example.android.wearable.timer" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -18,13 +18,19 @@
package="com.example.android.wearable.watchface" > package="com.example.android.wearable.watchface" >
<uses-sdk android:minSdkVersion="18" <uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="21" /> android:targetSdkVersion="23" />
<!-- Permissions required by the wearable app --> <!-- Permissions required by the wearable app -->
<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" /> <uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Requests to calendar are only made on the wear side (CalendarWatchFaceService.java), so
no runtime permissions are needed on the phone side. -->
<uses-permission android:name="android.permission.READ_CALENDAR" /> <uses-permission android:name="android.permission.READ_CALENDAR" />
<!-- Location permission used by FitDistanceWatchFaceService -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- All intent-filters for config actions must include the categories <!-- All intent-filters for config actions must include the categories
com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION and com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION and
android.intent.category.DEFAULT. --> android.intent.category.DEFAULT. -->
@@ -55,6 +61,18 @@
</intent-filter> </intent-filter>
</activity> </activity>
<!-- This activity is needed to allow the user to authorize Google Fit for the Fit Distance
WatchFace (required to view distance). -->
<activity
android:name=".FitDistanceWatchFaceConfigActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="com.example.android.wearable.watchface.CONFIG_FIT_DISTANCE" />
<category android:name="com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity <activity
android:name=".OpenGLWatchFaceConfigActivity" android:name=".OpenGLWatchFaceConfigActivity"
android:label="@string/app_name"> android:label="@string/app_name">

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
tools:context="com.example.android.wearable.watchface.FitDistanceWatchFaceConfigActivity">
<Switch
android:id="@+id/fit_auth_switch"
android:text="@string/fit_config_switch_text"
android:enabled="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onSwitchClicked"/>
</RelativeLayout>

View File

@@ -0,0 +1,6 @@
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
</resources>

View File

@@ -23,8 +23,13 @@
This sample demonstrates how to create watch faces for android wear and includes a phone app 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, 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. 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.
]]> ]]>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
<dimen name="activity_vertical_margin">10dp</dimen>
</resources>

View File

@@ -22,6 +22,8 @@
<string name="digital_config_minutes">Minutes</string> <string name="digital_config_minutes">Minutes</string>
<string name="digital_config_seconds">Seconds</string> <string name="digital_config_seconds">Seconds</string>
<string name="fit_config_switch_text">Google Fit</string>
<string name="title_no_device_connected">No wearable device is currently connected.</string> <string name="title_no_device_connected">No wearable device is currently connected.</string>
<string name="ok_no_device_connected">OK</string> <string name="ok_no_device_connected">OK</string>

View File

@@ -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<Status> pendingResult = Fitness.ConfigApi.disableFit(mGoogleApiClient);
pendingResult.setResultCallback(new ResultCallback<Status>() {
@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);
}
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project <!--
Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -13,12 +14,12 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.wearable.watchface" > package="com.example.android.wearable.watchface" >
<uses-sdk android:minSdkVersion="21" <uses-sdk
android:targetSdkVersion="21" /> android:minSdkVersion="21"
android:targetSdkVersion="23" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />
@@ -29,102 +30,113 @@
<!-- Calendar permission used by CalendarWatchFaceService --> <!-- Calendar permission used by CalendarWatchFaceService -->
<uses-permission android:name="android.permission.READ_CALENDAR" /> <uses-permission android:name="android.permission.READ_CALENDAR" />
<!-- Location permission used by FitDistanceWatchFaceService -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" > android:label="@string/app_name" >
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<uses-library android:name="com.google.android.wearable" android:required="false" />
<service <service
android:name=".AnalogWatchFaceService" android:name=".AnalogWatchFaceService"
android:label="@string/analog_name" android:label="@string/analog_name"
android:permission="android.permission.BIND_WALLPAPER" > android:permission="android.permission.BIND_WALLPAPER" >
<meta-data <meta-data
android:name="android.service.wallpaper" android:name="android.service.wallpaper"
android:resource="@xml/watch_face" /> android:resource="@xml/watch_face" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.preview" android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_analog" /> android:resource="@drawable/preview_analog" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.preview_circular" android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_analog_circular" /> android:resource="@drawable/preview_analog_circular" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction" android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="com.example.android.wearable.watchface.CONFIG_ANALOG" /> android:value="com.example.android.wearable.watchface.CONFIG_ANALOG" />
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<service
android:name=".SweepWatchFaceService"
android:label="@string/sweep_name"
android:permission="android.permission.BIND_WALLPAPER" >
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_analog" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_analog_circular" />
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<service
android:name=".OpenGLWatchFaceService"
android:label="@string/opengl_name"
android:permission="android.permission.BIND_WALLPAPER" >
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_opengl" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_opengl_circular" />
<meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="com.example.android.wearable.watchface.CONFIG_OPENGL" />
<intent-filter> <intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" /> <action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".CardBoundsWatchFaceService" android:name=".SweepWatchFaceService"
android:label="@string/card_bounds_name" android:label="@string/sweep_name"
android:permission="android.permission.BIND_WALLPAPER" > android:permission="android.permission.BIND_WALLPAPER" >
<meta-data <meta-data
android:name="android.service.wallpaper" android:name="android.service.wallpaper"
android:resource="@xml/watch_face" /> android:resource="@xml/watch_face" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.preview" android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_card_bounds" /> android:resource="@drawable/preview_analog" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.preview_circular" android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_card_bounds_circular" /> android:resource="@drawable/preview_analog_circular" />
<meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="com.example.android.wearable.watchface.CONFIG_CARD_BOUNDS" />
<intent-filter> <intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" /> <action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".InteractiveWatchFaceService" android:name=".OpenGLWatchFaceService"
android:label="@string/interactive_name" android:label="@string/opengl_name"
android:permission="android.permission.BIND_WALLPAPER" > android:permission="android.permission.BIND_WALLPAPER" >
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_opengl" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_opengl_circular" />
<meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="com.example.android.wearable.watchface.CONFIG_OPENGL" />
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<service
android:name=".CardBoundsWatchFaceService"
android:label="@string/card_bounds_name"
android:permission="android.permission.BIND_WALLPAPER" >
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_card_bounds" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_card_bounds_circular" />
<meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="com.example.android.wearable.watchface.CONFIG_CARD_BOUNDS" />
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<service
android:name=".InteractiveWatchFaceService"
android:label="@string/interactive_name"
android:permission="android.permission.BIND_WALLPAPER" >
<meta-data <meta-data
android:name="android.service.wallpaper" android:name="android.service.wallpaper"
android:resource="@xml/watch_face" /> android:resource="@xml/watch_face" />
@@ -134,81 +146,130 @@
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.preview_circular" android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_interactive_circular" /> android:resource="@drawable/preview_interactive_circular" />
<intent-filter> <intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" /> <action android:name="android.service.wallpaper.WallpaperService" />
<category
android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".DigitalWatchFaceService" android:name=".DigitalWatchFaceService"
android:label="@string/digital_name" android:label="@string/digital_name"
android:permission="android.permission.BIND_WALLPAPER" > android:permission="android.permission.BIND_WALLPAPER" >
<meta-data <meta-data
android:name="android.service.wallpaper" android:name="android.service.wallpaper"
android:resource="@xml/watch_face" /> android:resource="@xml/watch_face" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.preview" android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_digital" /> android:resource="@drawable/preview_digital" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.preview_circular" android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_digital_circular" /> android:resource="@drawable/preview_digital_circular" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction" android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" /> android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction" android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" /> android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
<intent-filter> <intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" /> <action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter> </intent-filter>
</service> </service>
<!-- All intent-filters for config actions must include the categories <!--
All intent-filters for config actions must include the categories
com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION
and android.intent.category.DEFAULT. --> and android.intent.category.DEFAULT.
-->
<activity <activity
android:name=".DigitalWatchFaceWearableConfigActivity" android:name=".DigitalWatchFaceWearableConfigActivity"
android:label="@string/digital_config_name"> android:label="@string/digital_config_name" >
<intent-filter> <intent-filter>
<action android:name="com.example.android.wearable.watchface.CONFIG_DIGITAL" /> <action android:name="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
<category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" /> <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
<service <service
android:name=".CalendarWatchFaceService" android:name=".CalendarWatchFaceService"
android:label="@string/calendar_name" android:label="@string/calendar_name"
android:permission="android.permission.BIND_WALLPAPER" > android:permission="android.permission.BIND_WALLPAPER" >
<meta-data <meta-data
android:name="android.service.wallpaper" android:name="android.service.wallpaper"
android:resource="@xml/watch_face" /> android:resource="@xml/watch_face" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.preview" android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_calendar" /> android:resource="@drawable/preview_calendar" />
<meta-data <meta-data
android:name="com.google.android.wearable.watchface.preview_circular" android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_calendar_circular" /> android:resource="@drawable/preview_calendar_circular" />
<intent-filter> <intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" /> <action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter> </intent-filter>
</service> </service>
<service android:name=".DigitalWatchFaceConfigListenerService" >
<service android:name=".DigitalWatchFaceConfigListenerService">
<intent-filter> <intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".FitDistanceWatchFaceService"
android:label="@string/fit_distance_name"
android:permission="android.permission.BIND_WALLPAPER" >
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_distance" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_distance_circular" />
<meta-data
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
android:value="com.example.android.wearable.watchface.CONFIG_FIT_DISTANCE" />
<meta-data <intent-filter>
android:name="com.google.android.gms.version" <action android:name="android.service.wallpaper.WallpaperService" />
android:value="@integer/google_play_services_version" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<service
android:name=".FitStepsWatchFaceService"
android:label="@string/fit_steps_name"
android:permission="android.permission.BIND_WALLPAPER" >
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_fit" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_fit_circular" />
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<activity
android:name=".CalendarWatchFacePermissionActivity"
android:label="@string/title_activity_calendar_watch_face_permission" >
</activity>
</application> </application>
</manifest> </manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/container"
android:background="@color/white"
android:paddingTop="32dp"
android:paddingLeft="36dp"
android:paddingRight="22dp"
tools:context="com.example.android.wearable.watchface.CalendarWatchFacePermissionActivity"
tools:deviceIds="wear">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="onClickEnablePermission"
android:orientation="vertical"
app:layout_box="all">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:paddingBottom="18dp"
android:textColor="#000000"
android:text="@string/calendar_permission_text"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<android.support.wearable.view.CircledImageView
android:id="@+id/circle"
android:layout_width="40dp"
android:layout_height="40dp"
app:circle_radius="20dp"
app:circle_color="#0086D4"
android:src="@drawable/ic_lock_open_white_24dp"/>
<android.support.v4.widget.Space
android:layout_width="8dp"
android:layout_height="8dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#0086D4"
android:text="Enable Permission"/>
</LinearLayout>
</LinearLayout>
</android.support.wearable.view.BoxInsetLayout>

View File

@@ -32,4 +32,15 @@
<dimen name="interactive_y_offset">72dp</dimen> <dimen name="interactive_y_offset">72dp</dimen>
<dimen name="interactive_y_offset_round">84dp</dimen> <dimen name="interactive_y_offset_round">84dp</dimen>
<dimen name="interactive_line_height">25dp</dimen> <dimen name="interactive_line_height">25dp</dimen>
<dimen name="fit_text_size">40dp</dimen>
<dimen name="fit_text_size_round">45dp</dimen>
<dimen name="fit_steps_or_distance_text_size">20dp</dimen>
<dimen name="fit_am_pm_size">25dp</dimen>
<dimen name="fit_am_pm_size_round">30dp</dimen>
<dimen name="fit_x_offset">15dp</dimen>
<dimen name="fit_x_offset_round">25dp</dimen>
<dimen name="fit_steps_or_distance_x_offset">20dp</dimen>
<dimen name="fit_steps_or_distance_x_offset_round">30dp</dimen>
<dimen name="fit_y_offset">80dp</dimen>
<dimen name="fit_line_height">25dp</dimen>
</resources> </resources>

View File

@@ -25,11 +25,22 @@
<string name="digital_config_name">Digital watch face configuration</string> <string name="digital_config_name">Digital watch face configuration</string>
<string name="digital_am">AM</string> <string name="digital_am">AM</string>
<string name="digital_pm">PM</string> <string name="digital_pm">PM</string>
<string name="fit_steps_name">Sample Fit Steps</string>
<string name="fit_distance_name">Sample Fit Distance</string>
<string name="fit_am">AM</string>
<string name="fit_pm">PM</string>
<string name="fit_steps">%1$d steps</string>
<string name="fit_distance">%1$,.2f meters</string>
<string name="calendar_name">Sample Calendar</string> <string name="calendar_name">Sample Calendar</string>
<string name="calendar_permission_not_approved">&lt;br&gt;&lt;br&gt;&lt;br&gt;WatchFace requires Calendar permission. Click on this WatchFace or visit Settings &gt; Permissions to approve.</string>
<plurals name="calendar_meetings"> <plurals name="calendar_meetings">
<item quantity="one">&lt;br&gt;&lt;br&gt;&lt;br&gt;You have &lt;b&gt;%1$d&lt;/b&gt; meeting in the next 24 hours.</item> <item quantity="one">&lt;br&gt;&lt;br&gt;&lt;br&gt;You have &lt;b&gt;%1$d&lt;/b&gt; meeting in the next 24 hours.</item>
<item quantity="other">&lt;br&gt;&lt;br&gt;&lt;br&gt;You have &lt;b&gt;%1$d&lt;/b&gt; meetings in the next 24 hours.</item> <item quantity="other">&lt;br&gt;&lt;br&gt;&lt;br&gt;You have &lt;b&gt;%1$d&lt;/b&gt; meetings in the next 24 hours.</item>
</plurals> </plurals>
<string name="title_activity_calendar_watch_face_permission">Calendar Permission Activity</string>
<string name="calendar_permission_text">WatchFace requires Calendar access.</string>
<!-- TODO: this should be shared (needs covering all the samples with Gradle build model) --> <!-- TODO: this should be shared (needs covering all the samples with Gradle build model) -->
<string name="color_black">Black</string> <string name="color_black">Black</string>

View File

@@ -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();
}
}
}
}

View File

@@ -16,11 +16,13 @@
package com.example.android.wearable.watchface; package com.example.android.wearable.watchface;
import android.Manifest;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
@@ -30,8 +32,10 @@ import android.os.AsyncTask;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.PowerManager; import android.os.PowerManager;
import android.support.v4.app.ActivityCompat;
import android.support.wearable.provider.WearableCalendarContract; import android.support.wearable.provider.WearableCalendarContract;
import android.support.wearable.watchface.CanvasWatchFaceService; import android.support.wearable.watchface.CanvasWatchFaceService;
import android.support.wearable.watchface.WatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle; import android.support.wearable.watchface.WatchFaceStyle;
import android.text.DynamicLayout; import android.text.DynamicLayout;
import android.text.Editable; import android.text.Editable;
@@ -74,31 +78,37 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
final TextPaint mTextPaint = new TextPaint(); final TextPaint mTextPaint = new TextPaint();
int mNumMeetings; int mNumMeetings;
private boolean mCalendarPermissionApproved;
private String mCalendarNotApprovedMessage;
private AsyncTask<Void, Void, Integer> mLoadMeetingsTask; private AsyncTask<Void, Void, Integer> mLoadMeetingsTask;
private boolean mIsReceiverRegistered;
/** Handler to load the meetings once a minute in interactive mode. */ /** Handler to load the meetings once a minute in interactive mode. */
final Handler mLoadMeetingsHandler = new Handler() { final Handler mLoadMeetingsHandler = new Handler() {
@Override @Override
public void handleMessage(Message message) { public void handleMessage(Message message) {
switch (message.what) { switch (message.what) {
case MSG_LOAD_MEETINGS: case MSG_LOAD_MEETINGS:
cancelLoadMeetingTask(); cancelLoadMeetingTask();
mLoadMeetingsTask = new LoadMeetingsTask();
mLoadMeetingsTask.execute(); // Loads meetings.
if (mCalendarPermissionApproved) {
mLoadMeetingsTask = new LoadMeetingsTask();
mLoadMeetingsTask.execute();
}
break; break;
} }
} }
}; };
private boolean mIsReceiverRegistered;
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_PROVIDER_CHANGED.equals(intent.getAction()) if (Intent.ACTION_PROVIDER_CHANGED.equals(intent.getAction())
&& WearableCalendarContract.CONTENT_URI.equals(intent.getData())) { && WearableCalendarContract.CONTENT_URI.equals(intent.getData())) {
cancelLoadMeetingTask();
mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS); mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
} }
} }
@@ -106,29 +116,59 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
@Override @Override
public void onCreate(SurfaceHolder holder) { public void onCreate(SurfaceHolder holder) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCreate");
}
super.onCreate(holder); 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) setWatchFaceStyle(new WatchFaceStyle.Builder(CalendarWatchFaceService.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE) .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false) .setShowSystemUiTime(false)
.setAcceptsTapEvents(true)
.build()); .build());
mTextPaint.setColor(FOREGROUND_COLOR); mTextPaint.setColor(FOREGROUND_COLOR);
mTextPaint.setTextSize(TEXT_SIZE); 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 @Override
public void onDestroy() { public void onDestroy() {
mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS); mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);
cancelLoadMeetingTask();
super.onDestroy(); 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 @Override
public void onDraw(Canvas canvas, Rect bounds) { public void onDraw(Canvas canvas, Rect bounds) {
// Create or update mLayout if necessary. // Create or update mLayout if necessary.
@@ -141,8 +181,13 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
// Update the contents of mEditable. // Update the contents of mEditable.
mEditable.clear(); 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. // Draw the text on a solid background.
canvas.drawColor(BACKGROUND_COLOR); canvas.drawColor(BACKGROUND_COLOR);
@@ -151,15 +196,24 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
@Override @Override
public void onVisibilityChanged(boolean visible) { public void onVisibilityChanged(boolean visible) {
Log.d(TAG, "onVisibilityChanged()");
super.onVisibilityChanged(visible); super.onVisibilityChanged(visible);
if (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 { } else {
if (mIsReceiverRegistered) { if (mIsReceiverRegistered) {
unregisterReceiver(mBroadcastReceiver); unregisterReceiver(mBroadcastReceiver);
@@ -204,9 +258,9 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
final Cursor cursor = getContentResolver().query(builder.build(), final Cursor cursor = getContentResolver().query(builder.build(),
null, null, null, null); null, null, null, null);
int numMeetings = cursor.getCount(); int numMeetings = cursor.getCount();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Num meetings: " + numMeetings); Log.d(TAG, "Num meetings: " + numMeetings);
}
return numMeetings; return numMeetings;
} }

View File

@@ -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<DailyTotalResult> {
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<DailyTotalResult> 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<Status>() {
@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<DataPoint> 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());
}
}
}
}

View File

@@ -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<DailyTotalResult> {
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<DailyTotalResult> 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<Status>() {
@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<DataPoint> 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());
}
}
}
}

View File

@@ -7,7 +7,12 @@ sample.group=Wearable
This sample demonstrates how to create watch faces for android wear and includes a phone app 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, 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. 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.
</p> </p>

View File

@@ -18,7 +18,7 @@
package="com.example.android.google.wearable.watchviewstub" > package="com.example.android.google.wearable.watchviewstub" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" /> android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.wearable.speaker" >
<uses-feature android:name="android.hardware.type.watch" />
<!-- the following permission is required to record audio using a microphone -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault" >
<uses-library android:name="com.google.android.wearable" android:required="false" />
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,14 @@
page.tags="WearSpeakerSample"
sample.group=Wearable
@jd:body
<p>
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").
</p>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
<size android:width="100dp"
android:height="100dp"/>
<stroke
android:width="3dp"
android:color="@color/circle_color"/>
<solid android:color="@color/circle_color"/>
</shape>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<vector android:height="120dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="120dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/large_icons_color" android:pathData="M12,3v9.28c-0.47,-0.17 -0.97,-0.28 -1.5,-0.28C8.01,12 6,14.01 6,16.5S8.01,21 10.5,21c2.31,0 4.2,-1.75 4.45,-4H15V6h4V3h-7z"/>
</vector>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<vector android:height="32dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/small_icons_color" android:pathData="M12,3v9.28c-0.47,-0.17 -0.97,-0.28 -1.5,-0.28C8.01,12 6,14.01 6,16.5S8.01,21 10.5,21c2.31,0 4.2,-1.75 4.45,-4H15V6h4V3h-7z"/>
</vector>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<vector android:height="120dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="120dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/large_icons_color" android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zm5.3,-3c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<vector android:height="32dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/small_icons_color" android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zm5.3,-3c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<vector android:height="120dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="120dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/large_icons_color" android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<vector android:height="32dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/small_icons_color" android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/container2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_color">
<View
android:id="@+id/circle"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_centerInParent="true"
android:background="@drawable/circle" />
<View
android:id="@+id/center"
android:layout_width="1dp"
android:layout_height="1dp"
android:layout_centerInParent="true"
android:visibility="invisible" />
<ImageView
android:id="@+id/mic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/center"
android:layout_centerHorizontal="true"
android:layout_marginBottom="13dp"
android:src="@drawable/ic_mic_32dp" />
<ImageView
android:id="@+id/play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/center"
android:layout_marginRight="13dp"
android:layout_marginTop="12dp"
android:layout_toLeftOf="@+id/center"
android:src="@drawable/ic_play_arrow_32dp" />
<ImageView
android:id="@+id/music"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/center"
android:layout_marginLeft="13dp"
android:layout_marginTop="12dp"
android:layout_toRightOf="@+id/center"
android:src="@drawable/ic_audiotrack_32dp" />
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@+id/circle"
android:layout_alignEnd="@+id/circle"
android:layout_below="@+id/circle"
android:progressTint="@color/progressbar_tint"
android:progressBackgroundTint="@color/progressbar_background_tint"
android:layout_marginTop="5dp"
android:visibility="invisible" />
</RelativeLayout>
<ImageView
android:id="@+id/expanded"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="center"
android:visibility="invisible" />
</FrameLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<resources>
<color name="small_icons_color">#FFF3E0</color>
<color name="large_icons_color">#FFF3E0</color>
<color name="background_color">#FF9100</color>
<color name="circle_color">#E65100</color>
<color name="progressbar_tint">#FFD180</color>
<color name="progressbar_background_tint">#E65100</color>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 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.
-->
<resources>
<string name="app_name">Wear Speaker Sample</string>
<string name="exiting_for_permissions">Recording Audio permission is required, exiting now!</string>
<string name="no_speaker_supported">Speaker is not supported</string>
</resources>

View File

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

View File

@@ -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<Void, Void, Void> mRecordingAsyncTask;
private AsyncTask<Void, Void, Void> 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<Void, Void, Void>() {
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<Void, Void, Void>() {
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();
}
}

View File

@@ -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();
}
}