diff options
132 files changed, 4633 insertions, 740 deletions
diff --git a/admin/AppRestrictionEnforcer/Application/src/main/java/com/example/android/apprestrictionenforcer/AppRestrictionEnforcerFragment.java b/admin/AppRestrictionEnforcer/Application/src/main/java/com/example/android/apprestrictionenforcer/AppRestrictionEnforcerFragment.java index e30a9a46..361c4ac3 100644 --- a/admin/AppRestrictionEnforcer/Application/src/main/java/com/example/android/apprestrictionenforcer/AppRestrictionEnforcerFragment.java +++ b/admin/AppRestrictionEnforcer/Application/src/main/java/com/example/android/apprestrictionenforcer/AppRestrictionEnforcerFragment.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.RestrictionEntry; import android.content.RestrictionsManager; import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.NonNull; @@ -105,6 +106,8 @@ public class AppRestrictionEnforcerFragment extends Fragment implements private static final String DELIMETER = ","; private static final String SEPARATOR = ":"; + private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23; + /** * Current status of the restrictions. */ @@ -138,6 +141,15 @@ public class AppRestrictionEnforcerFragment extends Fragment implements mEditProfileAge = (EditText) view.findViewById(R.id.profile_age); mLayoutItems = (LinearLayout) view.findViewById(R.id.items); view.findViewById(R.id.item_add).setOnClickListener(this); + View bundleLayout = view.findViewById(R.id.bundle_layout); + View bundleArrayLayout = view.findViewById(R.id.bundle_array_layout); + if (BUNDLE_SUPPORTED) { + bundleLayout.setVisibility(View.VISIBLE); + bundleArrayLayout.setVisibility(View.VISIBLE); + } else { + bundleLayout.setVisibility(View.GONE); + bundleArrayLayout.setVisibility(View.GONE); + } } @Override @@ -280,7 +292,7 @@ public class AppRestrictionEnforcerFragment extends Fragment implements TextUtils.join(DELIMETER, restriction.getAllSelectedStrings())), DELIMETER)); - } else if (RESTRICTION_KEY_PROFILE.equals(key)) { + } else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_PROFILE.equals(key)) { String name = null; int age = 0; for (RestrictionEntry entry : restriction.getRestrictions()) { @@ -294,7 +306,7 @@ public class AppRestrictionEnforcerFragment extends Fragment implements name = prefs.getString(RESTRICTION_KEY_PROFILE_NAME, name); age = prefs.getInt(RESTRICTION_KEY_PROFILE_AGE, age); updateProfile(name, age); - } else if (RESTRICTION_KEY_ITEMS.equals(key)) { + } else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_ITEMS.equals(key)) { String itemsString = prefs.getString(RESTRICTION_KEY_ITEMS, ""); HashMap<String, String> items = new HashMap<>(); for (String itemString : TextUtils.split(itemsString, DELIMETER)) { @@ -351,6 +363,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements } private void updateProfile(String name, int age) { + if (!BUNDLE_SUPPORTED) { + return; + } Bundle profile = new Bundle(); profile.putString(RESTRICTION_KEY_PROFILE_NAME, name); profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age); @@ -364,6 +379,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements } private void updateItems(Context context, Map<String, String> items) { + if (!BUNDLE_SUPPORTED) { + return; + } mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items)); LayoutInflater inflater = LayoutInflater.from(context); mLayoutItems.removeAllViews(); @@ -500,6 +518,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements * @param age The value to be set for the "age" field. */ private void saveProfile(Activity activity, String name, int age) { + if (!BUNDLE_SUPPORTED) { + return; + } Bundle profile = new Bundle(); profile.putString(RESTRICTION_KEY_PROFILE_NAME, name); profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age); @@ -515,6 +536,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements * @param items The values. */ private void saveItems(Activity activity, Map<String, String> items) { + if (!BUNDLE_SUPPORTED) { + return; + } mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items)); saveRestrictions(activity); StringBuilder builder = new StringBuilder(); diff --git a/admin/AppRestrictionEnforcer/Application/src/main/res/layout/fragment_app_restriction_enforcer.xml b/admin/AppRestrictionEnforcer/Application/src/main/res/layout/fragment_app_restriction_enforcer.xml index 56c9133a..b6839897 100644 --- a/admin/AppRestrictionEnforcer/Application/src/main/res/layout/fragment_app_restriction_enforcer.xml +++ b/admin/AppRestrictionEnforcer/Application/src/main/res/layout/fragment_app_restriction_enforcer.xml @@ -120,6 +120,7 @@ </LinearLayout> <LinearLayout + android:id="@+id/bundle_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> @@ -147,6 +148,7 @@ </LinearLayout> <RelativeLayout + android:id="@+id/bundle_array_layout" android:layout_width="match_parent" android:layout_height="wrap_content"> diff --git a/admin/AppRestrictionEnforcer/template-params.xml b/admin/AppRestrictionEnforcer/template-params.xml index 3fe913d8..eee3d7ec 100644 --- a/admin/AppRestrictionEnforcer/template-params.xml +++ b/admin/AppRestrictionEnforcer/template-params.xml @@ -22,7 +22,7 @@ <group>Admin</group> <package>com.example.android.apprestrictionenforcer</package> - <minSdk>23</minSdk> + <minSdk>21</minSdk> <strings> <intro> diff --git a/admin/AppRestrictionSchema/Application/src/main/java/com/example/android/apprestrictionschema/AppRestrictionSchemaFragment.java b/admin/AppRestrictionSchema/Application/src/main/java/com/example/android/apprestrictionschema/AppRestrictionSchemaFragment.java index ea1aad85..bbb1ef86 100644 --- a/admin/AppRestrictionSchema/Application/src/main/java/com/example/android/apprestrictionschema/AppRestrictionSchemaFragment.java +++ b/admin/AppRestrictionSchema/Application/src/main/java/com/example/android/apprestrictionschema/AppRestrictionSchemaFragment.java @@ -19,6 +19,7 @@ package com.example.android.apprestrictionschema; import android.content.Context; import android.content.RestrictionEntry; import android.content.RestrictionsManager; +import android.os.Build; import android.os.Bundle; import android.os.Parcelable; 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_VALUE = "value"; + private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23; + // Message to show when the button is clicked (String restriction) private String mMessage; @@ -82,9 +85,22 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli mTextNumber = (TextView) view.findViewById(R.id.your_number); mTextRank = (TextView) view.findViewById(R.id.your_rank); mTextApprovals = (TextView) view.findViewById(R.id.approvals_you_have); + View bundleSeparator = view.findViewById(R.id.bundle_separator); mTextProfile = (TextView) view.findViewById(R.id.your_profile); + View bundleArraySeparator = view.findViewById(R.id.bundle_array_separator); mTextItems = (TextView) view.findViewById(R.id.your_items); mButtonSayHello.setOnClickListener(this); + if (BUNDLE_SUPPORTED) { + bundleSeparator.setVisibility(View.VISIBLE); + mTextProfile.setVisibility(View.VISIBLE); + bundleArraySeparator.setVisibility(View.VISIBLE); + mTextItems.setVisibility(View.VISIBLE); + } else { + bundleSeparator.setVisibility(View.GONE); + mTextProfile.setVisibility(View.GONE); + bundleArraySeparator.setVisibility(View.GONE); + mTextItems.setVisibility(View.GONE); + } } @Override @@ -178,6 +194,9 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli } private void updateProfile(RestrictionEntry entry, Bundle restrictions) { + if (!BUNDLE_SUPPORTED) { + return; + } String name = null; int age = 0; if (restrictions == null || !restrictions.containsKey(KEY_PROFILE)) { @@ -201,6 +220,9 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli } private void updateItems(RestrictionEntry entry, Bundle restrictions) { + if (!BUNDLE_SUPPORTED) { + return; + } StringBuilder builder = new StringBuilder(); if (restrictions != null) { Parcelable[] parcelables = restrictions.getParcelableArray(KEY_ITEMS); diff --git a/admin/AppRestrictionSchema/Application/src/main/res/layout/fragment_app_restriction_schema.xml b/admin/AppRestrictionSchema/Application/src/main/res/layout/fragment_app_restriction_schema.xml index 85708691..02d83e61 100644 --- a/admin/AppRestrictionSchema/Application/src/main/res/layout/fragment_app_restriction_schema.xml +++ b/admin/AppRestrictionSchema/Application/src/main/res/layout/fragment_app_restriction_schema.xml @@ -59,7 +59,7 @@ limitations under the License. android:textAppearance="?android:attr/textAppearanceMedium" tools:text="@string/your_rank"/> - <include layout="@layout/separator"/> + <include layout="@layout/separator" android:id="@+id/bundle_separator"/> <TextView android:id="@+id/approvals_you_have" @@ -77,7 +77,7 @@ limitations under the License. android:textAppearance="?android:attr/textAppearanceMedium" tools:text="@string/your_profile"/> - <include layout="@layout/separator"/> + <include layout="@layout/separator" android:id="@+id/bundle_array_separator" /> <TextView android:id="@+id/your_items" diff --git a/admin/AppRestrictionSchema/template-params.xml b/admin/AppRestrictionSchema/template-params.xml index 3e7a2024..508393ec 100644 --- a/admin/AppRestrictionSchema/template-params.xml +++ b/admin/AppRestrictionSchema/template-params.xml @@ -20,7 +20,7 @@ <group>Admin</group> <package>com.example.android.apprestrictionschema</package> - <minSdk>23</minSdk> + <minSdk>21</minSdk> <strings> <intro> diff --git a/build.gradle b/build.gradle index a4be013b..7735d233 100644 --- a/build.gradle +++ b/build.gradle @@ -110,6 +110,7 @@ List<String> samples = [ "media/MidiScope", "media/MidiSynth", "security/AsymmetricFingerprintDialog", +"wearable/wear/WearSpeakerSample", ] List<String> taskNames = [ diff --git a/content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/MyCloudProvider.java b/content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/MyCloudProvider.java index d8be8138..9f9249a3 100644 --- a/content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/MyCloudProvider.java +++ b/content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/MyCloudProvider.java @@ -51,7 +51,7 @@ import java.util.Set; * Manages documents and exposes them to the Android system for sharing. */ public class MyCloudProvider extends DocumentsProvider { - private static final String TAG = MyCloudProvider.class.getSimpleName(); + private static final String TAG = "MyCloudProvider"; // Use these as the default columns to return information about a root if no specific // columns are requested in a query. diff --git a/content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/MyCloudFragment.java b/content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/StorageProviderFragment.java index f624e908..80d0296d 100644 --- a/content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/MyCloudFragment.java +++ b/content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/StorageProviderFragment.java @@ -31,9 +31,9 @@ import com.example.android.common.logger.Log; * Toggles the user's login status via a login menu option, and enables/disables the cloud storage * content provider. */ -public class MyCloudFragment extends Fragment { +public class StorageProviderFragment extends Fragment { - private static final String TAG = "MyCloudFragment"; + private static final String TAG = "StorageProviderFragment"; private static final String AUTHORITY = "com.example.android.storageprovider.documents"; private boolean mLoggedIn = false; diff --git a/content/documentsUi/StorageProvider/README.md b/content/documentsUi/StorageProvider/README.md index 9040e7b3..bc343bcd 100644 --- a/content/documentsUi/StorageProvider/README.md +++ b/content/documentsUi/StorageProvider/README.md @@ -1,5 +1,5 @@ -Android MyCloud Sample +Android StorageProvider Sample =================================== This sample shows how to implement a simple documents provider using the storage access @@ -42,7 +42,7 @@ Support - Stack Overflow: http://stackoverflow.com/questions/tagged/android If you've found an error in this sample, please file an issue: -https://github.com/googlesamples/android-MyCloud +https://github.com/googlesamples/android-StorageProvider Patches are encouraged, and may be submitted by forking this project and submitting a pull request through GitHub. Please see CONTRIBUTING.md for more details. diff --git a/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java b/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java index 4bf85343..c2b99bc4 100644 --- a/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java +++ b/media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java @@ -28,6 +28,7 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.ImageFormat; import android.graphics.Matrix; +import android.graphics.Point; import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; @@ -117,6 +118,16 @@ public class Camera2BasicFragment extends Fragment private static final int STATE_PICTURE_TAKEN = 4; /** + * Max preview width that is guaranteed by Camera2 API + */ + private static final int MAX_PREVIEW_WIDTH = 1920; + + /** + * Max preview height that is guaranteed by Camera2 API + */ + private static final int MAX_PREVIEW_HEIGHT = 1080; + + /** * {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a * {@link TextureView}. */ @@ -344,31 +355,48 @@ public class Camera2BasicFragment extends Fragment } /** - * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose - * width and height are at least as large as the respective requested values, and whose aspect - * ratio matches with the specified value. + * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that + * is at least as large as the respective texture view size, and that is at most as large as the + * respective max size, and whose aspect ratio matches with the specified value. If such size + * doesn't exist, choose the largest one that is at most as large as the respective max size, + * and whose aspect ratio matches with the specified value. * - * @param choices The list of sizes that the camera supports for the intended output class - * @param width The minimum desired width - * @param height The minimum desired height - * @param aspectRatio The aspect ratio + * @param choices The list of sizes that the camera supports for the intended output + * class + * @param textureViewWidth The width of the texture view relative to sensor coordinate + * @param textureViewHeight The height of the texture view relative to sensor coordinate + * @param maxWidth The maximum width that can be chosen + * @param maxHeight The maximum height that can be chosen + * @param aspectRatio The aspect ratio * @return The optimal {@code Size}, or an arbitrary one if none were big enough */ - private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) { + private static Size chooseOptimalSize(Size[] choices, int textureViewWidth, + int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) { + // Collect the supported resolutions that are at least as big as the preview Surface List<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 h = aspectRatio.getHeight(); for (Size option : choices) { - if (option.getHeight() == option.getWidth() * h / w && - option.getWidth() >= width && option.getHeight() >= height) { - bigEnough.add(option); + if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight && + option.getHeight() == option.getWidth() * h / w) { + if (option.getWidth() >= textureViewWidth && + option.getHeight() >= textureViewHeight) { + bigEnough.add(option); + } else { + notBigEnough.add(option); + } } } - // Pick the smallest of those, assuming we found any + // Pick the smallest of those big enough. If there is no one big enough, pick the + // largest of those not big enough. if (bigEnough.size() > 0) { return Collections.min(bigEnough, new CompareSizesByArea()); + } else if (notBigEnough.size() > 0) { + return Collections.max(notBigEnough, new CompareSizesByArea()); } else { Log.e(TAG, "Couldn't find any suitable preview size"); return choices[0]; @@ -478,11 +506,57 @@ public class Camera2BasicFragment extends Fragment mImageReader.setOnImageAvailableListener( mOnImageAvailableListener, mBackgroundHandler); + // Find out if we need to swap dimension to get the preview size relative to sensor + // coordinate. + int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + int sensorOrientation = + characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + boolean swappedDimensions = false; + switch (displayRotation) { + case Surface.ROTATION_0: + case Surface.ROTATION_180: + if (sensorOrientation == 90 || sensorOrientation == 270) { + swappedDimensions = true; + } + break; + case Surface.ROTATION_90: + case Surface.ROTATION_270: + if (sensorOrientation == 0 || sensorOrientation == 180) { + swappedDimensions = true; + } + break; + default: + Log.e(TAG, "Display rotation is invalid: " + displayRotation); + } + + Point displaySize = new Point(); + activity.getWindowManager().getDefaultDisplay().getSize(displaySize); + int rotatedPreviewWidth = width; + int rotatedPreviewHeight = height; + int maxPreviewWidth = displaySize.x; + int maxPreviewHeight = displaySize.y; + + if (swappedDimensions) { + rotatedPreviewWidth = height; + rotatedPreviewHeight = width; + maxPreviewWidth = displaySize.y; + maxPreviewHeight = displaySize.x; + } + + if (maxPreviewWidth > MAX_PREVIEW_WIDTH) { + maxPreviewWidth = MAX_PREVIEW_WIDTH; + } + + if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) { + maxPreviewHeight = MAX_PREVIEW_HEIGHT; + } + // Danger, W.R.! Attempting to use too large a preview size could exceed the camera // bus' bandwidth limitation, resulting in gorgeous previews but the storage of // garbage capture data. mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), - width, height, largest); + rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth, + maxPreviewHeight, largest); // We fit the aspect ratio of TextureView to the size of preview we picked. int orientation = getResources().getConfiguration().orientation; diff --git a/media/Camera2Basic/README.md b/media/Camera2Basic/README.md index a77df09d..cee71783 100644 --- a/media/Camera2Basic/README.md +++ b/media/Camera2Basic/README.md @@ -43,7 +43,7 @@ Pre-requisites -------------- - Android SDK v23 -- Android Build Tools v23.0.0 +- Android Build Tools v23.0.1 - Android Support Repository Screenshots diff --git a/media/Camera2Raw/Application/src/main/java/com/example/android/camera2raw/Camera2RawFragment.java b/media/Camera2Raw/Application/src/main/java/com/example/android/camera2raw/Camera2RawFragment.java index 47cce388..bf5efe58 100644 --- a/media/Camera2Raw/Application/src/main/java/com/example/android/camera2raw/Camera2RawFragment.java +++ b/media/Camera2Raw/Application/src/main/java/com/example/android/camera2raw/Camera2RawFragment.java @@ -27,6 +27,7 @@ import android.content.DialogInterface; import android.content.pm.PackageManager; import android.graphics.ImageFormat; import android.graphics.Matrix; +import android.graphics.Point; import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.hardware.SensorManager; @@ -157,6 +158,16 @@ public class Camera2RawFragment extends Fragment private static final double ASPECT_RATIO_TOLERANCE = 0.005; /** + * Max preview width that is guaranteed by Camera2 API + */ + private static final int MAX_PREVIEW_WIDTH = 1920; + + /** + * Max preview height that is guaranteed by Camera2 API + */ + private static final int MAX_PREVIEW_HEIGHT = 1080; + + /** * Tag for the {@link Log}. */ private static final String TAG = "Camera2RawFragment"; @@ -1033,6 +1044,8 @@ public class Camera2RawFragment extends Fragment // Find the rotation of the device relative to the native device orientation. int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + Point displaySize = new Point(); + activity.getWindowManager().getDefaultDisplay().getSize(displaySize); // Find the rotation of the device relative to the camera sensor's orientation. int totalRotation = sensorToDeviceRotation(mCharacteristics, deviceRotation); @@ -1042,14 +1055,29 @@ public class Camera2RawFragment extends Fragment boolean swappedDimensions = totalRotation == 90 || totalRotation == 270; int rotatedViewWidth = viewWidth; int rotatedViewHeight = viewHeight; + int maxPreviewWidth = displaySize.x; + int maxPreviewHeight = displaySize.y; + if (swappedDimensions) { rotatedViewWidth = viewHeight; rotatedViewHeight = viewWidth; + maxPreviewWidth = displaySize.y; + maxPreviewHeight = displaySize.x; + } + + // Preview should not be larger than display size and 1080p. + if (maxPreviewWidth > MAX_PREVIEW_WIDTH) { + maxPreviewWidth = MAX_PREVIEW_WIDTH; + } + + if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) { + maxPreviewHeight = MAX_PREVIEW_HEIGHT; } // Find the best preview size for these view dimensions and configured JPEG size. Size previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), - rotatedViewWidth, rotatedViewHeight, largestJpeg); + rotatedViewWidth, rotatedViewHeight, maxPreviewWidth, maxPreviewHeight, + largestJpeg); if (swappedDimensions) { mTextureView.setAspectRatio( @@ -1580,31 +1608,47 @@ public class Camera2RawFragment extends Fragment } /** - * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose - * width and height are at least as large as the respective requested values, and whose aspect - * ratio matches with the specified value. + * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that + * is at least as large as the respective texture view size, and that is at most as large as the + * respective max size, and whose aspect ratio matches with the specified value. If such size + * doesn't exist, choose the largest one that is at most as large as the respective max size, + * and whose aspect ratio matches with the specified value. * - * @param choices The list of sizes that the camera supports for the intended output class - * @param width The minimum desired width - * @param height The minimum desired height - * @param aspectRatio The aspect ratio + * @param choices The list of sizes that the camera supports for the intended output + * class + * @param textureViewWidth The width of the texture view relative to sensor coordinate + * @param textureViewHeight The height of the texture view relative to sensor coordinate + * @param maxWidth The maximum width that can be chosen + * @param maxHeight The maximum height that can be chosen + * @param aspectRatio The aspect ratio * @return The optimal {@code Size}, or an arbitrary one if none were big enough */ - private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) { + private static Size chooseOptimalSize(Size[] choices, int textureViewWidth, + int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) { // Collect the supported resolutions that are at least as big as the preview Surface List<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 h = aspectRatio.getHeight(); for (Size option : choices) { - if (option.getHeight() == option.getWidth() * h / w && - option.getWidth() >= width && option.getHeight() >= height) { - bigEnough.add(option); + if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight && + option.getHeight() == option.getWidth() * h / w) { + if (option.getWidth() >= textureViewWidth && + option.getHeight() >= textureViewHeight) { + bigEnough.add(option); + } else { + notBigEnough.add(option); + } } } - // Pick the smallest of those, assuming we found any + // Pick the smallest of those big enough. If there is no one big enough, pick the + // largest of those not big enough. if (bigEnough.size() > 0) { return Collections.min(bigEnough, new CompareSizesByArea()); + } else if (notBigEnough.size() > 0) { + return Collections.max(notBigEnough, new CompareSizesByArea()); } else { Log.e(TAG, "Couldn't find any suitable preview size"); return choices[0]; diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/MainActivity.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/MainActivity.java index 5086a173..26832f2c 100644 --- a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/MainActivity.java +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/MainActivity.java @@ -16,12 +16,10 @@ package com.example.android.asymmetricfingerprintdialog; -import android.Manifest; import android.app.Activity; import android.app.KeyguardManager; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.security.keystore.KeyGenParameterSpec; @@ -59,8 +57,6 @@ public class MainActivity extends Activity { /** Alias for our key in the Android Key Store */ public static final String KEY_NAME = "my_key"; - private static final int FINGERPRINT_PERMISSION_REQUEST_CODE = 0; - @Inject KeyguardManager mKeyguardManager; @Inject FingerprintManager mFingerprintManager; @Inject FingerprintAuthenticationDialogFragment mFragment; @@ -74,71 +70,63 @@ public class MainActivity extends Activity { super.onCreate(savedInstanceState); ((InjectedApplication) getApplication()).inject(this); - requestPermissions(new String[]{Manifest.permission.USE_FINGERPRINT}, - FINGERPRINT_PERMISSION_REQUEST_CODE); - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) { - if (requestCode == FINGERPRINT_PERMISSION_REQUEST_CODE - && state[0] == PackageManager.PERMISSION_GRANTED) { - setContentView(R.layout.activity_main); - Button purchaseButton = (Button) findViewById(R.id.purchase_button); - if (!mKeyguardManager.isKeyguardSecure()) { - // Show a message that the user hasn't set up a fingerprint or lock screen. - Toast.makeText(this, - "Secure lock screen hasn't set up.\n" - + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint", - Toast.LENGTH_LONG).show(); - purchaseButton.setEnabled(false); - return; - } - 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); + setContentView(R.layout.activity_main); + Button purchaseButton = (Button) findViewById(R.id.purchase_button); + if (!mKeyguardManager.isKeyguardSecure()) { + // Show a message that the user hasn't set up a fingerprint or lock screen. + Toast.makeText(this, + "Secure lock screen hasn't set up.\n" + + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint", + Toast.LENGTH_LONG).show(); + purchaseButton.setEnabled(false); + return; + } + //noinspection ResourceType + if (!mFingerprintManager.hasEnrolledFingerprints()) { + purchaseButton.setEnabled(false); + // This happens when no fingerprints are registered. + Toast.makeText(this, + "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint", + Toast.LENGTH_LONG).show(); + return; + } + createKeyPair(); + purchaseButton.setEnabled(true); + purchaseButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + findViewById(R.id.confirmation_message).setVisibility(View.GONE); + findViewById(R.id.encrypted_message).setVisibility(View.GONE); + + // Set up the crypto object for later. The object will be authenticated by use + // of the fingerprint. + if (initSignature()) { + + // Show the fingerprint dialog. The user has the option to use the fingerprint with + // crypto, or you can fall back to using a server-side verified password. + mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature)); + boolean useFingerprintPreference = mSharedPreferences + .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key), + true); + if (useFingerprintPreference) { + mFragment.setStage( + FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT); } else { - // 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); + FingerprintAuthenticationDialogFragment.Stage.PASSWORD); } + mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); + } else { + // This happens if the lock screen has been disabled or or a fingerprint got + // enrolled. Thus show the dialog to authenticate with their password first + // and ask the user if they want to authenticate with fingerprints in the + // future + mFragment.setStage( + FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED); + mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); } - }); - } + } + }); } /** diff --git a/security/AsymmetricFingerprintDialog/template-params.xml b/security/AsymmetricFingerprintDialog/template-params.xml index dfe47749..019a132c 100644 --- a/security/AsymmetricFingerprintDialog/template-params.xml +++ b/security/AsymmetricFingerprintDialog/template-params.xml @@ -20,7 +20,7 @@ E.g. Skipping device 'Nexus 5 - MNC', due to different API preview 'MNC' and 'android-MNC' --> <sample> - <name>Asymmetric Fingerprint Dialog Sample</name> + <name>AsymmetricFingerprintDialog</name> <group>Security</group> <package>com.example.android.asymmetricfingerprintdialog</package> @@ -90,7 +90,7 @@ in the way that its private key can only be used after the user has authenticate and transmit the public key to your backend with the user verified password (In a real world, the app should show proper UIs). -By setting [KeyGeneratorSpec.Builder.setUserAuthenticationRequired][2] to true, you can permit the +By setting [KeyGenParameterSpec.Builder.setUserAuthenticationRequired][2] to true, you can permit the use of the key only after the user authenticate it including when authenticated with the user's fingerprint. @@ -105,10 +105,10 @@ Then you can verify the purchase transaction on server side with the public key client, by verifying the piece of data signed by the Signature. [1]: https://developer.android.com/reference/java/security/KeyPairGenerator.html -[2]: https://developer.android.com/reference/android/security/KeyGenParameterSpec.Builder#setUserAuthenticationRequired().html -[3]: https://developer.android.com/reference/android/hardware/FingerprintManager#authenticate().html +[2]: https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder.html#setUserAuthenticationRequired%28boolean%29 +[3]: https://developer.android.com/reference/android/hardware/fingerprint/FingerprintManager.html#authenticate%28android.hardware.fingerprint.FingerprintManager.CryptoObject,%20android.os.CancellationSignal,%20int,%20android.hardware.fingerprint.FingerprintManager.AuthenticationCallback,%20android.os.Handler%29 [4]: https://developer.android.com/reference/java/security/Signature.html -[5]: https://developer.android.com/reference/android/hardware/FingerprintManager.AuthenticationCallback#onAuthenticationSucceeded().html +[5]: https://developer.android.com/reference/android/hardware/fingerprint/FingerprintManager.AuthenticationCallback.html#onAuthenticationSucceeded%28android.hardware.fingerprint.FingerprintManager.AuthenticationResult%29 ]]> </intro> </metadata> diff --git a/security/ConfirmCredential/template-params.xml b/security/ConfirmCredential/template-params.xml index 9877634b..d803a891 100644 --- a/security/ConfirmCredential/template-params.xml +++ b/security/ConfirmCredential/template-params.xml @@ -81,7 +81,7 @@ which can be only be used after the user has authenticated after the user is aut with their device credentials and pass [KeyGenParameterSpec][2]. By setting an integer value to the -[KeyGeneratorSpec.Builder.setUserAuthenticationValidityDurationSeconds][3], you can consider the +[KeyGenParameterSpec.Builder.setUserAuthenticationValidityDurationSeconds][3], you can consider the user as authenticated if the user has been authenticated with the device credentials within the last x seconds. @@ -89,9 +89,9 @@ Then by calling [KeyguardManager.createConfirmDeviceCredentialIntent][4], you ca to confirm device credentials to the user. [1]: https://developer.android.com/reference/javax/crypto/KeyGenerator.html -[2]: https://developer.android.com/reference/android/security/KeyGenParameterSpec.html -[3]: https://developer.android.com/reference/android/security/KeyGenParameterSpec.Builder#setUserAuthenticationValidityDurationSeconds().html -[4]: https://developer.android.com/reference/android/app/KeyguardManager.createConfirmDeviceCredentialIntent().html +[2]: https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.html +[3]: https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder.html#setUserAuthenticationValidityDurationSeconds%28int%29 +[4]: https://developer.android.com/reference/android/app/KeyguardManager.html#createConfirmDeviceCredentialIntent%28java.lang.CharSequence,%20java.lang.CharSequence%29 ]]> </intro> </metadata> diff --git a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java index c954bfa7..7caf9e69 100644 --- a/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java +++ b/security/FingerprintDialog/Application/src/main/java/com/example/android/fingerprintdialog/MainActivity.java @@ -16,12 +16,10 @@ package com.example.android.fingerprintdialog; -import android.Manifest; import android.app.Activity; import android.app.KeyguardManager; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.security.keystore.KeyGenParameterSpec; @@ -64,8 +62,6 @@ public class MainActivity extends Activity { /** Alias for our key in the Android Key Store */ private static final String KEY_NAME = "my_key"; - private static final int FINGERPRINT_PERMISSION_REQUEST_CODE = 0; - @Inject KeyguardManager mKeyguardManager; @Inject FingerprintManager mFingerprintManager; @Inject FingerprintAuthenticationDialogFragment mFragment; @@ -79,72 +75,65 @@ public class MainActivity extends Activity { super.onCreate(savedInstanceState); ((InjectedApplication) getApplication()).inject(this); - requestPermissions(new String[]{Manifest.permission.USE_FINGERPRINT}, - FINGERPRINT_PERMISSION_REQUEST_CODE); - } + setContentView(R.layout.activity_main); + Button purchaseButton = (Button) findViewById(R.id.purchase_button); + if (!mKeyguardManager.isKeyguardSecure()) { + // Show a message that the user hasn't set up a fingerprint or lock screen. + Toast.makeText(this, + "Secure lock screen hasn't set up.\n" + + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint", + Toast.LENGTH_LONG).show(); + purchaseButton.setEnabled(false); + return; + } - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) { - if (requestCode == FINGERPRINT_PERMISSION_REQUEST_CODE - && state[0] == PackageManager.PERMISSION_GRANTED) { - setContentView(R.layout.activity_main); - Button purchaseButton = (Button) findViewById(R.id.purchase_button); - if (!mKeyguardManager.isKeyguardSecure()) { - // Show a message that the user hasn't set up a fingerprint or lock screen. - Toast.makeText(this, - "Secure lock screen hasn't set up.\n" - + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint", - Toast.LENGTH_LONG).show(); - purchaseButton.setEnabled(false); - return; - } - 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); + //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 { - // 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); + FingerprintAuthenticationDialogFragment.Stage.PASSWORD); } + mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); + } else { + // This happens if the lock screen has been disabled or or a fingerprint got + // enrolled. Thus show the dialog to authenticate with their password first + // and ask the user if they want to authenticate with fingerprints in the + // future + mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher)); + mFragment.setStage( + FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED); + mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); } - }); - } + } + }); } /** diff --git a/security/FingerprintDialog/template-params.xml b/security/FingerprintDialog/template-params.xml index 0fa320c1..33db81c3 100644 --- a/security/FingerprintDialog/template-params.xml +++ b/security/FingerprintDialog/template-params.xml @@ -20,7 +20,7 @@ E.g. Skipping device 'Nexus 5 - MNC', due to different API preview 'MNC' and 'android-MNC' --> <sample> - <name>Fingerprint Dialog Sample</name> + <name>FingerprintDialog</name> <group>Security</group> <package>com.example.android.fingerprintdialog</package> @@ -87,9 +87,9 @@ before proceeding some actions such as purchasing an item. First you need to create a symmetric key in the Android Key Store using [KeyGenerator][1] which can be only be used after the user has authenticated with fingerprint and pass -a [KeyGeneratorSpec][2]. +a [KeyGenParameterSpec][2]. -By setting [KeyGeneratorSpec.Builder.setUserAuthenticationRequired][3] to true, you can permit the +By setting [KeyGenParameterSpec.Builder.setUserAuthenticationRequired][3] to true, you can permit the use of the key only after the user authenticate it including when authenticated with the user's fingerprint. @@ -101,11 +101,11 @@ Once the fingerprint (or password) is verified, the [FingerprintManager.AuthenticationCallback#onAuthenticationSucceeded()][6] callback is called. [1]: https://developer.android.com/reference/javax/crypto/KeyGenerator.html -[2]: https://developer.android.com/reference/android/security/KeyGenParameterSpec.html -[3]: https://developer.android.com/reference/android/security/KeyGenParameterSpec.Builder#setUserAuthenticationRequired().html -[4]: https://developer.android.com/reference/android/hardware/FingerprintManager#authenticate().html +[2]: https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.html +[3]: https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder.html#setUserAuthenticationRequired%28boolean%29 +[4]: https://developer.android.com/reference/android/hardware/fingerprint/FingerprintManager.html#authenticate%28android.hardware.fingerprint.FingerprintManager.CryptoObject,%20android.os.CancellationSignal,%20int,%20android.hardware.fingerprint.FingerprintManager.AuthenticationCallback,%20android.os.Handler%29 [5]: https://developer.android.com/reference/javax/crypto/Cipher.html -[6]: https://developer.android.com/reference/android/hardware/FingerprintManager.AuthenticationCallback#onAuthenticationSucceeded().html +[6]: https://developer.android.com/reference/android/hardware/fingerprint/FingerprintManager.AuthenticationCallback.html#onAuthenticationSucceeded%28android.hardware.fingerprint.FingerprintManager.AuthenticationResult%29 ]]> </intro> </metadata> diff --git a/security/keystore/BasicAndroidKeyStore/Application/src/main/java/com/example/android/basicandroidkeystore/BasicAndroidKeyStoreFragment.java b/security/keystore/BasicAndroidKeyStore/Application/src/main/java/com/example/android/basicandroidkeystore/BasicAndroidKeyStoreFragment.java index 12873e84..e6244bfb 100644 --- a/security/keystore/BasicAndroidKeyStore/Application/src/main/java/com/example/android/basicandroidkeystore/BasicAndroidKeyStoreFragment.java +++ b/security/keystore/BasicAndroidKeyStore/Application/src/main/java/com/example/android/basicandroidkeystore/BasicAndroidKeyStoreFragment.java @@ -156,7 +156,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment { // generated. Calendar start = new GregorianCalendar(); Calendar end = new GregorianCalendar(); - end.add(1, Calendar.YEAR); + end.add(Calendar.YEAR, 1); //END_INCLUDE(create_valid_dates) @@ -316,8 +316,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment { // Verify the data. s.initVerify(((KeyStore.PrivateKeyEntry) entry).getCertificate()); s.update(data); - boolean valid = s.verify(signature); - return valid; + return s.verify(signature); // END_INCLUDE(verify_data) } diff --git a/wearable/wear/AgendaData/Wearable/src/main/AndroidManifest.xml b/wearable/wear/AgendaData/Wearable/src/main/AndroidManifest.xml index dcab6227..e6dbab70 100644 --- a/wearable/wear/AgendaData/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/AgendaData/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.agendadata" > <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/AgendaData/template-params.xml b/wearable/wear/AgendaData/template-params.xml index 5c996f90..e5bdd228 100644 --- a/wearable/wear/AgendaData/template-params.xml +++ b/wearable/wear/AgendaData/template-params.xml @@ -24,8 +24,9 @@ <minSdk>18</minSdk> <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> - <dependency>com.android.support:design:23.0.0</dependency> + <dependency>com.android.support:design:23.1.0</dependency> <wearable> <has_handheld_app>true</has_handheld_app> diff --git a/wearable/wear/AlwaysOn/template-params.xml b/wearable/wear/AlwaysOn/template-params.xml index e5794ee4..b3aac9a3 100644 --- a/wearable/wear/AlwaysOn/template-params.xml +++ b/wearable/wear/AlwaysOn/template-params.xml @@ -19,11 +19,11 @@ <group>Wearable</group> <package>com.example.android.wearable.wear.alwayson</package> - <dependency_wearable>com.google.android.support:wearable:1.2.0</dependency_wearable> + <dependency_wearable>com.google.android.support:wearable:1.3.0</dependency_wearable> + <provided_dependency_wearable>com.google.android.wearable:wearable:1.0.0</provided_dependency_wearable> - <minSdk>20</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <strings> <intro> @@ -79,4 +79,4 @@ As always, you will still want to apply the [performance guidelines][3] outlined ]]> </intro> </metadata> -</sample>
\ No newline at end of file +</sample> diff --git a/wearable/wear/DataLayer/Application/src/main/AndroidManifest.xml b/wearable/wear/DataLayer/Application/src/main/AndroidManifest.xml index e80846de..ed1cec34 100644 --- a/wearable/wear/DataLayer/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/DataLayer/Application/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.datalayer" > <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="22" /> + android:targetSdkVersion="23" /> <uses-feature android:name="android.hardware.camera" android:required="false" /> diff --git a/wearable/wear/DataLayer/Wearable/src/main/java/com/example/android/wearable/datalayer/MainActivity.java b/wearable/wear/DataLayer/Wearable/src/main/java/com/example/android/wearable/datalayer/MainActivity.java index 678e428c..b3cb2530 100644 --- a/wearable/wear/DataLayer/Wearable/src/main/java/com/example/android/wearable/datalayer/MainActivity.java +++ b/wearable/wear/DataLayer/Wearable/src/main/java/com/example/android/wearable/datalayer/MainActivity.java @@ -23,6 +23,7 @@ import android.app.Fragment; import android.app.FragmentManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.support.wearable.view.DotsPageIndicator; @@ -41,6 +42,7 @@ import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; +import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.CapabilityApi; @@ -85,7 +87,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks, private static final String CAPABILITY_2_NAME = "capability_2"; private GoogleApiClient mGoogleApiClient; - private Handler mHandler; private GridViewPager mPager; private DataFragment mDataFragment; private AssetFragment mAssetFragment; @@ -93,7 +94,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks, @Override public void onCreate(Bundle b) { super.onCreate(b); - mHandler = new Handler(); setContentView(R.layout.main_activity); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); setupViews(); @@ -137,15 +137,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks, Log.e(TAG, "onConnectionFailed(): Failed to connect, with result: " + result); } - private void generateEvent(final String title, final String text) { - runOnUiThread(new Runnable() { - @Override - public void run() { - mDataFragment.appendItem(title, text); - } - }); - } - @Override public void onDataChanged(DataEventBuffer dataEvents) { LOGD(TAG, "onDataChanged(): " + dataEvents); @@ -155,29 +146,22 @@ public class MainActivity extends Activity implements ConnectionCallbacks, String path = event.getDataItem().getUri().getPath(); if (DataLayerListenerService.IMAGE_PATH.equals(path)) { DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem()); - Asset photo = dataMapItem.getDataMap() + Asset photoAsset = dataMapItem.getDataMap() .getAsset(DataLayerListenerService.IMAGE_KEY); - final Bitmap bitmap = loadBitmapFromAsset(mGoogleApiClient, photo); - mHandler.post(new Runnable() { - @Override - public void run() { - Log.d(TAG, "Setting background image on second page.."); - moveToPage(1); - mAssetFragment.setBackgroundImage(bitmap); - } - }); + // Loads image on background thread. + new LoadBitmapAsyncTask().execute(photoAsset); } else if (DataLayerListenerService.COUNT_PATH.equals(path)) { LOGD(TAG, "Data Changed for COUNT_PATH"); - generateEvent("DataItem Changed", event.getDataItem().toString()); + mDataFragment.appendItem("DataItem Changed", event.getDataItem().toString()); } else { LOGD(TAG, "Unrecognized path: " + path); } } else if (event.getType() == DataEvent.TYPE_DELETED) { - generateEvent("DataItem Deleted", event.getDataItem().toString()); + mDataFragment.appendItem("DataItem Deleted", event.getDataItem().toString()); } else { - generateEvent("Unknown data event type", "Type = " + event.getType()); + mDataFragment.appendItem("Unknown data event type", "Type = " + event.getType()); } } } @@ -199,20 +183,27 @@ public class MainActivity extends Activity implements ConnectionCallbacks, * Find the connected nodes that provide at least one of the given capabilities */ private void showNodes(final String... capabilityNames) { - Wearable.CapabilityApi.getAllCapabilities(mGoogleApiClient, - CapabilityApi.FILTER_REACHABLE).setResultCallback( + PendingResult<CapabilityApi.GetAllCapabilitiesResult> pendingCapabilityResult = + Wearable.CapabilityApi.getAllCapabilities( + mGoogleApiClient, + CapabilityApi.FILTER_REACHABLE); + + pendingCapabilityResult.setResultCallback( new ResultCallback<CapabilityApi.GetAllCapabilitiesResult>() { @Override public void onResult( CapabilityApi.GetAllCapabilitiesResult getAllCapabilitiesResult) { + if (!getAllCapabilitiesResult.getStatus().isSuccess()) { Log.e(TAG, "Failed to get capabilities"); return; } - Map<String, CapabilityInfo> - capabilitiesMap = getAllCapabilitiesResult.getAllCapabilities(); + + Map<String, CapabilityInfo> capabilitiesMap = + getAllCapabilitiesResult.getAllCapabilities(); Set<Node> nodes = new HashSet<>(); + if (capabilitiesMap.isEmpty()) { showDiscoveredNodes(nodes); return; @@ -231,7 +222,7 @@ public class MainActivity extends Activity implements ConnectionCallbacks, for (Node node : nodes) { nodesList.add(node.getDisplayName()); } - Log.d(TAG, "Connected Nodes: " + (nodesList.isEmpty() + LOGD(TAG, "Connected Nodes: " + (nodesList.isEmpty() ? "No connected device was found for the given capabilities" : TextUtils.join(",", nodesList))); String msg; @@ -246,39 +237,20 @@ public class MainActivity extends Activity implements ConnectionCallbacks, }); } - /** - * Extracts {@link android.graphics.Bitmap} data from the - * {@link com.google.android.gms.wearable.Asset} - */ - private Bitmap loadBitmapFromAsset(GoogleApiClient apiClient, Asset asset) { - if (asset == null) { - throw new IllegalArgumentException("Asset must be non-null"); - } - - InputStream assetInputStream = Wearable.DataApi.getFdForAsset( - apiClient, asset).await().getInputStream(); - - if (assetInputStream == null) { - Log.w(TAG, "Requested an unknown Asset."); - return null; - } - return BitmapFactory.decodeStream(assetInputStream); - } - @Override public void onMessageReceived(MessageEvent event) { LOGD(TAG, "onMessageReceived: " + event); - generateEvent("Message", event.toString()); + mDataFragment.appendItem("Message", event.toString()); } @Override public void onPeerConnected(Node node) { - generateEvent("Node Connected", node.getId()); + mDataFragment.appendItem("Node Connected", node.getId()); } @Override public void onPeerDisconnected(Node node) { - generateEvent("Node Disconnected", node.getId()); + mDataFragment.appendItem("Node Disconnected", node.getId()); } private void setupViews() { @@ -330,4 +302,43 @@ public class MainActivity extends Activity implements ConnectionCallbacks, } } + + /* + * Extracts {@link android.graphics.Bitmap} data from the + * {@link com.google.android.gms.wearable.Asset} + */ + private class LoadBitmapAsyncTask extends AsyncTask<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); + } + } + } } diff --git a/wearable/wear/DataLayer/template-params.xml b/wearable/wear/DataLayer/template-params.xml index d9bfc070..6df31f5b 100644 --- a/wearable/wear/DataLayer/template-params.xml +++ b/wearable/wear/DataLayer/template-params.xml @@ -20,7 +20,8 @@ <package>com.example.android.wearable.datalayer</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <wearable> <has_handheld_app>true</has_handheld_app> diff --git a/wearable/wear/DelayedConfirmation/Application/src/main/AndroidManifest.xml b/wearable/wear/DelayedConfirmation/Application/src/main/AndroidManifest.xml index d8060a8d..e3e6de17 100644 --- a/wearable/wear/DelayedConfirmation/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/DelayedConfirmation/Application/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.delayedconfirmation" > <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="22" /> + android:targetSdkVersion="23" /> <application android:allowBackup="true" diff --git a/wearable/wear/DelayedConfirmation/template-params.xml b/wearable/wear/DelayedConfirmation/template-params.xml index 5f77d655..239a0eab 100644 --- a/wearable/wear/DelayedConfirmation/template-params.xml +++ b/wearable/wear/DelayedConfirmation/template-params.xml @@ -23,7 +23,8 @@ <package>com.example.android.wearable.delayedconfirmation</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <wearable> <has_handheld_app>true</has_handheld_app> diff --git a/wearable/wear/ElizaChat/Application/src/main/AndroidManifest.xml b/wearable/wear/ElizaChat/Application/src/main/AndroidManifest.xml index 8f35c565..b544ed05 100644 --- a/wearable/wear/ElizaChat/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/ElizaChat/Application/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.elizachat" > <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="21" /> + android:targetSdkVersion="23" /> <application android:allowBackup="true" diff --git a/wearable/wear/ElizaChat/Application/src/main/java/com/example/android/wearable/elizachat/ResponderService.java b/wearable/wear/ElizaChat/Application/src/main/java/com/example/android/wearable/elizachat/ResponderService.java index 3bef19c6..2406668c 100644 --- a/wearable/wear/ElizaChat/Application/src/main/java/com/example/android/wearable/elizachat/ResponderService.java +++ b/wearable/wear/ElizaChat/Application/src/main/java/com/example/android/wearable/elizachat/ResponderService.java @@ -96,7 +96,7 @@ public class ResponderService extends Service { .setContentText(mLastResponse) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.bg_eliza)) .setSmallIcon(R.drawable.bg_eliza) - .setPriority(NotificationCompat.PRIORITY_MIN); + .setPriority(NotificationCompat.PRIORITY_DEFAULT); Intent intent = new Intent(ACTION_RESPONSE); PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, diff --git a/wearable/wear/ElizaChat/template-params.xml b/wearable/wear/ElizaChat/template-params.xml index ea26b5cc..ff762424 100644 --- a/wearable/wear/ElizaChat/template-params.xml +++ b/wearable/wear/ElizaChat/template-params.xml @@ -23,7 +23,7 @@ <package>com.example.android.wearable.elizachat</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> <strings> <intro> diff --git a/wearable/wear/EmbeddedApp/LICENSE b/wearable/wear/EmbeddedApp/LICENSE index 1af981f5..4f229463 100644 --- a/wearable/wear/EmbeddedApp/LICENSE +++ b/wearable/wear/EmbeddedApp/LICENSE @@ -1,4 +1,6 @@ - Apache License +Apache License +-------------- + Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -178,7 +180,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +188,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2014 The Android Open Source Project + Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -199,3 +201,447 @@ 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. + +All image and audio files (including *.png, *.jpg, *.svg, *.mp3, *.wav +and *.ogg) are licensed under the CC-BY-NC license. All other files are +licensed under the Apache 2 license. + +CC-BY-NC License +---------------- + +Attribution-NonCommercial-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International +Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial-ShareAlike 4.0 International Public License +("Public License"). To the extent this Public License may be +interpreted as a contract, You are granted the Licensed Rights in +consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the +Licensor receives from making the Licensed Material available under +these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-NC-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution, NonCommercial, and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + l. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + m. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + n. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-NC-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + including for purposes of Section 3(b); and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public licenses. +Notwithstanding, Creative Commons may elect to apply one of its public +licenses to material it publishes and in those instances will be +considered the "Licensor." Except for the limited purpose of indicating +that material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the public +licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/wearable/wear/EmbeddedApp/Wearable/src/main/AndroidManifest.xml b/wearable/wear/EmbeddedApp/Wearable/src/main/AndroidManifest.xml index 4863d66e..aab1348a 100644 --- a/wearable/wear/EmbeddedApp/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/EmbeddedApp/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.embeddedapp" > <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/EmbeddedApp/template-params.xml b/wearable/wear/EmbeddedApp/template-params.xml index 13186e52..424585da 100644 --- a/wearable/wear/EmbeddedApp/template-params.xml +++ b/wearable/wear/EmbeddedApp/template-params.xml @@ -23,7 +23,8 @@ <package>com.example.android.wearable.embeddedapp</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <wearable> <has_handheld_app>true</has_handheld_app> diff --git a/wearable/wear/FindMyPhone/Application/src/main/AndroidManifest.xml b/wearable/wear/FindMyPhone/Application/src/main/AndroidManifest.xml index af108af4..a59cd7d9 100644 --- a/wearable/wear/FindMyPhone/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/FindMyPhone/Application/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.findphone"> <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="22" /> + android:targetSdkVersion="23" /> <uses-permission android:name="android.permission.VIBRATE" /> <application diff --git a/wearable/wear/FindMyPhone/template-params.xml b/wearable/wear/FindMyPhone/template-params.xml index e8d71c63..ff13793c 100644 --- a/wearable/wear/FindMyPhone/template-params.xml +++ b/wearable/wear/FindMyPhone/template-params.xml @@ -23,7 +23,8 @@ <package>com.example.android.wearable.findphone</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <wearable> <has_handheld_app>true</has_handheld_app> diff --git a/wearable/wear/Flashlight/Wearable/src/main/AndroidManifest.xml b/wearable/wear/Flashlight/Wearable/src/main/AndroidManifest.xml index 738ba9d3..1eb15d07 100644 --- a/wearable/wear/Flashlight/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/Flashlight/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.flashlight" > <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/Flashlight/template-params.xml b/wearable/wear/Flashlight/template-params.xml index e1ec8698..3b9f2111 100644 --- a/wearable/wear/Flashlight/template-params.xml +++ b/wearable/wear/Flashlight/template-params.xml @@ -22,15 +22,15 @@ <group>Wearable</group> <package>com.example.android.wearable.flashlight</package> - <!-- change minSdk if needed--> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <strings> <intro> <![CDATA[ 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. ]]> </intro> </strings> diff --git a/wearable/wear/Geofencing/Application/src/main/AndroidManifest.xml b/wearable/wear/Geofencing/Application/src/main/AndroidManifest.xml index d07a2659..d1eabc3d 100644 --- a/wearable/wear/Geofencing/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/Geofencing/Application/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.geofencing"> <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> diff --git a/wearable/wear/Geofencing/Wearable/src/main/AndroidManifest.xml b/wearable/wear/Geofencing/Wearable/src/main/AndroidManifest.xml index 082f396b..f25cc447 100644 --- a/wearable/wear/Geofencing/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/Geofencing/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.geofencing" > <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/Geofencing/template-params.xml b/wearable/wear/Geofencing/template-params.xml index 00fd3b36..a69c82c6 100644 --- a/wearable/wear/Geofencing/template-params.xml +++ b/wearable/wear/Geofencing/template-params.xml @@ -24,8 +24,9 @@ <minSdk>18</minSdk> <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> - <dependency>com.google.android.gms:play-services-location:7.3.0</dependency> + <dependency>com.google.android.gms:play-services-location:8.1.0</dependency> <wearable> <has_handheld_app>true</has_handheld_app> diff --git a/wearable/wear/GridViewPager/Wearable/src/main/AndroidManifest.xml b/wearable/wear/GridViewPager/Wearable/src/main/AndroidManifest.xml index 5c362dcb..e25cd637 100644 --- a/wearable/wear/GridViewPager/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/GridViewPager/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.gridviewpager" > <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/GridViewPager/template-params.xml b/wearable/wear/GridViewPager/template-params.xml index f6dc567c..66e4bf4a 100644 --- a/wearable/wear/GridViewPager/template-params.xml +++ b/wearable/wear/GridViewPager/template-params.xml @@ -22,8 +22,7 @@ <group>Wearable</group> <package>com.example.android.wearable.gridviewpager</package> - <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <strings> <intro> diff --git a/wearable/wear/JumpingJack/Wearable/src/main/AndroidManifest.xml b/wearable/wear/JumpingJack/Wearable/src/main/AndroidManifest.xml index 02b7a4ff..f6cf2204 100644 --- a/wearable/wear/JumpingJack/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/JumpingJack/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.jumpingjack"> <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/JumpingJack/template-params.xml b/wearable/wear/JumpingJack/template-params.xml index 692a6a5e..9512f32e 100644 --- a/wearable/wear/JumpingJack/template-params.xml +++ b/wearable/wear/JumpingJack/template-params.xml @@ -22,8 +22,7 @@ <group>Wearable</group> <package>com.example.android.wearable.jumpingjack</package> - <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <strings> <intro> @@ -66,7 +65,7 @@ by counting how many jumping jacks you have performed. [SensorEventListener][1] offers you methods used for receiving notifications from the [SensorManager][2] when sensor values have changed. -This example counts how many times Jumping Jakcs are performed by detecting the value +This example counts how many times Jumping Jacks are performed by detecting the value of the Gravity sensor by the following code: ```java diff --git a/wearable/wear/Notifications/Application/src/main/AndroidManifest.xml b/wearable/wear/Notifications/Application/src/main/AndroidManifest.xml index 3f1274d8..6a17ad8c 100644 --- a/wearable/wear/Notifications/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/Notifications/Application/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.support.wearable.notifications" > <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="21" /> + android:targetSdkVersion="23" /> <uses-permission android:name="android.permission.VIBRATE" /> diff --git a/wearable/wear/Notifications/Wearable/src/main/AndroidManifest.xml b/wearable/wear/Notifications/Wearable/src/main/AndroidManifest.xml index 34a29ff6..a446fd9b 100644 --- a/wearable/wear/Notifications/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/Notifications/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.support.wearable.notifications" > <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/Notifications/template-params.xml b/wearable/wear/Notifications/template-params.xml index c4936ea6..64d2e5ba 100644 --- a/wearable/wear/Notifications/template-params.xml +++ b/wearable/wear/Notifications/template-params.xml @@ -23,7 +23,8 @@ <package>com.example.android.support.wearable.notifications</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <wearable> <has_handheld_app>true</has_handheld_app> diff --git a/wearable/wear/Quiz/Application/src/main/AndroidManifest.xml b/wearable/wear/Quiz/Application/src/main/AndroidManifest.xml index 801a4732..8fabd42d 100644 --- a/wearable/wear/Quiz/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/Quiz/Application/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.quiz" > <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="22" /> + android:targetSdkVersion="23" /> <application android:allowBackup="true" diff --git a/wearable/wear/Quiz/template-params.xml b/wearable/wear/Quiz/template-params.xml index 297bf190..4a920bc3 100644 --- a/wearable/wear/Quiz/template-params.xml +++ b/wearable/wear/Quiz/template-params.xml @@ -23,7 +23,8 @@ <package>com.example.android.wearable.quiz</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <wearable> <has_handheld_app>true</has_handheld_app> diff --git a/wearable/wear/RecipeAssistant/Application/src/main/AndroidManifest.xml b/wearable/wear/RecipeAssistant/Application/src/main/AndroidManifest.xml index 1786d278..141da9a0 100644 --- a/wearable/wear/RecipeAssistant/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/RecipeAssistant/Application/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.recipeassistant" > <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="21" /> + android:targetSdkVersion="23" /> <application android:allowBackup="true" diff --git a/wearable/wear/RecipeAssistant/template-params.xml b/wearable/wear/RecipeAssistant/template-params.xml index 943b368c..f1fa1021 100644 --- a/wearable/wear/RecipeAssistant/template-params.xml +++ b/wearable/wear/RecipeAssistant/template-params.xml @@ -23,7 +23,7 @@ <package>com.example.android.wearable.recipeassistant</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> <strings> <intro> diff --git a/wearable/wear/SkeletonWearableApp/Wearable/src/main/AndroidManifest.xml b/wearable/wear/SkeletonWearableApp/Wearable/src/main/AndroidManifest.xml index f99d785c..f9e89782 100644 --- a/wearable/wear/SkeletonWearableApp/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/SkeletonWearableApp/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.google.wearable.app" > <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/SkeletonWearableApp/template-params.xml b/wearable/wear/SkeletonWearableApp/template-params.xml index b0f4b366..f25c0351 100644 --- a/wearable/wear/SkeletonWearableApp/template-params.xml +++ b/wearable/wear/SkeletonWearableApp/template-params.xml @@ -19,8 +19,7 @@ <group>Wearable</group> <package>com.example.android.google.wearable.app</package> - <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <strings> <intro> diff --git a/wearable/wear/SpeedTracker/Application/src/main/AndroidManifest.xml b/wearable/wear/SpeedTracker/Application/src/main/AndroidManifest.xml index 44284d4f..be88f6d6 100644 --- a/wearable/wear/SpeedTracker/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/SpeedTracker/Application/src/main/AndroidManifest.xml @@ -2,25 +2,35 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" 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.ACCESS_NETWORK_STATE"/> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> - <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> + <uses-permission android:name="android.permission.WAKE_LOCK" /> + + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" + android:maxSdkVersion="18"/> <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:glEsVersion="0x00020000" android:required="true"/> - <uses-sdk - android:minSdkVersion="18" - android:targetSdkVersion="21" /> <application android:name=".PhoneApplication" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" - android:theme="@style/AppTheme" > + android:theme="@style/Theme.AppCompat.Light" > <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="@string/map_v2_api_key"/> diff --git a/wearable/wear/SpeedTracker/Application/src/main/java/com/example/android/wearable/speedtracker/PhoneMainActivity.java b/wearable/wear/SpeedTracker/Application/src/main/java/com/example/android/wearable/speedtracker/PhoneMainActivity.java index 76f609b1..c645bdd6 100644 --- a/wearable/wear/SpeedTracker/Application/src/main/java/com/example/android/wearable/speedtracker/PhoneMainActivity.java +++ b/wearable/wear/SpeedTracker/Application/src/main/java/com/example/android/wearable/speedtracker/PhoneMainActivity.java @@ -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.PolylineOptions; -import android.app.Activity; import android.app.DatePickerDialog; import android.os.AsyncTask; import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; import android.text.format.DateUtils; import android.util.Log; 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 * 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 int BOUNDING_BOX_PADDING_PX = 50; diff --git a/wearable/wear/SpeedTracker/Application/src/main/res/layout/main_activity.xml b/wearable/wear/SpeedTracker/Application/src/main/res/layout/main_activity.xml index a18c6448..17a8f6a9 100644 --- a/wearable/wear/SpeedTracker/Application/src/main/res/layout/main_activity.xml +++ b/wearable/wear/SpeedTracker/Application/src/main/res/layout/main_activity.xml @@ -21,7 +21,8 @@ <RelativeLayout android:id="@+id/top_container" android:layout_width="fill_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:layout_marginTop="10dp"> <Button android:id="@+id/date_picker" android:layout_width="wrap_content" diff --git a/wearable/wear/SpeedTracker/Wearable/src/main/AndroidManifest.xml b/wearable/wear/SpeedTracker/Wearable/src/main/AndroidManifest.xml index ab19d5e6..c9cbad18 100644 --- a/wearable/wear/SpeedTracker/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/SpeedTracker/Wearable/src/main/AndroidManifest.xml @@ -19,18 +19,22 @@ <uses-feature android:name="android.hardware.type.watch"/> <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.WAKE_LOCK" /> <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="23" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" 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" android:value="@integer/google_play_services_version"/> <activity @@ -38,7 +42,6 @@ android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> @@ -48,12 +51,6 @@ <action android:name="android.intent.action.MAIN"/> </intent-filter> </activity> - <activity - android:name=".ui.LocationSettingActivity"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - </intent-filter> - </activity> </application> </manifest> diff --git a/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/SpeedPickerActivity.java b/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/SpeedPickerActivity.java index d55d7dfb..d178891f 100644 --- a/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/SpeedPickerActivity.java +++ b/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/SpeedPickerActivity.java @@ -17,9 +17,8 @@ package com.example.android.wearable.speedtracker; import android.app.Activity; -import android.content.SharedPreferences; +import android.content.Intent; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.wearable.view.WearableListView; import android.widget.TextView; @@ -31,6 +30,9 @@ import com.example.android.wearable.speedtracker.ui.SpeedPickerListAdapter; */ public class SpeedPickerActivity extends Activity implements WearableListView.ClickListener { + public static final String EXTRA_NEW_SPEED_LIMIT = + "com.example.android.wearable.speedtracker.extra.NEW_SPEED_LIMIT"; + /* Speeds, in mph, that will be shown on the list */ private int[] speeds = {25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75}; @@ -75,9 +77,13 @@ public class SpeedPickerActivity extends Activity implements WearableListView.Cl @Override public void onClick(WearableListView.ViewHolder viewHolder) { - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); - pref.edit().putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY, - speeds[viewHolder.getPosition()]).apply(); + + int newSpeedLimit = speeds[viewHolder.getPosition()]; + + Intent resultIntent = new Intent(Intent.ACTION_PICK); + resultIntent.putExtra(EXTRA_NEW_SPEED_LIMIT, newSpeedLimit); + setResult(RESULT_OK, resultIntent); + finish(); } diff --git a/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/WearableMainActivity.java b/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/WearableMainActivity.java index f3015bf8..ee3c3ef9 100644 --- a/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/WearableMainActivity.java +++ b/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/WearableMainActivity.java @@ -28,7 +28,7 @@ import com.google.android.gms.wearable.PutDataMapRequest; import com.google.android.gms.wearable.PutDataRequest; import com.google.android.gms.wearable.Wearable; -import android.app.Activity; +import android.Manifest; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; @@ -38,18 +38,19 @@ import android.location.Location; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.wearable.activity.WearableActivity; import android.util.Log; import android.view.View; -import android.view.WindowManager; -import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import com.example.android.wearable.speedtracker.common.Constants; import com.example.android.wearable.speedtracker.common.LocationEntry; -import com.example.android.wearable.speedtracker.ui.LocationSettingActivity; import java.util.Calendar; +import java.util.concurrent.TimeUnit; /** * The main activity for the wearable app. User can pick a speed limit, and after this activity @@ -58,33 +59,54 @@ import java.util.Calendar; * and if the user exceeds the speed limit, it will turn red. In order to show the user that GPS * location data is coming in, a small green dot keeps on blinking while GPS data is available. */ -public class WearableMainActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, - GoogleApiClient.OnConnectionFailedListener, LocationListener { +public class WearableMainActivity extends WearableActivity implements + GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener, + ActivityCompat.OnRequestPermissionsResultCallback, + LocationListener { private static final String TAG = "WearableActivity"; - private static final long UPDATE_INTERVAL_MS = 5 * 1000; - private static final long FASTEST_INTERVAL_MS = 5 * 1000; + private static final long UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); + private static final long FASTEST_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); - public static final float MPH_IN_METERS_PER_SECOND = 2.23694f; + private static final float MPH_IN_METERS_PER_SECOND = 2.23694f; + + private static final int SPEED_LIMIT_DEFAULT_MPH = 45; - public static final String PREFS_SPEED_LIMIT_KEY = "speed_limit"; - public static final int SPEED_LIMIT_DEFAULT_MPH = 45; private static final long INDICATOR_DOT_FADE_AWAY_MS = 500L; + // Request codes for changing speed limit and location permissions. + private static final int REQUEST_PICK_SPEED_LIMIT = 0; + + // Id to identify Location permission request. + private static final int REQUEST_GPS_PERMISSION = 1; + + // Shared Preferences for saving speed limit and location permission between app launches. + private static final String PREFS_SPEED_LIMIT_KEY = "SpeedLimit"; + + private Calendar mCalendar; + + 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 TextView mSpeedLimitText; - private TextView mCurrentSpeedText; - private ImageView mSaveImageView; - private TextView mAcquiringGps; - private TextView mCurrentSpeedMphText; - - private int mCurrentSpeedLimit; - private float mCurrentSpeed; - private View mDot; + private Handler mHandler = new Handler(); - private Calendar mCalendar; - private boolean mSaveGpsLocation; private enum SpeedState { BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above); @@ -104,20 +126,53 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + Log.d(TAG, "onCreate()"); + + setContentView(R.layout.main_activity); + + /* + * Enables Always-on, so our app doesn't shut down when the watch goes into ambient mode. + * Best practice is to override onEnterAmbient(), onUpdateAmbient(), and onExitAmbient() to + * optimize the display for ambient mode. However, for brevity, we aren't doing that here + * to focus on learning location and permissions. For more information on best practices + * in ambient mode, check this page: + * https://developer.android.com/training/wearables/apps/always-on.html + */ + setAmbientEnabled(); + + mCalendar = Calendar.getInstance(); + + // Enables app to handle 23+ (M+) style permissions. + mGpsPermissionApproved = + ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED; + + mGpsPermissionNeededMessage = getString(R.string.permission_rationale); + mAcquiringGpsMessage = getString(R.string.acquiring_gps); + + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + mSpeedLimit = sharedPreferences.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH); + + mSpeed = 0; + + mWaitingForGpsSignal = true; + + + /* + * If this hardware doesn't support GPS, we warn the user. Note that when such device is + * connected to a phone with GPS capabilities, the framework automatically routes the + * location requests from the phone. However, if the phone becomes disconnected and the + * wearable doesn't support GPS, no location is recorded until the phone is reconnected. + */ if (!hasGps()) { - // If this hardware doesn't support GPS, we prefer to exit. - // Note that when such device is connected to a phone with GPS capabilities, the - // framework automatically routes the location requests to the phone. For this - // application, this would not be desirable so we exit the app but for some other - // applications, that might be a valid scenario. - Log.w(TAG, "This hardware doesn't have GPS, so we exit"); + Log.w(TAG, "This hardware doesn't have GPS, so we warn user."); new AlertDialog.Builder(this) .setMessage(getString(R.string.gps_not_available)) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { - finish(); dialog.cancel(); } }) @@ -125,7 +180,6 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co @Override public void onDismiss(DialogInterface dialog) { dialog.cancel(); - finish(); } }) .setCancelable(false) @@ -133,164 +187,216 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co .show(); } + setupViews(); - updateSpeedVisibility(false); - setSpeedLimit(); + mGoogleApiClient = new GoogleApiClient.Builder(this) .addApi(LocationServices.API) .addApi(Wearable.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); - mGoogleApiClient.connect(); } - private void setupViews() { - mSpeedLimitText = (TextView) findViewById(R.id.max_speed_text); - mCurrentSpeedText = (TextView) findViewById(R.id.current_speed_text); - mSaveImageView = (ImageView) findViewById(R.id.saving); - ImageButton settingButton = (ImageButton) findViewById(R.id.settings); - mAcquiringGps = (TextView) findViewById(R.id.acquiring_gps); - mCurrentSpeedMphText = (TextView) findViewById(R.id.current_speed_mph); - mDot = findViewById(R.id.dot); - - settingButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent speedIntent = new Intent(WearableMainActivity.this, - SpeedPickerActivity.class); - startActivity(speedIntent); - } - }); - - mSaveImageView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent savingIntent = new Intent(WearableMainActivity.this, - LocationSettingActivity.class); - startActivity(savingIntent); - } - }); + @Override + protected void onPause() { + super.onPause(); + if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected()) && + (mGoogleApiClient.isConnecting())) { + LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); + mGoogleApiClient.disconnect(); + } + } - private void setSpeedLimit(int speedLimit) { - mSpeedLimitText.setText(getString(R.string.speed_limit, speedLimit)); + @Override + protected void onResume() { + super.onResume(); + if (mGoogleApiClient != null) { + mGoogleApiClient.connect(); + } } - private void setSpeedLimit() { - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); - mCurrentSpeedLimit = pref.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH); - setSpeedLimit(mCurrentSpeedLimit); + private void setupViews() { + mSpeedLimitTextView = (TextView) findViewById(R.id.max_speed_text); + mSpeedTextView = (TextView) findViewById(R.id.current_speed_text); + mCurrentSpeedMphTextView = (TextView) findViewById(R.id.current_speed_mph); + + mGpsPermissionImageView = (ImageView) findViewById(R.id.gps_permission); + mGpsIssueTextView = (TextView) findViewById(R.id.gps_issue_text); + mBlinkingGpsStatusDotView = findViewById(R.id.dot); + + updateActivityViewsBasedOnLocationPermissions(); } - private void setCurrentSpeed(float speed) { - mCurrentSpeed = speed; - mCurrentSpeedText.setText(String.format(getString(R.string.speed_format), speed)); - adjustColor(); + public void onSpeedLimitClick(View view) { + Intent speedIntent = new Intent(WearableMainActivity.this, + SpeedPickerActivity.class); + startActivityForResult(speedIntent, REQUEST_PICK_SPEED_LIMIT); + } + + public void onGpsPermissionClick(View view) { + + if (!mGpsPermissionApproved) { + + Log.i(TAG, "Location permission has NOT been granted. Requesting permission."); + + // On 23+ (M+) devices, GPS permission not granted. Request permission. + ActivityCompat.requestPermissions( + this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + REQUEST_GPS_PERMISSION); + } } /** - * Adjusts the color of the speed based on its value relative to the speed limit. + * Adjusts the visibility of views based on location permissions. */ - private void adjustColor() { - SpeedState state = SpeedState.ABOVE; - if (mCurrentSpeed <= mCurrentSpeedLimit - 5) { - state = SpeedState.BELOW; - } else if (mCurrentSpeed <= mCurrentSpeedLimit) { - state = SpeedState.CLOSE; + private void updateActivityViewsBasedOnLocationPermissions() { + + /* + * 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)); - mCurrentSpeedText.setTextColor(getResources().getColor(state.getColor())); + // Adjusts the color of the speed based on its value relative to the speed limit. + SpeedState state = SpeedState.ABOVE; + if (mSpeed <= mSpeedLimit - 5) { + state = SpeedState.BELOW; + } else if (mSpeed <= mSpeedLimit) { + state = SpeedState.CLOSE; + } + + mSpeedTextView.setTextColor(getResources().getColor(state.getColor())); + + // Causes the (green) dot blinks when new GPS location data is acquired. + mHandler.post(new Runnable() { + @Override + public void run() { + mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE); + } + }); + mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + mBlinkingGpsStatusDotView.setVisibility(View.INVISIBLE); + } + }, INDICATOR_DOT_FADE_AWAY_MS); + } } @Override public void onConnected(Bundle bundle) { - LocationRequest locationRequest = LocationRequest.create() - .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) - .setInterval(UPDATE_INTERVAL_MS) - .setFastestInterval(FASTEST_INTERVAL_MS); - LocationServices.FusedLocationApi - .requestLocationUpdates(mGoogleApiClient, locationRequest, this) - .setResultCallback(new ResultCallback<Status>() { + Log.d(TAG, "onConnected()"); - @Override - public void onResult(Status status) { - if (status.getStatus().isSuccess()) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Successfully requested location updates"); + /* + * mGpsPermissionApproved covers 23+ (M+) style permissions. If that is already approved or + * the device is pre-23, the app uses mSaveGpsLocation to save the user's location + * preference. + */ + if (mGpsPermissionApproved) { + + LocationRequest locationRequest = LocationRequest.create() + .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) + .setInterval(UPDATE_INTERVAL_MS) + .setFastestInterval(FASTEST_INTERVAL_MS); + + LocationServices.FusedLocationApi + .requestLocationUpdates(mGoogleApiClient, locationRequest, this) + .setResultCallback(new ResultCallback<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 public void onConnectionSuspended(int i) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "onConnectionSuspended(): connection to location client suspended"); - } + Log.d(TAG, "onConnectionSuspended(): connection to location client suspended"); + LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); } @Override public void onConnectionFailed(ConnectionResult connectionResult) { - Log.e(TAG, "onConnectionFailed(): connection to location client failed"); + Log.e(TAG, "onConnectionFailed(): " + connectionResult.getErrorMessage()); } @Override public void onLocationChanged(Location location) { - updateSpeedVisibility(true); - setCurrentSpeed(location.getSpeed() * MPH_IN_METERS_PER_SECOND); - flashDot(); - addLocationEntry(location.getLatitude(), location.getLongitude()); - } + Log.d(TAG, "onLocationChanged() : " + location); - /** - * Causes the (green) dot blinks when new GPS location data is acquired. - */ - private void flashDot() { - mHandler.post(new Runnable() { - @Override - public void run() { - mDot.setVisibility(View.VISIBLE); - } - }); - mDot.setVisibility(View.VISIBLE); - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - mDot.setVisibility(View.INVISIBLE); - } - }, INDICATOR_DOT_FADE_AWAY_MS); - } - /** - * Adjusts the visibility of speed indicator based on the arrival of GPS data. - */ - private void updateSpeedVisibility(boolean speedVisible) { - if (speedVisible) { - mAcquiringGps.setVisibility(View.GONE); - mCurrentSpeedText.setVisibility(View.VISIBLE); - mCurrentSpeedMphText.setVisibility(View.VISIBLE); - } else { - mAcquiringGps.setVisibility(View.VISIBLE); - mCurrentSpeedText.setVisibility(View.GONE); - mCurrentSpeedMphText.setVisibility(View.GONE); + if (mWaitingForGpsSignal) { + mWaitingForGpsSignal = false; + updateActivityViewsBasedOnLocationPermissions(); } + + mSpeed = location.getSpeed() * MPH_IN_METERS_PER_SECOND; + updateSpeedInViews(); + addLocationEntry(location.getLatitude(), location.getLongitude()); } - /** - * Adds a data item to the data Layer storage + /* + * Adds a data item to the data Layer storage. */ private void addLocationEntry(double latitude, double longitude) { - if (!mSaveGpsLocation || !mGoogleApiClient.isConnected()) { + if (!mGpsPermissionApproved || !mGoogleApiClient.isConnected()) { return; } mCalendar.setTimeInMillis(System.currentTimeMillis()); @@ -315,29 +421,56 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co }); } + /** + * Handles user choices for both speed limit and location permissions (GPS tracking). + */ @Override - protected void onStop() { - super.onStop(); - if (mGoogleApiClient.isConnected()) { - LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + + if (requestCode == REQUEST_PICK_SPEED_LIMIT) { + if (resultCode == RESULT_OK) { + // The user updated the speed limit. + int newSpeedLimit = + data.getIntExtra(SpeedPickerActivity.EXTRA_NEW_SPEED_LIMIT, mSpeedLimit); + + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY, newSpeedLimit); + editor.apply(); + + mSpeedLimit = newSpeedLimit; + + updateSpeedInViews(); + } } - mGoogleApiClient.disconnect(); } + /** + * Callback received when a permissions request has been completed. + */ @Override - protected void onResume() { - super.onResume(); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - mCalendar = Calendar.getInstance(); - setSpeedLimit(); - adjustColor(); - updateRecordingIcon(); - } + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + + Log.d(TAG, "onRequestPermissionsResult(): " + permissions); - private void updateRecordingIcon() { - mSaveGpsLocation = LocationSettingActivity.getGpsRecordingStatusFromPreferences(this); - mSaveImageView.setImageResource(mSaveGpsLocation ? R.drawable.ic_gps_saving_grey600_96dp - : R.drawable.ic_gps_not_saving_grey600_96dp); + + if (requestCode == REQUEST_GPS_PERMISSION) { + Log.i(TAG, "Received response for GPS permission request."); + + if ((grantResults.length == 1) + && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { + Log.i(TAG, "GPS permission granted."); + mGpsPermissionApproved = true; + } else { + Log.i(TAG, "GPS permission NOT granted."); + mGpsPermissionApproved = false; + } + + updateActivityViewsBasedOnLocationPermissions(); + + } } /** @@ -346,4 +479,4 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co private boolean hasGps() { return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS); } -} +}
\ No newline at end of file diff --git a/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/ui/LocationSettingActivity.java b/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/ui/LocationSettingActivity.java deleted file mode 100644 index 1f8be71c..00000000 --- a/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/ui/LocationSettingActivity.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2014 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.android.wearable.speedtracker.ui; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.TextView; - -import com.example.android.wearable.speedtracker.R; - -/** - * A simple activity that allows the user to start or stop recording of GPS location data. - */ -public class LocationSettingActivity extends Activity { - - private static final String PREFS_KEY_SAVE_GPS = "save-gps"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.saving_activity); - TextView textView = (TextView) findViewById(R.id.textView); - textView.setText(getGpsRecordingStatusFromPreferences(this) ? R.string.stop_saving_gps - : R.string.start_saving_gps); - - } - - public void onClick(View view) { - switch (view.getId()) { - case R.id.submitBtn: - saveGpsRecordingStatusToPreferences(LocationSettingActivity.this, - !getGpsRecordingStatusFromPreferences(this)); - break; - case R.id.cancelBtn: - break; - } - finish(); - } - - /** - * Get the persisted value for whether the app should record the GPS location data or not. If - * there is no prior value persisted, it returns {@code false}. - */ - public static boolean getGpsRecordingStatusFromPreferences(Context context) { - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); - return pref.getBoolean(PREFS_KEY_SAVE_GPS, false); - } - - /** - * Persists the user selection to whether save the GPS location data or not. - */ - public static void saveGpsRecordingStatusToPreferences(Context context, boolean value) { - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); - pref.edit().putBoolean(PREFS_KEY_SAVE_GPS, value).apply(); - - } -} diff --git a/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/ui/SpeedPickerListAdapter.java b/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/ui/SpeedPickerListAdapter.java index e3b284bf..df25a6a8 100644 --- a/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/ui/SpeedPickerListAdapter.java +++ b/wearable/wear/SpeedTracker/Wearable/src/main/java/com/example/android/wearable/speedtracker/ui/SpeedPickerListAdapter.java @@ -41,6 +41,9 @@ public class SpeedPickerListAdapter extends WearableListView.Adapter { mDataSet = dataset; } + /** + * Displays all possible speed limit choices. + */ public static class ItemViewHolder extends WearableListView.ViewHolder { private TextView mTextView; diff --git a/wearable/wear/SpeedTracker/Wearable/src/main/res/layout/main_activity.xml b/wearable/wear/SpeedTracker/Wearable/src/main/res/layout/main_activity.xml index a1b9081a..a2b678eb 100644 --- a/wearable/wear/SpeedTracker/Wearable/src/main/res/layout/main_activity.xml +++ b/wearable/wear/SpeedTracker/Wearable/src/main/res/layout/main_activity.xml @@ -29,11 +29,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" + android:paddingLeft="16dp" android:fontFamily="sans-serif-light" + android:textAlignment="center" android:textSize="17sp" android:textStyle="italic" - android:id="@+id/acquiring_gps" - android:text="@string/acquiring_gps"/> + android:id="@+id/gps_issue_text" + android:text=""/> <TextView android:layout_width="wrap_content" @@ -84,18 +86,20 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" 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_alignParentLeft="true" android:layout_marginBottom="20dp" - android:layout_marginLeft="60dp" /> + android:layout_marginLeft="50dp" /> <ImageButton android:layout_width="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:layout_alignParentRight="true" - android:layout_alignBottom="@+id/saving" - android:layout_marginRight="60dp"/> + android:layout_alignBottom="@+id/gps_permission" + android:layout_marginRight="50dp"/> </RelativeLayout> diff --git a/wearable/wear/SpeedTracker/Wearable/src/main/res/layout/saving_activity.xml b/wearable/wear/SpeedTracker/Wearable/src/main/res/layout/saving_activity.xml deleted file mode 100644 index c37d9593..00000000 --- a/wearable/wear/SpeedTracker/Wearable/src/main/res/layout/saving_activity.xml +++ /dev/null @@ -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>
\ No newline at end of file diff --git a/wearable/wear/SpeedTracker/Wearable/src/main/res/values/strings.xml b/wearable/wear/SpeedTracker/Wearable/src/main/res/values/strings.xml index dda3ecd6..b0c37478 100644 --- a/wearable/wear/SpeedTracker/Wearable/src/main/res/values/strings.xml +++ b/wearable/wear/SpeedTracker/Wearable/src/main/res/values/strings.xml @@ -25,11 +25,16 @@ <string name="speed_limit">Limit: %1$d mph</string> <string name="acquiring_gps">Acquiring GPS Fix ...</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="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="speed_format">%.0f</string> + + <string name="permission_rationale">App requires location permission to function, tap GPS icon.</string> + + </resources> diff --git a/wearable/wear/SpeedTracker/template-params.xml b/wearable/wear/SpeedTracker/template-params.xml index 1cf31619..8730de13 100644 --- a/wearable/wear/SpeedTracker/template-params.xml +++ b/wearable/wear/SpeedTracker/template-params.xml @@ -23,15 +23,17 @@ <package>com.example.android.wearable.speedtracker</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>23</targetSdkVersionWear> <wearable> <has_handheld_app>true</has_handheld_app> </wearable> - <dependency>com.google.android.gms:play-services-location:7.3.0</dependency> - <dependency_wearable>com.google.android.gms:play-services-location:7.3.0</dependency_wearable> + <dependency>com.android.support:design:23.0.1</dependency> + <dependency>com.google.android.gms:play-services-location:8.1.0</dependency> + <dependency_wearable>com.google.android.gms:play-services-location:8.1.0</dependency_wearable> <strings> <intro> diff --git a/wearable/wear/SynchronizedNotifications/Application/src/main/AndroidManifest.xml b/wearable/wear/SynchronizedNotifications/Application/src/main/AndroidManifest.xml index 1737c7da..04a69e01 100644 --- a/wearable/wear/SynchronizedNotifications/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/SynchronizedNotifications/Application/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ android:versionName="1.0"> <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="21" /> + android:targetSdkVersion="23" /> <application android:allowBackup="true" android:label="@string/app_name" diff --git a/wearable/wear/SynchronizedNotifications/Wearable/src/main/AndroidManifest.xml b/wearable/wear/SynchronizedNotifications/Wearable/src/main/AndroidManifest.xml index f9b0d9c8..5c4d259d 100644 --- a/wearable/wear/SynchronizedNotifications/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/SynchronizedNotifications/Wearable/src/main/AndroidManifest.xml @@ -20,7 +20,7 @@ package="com.example.android.wearable.synchronizednotifications"> <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/SynchronizedNotifications/template-params.xml b/wearable/wear/SynchronizedNotifications/template-params.xml index 799b2267..97c22441 100644 --- a/wearable/wear/SynchronizedNotifications/template-params.xml +++ b/wearable/wear/SynchronizedNotifications/template-params.xml @@ -23,7 +23,8 @@ <package>com.example.android.wearable.synchronizednotifications</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <wearable> <has_handheld_app>true</has_handheld_app> diff --git a/wearable/wear/Timer/Wearable/src/main/AndroidManifest.xml b/wearable/wear/Timer/Wearable/src/main/AndroidManifest.xml index 364fb5a6..59634fc4 100644 --- a/wearable/wear/Timer/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/Timer/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.wearable.timer" > <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/Timer/template-params.xml b/wearable/wear/Timer/template-params.xml index 11884028..a3293f30 100644 --- a/wearable/wear/Timer/template-params.xml +++ b/wearable/wear/Timer/template-params.xml @@ -22,8 +22,7 @@ <group>Wearable</group> <package>com.example.android.wearable.timer</package> - <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <strings> <intro> diff --git a/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml b/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml index 5433c94f..723e4e57 100644 --- a/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml +++ b/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml @@ -18,13 +18,19 @@ package="com.example.android.wearable.watchface" > <uses-sdk android:minSdkVersion="18" - android:targetSdkVersion="21" /> + android:targetSdkVersion="23" /> <!-- Permissions required by the wearable app --> <uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" /> <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" /> + <!-- Location permission used by FitDistanceWatchFaceService --> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <!-- All intent-filters for config actions must include the categories com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION and android.intent.category.DEFAULT. --> @@ -55,6 +61,18 @@ </intent-filter> </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 android:name=".OpenGLWatchFaceConfigActivity" android:label="@string/app_name"> diff --git a/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/FitDistanceWatchFaceConfigActivity.java b/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/FitDistanceWatchFaceConfigActivity.java new file mode 100644 index 00000000..1d8e4c9b --- /dev/null +++ b/wearable/wear/WatchFace/Application/src/main/java/com/example/android/wearable/watchface/FitDistanceWatchFaceConfigActivity.java @@ -0,0 +1,255 @@ +package com.example.android.wearable.watchface; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.common.Scopes; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Scope; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Switch; +import android.widget.Toast; + +import java.util.concurrent.TimeUnit; + +/** + * Allows users of the Fit WatchFace to tie their Google Fit account to the WatchFace. + */ +public class FitDistanceWatchFaceConfigActivity extends Activity implements + GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener { + + private static final String TAG = "FitDistanceConfig"; + + // Request code for launching the Intent to resolve authorization. + private static final int REQUEST_OAUTH = 1; + + // Shared Preference used to record if the user has enabled Google Fit previously. + private static final String PREFS_FIT_ENABLED_BY_USER = + "com.example.android.wearable.watchface.preferences.FIT_ENABLED_BY_USER"; + + /* Tracks whether an authorization activity is stacking over the current activity, i.e., when + * a known auth error is being resolved, such as showing the account chooser or presenting a + * consent dialog. This avoids common duplications as might happen on screen rotations, etc. + */ + private static final String EXTRA_AUTH_STATE_PENDING = + "com.example.android.wearable.watchface.extra.AUTH_STATE_PENDING"; + + private static final long FIT_DISABLE_TIMEOUT_SECS = TimeUnit.SECONDS.toMillis(5);; + + private boolean mResolvingAuthorization; + + private boolean mFitEnabled; + + private GoogleApiClient mGoogleApiClient; + + private Switch mFitAuthSwitch; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_fit_watch_face_config); + + mFitAuthSwitch = (Switch) findViewById(R.id.fit_auth_switch); + + if (savedInstanceState != null) { + mResolvingAuthorization = + savedInstanceState.getBoolean(EXTRA_AUTH_STATE_PENDING, false); + } else { + mResolvingAuthorization = false; + } + + // Checks if user previously enabled/approved Google Fit. + SharedPreferences sharedPreferences = getPreferences(Context.MODE_PRIVATE); + mFitEnabled = + sharedPreferences.getBoolean(PREFS_FIT_ENABLED_BY_USER, false); + + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Fitness.HISTORY_API) + .addApi(Fitness.RECORDING_API) + .addApi(Fitness.CONFIG_API) + .addScope(new Scope(Scopes.FITNESS_LOCATION_READ_WRITE)) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + @Override + protected void onStart() { + super.onStart(); + + if ((mFitEnabled) && (mGoogleApiClient != null)) { + + mFitAuthSwitch.setChecked(true); + mFitAuthSwitch.setEnabled(true); + + mGoogleApiClient.connect(); + + } else { + + mFitAuthSwitch.setChecked(false); + mFitAuthSwitch.setEnabled(true); + } + } + + @Override + protected void onStop() { + super.onStop(); + + if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) { + mGoogleApiClient.disconnect(); + } + } + + @Override + protected void onSaveInstanceState(Bundle bundle) { + super.onSaveInstanceState(bundle); + bundle.putBoolean(EXTRA_AUTH_STATE_PENDING, mResolvingAuthorization); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + if (savedInstanceState != null) { + mResolvingAuthorization = + savedInstanceState.getBoolean(EXTRA_AUTH_STATE_PENDING, false); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.d(TAG, "onActivityResult()"); + + if (requestCode == REQUEST_OAUTH) { + mResolvingAuthorization = false; + + if (resultCode == RESULT_OK) { + setUserFitPreferences(true); + + if (!mGoogleApiClient.isConnecting() && !mGoogleApiClient.isConnected()) { + mGoogleApiClient.connect(); + } + } else { + // User cancelled authorization, reset the switch. + setUserFitPreferences(false); + } + } + } + + @Override + public void onConnected(Bundle connectionHint) { + Log.d(TAG, "onConnected: " + connectionHint); + } + + @Override + public void onConnectionSuspended(int cause) { + + if (cause == GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST) { + Log.i(TAG, "Connection lost. Cause: Network Lost."); + } else if (cause == GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED) { + Log.i(TAG, "Connection lost. Reason: Service Disconnected"); + } else { + Log.i(TAG, "onConnectionSuspended: " + cause); + } + + mFitAuthSwitch.setChecked(false); + mFitAuthSwitch.setEnabled(true); + } + + @Override + public void onConnectionFailed(ConnectionResult result) { + Log.d(TAG, "Connection to Google Fit failed. Cause: " + result.toString()); + + if (!result.hasResolution()) { + // User cancelled authorization, reset the switch. + mFitAuthSwitch.setChecked(false); + mFitAuthSwitch.setEnabled(true); + // Show the localized error dialog + GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), this, 0).show(); + return; + } + + // Resolve failure if not already trying/authorizing. + if (!mResolvingAuthorization) { + try { + Log.i(TAG, "Attempting to resolve failed GoogleApiClient connection"); + mResolvingAuthorization = true; + result.startResolutionForResult(this, REQUEST_OAUTH); + } catch (IntentSender.SendIntentException e) { + Log.e(TAG, "Exception while starting resolution activity", e); + } + } + } + + public void onSwitchClicked(View view) { + + boolean userWantsToEnableFit = mFitAuthSwitch.isChecked(); + + if (userWantsToEnableFit) { + + Log.d(TAG, "User wants to enable Fit."); + if ((mGoogleApiClient != null) && (!mGoogleApiClient.isConnected())) { + mGoogleApiClient.connect(); + } + + } else { + Log.d(TAG, "User wants to disable Fit."); + + // Disable switch until disconnect request is finished. + mFitAuthSwitch.setEnabled(false); + + PendingResult<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); + } +} diff --git a/wearable/wear/WatchFace/Application/src/main/res/layout/activity_fit_watch_face_config.xml b/wearable/wear/WatchFace/Application/src/main/res/layout/activity_fit_watch_face_config.xml new file mode 100644 index 00000000..73d14891 --- /dev/null +++ b/wearable/wear/WatchFace/Application/src/main/res/layout/activity_fit_watch_face_config.xml @@ -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>
\ No newline at end of file diff --git a/wearable/wear/WatchFace/Application/src/main/res/values-w820dp/dimens.xml b/wearable/wear/WatchFace/Application/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 00000000..63fc8164 --- /dev/null +++ b/wearable/wear/WatchFace/Application/src/main/res/values-w820dp/dimens.xml @@ -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> diff --git a/wearable/wear/WatchFace/Application/src/main/res/values/dimens.xml b/wearable/wear/WatchFace/Application/src/main/res/values/dimens.xml new file mode 100644 index 00000000..56dca871 --- /dev/null +++ b/wearable/wear/WatchFace/Application/src/main/res/values/dimens.xml @@ -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> diff --git a/wearable/wear/WatchFace/Application/src/main/res/values/strings.xml b/wearable/wear/WatchFace/Application/src/main/res/values/strings.xml index 6c6834f0..275dcd3f 100644 --- a/wearable/wear/WatchFace/Application/src/main/res/values/strings.xml +++ b/wearable/wear/WatchFace/Application/src/main/res/values/strings.xml @@ -22,6 +22,8 @@ <string name="digital_config_minutes">Minutes</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="ok_no_device_connected">OK</string> diff --git a/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml b/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml index c96d7305..026107e7 100644 --- a/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/WatchFace/Wearable/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ <?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"); 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 limitations under the License. --> - <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" - android:targetSdkVersion="21" /> + <uses-sdk + android:minSdkVersion="21" + android:targetSdkVersion="23" /> <uses-feature android:name="android.hardware.type.watch" /> @@ -29,102 +30,113 @@ <!-- Calendar permission used by CalendarWatchFaceService --> <uses-permission android:name="android.permission.READ_CALENDAR" /> + <!-- Location permission used by FitDistanceWatchFaceService --> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <application - android:allowBackup="true" - android:icon="@drawable/ic_launcher" - android:label="@string/app_name" > + android:allowBackup="true" + android:icon="@drawable/ic_launcher" + 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 - android:name=".AnalogWatchFaceService" - android:label="@string/analog_name" - android:permission="android.permission.BIND_WALLPAPER" > + android:name=".AnalogWatchFaceService" + android:label="@string/analog_name" + android:permission="android.permission.BIND_WALLPAPER" > <meta-data - android:name="android.service.wallpaper" - android:resource="@xml/watch_face" /> + 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" /> + 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" /> + android:name="com.google.android.wearable.watchface.preview_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_ANALOG" /> + android:name="com.google.android.wearable.watchface.companionConfigurationAction" + android:value="com.example.android.wearable.watchface.CONFIG_ANALOG" /> + <intent-filter> <action android:name="android.service.wallpaper.WallpaperService" /> + <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> </intent-filter> </service> - <service - android:name=".SweepWatchFaceService" - android:label="@string/sweep_name" - android:permission="android.permission.BIND_WALLPAPER" > + 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" /> + 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" /> + 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" /> + 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" > + 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" /> + 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" /> + 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" /> + 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" /> + 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" > + 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" /> + 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" /> + 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" /> + 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" /> + 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" > + android:name=".InteractiveWatchFaceService" + android:label="@string/interactive_name" + android:permission="android.permission.BIND_WALLPAPER" > <meta-data android:name="android.service.wallpaper" android:resource="@xml/watch_face" /> @@ -134,81 +146,130 @@ <meta-data android:name="com.google.android.wearable.watchface.preview_circular" android:resource="@drawable/preview_interactive_circular" /> + <intent-filter> <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> </service> - <service - android:name=".DigitalWatchFaceService" - android:label="@string/digital_name" - android:permission="android.permission.BIND_WALLPAPER" > + android:name=".DigitalWatchFaceService" + android:label="@string/digital_name" + android:permission="android.permission.BIND_WALLPAPER" > <meta-data - android:name="android.service.wallpaper" - android:resource="@xml/watch_face" /> + android:name="android.service.wallpaper" + android:resource="@xml/watch_face" /> <meta-data - android:name="com.google.android.wearable.watchface.preview" - android:resource="@drawable/preview_digital" /> + android:name="com.google.android.wearable.watchface.preview" + android:resource="@drawable/preview_digital" /> <meta-data - android:name="com.google.android.wearable.watchface.preview_circular" - android:resource="@drawable/preview_digital_circular" /> + android:name="com.google.android.wearable.watchface.preview_circular" + android:resource="@drawable/preview_digital_circular" /> <meta-data - android:name="com.google.android.wearable.watchface.companionConfigurationAction" - android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" /> + android:name="com.google.android.wearable.watchface.companionConfigurationAction" + android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" /> <meta-data - android:name="com.google.android.wearable.watchface.wearableConfigurationAction" - android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" /> + android:name="com.google.android.wearable.watchface.wearableConfigurationAction" + android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" /> + <intent-filter> <action android:name="android.service.wallpaper.WallpaperService" /> + <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> </intent-filter> </service> - <!-- All intent-filters for config actions must include the categories + <!-- + All intent-filters for config actions must include the categories com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION - and android.intent.category.DEFAULT. --> + and android.intent.category.DEFAULT. + --> <activity - android:name=".DigitalWatchFaceWearableConfigActivity" - android:label="@string/digital_config_name"> + android:name=".DigitalWatchFaceWearableConfigActivity" + android:label="@string/digital_config_name" > <intent-filter> <action android:name="com.example.android.wearable.watchface.CONFIG_DIGITAL" /> + <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <service - android:name=".CalendarWatchFaceService" - android:label="@string/calendar_name" - android:permission="android.permission.BIND_WALLPAPER" > + android:name=".CalendarWatchFaceService" + android:label="@string/calendar_name" + android:permission="android.permission.BIND_WALLPAPER" > <meta-data - android:name="android.service.wallpaper" - android:resource="@xml/watch_face" /> + android:name="android.service.wallpaper" + android:resource="@xml/watch_face" /> <meta-data - android:name="com.google.android.wearable.watchface.preview" - android:resource="@drawable/preview_calendar" /> + android:name="com.google.android.wearable.watchface.preview" + android:resource="@drawable/preview_calendar" /> <meta-data - android:name="com.google.android.wearable.watchface.preview_circular" - android:resource="@drawable/preview_calendar_circular" /> + android:name="com.google.android.wearable.watchface.preview_circular" + android:resource="@drawable/preview_calendar_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=".DigitalWatchFaceConfigListenerService"> + <service android:name=".DigitalWatchFaceConfigListenerService" > <intent-filter> <action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> </intent-filter> </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 - android:name="com.google.android.gms.version" - android:value="@integer/google_play_services_version" /> + <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=".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> </manifest> diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFacePermissionActivity.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFacePermissionActivity.java new file mode 100644 index 00000000..7effd33d --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFacePermissionActivity.java @@ -0,0 +1,56 @@ +package com.example.android.wearable.watchface; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.wearable.activity.WearableActivity; +import android.util.Log; +import android.view.View; + +/** + * Simple Activity for displaying Calendar Permission Rationale to user. + */ +public class CalendarWatchFacePermissionActivity extends WearableActivity { + + private static final String TAG = "PermissionActivity"; + + /* Id to identify permission request for calendar. */ + private static final int PERMISSION_REQUEST_READ_CALENDAR = 1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_calendar_watch_face_permission); + setAmbientEnabled(); + } + + public void onClickEnablePermission(View view) { + Log.d(TAG, "onClickEnablePermission()"); + + // On 23+ (M+) devices, GPS permission not granted. Request permission. + ActivityCompat.requestPermissions( + this, + new String[]{Manifest.permission.READ_CALENDAR}, + PERMISSION_REQUEST_READ_CALENDAR); + + } + + /* + * Callback received when a permissions request has been completed. + */ + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + + Log.d(TAG, "onRequestPermissionsResult()"); + + if (requestCode == PERMISSION_REQUEST_READ_CALENDAR) { + if ((grantResults.length == 1) + && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { + finish(); + } + } + } +}
\ No newline at end of file diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFaceService.java index a8ab9556..98a251cd 100644 --- a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFaceService.java +++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/CalendarWatchFaceService.java @@ -16,11 +16,13 @@ package com.example.android.wearable.watchface; +import android.Manifest; import android.content.BroadcastReceiver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.Canvas; import android.graphics.Color; @@ -30,8 +32,10 @@ import android.os.AsyncTask; import android.os.Handler; import android.os.Message; import android.os.PowerManager; +import android.support.v4.app.ActivityCompat; import android.support.wearable.provider.WearableCalendarContract; import android.support.wearable.watchface.CanvasWatchFaceService; +import android.support.wearable.watchface.WatchFaceService; import android.support.wearable.watchface.WatchFaceStyle; import android.text.DynamicLayout; import android.text.Editable; @@ -74,31 +78,37 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService { final TextPaint mTextPaint = new TextPaint(); int mNumMeetings; + private boolean mCalendarPermissionApproved; + private String mCalendarNotApprovedMessage; private AsyncTask<Void, Void, Integer> mLoadMeetingsTask; + private boolean mIsReceiverRegistered; + /** Handler to load the meetings once a minute in interactive mode. */ final Handler mLoadMeetingsHandler = new Handler() { @Override public void handleMessage(Message message) { switch (message.what) { case MSG_LOAD_MEETINGS: + cancelLoadMeetingTask(); - mLoadMeetingsTask = new LoadMeetingsTask(); - mLoadMeetingsTask.execute(); + + // Loads meetings. + if (mCalendarPermissionApproved) { + mLoadMeetingsTask = new LoadMeetingsTask(); + mLoadMeetingsTask.execute(); + } break; } } }; - private boolean mIsReceiverRegistered; - private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_PROVIDER_CHANGED.equals(intent.getAction()) && WearableCalendarContract.CONTENT_URI.equals(intent.getData())) { - cancelLoadMeetingTask(); mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS); } } @@ -106,29 +116,59 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService { @Override public void onCreate(SurfaceHolder holder) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "onCreate"); - } super.onCreate(holder); + Log.d(TAG, "onCreate"); + + mCalendarNotApprovedMessage = + getResources().getString(R.string.calendar_permission_not_approved); + + /* Accepts tap events to allow permission changes by user. */ setWatchFaceStyle(new WatchFaceStyle.Builder(CalendarWatchFaceService.this) .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE) .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) .setShowSystemUiTime(false) + .setAcceptsTapEvents(true) .build()); mTextPaint.setColor(FOREGROUND_COLOR); mTextPaint.setTextSize(TEXT_SIZE); - mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS); + // Enables app to handle 23+ (M+) style permissions. + mCalendarPermissionApproved = + ActivityCompat.checkSelfPermission( + getApplicationContext(), + Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED; + + if (mCalendarPermissionApproved) { + mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS); + } } @Override public void onDestroy() { mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS); - cancelLoadMeetingTask(); super.onDestroy(); } + /* + * Captures tap event (and tap type) and increments correct tap type total. + */ + @Override + public void onTapCommand(int tapType, int x, int y, long eventTime) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Tap Command: " + tapType); + } + + // Ignore lint error (fixed in wearable support library 1.4) + if (tapType == WatchFaceService.TAP_TYPE_TAP && !mCalendarPermissionApproved) { + Intent permissionIntent = new Intent( + getApplicationContext(), + CalendarWatchFacePermissionActivity.class); + permissionIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(permissionIntent); + } + } + @Override public void onDraw(Canvas canvas, Rect bounds) { // Create or update mLayout if necessary. @@ -141,8 +181,13 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService { // Update the contents of mEditable. mEditable.clear(); - mEditable.append(Html.fromHtml(getResources().getQuantityString( - R.plurals.calendar_meetings, mNumMeetings, mNumMeetings))); + + if (mCalendarPermissionApproved) { + mEditable.append(Html.fromHtml(getResources().getQuantityString( + R.plurals.calendar_meetings, mNumMeetings, mNumMeetings))); + } else { + mEditable.append(Html.fromHtml(mCalendarNotApprovedMessage)); + } // Draw the text on a solid background. canvas.drawColor(BACKGROUND_COLOR); @@ -151,15 +196,24 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService { @Override public void onVisibilityChanged(boolean visible) { + Log.d(TAG, "onVisibilityChanged()"); super.onVisibilityChanged(visible); if (visible) { - IntentFilter filter = new IntentFilter(Intent.ACTION_PROVIDER_CHANGED); - filter.addDataScheme("content"); - filter.addDataAuthority(WearableCalendarContract.AUTHORITY, null); - registerReceiver(mBroadcastReceiver, filter); - mIsReceiverRegistered = true; - mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS); + // Enables app to handle 23+ (M+) style permissions. + mCalendarPermissionApproved = ActivityCompat.checkSelfPermission( + getApplicationContext(), + Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED; + + if (mCalendarPermissionApproved) { + IntentFilter filter = new IntentFilter(Intent.ACTION_PROVIDER_CHANGED); + filter.addDataScheme("content"); + filter.addDataAuthority(WearableCalendarContract.AUTHORITY, null); + registerReceiver(mBroadcastReceiver, filter); + mIsReceiverRegistered = true; + + mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS); + } } else { if (mIsReceiverRegistered) { unregisterReceiver(mBroadcastReceiver); @@ -204,9 +258,9 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService { final Cursor cursor = getContentResolver().query(builder.build(), null, null, null, null); int numMeetings = cursor.getCount(); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Num meetings: " + numMeetings); - } + + Log.d(TAG, "Num meetings: " + numMeetings); + return numMeetings; } diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/FitDistanceWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/FitDistanceWatchFaceService.java new file mode 100644 index 00000000..b29a1902 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/FitDistanceWatchFaceService.java @@ -0,0 +1,533 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.wearable.watchface; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.wearable.watchface.CanvasWatchFaceService; +import android.support.wearable.watchface.WatchFaceStyle; +import android.text.format.DateFormat; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.WindowInsets; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.Scopes; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Scope; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.FitnessStatusCodes; +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.data.Field; +import com.google.android.gms.fitness.result.DailyTotalResult; + +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +/** + * Displays the user's daily distance total via Google Fit. Distance is polled initially when the + * Google API Client successfully connects and once a minute after that via the onTimeTick callback. + * If you want more frequent updates, you will want to add your own Handler. + * + * Authentication IS a requirement to request distance from Google Fit on Wear. Otherwise, distance + * will always come back as zero (or stay at whatever the distance was prior to you + * de-authorizing watchface). + * + * In ambient mode, the seconds are replaced with an AM/PM indicator. + * + * On devices with low-bit ambient mode, the text is drawn without anti-aliasing. On devices which + * require burn-in protection, the hours are drawn in normal rather than bold. + * + */ +public class FitDistanceWatchFaceService extends CanvasWatchFaceService { + + private static final String TAG = "DistanceWatchFace"; + + private static final Typeface BOLD_TYPEFACE = + Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD); + private static final Typeface NORMAL_TYPEFACE = + Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL); + + /** + * Update rate in milliseconds for active mode (non-ambient). + */ + private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1); + + @Override + public Engine onCreateEngine() { + return new Engine(); + } + + private class Engine extends CanvasWatchFaceService.Engine implements + GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener, + ResultCallback<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()); + } + } + } +}
\ No newline at end of file diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/FitStepsWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/FitStepsWatchFaceService.java new file mode 100644 index 00000000..1f7b298f --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/FitStepsWatchFaceService.java @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.wearable.watchface; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.fitness.Fitness; +import com.google.android.gms.fitness.FitnessStatusCodes; +import com.google.android.gms.fitness.data.DataPoint; +import com.google.android.gms.fitness.data.DataType; +import com.google.android.gms.fitness.data.Field; +import com.google.android.gms.fitness.result.DailyTotalResult; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.wearable.watchface.CanvasWatchFaceService; +import android.support.wearable.watchface.WatchFaceStyle; +import android.text.format.DateFormat; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.WindowInsets; + +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +/** + * The step count watch face shows user's daily step total via Google Fit (matches Google Fit app). + * Steps are polled initially when the Google API Client successfully connects and once a minute + * after that via the onTimeTick callback. If you want more frequent updates, you will want to add + * your own Handler. + * + * Authentication is not a requirement to request steps from Google Fit on Wear. + * + * In ambient mode, the seconds are replaced with an AM/PM indicator. + * + * On devices with low-bit ambient mode, the text is drawn without anti-aliasing. On devices which + * require burn-in protection, the hours are drawn in normal rather than bold. + * + */ +public class FitStepsWatchFaceService extends CanvasWatchFaceService { + + private static final String TAG = "StepCountWatchFace"; + + private static final Typeface BOLD_TYPEFACE = + Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD); + private static final Typeface NORMAL_TYPEFACE = + Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL); + + /** + * Update rate in milliseconds for active mode (non-ambient). + */ + private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1); + + @Override + public Engine onCreateEngine() { + return new Engine(); + } + + private class Engine extends CanvasWatchFaceService.Engine implements + GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener, + ResultCallback<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()); + } + } + } +} diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png Binary files differnew file mode 100644 index 00000000..6bae68f5 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_distance.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_distance.png Binary files differnew file mode 100644 index 00000000..a96f3557 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_distance.png diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_distance_circular.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_distance_circular.png Binary files differnew file mode 100644 index 00000000..912d85bb --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_distance_circular.png diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_fit.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_fit.png Binary files differnew file mode 100644 index 00000000..04b8b5e9 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_fit.png diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_fit_circular.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_fit_circular.png Binary files differnew file mode 100644 index 00000000..b421e289 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_fit_circular.png diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png Binary files differnew file mode 100644 index 00000000..3f47b54c --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png Binary files differnew file mode 100644 index 00000000..cbe9e1cd --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png Binary files differnew file mode 100644 index 00000000..1d1b0f4d --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_calendar_watch_face_permission.xml b/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_calendar_watch_face_permission.xml new file mode 100644 index 00000000..bf0e3f67 --- /dev/null +++ b/wearable/wear/WatchFace/Wearable/src/main/res/layout/activity_calendar_watch_face_permission.xml @@ -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>
\ No newline at end of file diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/values/dimens.xml b/wearable/wear/WatchFace/Wearable/src/main/res/values/dimens.xml index 4973466e..0b0672b6 100644 --- a/wearable/wear/WatchFace/Wearable/src/main/res/values/dimens.xml +++ b/wearable/wear/WatchFace/Wearable/src/main/res/values/dimens.xml @@ -32,4 +32,15 @@ <dimen name="interactive_y_offset">72dp</dimen> <dimen name="interactive_y_offset_round">84dp</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> diff --git a/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml b/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml index 19bc3e7f..4090995d 100644 --- a/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml +++ b/wearable/wear/WatchFace/Wearable/src/main/res/values/strings.xml @@ -25,11 +25,22 @@ <string name="digital_config_name">Digital watch face configuration</string> <string name="digital_am">AM</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_permission_not_approved"><br><br><br>WatchFace requires Calendar permission. Click on this WatchFace or visit Settings > Permissions to approve.</string> <plurals name="calendar_meetings"> <item quantity="one"><br><br><br>You have <b>%1$d</b> meeting in the next 24 hours.</item> <item quantity="other"><br><br><br>You have <b>%1$d</b> meetings in the next 24 hours.</item> </plurals> + <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) --> <string name="color_black">Black</string> diff --git a/wearable/wear/WatchFace/template-params.xml b/wearable/wear/WatchFace/template-params.xml index cedf8db0..15df71a3 100644 --- a/wearable/wear/WatchFace/template-params.xml +++ b/wearable/wear/WatchFace/template-params.xml @@ -23,10 +23,13 @@ <package>com.example.android.wearable.watchface</package> <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>23</targetSdkVersionWear> - <dependency_wearable>com.android.support:palette-v7:21.0.0</dependency_wearable> + <dependency_wearable>com.android.support:palette-v7:23.1.0</dependency_wearable> <dependency>com.google.android.support:wearable:1.3.0</dependency> + <dependency>com.google.android.gms:play-services-fitness:8.3.0</dependency> + <dependency_wearable>com.google.android.gms:play-services-fitness:8.3.0</dependency_wearable> <wearable> <has_handheld_app>true</has_handheld_app> @@ -37,8 +40,13 @@ <![CDATA[ This sample demonstrates how to create watch faces for android wear and includes a phone app and a wearable app. The wearable app has a variety of watch faces including analog, digital, -opengl, calendar, interactive, etc. It also includes a watch-side configuration example. +opengl, calendar, steps, interactive, etc. It also includes a watch-side configuration example. The phone app includes a phone-side configuration example. + +Additional note on Steps WatchFace Sample, if the user has not installed or setup the Google Fit app +on their phone and their Wear device has not configured the Google Fit Wear App, then you may get +zero steps until one of the two is setup. Please note, many Wear devices configure the Google Fit +Wear App beforehand. ]]> </intro> </strings> diff --git a/wearable/wear/WatchViewStub/Wearable/src/main/AndroidManifest.xml b/wearable/wear/WatchViewStub/Wearable/src/main/AndroidManifest.xml index 33a266d6..774817ba 100644 --- a/wearable/wear/WatchViewStub/Wearable/src/main/AndroidManifest.xml +++ b/wearable/wear/WatchViewStub/Wearable/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.example.android.google.wearable.watchviewstub" > <uses-sdk android:minSdkVersion="20" - android:targetSdkVersion="21" /> + android:targetSdkVersion="22" /> <uses-feature android:name="android.hardware.type.watch" /> diff --git a/wearable/wear/WatchViewStub/template-params.xml b/wearable/wear/WatchViewStub/template-params.xml index ff7e7f83..39568aa9 100644 --- a/wearable/wear/WatchViewStub/template-params.xml +++ b/wearable/wear/WatchViewStub/template-params.xml @@ -19,8 +19,7 @@ <group>Wearable</group> <package>com.example.android.google.wearable.watchviewstub</package> - <minSdk>18</minSdk> - <targetSdkVersion>22</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <strings> <intro> diff --git a/wearable/wear/WearSpeakerSample/CONTRIBUTING b/wearable/wear/WearSpeakerSample/CONTRIBUTING new file mode 100644 index 00000000..fe1f5883 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/CONTRIBUTING @@ -0,0 +1,34 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement (CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA] + (https://cla.developers.google.com). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA] + (https://cla.developers.google.com). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which + you are contributing. Refer to the + [Android Code Style Guide] + (https://source.android.com/source/code-style.html) for the + recommended coding standards for this organization. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request. diff --git a/wearable/wear/WearSpeakerSample/build.gradle b/wearable/wear/WearSpeakerSample/build.gradle new file mode 100644 index 00000000..25ba12af --- /dev/null +++ b/wearable/wear/WearSpeakerSample/build.gradle @@ -0,0 +1,50 @@ +/* + * 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. + */ + +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.0' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +// BEGIN_EXCLUDE +import com.example.android.samples.build.SampleGenPlugin +apply plugin: SampleGenPlugin + +samplegen { + pathToBuild "../../../../../build" + pathToSamplesCommon "../../../common" +} +apply from: "../../../../../build/build.gradle" +// END_EXCLUDE diff --git a/wearable/wear/WearSpeakerSample/buildSrc/build.gradle b/wearable/wear/WearSpeakerSample/buildSrc/build.gradle new file mode 100644 index 00000000..7cebf71c --- /dev/null +++ b/wearable/wear/WearSpeakerSample/buildSrc/build.gradle @@ -0,0 +1,15 @@ +repositories { + mavenCentral() +} +dependencies { + compile 'org.freemarker:freemarker:2.3.20' +} + +sourceSets { + main { + groovy { + srcDir new File(rootDir, "../../../../../../build/buildSrc/src/main/groovy") + } + } +} + diff --git a/wearable/wear/WearSpeakerSample/gradle.properties b/wearable/wear/WearSpeakerSample/gradle.properties new file mode 100644 index 00000000..1d3591c8 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true
\ No newline at end of file diff --git a/wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.jar b/wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 00000000..8c0fb64a --- /dev/null +++ b/wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.jar diff --git a/wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1a127e91 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Oct 04 13:39:51 PDT 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip diff --git a/wearable/wear/WearSpeakerSample/gradlew b/wearable/wear/WearSpeakerSample/gradlew new file mode 100755 index 00000000..91a7e269 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/wearable/wear/WearSpeakerSample/gradlew.bat b/wearable/wear/WearSpeakerSample/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/wearable/wear/WearSpeakerSample/screenshots/1.png b/wearable/wear/WearSpeakerSample/screenshots/1.png Binary files differnew file mode 100644 index 00000000..d98e6fa3 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/screenshots/1.png diff --git a/wearable/wear/WearSpeakerSample/screenshots/2.png b/wearable/wear/WearSpeakerSample/screenshots/2.png Binary files differnew file mode 100644 index 00000000..226da1f8 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/screenshots/2.png diff --git a/wearable/wear/WearSpeakerSample/screenshots/3.png b/wearable/wear/WearSpeakerSample/screenshots/3.png Binary files differnew file mode 100644 index 00000000..d7467d78 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/screenshots/3.png diff --git a/wearable/wear/WearSpeakerSample/screenshots/4.png b/wearable/wear/WearSpeakerSample/screenshots/4.png Binary files differnew file mode 100644 index 00000000..1044ee16 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/screenshots/4.png diff --git a/wearable/wear/WearSpeakerSample/settings.gradle b/wearable/wear/WearSpeakerSample/settings.gradle new file mode 100644 index 00000000..8d97c994 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/settings.gradle @@ -0,0 +1,17 @@ +/* + * 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. + */ + +include ':wear' diff --git a/wearable/wear/WearSpeakerSample/template-params.xml b/wearable/wear/WearSpeakerSample/template-params.xml new file mode 100644 index 00000000..f07d4c5d --- /dev/null +++ b/wearable/wear/WearSpeakerSample/template-params.xml @@ -0,0 +1,84 @@ +<?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. +--> +<sample> + <name>WearSpeakerSample</name> + <group>Wearable</group> + <package>com.example.android.wearable.speaker</package> + + <strings> + <intro> +<![CDATA[ +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"). +]]> + </intro> + </strings> + + <template src="unmanaged" /> + + <metadata> + <!-- Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED} --> + <status>PUBLISHED</status> + <!-- See http://go/sample-categories for details on the next 4 fields. --> + <categories>Wearable</categories> + <technologies>Android</technologies> + <languages>Java</languages> + <solutions>Mobile</solutions> + <!-- Values: {BEGINNER | INTERMEDIATE | ADVANCED | EXPERT} --> + <level>INTERMEDIATE</level> + <!-- Dimensions: 512x512, PNG fomrat --> + <icon>screenshots/1.png</icon> + <!-- Path to screenshots. Use <img> tags for each. --> + <!-- <screenshots> + <img>screenshots/composite-1.png</img> + </screenshots> --> + <!-- List of APIs that this sample should be cross-referenced under. Use <android> + for fully-qualified Framework class names ("android:" namespace). + + Use <ext> for custom namespaces, if needed. See "Samples Index API" documentation + for more details. --> + <api_refs> + <android>android.media.AudioTrack</android> + <android>android.media.AudioRecord</android> + </api_refs> + + <!-- 1-3 line description of the sample here. + + Avoid simply rearranging the sample's title. What does this sample actually + accomplish, and how does it do it? --> + <description> +<![CDATA[ +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"). +]]> + </description> + + <!-- Multi-paragraph introduction to sample, from an educational point-of-view. + Makrdown formatting allowed. This will be used to generate a mini-article for the + sample on DAC. --> + <!-- <intro> +<![CDATA[ +]]> + </intro> --> + </metadata> +</sample> diff --git a/wearable/wear/WearSpeakerSample/wear/.gitignore b/wearable/wear/WearSpeakerSample/wear/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/.gitignore @@ -0,0 +1 @@ +/build diff --git a/wearable/wear/WearSpeakerSample/wear/build.gradle b/wearable/wear/WearSpeakerSample/wear/build.gradle new file mode 100644 index 00000000..8d3e550f --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/build.gradle @@ -0,0 +1,44 @@ +/* + * 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. + */ + +apply plugin: 'com.android.application' + + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.example.android.wearable.speaker" + minSdkVersion 21 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile 'com.google.android.support:wearable:1.3.0' + compile 'com.google.android.gms:play-services-wearable:8.3.0' + compile 'com.android.support:appcompat-v7:23.1.0' + +} diff --git a/wearable/wear/WearSpeakerSample/wear/proguard-rules.pro b/wearable/wear/WearSpeakerSample/wear/proguard-rules.pro new file mode 100644 index 00000000..002bc05a --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${SDK}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/AndroidManifest.xml b/wearable/wear/WearSpeakerSample/wear/src/main/AndroidManifest.xml new file mode 100644 index 00000000..135d3e08 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/AndroidManifest.xml @@ -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> diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/MainActivity.java b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/MainActivity.java new file mode 100644 index 00000000..e7a4870f --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/MainActivity.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.wearable.speaker; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.os.Build; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.wearable.activity.WearableActivity; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.Toast; + +import java.util.concurrent.TimeUnit; + +/** + * We first get the required permission to use the MIC. If it is granted, then we continue with + * the application and present the UI with three icons: a MIC icon (if pressed, user can record up + * to 10 seconds), a Play icon (if clicked, it wil playback the recorded audio file) and a music + * note icon (if clicked, it plays an MP3 file that is included in the app). + */ +public class MainActivity extends WearableActivity implements UIAnimation.UIStateListener, + SoundRecorder.OnVoicePlaybackStateChangedListener { + + private static final String TAG = "MainActivity"; + private static final int PERMISSIONS_REQUEST_CODE = 100; + private static final long COUNT_DOWN_MS = TimeUnit.SECONDS.toMillis(10); + private static final long MILLIS_IN_SECOND = TimeUnit.SECONDS.toMillis(1); + private static final String VOICE_FILE_NAME = "audiorecord.pcm"; + private MediaPlayer mMediaPlayer; + private AppState mState = AppState.READY; + private UIAnimation.UIState mUiState = UIAnimation.UIState.HOME; + private SoundRecorder mSoundRecorder; + + private UIAnimation mUIAnimation; + private ProgressBar mProgressBar; + private CountDownTimer mCountDownTimer; + + enum AppState { + READY, PLAYING_VOICE, PLAYING_MUSIC, RECORDING + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + mProgressBar = (ProgressBar) findViewById(R.id.progress); + mProgressBar.setMax((int) (COUNT_DOWN_MS / MILLIS_IN_SECOND)); + setAmbientEnabled(); + } + + private void setProgressBar(long progressInMillis) { + mProgressBar.setProgress((int) (progressInMillis / MILLIS_IN_SECOND)); + } + + @Override + public void onUIStateChanged(UIAnimation.UIState state) { + Log.d(TAG, "UI State is: " + state); + if (mUiState == state) { + return; + } + switch (state) { + case MUSIC_UP: + mState = AppState.PLAYING_MUSIC; + mUiState = state; + playMusic(); + break; + case MIC_UP: + mState = AppState.RECORDING; + mUiState = state; + mSoundRecorder.startRecording(); + setProgressBar(COUNT_DOWN_MS); + mCountDownTimer = new CountDownTimer(COUNT_DOWN_MS, MILLIS_IN_SECOND) { + @Override + public void onTick(long millisUntilFinished) { + mProgressBar.setVisibility(View.VISIBLE); + setProgressBar(millisUntilFinished); + Log.d(TAG, "Time Left: " + millisUntilFinished / MILLIS_IN_SECOND); + } + + @Override + public void onFinish() { + mProgressBar.setProgress(0); + mProgressBar.setVisibility(View.INVISIBLE); + mSoundRecorder.stopRecording(); + mUIAnimation.transitionToHome(); + mUiState = UIAnimation.UIState.HOME; + mState = AppState.READY; + mCountDownTimer = null; + } + }; + mCountDownTimer.start(); + break; + case SOUND_UP: + mState = AppState.PLAYING_VOICE; + mUiState = state; + mSoundRecorder.startPlay(); + break; + case HOME: + switch (mState) { + case PLAYING_MUSIC: + mState = AppState.READY; + mUiState = state; + stopMusic(); + break; + case PLAYING_VOICE: + mState = AppState.READY; + mUiState = state; + mSoundRecorder.stopPlaying(); + break; + case RECORDING: + mState = AppState.READY; + mUiState = state; + mSoundRecorder.stopRecording(); + if (mCountDownTimer != null) { + mCountDownTimer.cancel(); + mCountDownTimer = null; + } + mProgressBar.setVisibility(View.INVISIBLE); + setProgressBar(COUNT_DOWN_MS); + break; + } + break; + } + } + + /** + * Plays back the MP3 file embedded in the application + */ + private void playMusic() { + if (mMediaPlayer == null) { + mMediaPlayer = MediaPlayer.create(this, R.raw.sound); + mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + // we need to transition to the READY/Home state + Log.d(TAG, "Music Finished"); + mUIAnimation.transitionToHome(); + } + }); + } + mMediaPlayer.start(); + } + + /** + * Stops the playback of the MP3 file. + */ + private void stopMusic() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + } + + /** + * Checks the permission that this app needs and if it has not been granted, it will + * prompt the user to grant it, otherwise it shuts down the app. + */ + private void checkPermissions() { + boolean recordAudioPermissionGranted = + ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED; + + if (recordAudioPermissionGranted) { + start(); + } else { + ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.RECORD_AUDIO}, + PERMISSIONS_REQUEST_CODE); + } + + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + if (requestCode == PERMISSIONS_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + start(); + } else { + // Permission has been denied before. At this point we should show a dialog to + // user and explain why this permission is needed and direct him to go to the + // Permissions settings for the app in the System settings. For this sample, we + // simply exit to get to the important part. + Toast.makeText(this, R.string.exiting_for_permissions, Toast.LENGTH_LONG).show(); + finish(); + } + } + } + + /** + * Starts the main flow of the application. + */ + private void start() { + mSoundRecorder = new SoundRecorder(this, VOICE_FILE_NAME, this); + int[] thumbResources = new int[] {R.id.mic, R.id.play, R.id.music}; + ImageView[] thumbs = new ImageView[3]; + for(int i=0; i < 3; i++) { + thumbs[i] = (ImageView) findViewById(thumbResources[i]); + } + View containerView = findViewById(R.id.container); + ImageView expandedView = (ImageView) findViewById(R.id.expanded); + int animationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); + mUIAnimation = new UIAnimation(containerView, thumbs, expandedView, animationDuration, + this); + } + + @Override + protected void onStart() { + super.onStart(); + if (speakerIsSupported()) { + checkPermissions(); + } else { + findViewById(R.id.container2).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Toast.makeText(MainActivity.this, R.string.no_speaker_supported, + Toast.LENGTH_SHORT).show(); + } + }); + } + } + + @Override + protected void onStop() { + if (mSoundRecorder != null) { + mSoundRecorder.cleanup(); + mSoundRecorder = null; + } + if (mCountDownTimer != null) { + mCountDownTimer.cancel(); + } + + if (mMediaPlayer != null) { + mMediaPlayer.release(); + mMediaPlayer = null; + } + super.onStop(); + } + + @Override + public void onPlaybackStopped() { + mUIAnimation.transitionToHome(); + mUiState = UIAnimation.UIState.HOME; + mState = AppState.READY; + } + + /** + * Determines if the wear device has a built-in speaker and if it is supported. Speaker, even if + * physically present, is only supported in Android M+ on a wear device.. + */ + public final boolean speakerIsSupported() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PackageManager packageManager = getPackageManager(); + // The results from AudioManager.getDevices can't be trusted unless the device + // advertises FEATURE_AUDIO_OUTPUT. + if (!packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) { + return false; + } + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); + for (AudioDeviceInfo device : devices) { + if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { + return true; + } + } + } + return false; + } +} diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/SoundRecorder.java b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/SoundRecorder.java new file mode 100644 index 00000000..a45bdd27 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/SoundRecorder.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.wearable.speaker; + +import android.content.Context; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioRecord; +import android.media.AudioTrack; +import android.media.MediaRecorder; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * A helper class to provide methods to record audio input from the MIC to the internal storage + * and to playback the same recorded audio file. + */ +public class SoundRecorder { + + private static final String TAG = "SoundRecorder"; + private static final int RECORDING_RATE = 8000; // can go up to 44K, if needed + private static final int CHANNEL_IN = AudioFormat.CHANNEL_IN_MONO; + private static final int CHANNELS_OUT = AudioFormat.CHANNEL_OUT_MONO; + private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT; + private static int BUFFER_SIZE = AudioRecord + .getMinBufferSize(RECORDING_RATE, CHANNEL_IN, FORMAT); + + private final String mOutputFileName; + private final AudioManager mAudioManager; + private final Handler mHandler; + private final Context mContext; + private State mState = State.IDLE; + + private OnVoicePlaybackStateChangedListener mListener; + private AsyncTask<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(); + } +} diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/UIAnimation.java b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/UIAnimation.java new file mode 100644 index 00000000..7ce2fd53 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/java/com/example/android/wearable/speaker/UIAnimation.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.wearable.speaker; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; + +/** + * A helper class to provide a simple animation when user selects any of the three icons on the + * main UI. + */ +public class UIAnimation { + + private AnimatorSet mCurrentAnimator; + private final int[] mLargeDrawables = new int[]{R.drawable.ic_mic_120dp, + R.drawable.ic_play_arrow_120dp, R.drawable.ic_audiotrack_120dp}; + private final ImageView[] mThumbs; + private ImageView expandedImageView; + private final View mContainerView; + private final int mAnimationDurationTime; + + private UIStateListener mListener; + private UIState mState = UIState.HOME; + + public UIAnimation(View containerView, ImageView[] thumbs, ImageView expandedView, + int animationDuration, UIStateListener listener) { + mContainerView = containerView; + mThumbs = thumbs; + expandedImageView = expandedView; + mAnimationDurationTime = animationDuration; + mListener = listener; + + mThumbs[0].setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + zoomImageFromThumb(0); + } + }); + + mThumbs[1].setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + zoomImageFromThumb(1); + } + }); + + mThumbs[2].setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + zoomImageFromThumb(2); + } + }); + } + + private void zoomImageFromThumb(final int index) { + int imageResId = mLargeDrawables[index]; + final ImageView thumbView = mThumbs[index]; + if (mCurrentAnimator != null) { + return; + } + + expandedImageView.setImageResource(imageResId); + + final Rect startBounds = new Rect(); + final Rect finalBounds = new Rect(); + final Point globalOffset = new Point(); + thumbView.getGlobalVisibleRect(startBounds); + mContainerView.getGlobalVisibleRect(finalBounds, globalOffset); + startBounds.offset(-globalOffset.x, -globalOffset.y); + finalBounds.offset(-globalOffset.x, -globalOffset.y); + float startScale; + if ((float) finalBounds.width() / finalBounds.height() + > (float) startBounds.width() / startBounds.height()) { + startScale = (float) startBounds.height() / finalBounds.height(); + float startWidth = startScale * finalBounds.width(); + float deltaWidth = (startWidth - startBounds.width()) / 2; + startBounds.left -= deltaWidth; + startBounds.right += deltaWidth; + } else { + startScale = (float) startBounds.width() / finalBounds.width(); + float startHeight = startScale * finalBounds.height(); + float deltaHeight = (startHeight - startBounds.height()) / 2; + startBounds.top -= deltaHeight; + startBounds.bottom += deltaHeight; + } + + for(int k=0; k < 3; k++) { + mThumbs[k].setAlpha(0f); + } + expandedImageView.setVisibility(View.VISIBLE); + + expandedImageView.setPivotX(0f); + expandedImageView.setPivotY(0f); + + AnimatorSet zommInAnimator = new AnimatorSet(); + zommInAnimator.play(ObjectAnimator + .ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left)).with( + ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds + .top)).with( + ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f)); + zommInAnimator.setDuration(mAnimationDurationTime); + zommInAnimator.setInterpolator(new DecelerateInterpolator()); + zommInAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCurrentAnimator = null; + if (mListener != null) { + mState = UIState.getUIState(index); + mListener.onUIStateChanged(mState); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCurrentAnimator = null; + } + }); + zommInAnimator.start(); + mCurrentAnimator = zommInAnimator; + + final float startScaleFinal = startScale; + expandedImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mCurrentAnimator != null) { + return; + } + AnimatorSet zoomOutAnimator = new AnimatorSet(); + zoomOutAnimator.play(ObjectAnimator + .ofFloat(expandedImageView, View.X, startBounds.left)) + .with(ObjectAnimator + .ofFloat(expandedImageView, + View.Y, startBounds.top)) + .with(ObjectAnimator + .ofFloat(expandedImageView, + View.SCALE_X, startScaleFinal)) + .with(ObjectAnimator + .ofFloat(expandedImageView, + View.SCALE_Y, startScaleFinal)); + zoomOutAnimator.setDuration(mAnimationDurationTime); + zoomOutAnimator.setInterpolator(new DecelerateInterpolator()); + zoomOutAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + for (int k = 0; k < 3; k++) { + mThumbs[k].setAlpha(1f); + } + expandedImageView.setVisibility(View.GONE); + mCurrentAnimator = null; + if (mListener != null) { + mState = UIState.HOME; + mListener.onUIStateChanged(mState); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + thumbView.setAlpha(1f); + expandedImageView.setVisibility(View.GONE); + mCurrentAnimator = null; + } + }); + zoomOutAnimator.start(); + mCurrentAnimator = zoomOutAnimator; + } + }); + } + + public enum UIState { + MIC_UP(0), SOUND_UP(1), MUSIC_UP(2), HOME(3); + private int mState; + + UIState(int state) { + mState = state; + } + + static UIState getUIState(int state) { + for(UIState uiState : values()) { + if (uiState.mState == state) { + return uiState; + } + } + return null; + } + } + + public interface UIStateListener { + void onUIStateChanged(UIState state); + } + + public void transitionToHome() { + if (mState == UIState.HOME) { + return; + } + expandedImageView.callOnClick(); + + } +} diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/circle.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/circle.xml new file mode 100644 index 00000000..df4abe52 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/circle.xml @@ -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>
\ No newline at end of file diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_audiotrack_120dp.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_audiotrack_120dp.xml new file mode 100644 index 00000000..0971d96d --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_audiotrack_120dp.xml @@ -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> diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_audiotrack_32dp.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_audiotrack_32dp.xml new file mode 100644 index 00000000..70de799c --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_audiotrack_32dp.xml @@ -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> diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_mic_120dp.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_mic_120dp.xml new file mode 100644 index 00000000..15e798a2 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_mic_120dp.xml @@ -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> diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_mic_32dp.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_mic_32dp.xml new file mode 100644 index 00000000..c9417dd2 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_mic_32dp.xml @@ -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> diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_play_arrow_120dp.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_play_arrow_120dp.xml new file mode 100644 index 00000000..e87660d2 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_play_arrow_120dp.xml @@ -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> diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_play_arrow_32dp.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_play_arrow_32dp.xml new file mode 100644 index 00000000..9dd86787 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/drawable/ic_play_arrow_32dp.xml @@ -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> diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/layout/main_activity.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/layout/main_activity.xml new file mode 100644 index 00000000..7e004ad4 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/layout/main_activity.xml @@ -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>
\ No newline at end of file diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-hdpi/ic_launcher.png b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..cde69bcc --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-mdpi/ic_launcher.png b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..c133a0cb --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-xhdpi/ic_launcher.png b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..bfa42f0e --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..324e72cd --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/raw/sound.mp3 b/wearable/wear/WearSpeakerSample/wear/src/main/res/raw/sound.mp3 Binary files differnew file mode 100644 index 00000000..94e3d0e5 --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/raw/sound.mp3 diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/values/colors.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/values/colors.xml new file mode 100644 index 00000000..e9b8605f --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/values/colors.xml @@ -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>
\ No newline at end of file diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/values/strings.xml b/wearable/wear/WearSpeakerSample/wear/src/main/res/values/strings.xml new file mode 100644 index 00000000..cc342b5c --- /dev/null +++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/values/strings.xml @@ -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> diff --git a/wearable/wear/XYZTouristAttractions/template-params.xml b/wearable/wear/XYZTouristAttractions/template-params.xml index 473b1110..69f2f779 100644 --- a/wearable/wear/XYZTouristAttractions/template-params.xml +++ b/wearable/wear/XYZTouristAttractions/template-params.xml @@ -20,6 +20,7 @@ <package>com.example.android.xyztouristattractions</package> <minSdk>18</minSdk> <targetSdkVersion>23</targetSdkVersion> + <targetSdkVersionWear>22</targetSdkVersionWear> <wearable> <has_handheld_app>true</has_handheld_app> |