Merge branch 'mnc-mr-docs' into mnc-ub-dev
Large merge to reconnect automerger for docs branch to mainline.
Conflicts:
media/Camera2Basic/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java
media/Camera2Basic/README.md
media/Camera2Video/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java
media/Camera2Video/README.md
media/MidiSynth/template-params.xml
Issue: 28000173
Change-Id: I1fe910677f9505caeca1a6bbc7ca508c414ec839
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 8b0620f..361c4ac 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,7 +22,10 @@
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;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.Editable;
@@ -33,23 +36,28 @@
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
+import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.Switch;
+import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* This fragment provides UI and functionality to set restrictions on the AppRestrictionSchema
* sample.
*/
public class AppRestrictionEnforcerFragment extends Fragment implements
- CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
+ CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener,
+ View.OnClickListener, ItemAddFragment.OnItemAddedListener {
/**
* Key for {@link SharedPreferences}
@@ -81,7 +89,24 @@
*/
private static final String RESTRICTION_KEY_APPROVALS = "approvals";
+ /**
+ * Key for the bundle restriction in AppRestrictionSchema.
+ */
+ private static final String RESTRICTION_KEY_PROFILE = "profile";
+ private static final String RESTRICTION_KEY_PROFILE_NAME = "name";
+ private static final String RESTRICTION_KEY_PROFILE_AGE = "age";
+
+ /**
+ * Key for the bundle array restriction in AppRestrictionSchema.
+ */
+ private static final String RESTRICTION_KEY_ITEMS = "items";
+ private static final String RESTRICTION_KEY_ITEM_KEY = "key";
+ private static final String RESTRICTION_KEY_ITEM_VALUE = "value";
+
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.
@@ -94,6 +119,9 @@
private EditText mEditNumber;
private Spinner mSpinnerRank;
private LinearLayout mLayoutApprovals;
+ private EditText mEditProfileName;
+ private EditText mEditProfileAge;
+ private LinearLayout mLayoutItems;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -109,6 +137,19 @@
mEditNumber = (EditText) view.findViewById(R.id.number);
mSpinnerRank = (Spinner) view.findViewById(R.id.rank);
mLayoutApprovals = (LinearLayout) view.findViewById(R.id.approvals);
+ mEditProfileName = (EditText) view.findViewById(R.id.profile_name);
+ 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
@@ -156,6 +197,21 @@
}
};
+ private TextWatcher mWatcherProfile = new EasyTextWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ try {
+ String name = mEditProfileName.getText().toString();
+ String ageString = mEditProfileAge.getText().toString();
+ if (!TextUtils.isEmpty(ageString)) {
+ saveProfile(getActivity(), name, Integer.parseInt(ageString));
+ }
+ } catch (NumberFormatException e) {
+ Toast.makeText(getActivity(), "Not an integer!", Toast.LENGTH_SHORT).show();
+ }
+ }
+ };
+
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
switch (parent.getId()) {
@@ -171,9 +227,42 @@
// Nothing to do
}
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.item_add:
+ new ItemAddFragment().show(getChildFragmentManager(), "dialog");
+ break;
+ case R.id.item_remove:
+ String key = (String) v.getTag();
+ removeItem(key);
+ mLayoutItems.removeView((View) v.getParent());
+ break;
+ }
+ }
+
+ @Override
+ public void onItemAdded(String key, String value) {
+ key = TextUtils.replace(key,
+ new String[]{DELIMETER, SEPARATOR}, new String[]{"", ""}).toString();
+ value = TextUtils.replace(value,
+ new String[]{DELIMETER, SEPARATOR}, new String[]{"", ""}).toString();
+ Parcelable[] parcelables = mCurrentRestrictions.getParcelableArray(RESTRICTION_KEY_ITEMS);
+ Map<String, String> items = new HashMap<>();
+ if (parcelables != null) {
+ for (Parcelable parcelable : parcelables) {
+ Bundle bundle = (Bundle) parcelable;
+ items.put(bundle.getString(RESTRICTION_KEY_ITEM_KEY),
+ bundle.getString(RESTRICTION_KEY_ITEM_VALUE));
+ }
+ }
+ items.put(key, value);
+ insertItemRow(LayoutInflater.from(getActivity()), key, value);
+ saveItems(getActivity(), items);
+ }
+
/**
- * Loads the restrictions for the AppRestrictionSchema sample. In this implementation, we just
- * read the default value for the "can_say_hello" restriction.
+ * Loads the restrictions for the AppRestrictionSchema sample.
*
* @param activity The activity
*/
@@ -203,6 +292,28 @@
TextUtils.join(DELIMETER,
restriction.getAllSelectedStrings())),
DELIMETER));
+ } else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_PROFILE.equals(key)) {
+ String name = null;
+ int age = 0;
+ for (RestrictionEntry entry : restriction.getRestrictions()) {
+ String profileKey = entry.getKey();
+ if (RESTRICTION_KEY_PROFILE_NAME.equals(profileKey)) {
+ name = entry.getSelectedString();
+ } else if (RESTRICTION_KEY_PROFILE_AGE.equals(profileKey)) {
+ age = entry.getIntValue();
+ }
+ }
+ name = prefs.getString(RESTRICTION_KEY_PROFILE_NAME, name);
+ age = prefs.getInt(RESTRICTION_KEY_PROFILE_AGE, age);
+ updateProfile(name, age);
+ } 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)) {
+ String[] strings = itemString.split(SEPARATOR, 2);
+ items.put(strings[0], strings[1]);
+ }
+ updateItems(activity, items);
}
}
}
@@ -251,6 +362,72 @@
}
}
+ 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);
+ mCurrentRestrictions.putBundle(RESTRICTION_KEY_PROFILE, profile);
+ mEditProfileName.removeTextChangedListener(mWatcherProfile);
+ mEditProfileName.setText(name);
+ mEditProfileName.addTextChangedListener(mWatcherProfile);
+ mEditProfileAge.removeTextChangedListener(mWatcherProfile);
+ mEditProfileAge.setText(String.valueOf(age));
+ mEditProfileAge.addTextChangedListener((mWatcherProfile));
+ }
+
+ 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();
+ for (String key : items.keySet()) {
+ insertItemRow(inflater, key, items.get(key));
+ }
+ }
+
+ private void insertItemRow(LayoutInflater inflater, String key, String value) {
+ View view = inflater.inflate(R.layout.item, mLayoutItems, false);
+ TextView textView = (TextView) view.findViewById(R.id.item_text);
+ textView.setText(getString(R.string.item, key, value));
+ Button remove = (Button) view.findViewById(R.id.item_remove);
+ remove.setTag(key);
+ remove.setOnClickListener(this);
+ mLayoutItems.addView(view);
+ }
+
+ @NonNull
+ private Bundle[] convertToBundles(Map<String, String> items) {
+ Bundle[] bundles = new Bundle[items.size()];
+ int i = 0;
+ for (String key : items.keySet()) {
+ Bundle bundle = new Bundle();
+ bundle.putString(RESTRICTION_KEY_ITEM_KEY, key);
+ bundle.putString(RESTRICTION_KEY_ITEM_VALUE, items.get(key));
+ bundles[i++] = bundle;
+ }
+ return bundles;
+ }
+
+ private void removeItem(String key) {
+ Parcelable[] parcelables = mCurrentRestrictions.getParcelableArray(RESTRICTION_KEY_ITEMS);
+ if (parcelables != null) {
+ Map<String, String> items = new HashMap<>();
+ for (Parcelable parcelable : parcelables) {
+ Bundle bundle = (Bundle) parcelable;
+ if (!key.equals(bundle.getString(RESTRICTION_KEY_ITEM_KEY))) {
+ items.put(bundle.getString(RESTRICTION_KEY_ITEM_KEY),
+ bundle.getString(RESTRICTION_KEY_ITEM_VALUE));
+ }
+ }
+ saveItems(getActivity(), items);
+ }
+ }
+
/**
* Saves the value for the "cay_say_hello" restriction of AppRestrictionSchema.
*
@@ -333,6 +510,57 @@
TextUtils.join(DELIMETER, approvals)).apply();
}
+ /**
+ * Saves the value for the "profile" restriction of AppRestrictionSchema.
+ *
+ * @param activity The activity
+ * @param name The value to be set for the "name" field.
+ * @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);
+ mCurrentRestrictions.putBundle(RESTRICTION_KEY_PROFILE, profile);
+ saveRestrictions(activity);
+ editPreferences(activity).putString(RESTRICTION_KEY_PROFILE_NAME, name).apply();
+ }
+
+ /**
+ * Saves the value for the "items" restriction of AppRestrictionSchema.
+ *
+ * @param activity The activity.
+ * @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();
+ boolean first = true;
+ for (String key : items.keySet()) {
+ if (first) {
+ first = false;
+ } else {
+ builder.append(DELIMETER);
+ }
+ builder.append(key);
+ builder.append(SEPARATOR);
+ builder.append(items.get(key));
+ }
+ editPreferences(activity).putString(RESTRICTION_KEY_ITEMS, builder.toString()).apply();
+ }
+
+ /**
+ * Saves all the restrictions.
+ *
+ * @param activity The activity.
+ */
private void saveRestrictions(Activity activity) {
DevicePolicyManager devicePolicyManager
= (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
diff --git a/admin/AppRestrictionEnforcer/Application/src/main/java/com/example/android/apprestrictionenforcer/ItemAddFragment.java b/admin/AppRestrictionEnforcer/Application/src/main/java/com/example/android/apprestrictionenforcer/ItemAddFragment.java
new file mode 100644
index 0000000..cda2726
--- /dev/null
+++ b/admin/AppRestrictionEnforcer/Application/src/main/java/com/example/android/apprestrictionenforcer/ItemAddFragment.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 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.apprestrictionenforcer;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.Toast;
+
+/**
+ * Provides a dialog to create a new restriction item for the sample bundle array.
+ */
+public class ItemAddFragment extends DialogFragment implements View.OnClickListener {
+
+ public interface OnItemAddedListener {
+ void onItemAdded(String key, String value);
+ }
+
+ private OnItemAddedListener mListener;
+ private EditText mEditKey;
+ private EditText mEditValue;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ Fragment parentFragment = getParentFragment();
+ mListener = (OnItemAddedListener) (parentFragment == null ? activity : parentFragment);
+ }
+
+ @Override
+ public void onDetach() {
+ mListener = null;
+ super.onDetach();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getDialog().setTitle(R.string.add_item);
+ return inflater.inflate(R.layout.fragment_item_add, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ mEditKey = (EditText) view.findViewById(R.id.key);
+ mEditValue = (EditText) view.findViewById(R.id.value);
+ view.findViewById(R.id.ok).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.ok:
+ if (addItem()) {
+ dismiss();
+ }
+ break;
+ }
+ }
+
+ private boolean addItem() {
+ String key = mEditKey.getText().toString();
+ if (TextUtils.isEmpty(key)) {
+ Toast.makeText(getActivity(), "Input the key.", Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ String value = mEditValue.getText().toString();
+ if (TextUtils.isEmpty(value)) {
+ Toast.makeText(getActivity(), "Input the value.", Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ if (mListener != null) {
+ mListener.onItemAdded(key, value);
+ }
+ return true;
+ }
+
+}
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 0118191..b683989 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
@@ -119,6 +119,67 @@
</LinearLayout>
+ <LinearLayout
+ android:id="@+id/bundle_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/profile"/>
+
+ <EditText
+ android:id="@+id/profile_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="0.75"
+ android:hint="@string/name"/>
+
+ <EditText
+ android:id="@+id/profile_age"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="0.25"
+ android:hint="@string/age"
+ android:inputType="number"/>
+
+ </LinearLayout>
+
+ <RelativeLayout
+ android:id="@+id/bundle_array_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/items_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:text="@string/items"/>
+
+ <LinearLayout
+ android:id="@+id/items"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true"
+ android:layout_toEndOf="@id/items_label"
+ android:orientation="vertical"
+ android:paddingEnd="16dp"
+ android:paddingStart="16dp"/>
+
+ <Button
+ android:id="@+id/item_add"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/items"
+ android:layout_toEndOf="@id/items_label"
+ android:text="@string/add"/>
+
+ </RelativeLayout>
+
</LinearLayout>
</ScrollView>
diff --git a/admin/AppRestrictionEnforcer/Application/src/main/res/layout/fragment_item_add.xml b/admin/AppRestrictionEnforcer/Application/src/main/res/layout/fragment_item_add.xml
new file mode 100644
index 0000000..f60bb15
--- /dev/null
+++ b/admin/AppRestrictionEnforcer/Application/src/main/res/layout/fragment_item_add.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <EditText
+ android:id="@+id/key"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="16dp"
+ android:hint="@string/key"/>
+
+ <EditText
+ android:id="@+id/value"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_marginStart="16dp"
+ android:hint="@string/value"/>
+
+ <Button
+ android:id="@+id/ok"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@android:string/ok"/>
+
+</LinearLayout>
diff --git a/admin/AppRestrictionEnforcer/Application/src/main/res/layout/item.xml b/admin/AppRestrictionEnforcer/Application/src/main/res/layout/item.xml
new file mode 100644
index 0000000..66e6b3d
--- /dev/null
+++ b/admin/AppRestrictionEnforcer/Application/src/main/res/layout/item.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/item_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ tools:text="key:value"/>
+
+ <Button
+ android:id="@+id/item_remove"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/remove"/>
+
+</LinearLayout>
diff --git a/admin/AppRestrictionEnforcer/Application/src/main/res/values/strings.xml b/admin/AppRestrictionEnforcer/Application/src/main/res/values/strings.xml
index e35daee..ead4152 100644
--- a/admin/AppRestrictionEnforcer/Application/src/main/res/values/strings.xml
+++ b/admin/AppRestrictionEnforcer/Application/src/main/res/values/strings.xml
@@ -29,4 +29,14 @@
<string name="number">Number: </string>
<string name="rank">Rank: </string>
<string name="approvals">Approvals: </string>
+ <string name="profile">Profile: </string>
+ <string name="name">Name</string>
+ <string name="age">Age</string>
+ <string name="items">Items: </string>
+ <string name="add">Add</string>
+ <string name="key">Key</string>
+ <string name="value">Value</string>
+ <string name="remove">Remove</string>
+ <string name="item">%1$s: %2$s</string>
+ <string name="add_item">Add a new item</string>
</resources>
diff --git a/admin/AppRestrictionEnforcer/gradle/wrapper/gradle-wrapper.properties b/admin/AppRestrictionEnforcer/gradle/wrapper/gradle-wrapper.properties
index f2e517b..377495f 100644
--- a/admin/AppRestrictionEnforcer/gradle/wrapper/gradle-wrapper.properties
+++ b/admin/AppRestrictionEnforcer/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/admin/AppRestrictionEnforcer/template-params.xml b/admin/AppRestrictionEnforcer/template-params.xml
index ff1a7a0..eee3d7e 100644
--- a/admin/AppRestrictionEnforcer/template-params.xml
+++ b/admin/AppRestrictionEnforcer/template-params.xml
@@ -23,7 +23,6 @@
<package>com.example.android.apprestrictionenforcer</package>
<minSdk>21</minSdk>
- <compileSdkVersion>21</compileSdkVersion>
<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 7b8dba8..bbb1ef8 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,7 +19,9 @@
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;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
@@ -49,6 +51,14 @@
private static final String KEY_NUMBER = "number";
private static final String KEY_RANK = "rank";
private static final String KEY_APPROVALS = "approvals";
+ private static final String KEY_PROFILE = "profile";
+ private static final String KEY_PROFILE_NAME = "name";
+ private static final String KEY_PROFILE_AGE = "age";
+ private static final String KEY_ITEMS = "items";
+ 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;
@@ -59,6 +69,8 @@
private TextView mTextNumber;
private TextView mTextRank;
private TextView mTextApprovals;
+ private TextView mTextProfile;
+ private TextView mTextItems;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -73,7 +85,22 @@
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
@@ -86,7 +113,8 @@
RestrictionsManager manager =
(RestrictionsManager) getActivity().getSystemService(Context.RESTRICTIONS_SERVICE);
Bundle restrictions = manager.getApplicationRestrictions();
- List<RestrictionEntry> entries = manager.getManifestRestrictions(getActivity().getApplicationContext().getPackageName());
+ List<RestrictionEntry> entries = manager.getManifestRestrictions(
+ getActivity().getApplicationContext().getPackageName());
for (RestrictionEntry entry : entries) {
String key = entry.getKey();
Log.d(TAG, "key: " + key);
@@ -100,6 +128,10 @@
updateRank(entry, restrictions);
} else if (key.equals(KEY_APPROVALS)) {
updateApprovals(entry, restrictions);
+ } else if (key.equals(KEY_PROFILE)) {
+ updateProfile(entry, restrictions);
+ } else if (key.equals(KEY_ITEMS)) {
+ updateItems(entry, restrictions);
}
}
}
@@ -161,6 +193,67 @@
mTextApprovals.setText(getString(R.string.approvals_you_have, text));
}
+ private void updateProfile(RestrictionEntry entry, Bundle restrictions) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
+ String name = null;
+ int age = 0;
+ if (restrictions == null || !restrictions.containsKey(KEY_PROFILE)) {
+ RestrictionEntry[] entries = entry.getRestrictions();
+ for (RestrictionEntry profileEntry : entries) {
+ String key = profileEntry.getKey();
+ if (key.equals(KEY_PROFILE_NAME)) {
+ name = profileEntry.getSelectedString();
+ } else if (key.equals(KEY_PROFILE_AGE)) {
+ age = profileEntry.getIntValue();
+ }
+ }
+ } else {
+ Bundle profile = restrictions.getBundle(KEY_PROFILE);
+ if (profile != null) {
+ name = profile.getString(KEY_PROFILE_NAME);
+ age = profile.getInt(KEY_PROFILE_AGE);
+ }
+ }
+ mTextProfile.setText(getString(R.string.your_profile, name, age));
+ }
+
+ private void updateItems(RestrictionEntry entry, Bundle restrictions) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
+ StringBuilder builder = new StringBuilder();
+ if (restrictions != null) {
+ Parcelable[] parcelables = restrictions.getParcelableArray(KEY_ITEMS);
+ if (parcelables != null && parcelables.length > 0) {
+ Bundle[] items = new Bundle[parcelables.length];
+ for (int i = 0; i < parcelables.length; i++) {
+ items[i] = (Bundle) parcelables[i];
+ }
+ boolean first = true;
+ for (Bundle item : items) {
+ if (!item.containsKey(KEY_ITEM_KEY) || !item.containsKey(KEY_ITEM_VALUE)) {
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ builder.append(", ");
+ }
+ builder.append(item.getString(KEY_ITEM_KEY));
+ builder.append(":");
+ builder.append(item.getString(KEY_ITEM_VALUE));
+ }
+ } else {
+ builder.append(getString(R.string.none));
+ }
+ } else {
+ builder.append(getString(R.string.none));
+ }
+ mTextItems.setText(getString(R.string.your_items, builder));
+ }
+
@Override
public void onClick(View view) {
switch (view.getId()) {
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 18ca0a4..02d83e6 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 @@
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"
@@ -68,6 +68,24 @@
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="@string/approvals_you_have"/>
+ <include layout="@layout/separator"/>
+
+ <TextView
+ android:id="@+id/your_profile"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ tools:text="@string/your_profile"/>
+
+ <include layout="@layout/separator" android:id="@+id/bundle_array_separator" />
+
+ <TextView
+ android:id="@+id/your_items"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ tools:text="@string/your_items"/>
+
</LinearLayout>
</ScrollView>
diff --git a/admin/AppRestrictionSchema/Application/src/main/res/values/restriction_values.xml b/admin/AppRestrictionSchema/Application/src/main/res/values/restriction_values.xml
index 558d097..53be746 100644
--- a/admin/AppRestrictionSchema/Application/src/main/res/values/restriction_values.xml
+++ b/admin/AppRestrictionSchema/Application/src/main/res/values/restriction_values.xml
@@ -74,4 +74,23 @@
<string name="description_secret_code">This restriction is hidden and will not be shown to the administrator.</string>
<string name="default_secret_code">(Hidden restriction must have some default value)</string>
+ <!-- Bundle restriction -->
+ <string name="description_profile">Sample profile</string>
+ <string name="title_profile">Profile</string>
+
+ <string name="default_profile_name">John</string>
+ <string name="description_profile_name">The name of this person</string>
+ <string name="title_profile_name">Name</string>
+
+ <integer name="default_profile_age">25</integer>
+ <string name="description_profile_age">The age of this person</string>
+ <string name="title_profile_age">Age</string>
+
+ <!-- Bundle array restriction -->
+ <string name="description_items">Sample items</string>
+ <string name="title_items">Items</string>
+ <string name="title_item">Item</string>
+ <string name="title_key">Key</string>
+ <string name="title_value">Value</string>
+
</resources>
diff --git a/admin/AppRestrictionSchema/Application/src/main/res/values/strings.xml b/admin/AppRestrictionSchema/Application/src/main/res/values/strings.xml
index 6dce123..1ec68d5 100644
--- a/admin/AppRestrictionSchema/Application/src/main/res/values/strings.xml
+++ b/admin/AppRestrictionSchema/Application/src/main/res/values/strings.xml
@@ -25,5 +25,7 @@
<string name="your_rank">Your rank: %s</string>
<string name="approvals_you_have">Approvals you have: %s</string>
<string name="none">none</string>
+ <string name="your_profile">Your profile: %1$s (%2$d)</string>
+ <string name="your_items">Your items: %s</string>
</resources>
diff --git a/admin/AppRestrictionSchema/Application/src/main/res/xml/app_restrictions.xml b/admin/AppRestrictionSchema/Application/src/main/res/xml/app_restrictions.xml
index 9e47f45..1e2ea45 100644
--- a/admin/AppRestrictionSchema/Application/src/main/res/xml/app_restrictions.xml
+++ b/admin/AppRestrictionSchema/Application/src/main/res/xml/app_restrictions.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,6 +20,7 @@
https://developer.android.com/reference/android/content/RestrictionsManager.html
-->
+ <!-- Boolean restriction -->
<restriction
android:defaultValue="@bool/default_can_say_hello"
android:description="@string/description_can_say_hello"
@@ -28,6 +28,7 @@
android:restrictionType="bool"
android:title="@string/title_can_say_hello"/>
+ <!-- String restriction -->
<restriction
android:defaultValue="@string/default_message"
android:description="@string/description_message"
@@ -35,6 +36,7 @@
android:restrictionType="string"
android:title="@string/title_message"/>
+ <!-- Integer restriction -->
<restriction
android:defaultValue="@integer/default_number"
android:description="@string/description_number"
@@ -42,6 +44,7 @@
android:restrictionType="integer"
android:title="@string/title_number"/>
+ <!-- Choice restriction -->
<restriction
android:defaultValue="@string/default_rank"
android:description="@string/description_rank"
@@ -51,6 +54,7 @@
android:restrictionType="choice"
android:title="@string/title_rank"/>
+ <!-- Multi-select restriction -->
<restriction
android:defaultValue="@array/default_approvals"
android:description="@string/description_approvals"
@@ -60,6 +64,7 @@
android:restrictionType="multi-select"
android:title="@string/title_approvals"/>
+ <!-- Hidden restriction -->
<restriction
android:defaultValue="@string/default_secret_code"
android:description="@string/description_secret_code"
@@ -67,4 +72,46 @@
android:restrictionType="hidden"
android:title="@string/title_secret_code"/>
+ <!-- Bundle restriction; useful for grouping restrictions -->
+ <restriction
+ android:description="@string/description_profile"
+ android:key="profile"
+ android:restrictionType="bundle"
+ android:title="@string/title_profile">
+ <restriction
+ android:defaultValue="@string/default_profile_name"
+ android:description="@string/description_profile_name"
+ android:key="name"
+ android:restrictionType="string"
+ android:title="@string/title_profile_name"/>
+ <restriction
+ android:defaultValue="@integer/default_profile_age"
+ android:description="@string/description_profile_age"
+ android:key="age"
+ android:restrictionType="integer"
+ android:title="@string/title_profile_age"/>
+ </restriction>
+
+ <!-- Bundle array restriction -->
+ <restriction
+ android:description="@string/description_items"
+ android:key="items"
+ android:restrictionType="bundle_array"
+ android:title="@string/title_items">
+ <!-- Bundle array must have one bundle restriction -->
+ <restriction
+ android:key="item"
+ android:restrictionType="bundle"
+ android:title="@string/title_item">
+ <restriction
+ android:key="key"
+ android:restrictionType="string"
+ android:title="@string/title_key"/>
+ <restriction
+ android:key="value"
+ android:restrictionType="string"
+ android:title="@string/title_value"/>
+ </restriction>
+ </restriction>
+
</restrictions>
diff --git a/admin/AppRestrictionSchema/gradle/wrapper/gradle-wrapper.properties b/admin/AppRestrictionSchema/gradle/wrapper/gradle-wrapper.properties
index fb79885..c5454eb 100644
--- a/admin/AppRestrictionSchema/gradle/wrapper/gradle-wrapper.properties
+++ b/admin/AppRestrictionSchema/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/admin/AppRestrictionSchema/template-params.xml b/admin/AppRestrictionSchema/template-params.xml
index 6603034..508393e 100644
--- a/admin/AppRestrictionSchema/template-params.xml
+++ b/admin/AppRestrictionSchema/template-params.xml
@@ -21,7 +21,6 @@
<package>com.example.android.apprestrictionschema</package>
<minSdk>21</minSdk>
- <compileSdkVersion>21</compileSdkVersion>
<strings>
<intro>
diff --git a/admin/BasicManagedProfile/gradle/wrapper/gradle-wrapper.properties b/admin/BasicManagedProfile/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/admin/BasicManagedProfile/gradle/wrapper/gradle-wrapper.properties
+++ b/admin/BasicManagedProfile/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/admin/DeviceOwner/gradle/wrapper/gradle-wrapper.properties b/admin/DeviceOwner/gradle/wrapper/gradle-wrapper.properties
index 0973c31..22997e5 100644
--- a/admin/DeviceOwner/gradle/wrapper/gradle-wrapper.properties
+++ b/admin/DeviceOwner/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/admin/NfcProvisioning/gradle/wrapper/gradle-wrapper.properties b/admin/NfcProvisioning/gradle/wrapper/gradle-wrapper.properties
index 23e8e21..862673d 100644
--- a/admin/NfcProvisioning/gradle/wrapper/gradle-wrapper.properties
+++ b/admin/NfcProvisioning/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/background/JobScheduler/gradle/wrapper/gradle-wrapper.properties b/background/JobScheduler/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/background/JobScheduler/gradle/wrapper/gradle-wrapper.properties
+++ b/background/JobScheduler/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/background/alarms/RepeatingAlarm/gradle/wrapper/gradle-wrapper.properties b/background/alarms/RepeatingAlarm/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/background/alarms/RepeatingAlarm/gradle/wrapper/gradle-wrapper.properties
+++ b/background/alarms/RepeatingAlarm/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/build.gradle b/build.gradle
index 05ed3ea..1e3cf8a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -103,9 +103,15 @@
"security/FingerprintDialog",
"system/RuntimePermissions",
"system/RuntimePermissionsBasic",
+"wearable/wear/RuntimePermissionsWear",
"notification/ActiveNotifications",
"media/Camera2Raw",
"content/AutoBackupForApps",
+"content/DirectShare",
+"media/MidiScope",
+"media/MidiSynth",
+"security/AsymmetricFingerprintDialog",
+"wearable/wear/WearSpeakerSample",
]
List<String> taskNames = [
diff --git a/common/src/java/com/example/android/common/media/CameraHelper.java b/common/src/java/com/example/android/common/media/CameraHelper.java
index 1fa8416..b578767 100644
--- a/common/src/java/com/example/android/common/media/CameraHelper.java
+++ b/common/src/java/com/example/android/common/media/CameraHelper.java
@@ -36,49 +36,57 @@
public static final int MEDIA_TYPE_VIDEO = 2;
/**
- * Iterate over supported camera preview sizes to see which one best fits the
+ * Iterate over supported camera video sizes to see which one best fits the
* dimensions of the given view while maintaining the aspect ratio. If none can,
* be lenient with the aspect ratio.
*
- * @param sizes Supported camera preview sizes.
- * @param w The width of the view.
- * @param h The height of the view.
- * @return Best match camera preview size to fit in the view.
+ * @param supportedVideoSizes Supported camera video sizes.
+ * @param previewSizes Supported camera preview sizes.
+ * @param w The width of the view.
+ * @param h The height of the view.
+ * @return Best match camera video size to fit in the view.
*/
- public static Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
+ public static Camera.Size getOptimalVideoSize(List<Camera.Size> supportedVideoSizes,
+ List<Camera.Size> previewSizes, int w, int h) {
// Use a very small tolerance because we want an exact match.
final double ASPECT_TOLERANCE = 0.1;
double targetRatio = (double) w / h;
- if (sizes == null)
- return null;
+ // Supported video sizes list might be null, it means that we are allowed to use the preview
+ // sizes
+ List<Camera.Size> videoSizes;
+ if (supportedVideoSizes != null) {
+ videoSizes = supportedVideoSizes;
+ } else {
+ videoSizes = previewSizes;
+ }
Camera.Size optimalSize = null;
- // Start with max value and refine as we iterate over available preview sizes. This is the
+ // Start with max value and refine as we iterate over available video sizes. This is the
// minimum difference between view and camera height.
double minDiff = Double.MAX_VALUE;
// Target view height
int targetHeight = h;
- // Try to find a preview size that matches aspect ratio and the target view size.
+ // Try to find a video size that matches aspect ratio and the target view size.
// Iterate over all available sizes and pick the largest size that can fit in the view and
// still maintain the aspect ratio.
- for (Camera.Size size : sizes) {
+ for (Camera.Size size : videoSizes) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
continue;
- if (Math.abs(size.height - targetHeight) < minDiff) {
+ if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
- // Cannot find preview size that matches the aspect ratio, ignore the requirement
+ // Cannot find video size that matches the aspect ratio, ignore the requirement
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
- for (Camera.Size size : sizes) {
- if (Math.abs(size.height - targetHeight) < minDiff) {
+ for (Camera.Size size : videoSizes) {
+ if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
diff --git a/connectivity/bluetooth/BluetoothAdvertisements/gradle/wrapper/gradle-wrapper.properties b/connectivity/bluetooth/BluetoothAdvertisements/gradle/wrapper/gradle-wrapper.properties
index 8712eac..25c3a92 100644
--- a/connectivity/bluetooth/BluetoothAdvertisements/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/bluetooth/BluetoothAdvertisements/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/connectivity/bluetooth/BluetoothChat/Application/src/main/java/com/example/android/bluetoothchat/BluetoothChatService.java b/connectivity/bluetooth/BluetoothChat/Application/src/main/java/com/example/android/bluetoothchat/BluetoothChatService.java
index b88b160..a1e7cc0 100644
--- a/connectivity/bluetooth/BluetoothChat/Application/src/main/java/com/example/android/bluetoothchat/BluetoothChatService.java
+++ b/connectivity/bluetooth/BluetoothChat/Application/src/main/java/com/example/android/bluetoothchat/BluetoothChatService.java
@@ -473,7 +473,7 @@
int bytes;
// Keep listening to the InputStream while connected
- while (true) {
+ while (mState == STATE_CONNECTED) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
diff --git a/connectivity/bluetooth/BluetoothChat/gradle/wrapper/gradle-wrapper.properties b/connectivity/bluetooth/BluetoothChat/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/connectivity/bluetooth/BluetoothChat/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/bluetooth/BluetoothChat/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/connectivity/bluetooth/BluetoothLeGatt/gradle/wrapper/gradle-wrapper.properties b/connectivity/bluetooth/BluetoothLeGatt/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/connectivity/bluetooth/BluetoothLeGatt/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/bluetooth/BluetoothLeGatt/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/connectivity/network/BasicNetworking/gradle/wrapper/gradle-wrapper.properties b/connectivity/network/BasicNetworking/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/connectivity/network/BasicNetworking/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/network/BasicNetworking/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/connectivity/network/NetworkConnect/gradle/wrapper/gradle-wrapper.properties b/connectivity/network/NetworkConnect/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/connectivity/network/NetworkConnect/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/network/NetworkConnect/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/connectivity/nfc/BeamLargeFiles/gradle/wrapper/gradle-wrapper.properties b/connectivity/nfc/BeamLargeFiles/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/connectivity/nfc/BeamLargeFiles/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/nfc/BeamLargeFiles/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/connectivity/nfc/CardEmulation/gradle/wrapper/gradle-wrapper.properties b/connectivity/nfc/CardEmulation/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/connectivity/nfc/CardEmulation/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/nfc/CardEmulation/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/connectivity/nfc/CardReader/gradle/wrapper/gradle-wrapper.properties b/connectivity/nfc/CardReader/gradle/wrapper/gradle-wrapper.properties
index 4819af8..34ff29a 100644
--- a/connectivity/nfc/CardReader/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/nfc/CardReader/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/connectivity/sync/BasicSyncAdapter/gradle/wrapper/gradle-wrapper.properties b/connectivity/sync/BasicSyncAdapter/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/connectivity/sync/BasicSyncAdapter/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/sync/BasicSyncAdapter/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/connectivity/wifidirect/DirectP2P/gradle/wrapper/gradle-wrapper.properties b/connectivity/wifidirect/DirectP2P/gradle/wrapper/gradle-wrapper.properties
index 1b32573..039bfb4 100644
--- a/connectivity/wifidirect/DirectP2P/gradle/wrapper/gradle-wrapper.properties
+++ b/connectivity/wifidirect/DirectP2P/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
\ No newline at end of file
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
\ No newline at end of file
diff --git a/content/AutoBackupForApps/gradle/wrapper/gradle-wrapper.properties b/content/AutoBackupForApps/gradle/wrapper/gradle-wrapper.properties
index afb3296..07fc193 100644
--- a/content/AutoBackupForApps/gradle/wrapper/gradle-wrapper.properties
+++ b/content/AutoBackupForApps/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/content/DirectShare/Application/src/main/res/layout/select_contact.xml b/content/DirectShare/Application/src/main/res/layout/select_contact.xml
new file mode 100644
index 0000000..f219805
--- /dev/null
+++ b/content/DirectShare/Application/src/main/res/layout/select_contact.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2015 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.
+-->
+<ListView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="16dp"
+ android:clipToPadding="false"
+ android:divider="@null"
+ android:paddingBottom="4dp"
+ android:paddingTop="4dp" />
diff --git a/content/DirectShare/gradle/wrapper/gradle-wrapper.properties b/content/DirectShare/gradle/wrapper/gradle-wrapper.properties
index afb3296..07fc193 100644
--- a/content/DirectShare/gradle/wrapper/gradle-wrapper.properties
+++ b/content/DirectShare/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/content/DirectShare/template-params.xml b/content/DirectShare/template-params.xml
index d1a6f20..bc69228 100644
--- a/content/DirectShare/template-params.xml
+++ b/content/DirectShare/template-params.xml
@@ -73,7 +73,7 @@
[1]: https://developer.android.com/reference/android/service/chooser/package-summary.html
[2]: https://developer.android.com/reference/android/service/chooser/ChooserTargetService.html
-[3]: https://developer.android.com/reference/android/service/chooser/ChooserTargetService.html#onGetChooserTargets(android.content.ComponentName, android.content.IntentFilter)
+[3]: https://developer.android.com/reference/android/service/chooser/ChooserTargetService.html#onGetChooserTargets(android.content.ComponentName%2C%20android.content.IntentFilter)
]]>
</intro>
</metadata>
diff --git a/content/WidgetData/gradle/wrapper/gradle-wrapper.properties b/content/WidgetData/gradle/wrapper/gradle-wrapper.properties
index fe55261..410f92a 100644
--- a/content/WidgetData/gradle/wrapper/gradle-wrapper.properties
+++ b/content/WidgetData/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/content/contacts/BasicContactables/gradle/wrapper/gradle-wrapper.properties b/content/contacts/BasicContactables/gradle/wrapper/gradle-wrapper.properties
index 542898d..07fc193 100644
--- a/content/contacts/BasicContactables/gradle/wrapper/gradle-wrapper.properties
+++ b/content/contacts/BasicContactables/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,6 @@
-
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/content/documentsUi/DirectorySelection/Application/src/main/java/com/example/android/directoryselection/DirectorySelectionFragment.java b/content/documentsUi/DirectorySelection/Application/src/main/java/com/example/android/directoryselection/DirectorySelectionFragment.java
index 4af55db..075f39b 100644
--- a/content/documentsUi/DirectorySelection/Application/src/main/java/com/example/android/directoryselection/DirectorySelectionFragment.java
+++ b/content/documentsUi/DirectorySelection/Application/src/main/java/com/example/android/directoryselection/DirectorySelectionFragment.java
@@ -27,7 +27,6 @@
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.support.v4.app.Fragment;
-import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
@@ -124,8 +123,7 @@
}
});
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_directory_entries);
- mLayoutManager = new LinearLayoutManager(getActivity());
- mRecyclerView.setLayoutManager(mLayoutManager);
+ mLayoutManager = mRecyclerView.getLayoutManager();
mRecyclerView.scrollToPosition(0);
mAdapter = new DirectoryEntryAdapter(new ArrayList<DirectoryEntry>());
mRecyclerView.setAdapter(mAdapter);
diff --git a/content/documentsUi/DirectorySelection/Application/src/main/res/layout/fragment_directory_selection.xml b/content/documentsUi/DirectorySelection/Application/src/main/res/layout/fragment_directory_selection.xml
index d63219c..631a8fc 100644
--- a/content/documentsUi/DirectorySelection/Application/src/main/res/layout/fragment_directory_selection.xml
+++ b/content/documentsUi/DirectorySelection/Application/src/main/res/layout/fragment_directory_selection.xml
@@ -16,6 +16,7 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -68,7 +69,9 @@
android:scrollbars="vertical"
android:drawSelectorOnTop="true"
android:layout_width="match_parent"
- android:layout_height="match_parent"/>
+ android:layout_height="match_parent"
+ app:layoutManager="LinearLayoutManager"
+ />
</LinearLayout>
diff --git a/content/documentsUi/DirectorySelection/gradle/wrapper/gradle-wrapper.properties b/content/documentsUi/DirectorySelection/gradle/wrapper/gradle-wrapper.properties
index 3e37868..263f57d 100644
--- a/content/documentsUi/DirectorySelection/gradle/wrapper/gradle-wrapper.properties
+++ b/content/documentsUi/DirectorySelection/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/content/documentsUi/StorageClient/gradle/wrapper/gradle-wrapper.properties b/content/documentsUi/StorageClient/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/content/documentsUi/StorageClient/gradle/wrapper/gradle-wrapper.properties
+++ b/content/documentsUi/StorageClient/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
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 d8be813..9f9249a 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 @@
* 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
similarity index 96%
rename from content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/MyCloudFragment.java
rename to content/documentsUi/StorageProvider/Application/src/main/java/com/example/android/storageprovider/StorageProviderFragment.java
index f624e90..80d0296 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 @@
* 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 9040e7b..bc343bc 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 @@
- 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/content/documentsUi/StorageProvider/gradle/wrapper/gradle-wrapper.properties b/content/documentsUi/StorageProvider/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/content/documentsUi/StorageProvider/gradle/wrapper/gradle-wrapper.properties
+++ b/content/documentsUi/StorageProvider/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/content/multiuser/AppRestrictions/gradle/wrapper/gradle-wrapper.properties b/content/multiuser/AppRestrictions/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/content/multiuser/AppRestrictions/gradle/wrapper/gradle-wrapper.properties
+++ b/content/multiuser/AppRestrictions/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/content/webview/PermissionRequest/gradle/wrapper/gradle-wrapper.properties b/content/webview/PermissionRequest/gradle/wrapper/gradle-wrapper.properties
index 4eda7c5..2b7c590 100644
--- a/content/webview/PermissionRequest/gradle/wrapper/gradle-wrapper.properties
+++ b/content/webview/PermissionRequest/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/experimental/ndkSampleGen/gradle/wrapper/gradle-wrapper.properties b/experimental/ndkSampleGen/gradle/wrapper/gradle-wrapper.properties
index b0ae9b5..f9ac955 100644
--- a/experimental/ndkSampleGen/gradle/wrapper/gradle-wrapper.properties
+++ b/experimental/ndkSampleGen/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/input/gestures/BasicGestureDetect/gradle/wrapper/gradle-wrapper.properties b/input/gestures/BasicGestureDetect/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/input/gestures/BasicGestureDetect/gradle/wrapper/gradle-wrapper.properties
+++ b/input/gestures/BasicGestureDetect/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/input/multitouch/BasicMultitouch/gradle/wrapper/gradle-wrapper.properties b/input/multitouch/BasicMultitouch/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/input/multitouch/BasicMultitouch/gradle/wrapper/gradle-wrapper.properties
+++ b/input/multitouch/BasicMultitouch/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/BasicMediaDecoder/gradle/wrapper/gradle-wrapper.properties b/media/BasicMediaDecoder/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/BasicMediaDecoder/gradle/wrapper/gradle-wrapper.properties
+++ b/media/BasicMediaDecoder/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/BasicMediaRouter/gradle/wrapper/gradle-wrapper.properties b/media/BasicMediaRouter/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/BasicMediaRouter/gradle/wrapper/gradle-wrapper.properties
+++ b/media/BasicMediaRouter/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
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 4bf8534..a29fa14 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.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;
@@ -47,6 +48,7 @@
import android.os.HandlerThread;
import android.support.annotation.NonNull;
import android.support.v13.app.FragmentCompat;
+import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
@@ -117,6 +119,16 @@
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}.
*/
@@ -259,6 +271,16 @@
private Semaphore mCameraOpenCloseLock = new Semaphore(1);
/**
+ * Whether the current camera device supports Flash or not.
+ */
+ private boolean mFlashSupported;
+
+ /**
+ * Orientation of the camera sensor
+ */
+ private int mSensorOrientation;
+
+ /**
* A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture.
*/
private CameraCaptureSession.CaptureCallback mCaptureCallback
@@ -344,31 +366,48 @@
}
/**
- * 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 +517,57 @@
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();
+ //noinspection ConstantConditions
+ mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ boolean swappedDimensions = false;
+ switch (displayRotation) {
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_180:
+ if (mSensorOrientation == 90 || mSensorOrientation == 270) {
+ swappedDimensions = true;
+ }
+ break;
+ case Surface.ROTATION_90:
+ case Surface.ROTATION_270:
+ if (mSensorOrientation == 0 || mSensorOrientation == 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;
@@ -494,6 +579,10 @@
mPreviewSize.getHeight(), mPreviewSize.getWidth());
}
+ // Check if the flash is supported.
+ Boolean available = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+ mFlashSupported = available == null ? false : available;
+
mCameraId = cameraId;
return;
}
@@ -511,7 +600,7 @@
* Opens the camera specified by {@link Camera2BasicFragment#mCameraId}.
*/
private void openCamera(int width, int height) {
- if (getActivity().checkSelfPermission(Manifest.permission.CAMERA)
+ if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestCameraPermission();
return;
@@ -617,8 +706,7 @@
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// Flash is automatically enabled when necessary.
- mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
- CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ setAutoFlash(mPreviewRequestBuilder);
// Finally, we start displaying the camera preview.
mPreviewRequest = mPreviewRequestBuilder.build();
@@ -734,12 +822,11 @@
// Use the same AE and AF modes as the preview.
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
- captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,
- CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ setAutoFlash(captureBuilder);
// Orientation
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
- captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));
+ captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
CameraCaptureSession.CaptureCallback CaptureCallback
= new CameraCaptureSession.CaptureCallback() {
@@ -762,6 +849,20 @@
}
/**
+ * Retrieves the JPEG orientation from the specified screen rotation.
+ *
+ * @param rotation The screen rotation.
+ * @return The JPEG orientation (one of 0, 90, 270, and 360)
+ */
+ private int getOrientation(int rotation) {
+ // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X)
+ // We have to take that into account and rotate JPEG properly.
+ // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS.
+ // For devices with orientation of 270, we need to rotate the JPEG 180 degrees.
+ return (ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360;
+ }
+
+ /**
* Unlock the focus. This method should be called when still image capture sequence is
* finished.
*/
@@ -770,8 +871,7 @@
// Reset the auto-focus trigger
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
- mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
- CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ setAutoFlash(mPreviewRequestBuilder);
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
mBackgroundHandler);
// After this, the camera will go back to the normal state of preview.
@@ -803,6 +903,13 @@
}
}
+ private void setAutoFlash(CaptureRequest.Builder requestBuilder) {
+ if (mFlashSupported) {
+ requestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ }
+ }
+
/**
* Saves a JPEG {@link Image} into the specified {@link File}.
*/
diff --git a/media/Camera2Basic/README.md b/media/Camera2Basic/README.md
index a77df09..311e5bb 100644
--- a/media/Camera2Basic/README.md
+++ b/media/Camera2Basic/README.md
@@ -43,7 +43,7 @@
--------------
- Android SDK v23
-- Android Build Tools v23.0.0
+- Android Build Tools v23.0.2
- Android Support Repository
Screenshots
diff --git a/media/Camera2Basic/gradle/wrapper/gradle-wrapper.properties b/media/Camera2Basic/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/Camera2Basic/gradle/wrapper/gradle-wrapper.properties
+++ b/media/Camera2Basic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
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 47cce38..bf5efe5 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.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 @@
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 @@
// 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 @@
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 @@
}
/**
- * 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/media/Camera2Raw/gradle/wrapper/gradle-wrapper.properties b/media/Camera2Raw/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/Camera2Raw/gradle/wrapper/gradle-wrapper.properties
+++ b/media/Camera2Raw/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/Camera2Video/Application/src/main/AndroidManifest.xml b/media/Camera2Video/Application/src/main/AndroidManifest.xml
index 5cb5428..67a90d0 100644
--- a/media/Camera2Video/Application/src/main/AndroidManifest.xml
+++ b/media/Camera2Video/Application/src/main/AndroidManifest.xml
@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application android:allowBackup="true"
android:label="@string/app_name"
diff --git a/media/Camera2Video/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java b/media/Camera2Video/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java
index 1ea5318..e2853b3 100644
--- a/media/Camera2Video/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java
+++ b/media/Camera2Video/Application/src/main/java/com/example/android/camera2video/Camera2VideoFragment.java
@@ -55,9 +55,9 @@
import android.widget.Button;
import android.widget.Toast;
-import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -67,7 +67,10 @@
public class Camera2VideoFragment extends Fragment
implements View.OnClickListener, FragmentCompat.OnRequestPermissionsResultCallback {
- private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
+ private static final int SENSOR_ORIENTATION_DEFAULT_DEGREES = 90;
+ private static final int SENSOR_ORIENTATION_INVERSE_DEGREES = 270;
+ private static final SparseIntArray DEFAULT_ORIENTATIONS = new SparseIntArray();
+ private static final SparseIntArray INVERSE_ORIENTATIONS = new SparseIntArray();
private static final String TAG = "Camera2VideoFragment";
private static final int REQUEST_VIDEO_PERMISSIONS = 1;
@@ -79,10 +82,17 @@
};
static {
- ORIENTATIONS.append(Surface.ROTATION_0, 90);
- ORIENTATIONS.append(Surface.ROTATION_90, 0);
- ORIENTATIONS.append(Surface.ROTATION_180, 270);
- ORIENTATIONS.append(Surface.ROTATION_270, 180);
+ DEFAULT_ORIENTATIONS.append(Surface.ROTATION_0, 90);
+ DEFAULT_ORIENTATIONS.append(Surface.ROTATION_90, 0);
+ DEFAULT_ORIENTATIONS.append(Surface.ROTATION_180, 270);
+ DEFAULT_ORIENTATIONS.append(Surface.ROTATION_270, 180);
+ }
+
+ static {
+ INVERSE_ORIENTATIONS.append(Surface.ROTATION_0, 270);
+ INVERSE_ORIENTATIONS.append(Surface.ROTATION_90, 180);
+ INVERSE_ORIENTATIONS.append(Surface.ROTATION_180, 90);
+ INVERSE_ORIENTATIONS.append(Surface.ROTATION_270, 0);
}
/**
@@ -147,11 +157,6 @@
private Size mVideoSize;
/**
- * Camera preview.
- */
- private CaptureRequest.Builder mPreviewBuilder;
-
- /**
* MediaRecorder
*/
private MediaRecorder mMediaRecorder;
@@ -210,6 +215,10 @@
}
};
+ private Integer mSensorOrientation;
+ private String mNextVideoAbsolutePath;
+ private CaptureRequest.Builder mPreviewBuilder;
+ private Surface mRecorderSurface;
public static Camera2VideoFragment newInstance() {
return new Camera2VideoFragment();
@@ -425,6 +434,7 @@
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
StreamConfigurationMap map = characteristics
.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class));
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
width, height, mVideoSize);
@@ -454,6 +464,7 @@
private void closeCamera() {
try {
mCameraOpenCloseLock.acquire();
+ closePreviewSession();
if (null != mCameraDevice) {
mCameraDevice.close();
mCameraDevice = null;
@@ -477,22 +488,16 @@
return;
}
try {
- setUpMediaRecorder();
+ closePreviewSession();
SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null;
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
- mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
- List<Surface> surfaces = new ArrayList<Surface>();
+ mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
Surface previewSurface = new Surface(texture);
- surfaces.add(previewSurface);
mPreviewBuilder.addTarget(previewSurface);
- Surface recorderSurface = mMediaRecorder.getSurface();
- surfaces.add(recorderSurface);
- mPreviewBuilder.addTarget(recorderSurface);
-
- mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
+ mCameraDevice.createCaptureSession(Arrays.asList(previewSurface), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
@@ -510,8 +515,6 @@
}, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
}
}
@@ -575,32 +578,96 @@
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
- mMediaRecorder.setOutputFile(getVideoFile(activity).getAbsolutePath());
+ if (mNextVideoAbsolutePath == null || mNextVideoAbsolutePath.isEmpty()) {
+ mNextVideoAbsolutePath = getVideoFilePath(getActivity());
+ }
+ mMediaRecorder.setOutputFile(mNextVideoAbsolutePath);
mMediaRecorder.setVideoEncodingBitRate(10000000);
mMediaRecorder.setVideoFrameRate(30);
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
- int orientation = ORIENTATIONS.get(rotation);
- mMediaRecorder.setOrientationHint(orientation);
+ switch (mSensorOrientation) {
+ case SENSOR_ORIENTATION_DEFAULT_DEGREES:
+ mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
+ break;
+ case SENSOR_ORIENTATION_INVERSE_DEGREES:
+ mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation));
+ break;
+ }
mMediaRecorder.prepare();
}
- private File getVideoFile(Context context) {
- return new File(context.getExternalFilesDir(null), "video.mp4");
+ private String getVideoFilePath(Context context) {
+ return context.getExternalFilesDir(null).getAbsolutePath() + "/"
+ + System.currentTimeMillis() + ".mp4";
}
private void startRecordingVideo() {
+ if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
+ return;
+ }
try {
- // UI
- mButtonVideo.setText(R.string.stop);
- mIsRecordingVideo = true;
+ closePreviewSession();
+ setUpMediaRecorder();
+ SurfaceTexture texture = mTextureView.getSurfaceTexture();
+ assert texture != null;
+ texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
+ mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
+ List<Surface> surfaces = new ArrayList<>();
- // Start recording
- mMediaRecorder.start();
- } catch (IllegalStateException e) {
+ // Set up Surface for the camera preview
+ Surface previewSurface = new Surface(texture);
+ surfaces.add(previewSurface);
+ mPreviewBuilder.addTarget(previewSurface);
+
+ // Set up Surface for the MediaRecorder
+ mRecorderSurface = mMediaRecorder.getSurface();
+ surfaces.add(mRecorderSurface);
+ mPreviewBuilder.addTarget(mRecorderSurface);
+
+ // Start a capture session
+ // Once the session starts, we can update the UI and start recording
+ mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
+
+ @Override
+ public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
+ mPreviewSession = cameraCaptureSession;
+ updatePreview();
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // UI
+ mButtonVideo.setText(R.string.stop);
+ mIsRecordingVideo = true;
+
+ // Start recording
+ mMediaRecorder.start();
+ }
+ });
+ }
+
+ @Override
+ public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
+ Activity activity = getActivity();
+ if (null != activity) {
+ Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
+ }
+ }
+ }, mBackgroundHandler);
+ } catch (CameraAccessException e) {
e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ private void closePreviewSession() {
+ if (mPreviewSession != null) {
+ mPreviewSession.close();
+ mPreviewSession = null;
}
}
@@ -611,11 +678,14 @@
// Stop recording
mMediaRecorder.stop();
mMediaRecorder.reset();
+
Activity activity = getActivity();
if (null != activity) {
- Toast.makeText(activity, "Video saved: " + getVideoFile(activity),
+ Toast.makeText(activity, "Video saved: " + mNextVideoAbsolutePath,
Toast.LENGTH_SHORT).show();
+ Log.d(TAG, "Video saved: " + mNextVideoAbsolutePath);
}
+ mNextVideoAbsolutePath = null;
startPreview();
}
diff --git a/media/Camera2Video/README.md b/media/Camera2Video/README.md
index ac5084c..e1f07bc 100644
--- a/media/Camera2Video/README.md
+++ b/media/Camera2Video/README.md
@@ -44,7 +44,7 @@
--------------
- Android SDK v23
-- Android Build Tools v23.0.0
+- Android Build Tools v23.0.2
- Android Support Repository
Screenshots
diff --git a/media/Camera2Video/gradle/wrapper/gradle-wrapper.properties b/media/Camera2Video/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/Camera2Video/gradle/wrapper/gradle-wrapper.properties
+++ b/media/Camera2Video/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/HdrViewfinder/gradle/wrapper/gradle-wrapper.properties b/media/HdrViewfinder/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/HdrViewfinder/gradle/wrapper/gradle-wrapper.properties
+++ b/media/HdrViewfinder/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties b/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties
+++ b/media/MediaBrowserService/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/MediaEffects/gradle/wrapper/gradle-wrapper.properties b/media/MediaEffects/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/MediaEffects/gradle/wrapper/gradle-wrapper.properties
+++ b/media/MediaEffects/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/MediaRecorder/Application/src/main/java/com/example/android/mediarecorder/MainActivity.java b/media/MediaRecorder/Application/src/main/java/com/example/android/mediarecorder/MainActivity.java
index 8587636..102ff4d 100644
--- a/media/MediaRecorder/Application/src/main/java/com/example/android/mediarecorder/MainActivity.java
+++ b/media/MediaRecorder/Application/src/main/java/com/example/android/mediarecorder/MainActivity.java
@@ -32,6 +32,7 @@
import com.example.android.common.media.CameraHelper;
+import java.io.File;
import java.io.IOException;
import java.util.List;
@@ -45,6 +46,7 @@
private Camera mCamera;
private TextureView mPreview;
private MediaRecorder mMediaRecorder;
+ private File mOutputFile;
private boolean isRecording = false;
private static final String TAG = "Recorder";
@@ -71,7 +73,15 @@
// BEGIN_INCLUDE(stop_release_media_recorder)
// stop recording and release camera
- mMediaRecorder.stop(); // stop the recording
+ try {
+ mMediaRecorder.stop(); // stop the recording
+ } catch (RuntimeException e) {
+ // RuntimeException is thrown when stop() is called immediately after start().
+ // In this case the output file is not properly constructed ans should be deleted.
+ Log.d(TAG, "RuntimeException: stop() is called immediately after start()");
+ //noinspection ResultOfMethodCallIgnored
+ mOutputFile.delete();
+ }
releaseMediaRecorder(); // release the MediaRecorder object
mCamera.lock(); // take camera access back from MediaRecorder
@@ -137,8 +147,9 @@
// dimensions of our preview surface.
Camera.Parameters parameters = mCamera.getParameters();
List<Camera.Size> mSupportedPreviewSizes = parameters.getSupportedPreviewSizes();
- Camera.Size optimalSize = CameraHelper.getOptimalPreviewSize(mSupportedPreviewSizes,
- mPreview.getWidth(), mPreview.getHeight());
+ List<Camera.Size> mSupportedVideoSizes = parameters.getSupportedVideoSizes();
+ Camera.Size optimalSize = CameraHelper.getOptimalVideoSize(mSupportedVideoSizes,
+ mSupportedPreviewSizes, mPreview.getWidth(), mPreview.getHeight());
// Use the same size for recording profile.
CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
@@ -174,8 +185,11 @@
mMediaRecorder.setProfile(profile);
// Step 4: Set output file
- mMediaRecorder.setOutputFile(CameraHelper.getOutputMediaFile(
- CameraHelper.MEDIA_TYPE_VIDEO).toString());
+ mOutputFile = CameraHelper.getOutputMediaFile(CameraHelper.MEDIA_TYPE_VIDEO);
+ if (mOutputFile == null) {
+ return false;
+ }
+ mMediaRecorder.setOutputFile(mOutputFile.getPath());
// END_INCLUDE (configure_media_recorder)
// Step 5: Prepare configured MediaRecorder
@@ -227,4 +241,4 @@
}
}
-}
\ No newline at end of file
+}
diff --git a/media/MediaRecorder/README.md b/media/MediaRecorder/README.md
index 32c14cd..ed234cd 100644
--- a/media/MediaRecorder/README.md
+++ b/media/MediaRecorder/README.md
@@ -26,7 +26,7 @@
--------------
- Android SDK v23
-- Android Build Tools v23.0.0
+- Android Build Tools v23.0.2
- Android Support Repository
Screenshots
diff --git a/media/MediaRecorder/gradle/wrapper/gradle-wrapper.properties b/media/MediaRecorder/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/MediaRecorder/gradle/wrapper/gradle-wrapper.properties
+++ b/media/MediaRecorder/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/MediaRouter/gradle/wrapper/gradle-wrapper.properties b/media/MediaRouter/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/MediaRouter/gradle/wrapper/gradle-wrapper.properties
+++ b/media/MediaRouter/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/MidiScope/gradle/wrapper/gradle-wrapper.properties b/media/MidiScope/gradle/wrapper/gradle-wrapper.properties
index afb3296..07fc193 100644
--- a/media/MidiScope/gradle/wrapper/gradle-wrapper.properties
+++ b/media/MidiScope/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/MidiScope/template-params.xml b/media/MidiScope/template-params.xml
index 8caea80..4125248 100644
--- a/media/MidiScope/template-params.xml
+++ b/media/MidiScope/template-params.xml
@@ -43,7 +43,7 @@
<icon>screenshots/icon-web.png</icon>
<screenshots>
<img>screenshots/1-main.png</img>
- <img>screenshots/2-settings.png</img>
+ <img>screenshots/2-signals.png</img>
</screenshots>
<api_refs>
<android>android.media.midi.MidiManager</android>
diff --git a/media/MidiSynth/gradle/wrapper/gradle-wrapper.properties b/media/MidiSynth/gradle/wrapper/gradle-wrapper.properties
index afb3296..07fc193 100644
--- a/media/MidiSynth/gradle/wrapper/gradle-wrapper.properties
+++ b/media/MidiSynth/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/media/MidiSynth/template-params.xml b/media/MidiSynth/template-params.xml
index 7e35471..0d6ef4b 100644
--- a/media/MidiSynth/template-params.xml
+++ b/media/MidiSynth/template-params.xml
@@ -20,14 +20,12 @@
<package>com.example.android.midisynth</package>
<minSdk>23</minSdk>
- <targetSdkVersion>23</targetSdkVersion>
- <compileSdkVersion>23</compileSdkVersion>
<strings>
<intro>
- <![CDATA[
-This sample demonstrates how to use the MIDI API to receive and play MIDI signals coming from an
+<![CDATA[
+This sample demonstrates how to use the MIDI API to receive and play MIDI messages coming from an
attached input device.
- ]]>
+]]>
</intro>
</strings>
@@ -40,7 +38,7 @@
<technologies>Android</technologies>
<languages>Java</languages>
<solutions>Mobile</solutions>
- <level>INTERMEDIATE</level>
+ <level>EXPERT</level>
<icon>screenshots/icon-web.png</icon>
<screenshots>
<img>screenshots/1-main.png</img>
@@ -50,25 +48,30 @@
<android>android.media.midi.MidiReceiver</android>
</api_refs>
<description>
- <![CDATA[
-Sample demonstrating how to use the MIDI API to receive and play MIDI signals coming from an
+<![CDATA[
+Sample demonstrating how to use the MIDI API to receive and play MIDI messages coming from an
attached input device (MIDI keyboard).
- ]]>
+]]>
</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[
-The Android MIDI API ([android.media.midi][1]) allows developers to connect a MIDI device to Android
-and process MIDI signals coming from it. This sample demonstrates some basic features of the MIDI
-API, such as enumeration of currently available devices (Information includes name, vendor,
-capabilities, etc), notification when MIDI devices are plugged in or unplugged, and receiving MIDI
-signals. This sample contains a simple implementation of oscillator and play sound for incoming MIDI
-signals.
+<![CDATA[
+The Android MIDI API ([android.media.midi][1]) allows developers to connect a MIDI device to
+an Android device and process MIDI messages coming from it.
+
+This sample demonstrates some basic features of the MIDI API, such as:
+
+- Enumeration of currently available devices (including name, vendor, capabilities, etc)
+- Notification when MIDI devices are plugged in or unplugged
+- Receiving and processing MIDI messages
+
+This sample contains a simple implementation of an oscillator and note playback.
+
[1]: https://developer.android.com/reference/android/media/midi/package-summary.html
- ]]>
+]]>
</intro>
</metadata>
</sample>
diff --git a/media/ScreenCapture/gradle/wrapper/gradle-wrapper.properties b/media/ScreenCapture/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/media/ScreenCapture/gradle/wrapper/gradle-wrapper.properties
+++ b/media/ScreenCapture/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/notification/ActiveNotifications/gradle/wrapper/gradle-wrapper.properties b/notification/ActiveNotifications/gradle/wrapper/gradle-wrapper.properties
index afb3296..07fc193 100644
--- a/notification/ActiveNotifications/gradle/wrapper/gradle-wrapper.properties
+++ b/notification/ActiveNotifications/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/notification/BasicNotifications/gradle/wrapper/gradle-wrapper.properties b/notification/BasicNotifications/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/notification/BasicNotifications/gradle/wrapper/gradle-wrapper.properties
+++ b/notification/BasicNotifications/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/notification/CustomNotifications/gradle/wrapper/gradle-wrapper.properties b/notification/CustomNotifications/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/notification/CustomNotifications/gradle/wrapper/gradle-wrapper.properties
+++ b/notification/CustomNotifications/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/notification/LNotifications/gradle/wrapper/gradle-wrapper.properties b/notification/LNotifications/gradle/wrapper/gradle-wrapper.properties
index 9f9f676..828f2e6 100644
--- a/notification/LNotifications/gradle/wrapper/gradle-wrapper.properties
+++ b/notification/LNotifications/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/notification/MessagingService/gradle/wrapper/gradle-wrapper.properties b/notification/MessagingService/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/notification/MessagingService/gradle/wrapper/gradle-wrapper.properties
+++ b/notification/MessagingService/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/renderScript/BasicRenderScript/gradle/wrapper/gradle-wrapper.properties b/renderScript/BasicRenderScript/gradle/wrapper/gradle-wrapper.properties
index 6c90366..cb3018f 100644
--- a/renderScript/BasicRenderScript/gradle/wrapper/gradle-wrapper.properties
+++ b/renderScript/BasicRenderScript/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/renderScript/RenderScriptIntrinsic/gradle/wrapper/gradle-wrapper.properties b/renderScript/RenderScriptIntrinsic/gradle/wrapper/gradle-wrapper.properties
index 28f01d8..d2ce766 100644
--- a/renderScript/RenderScriptIntrinsic/gradle/wrapper/gradle-wrapper.properties
+++ b/renderScript/RenderScriptIntrinsic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/security/AsymmetricFingerprintDialog/Application/.gitignore b/security/AsymmetricFingerprintDialog/Application/.gitignore
new file mode 100644
index 0000000..6eb878d
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/.gitignore
@@ -0,0 +1,16 @@
+# Copyright 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+src/template/
+src/common/
+build.gradle
diff --git a/security/AsymmetricFingerprintDialog/Application/proguard-project.txt b/security/AsymmetricFingerprintDialog/Application/proguard-project.txt
new file mode 100644
index 0000000..f2fe155
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/AndroidManifest.xml b/security/AsymmetricFingerprintDialog/Application/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d1cf9f8
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.asymmetricfingerprintdialog"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
+
+ <application
+ android:name=".InjectedApplication"
+ android:allowBackup="true"
+ android:label="@string/app_name"
+ android:icon="@mipmap/ic_launcher"
+ android:theme="@style/AppTheme">
+
+ <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>
+
+ <activity
+ android:name=".SettingsActivity"
+ android:label="@string/action_settings" />
+ </application>
+</manifest>
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintAuthenticationDialogFragment.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintAuthenticationDialogFragment.java
new file mode 100644
index 0000000..a56556f
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintAuthenticationDialogFragment.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2015 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.asymmetricfingerprintdialog;
+
+import com.example.android.asymmetricfingerprintdialog.server.StoreBackend;
+import com.example.android.asymmetricfingerprintdialog.server.Transaction;
+
+import android.app.Activity;
+import android.app.DialogFragment;
+import android.content.SharedPreferences;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+import javax.inject.Inject;
+
+/**
+ * A dialog which uses fingerprint APIs to authenticate the user, and falls back to password
+ * authentication if fingerprint is not available.
+ */
+public class FingerprintAuthenticationDialogFragment extends DialogFragment
+ implements TextView.OnEditorActionListener, FingerprintUiHelper.Callback {
+
+ private Button mCancelButton;
+ private Button mSecondDialogButton;
+ private View mFingerprintContent;
+ private View mBackupContent;
+ private EditText mPassword;
+ private CheckBox mUseFingerprintFutureCheckBox;
+ private TextView mPasswordDescriptionTextView;
+ private TextView mNewFingerprintEnrolledTextView;
+
+ private Stage mStage = Stage.FINGERPRINT;
+
+ private FingerprintManager.CryptoObject mCryptoObject;
+ private FingerprintUiHelper mFingerprintUiHelper;
+ private MainActivity mActivity;
+
+ @Inject FingerprintUiHelper.FingerprintUiHelperBuilder mFingerprintUiHelperBuilder;
+ @Inject InputMethodManager mInputMethodManager;
+ @Inject SharedPreferences mSharedPreferences;
+ @Inject StoreBackend mStoreBackend;
+
+ @Inject
+ public FingerprintAuthenticationDialogFragment() {}
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Do not create a new Fragment when the Activity is re-created such as orientation changes.
+ setRetainInstance(true);
+ setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog);
+
+ // We register a new user account here. Real apps should do this with proper UIs.
+ enroll();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getDialog().setTitle(getString(R.string.sign_in));
+ View v = inflater.inflate(R.layout.fingerprint_dialog_container, container, false);
+ mCancelButton = (Button) v.findViewById(R.id.cancel_button);
+ mCancelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dismiss();
+ }
+ });
+
+ mSecondDialogButton = (Button) v.findViewById(R.id.second_dialog_button);
+ mSecondDialogButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mStage == Stage.FINGERPRINT) {
+ goToBackup();
+ } else {
+ verifyPassword();
+ }
+ }
+ });
+ mFingerprintContent = v.findViewById(R.id.fingerprint_container);
+ mBackupContent = v.findViewById(R.id.backup_container);
+ mPassword = (EditText) v.findViewById(R.id.password);
+ mPassword.setOnEditorActionListener(this);
+ mPasswordDescriptionTextView = (TextView) v.findViewById(R.id.password_description);
+ mUseFingerprintFutureCheckBox = (CheckBox)
+ v.findViewById(R.id.use_fingerprint_in_future_check);
+ mNewFingerprintEnrolledTextView = (TextView)
+ v.findViewById(R.id.new_fingerprint_enrolled_description);
+ mFingerprintUiHelper = mFingerprintUiHelperBuilder.build(
+ (ImageView) v.findViewById(R.id.fingerprint_icon),
+ (TextView) v.findViewById(R.id.fingerprint_status), this);
+ updateStage();
+
+ // If fingerprint authentication is not available, switch immediately to the backup
+ // (password) screen.
+ if (!mFingerprintUiHelper.isFingerprintAuthAvailable()) {
+ goToBackup();
+ }
+ return v;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mStage == Stage.FINGERPRINT) {
+ mFingerprintUiHelper.startListening(mCryptoObject);
+ }
+ }
+
+ public void setStage(Stage stage) {
+ mStage = stage;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mFingerprintUiHelper.stopListening();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mActivity = (MainActivity) activity;
+ }
+
+ /**
+ * Sets the crypto object to be passed in when authenticating with fingerprint.
+ */
+ public void setCryptoObject(FingerprintManager.CryptoObject cryptoObject) {
+ mCryptoObject = cryptoObject;
+ }
+
+ /**
+ * Switches to backup (password) screen. This either can happen when fingerprint is not
+ * available or the user chooses to use the password authentication method by pressing the
+ * button. This can also happen when the user had too many fingerprint attempts.
+ */
+ private void goToBackup() {
+ mStage = Stage.PASSWORD;
+ updateStage();
+ mPassword.requestFocus();
+
+ // Show the keyboard.
+ mPassword.postDelayed(mShowKeyboardRunnable, 500);
+
+ // Fingerprint is not used anymore. Stop listening for it.
+ mFingerprintUiHelper.stopListening();
+ }
+
+ /**
+ * Enrolls a user to the fake backend.
+ */
+ private void enroll() {
+ try {
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ PublicKey publicKey = keyStore.getCertificate(MainActivity.KEY_NAME).getPublicKey();
+ // Provide the public key to the backend. In most cases, the key needs to be transmitted
+ // to the backend over the network, for which Key.getEncoded provides a suitable wire
+ // format (X.509 DER-encoded). The backend can then create a PublicKey instance from the
+ // X.509 encoded form using KeyFactory.generatePublic. This conversion is also currently
+ // needed on API Level 23 (Android M) due to a platform bug which prevents the use of
+ // Android Keystore public keys when their private keys require user authentication.
+ // This conversion creates a new public key which is not backed by Android Keystore and
+ // thus is not affected by the bug.
+ KeyFactory factory = KeyFactory.getInstance(publicKey.getAlgorithm());
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey.getEncoded());
+ PublicKey verificationKey = factory.generatePublic(spec);
+ mStoreBackend.enroll("user", "password", verificationKey);
+ } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException |
+ IOException | InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Checks whether the current entered password is correct, and dismisses the the dialog and lets
+ * the activity know about the result.
+ */
+ private void verifyPassword() {
+ Transaction transaction = new Transaction("user", 1, new SecureRandom().nextLong());
+ if (!mStoreBackend.verify(transaction, mPassword.getText().toString())) {
+ return;
+ }
+ if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
+ mUseFingerprintFutureCheckBox.isChecked());
+ editor.apply();
+
+ if (mUseFingerprintFutureCheckBox.isChecked()) {
+ // Re-create the key so that fingerprints including new ones are validated.
+ mActivity.createKeyPair();
+ mStage = Stage.FINGERPRINT;
+ }
+ }
+ mPassword.setText("");
+ mActivity.onPurchased(null);
+ dismiss();
+ }
+
+ private final Runnable mShowKeyboardRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mInputMethodManager.showSoftInput(mPassword, 0);
+ }
+ };
+
+ private void updateStage() {
+ switch (mStage) {
+ case FINGERPRINT:
+ mCancelButton.setText(R.string.cancel);
+ mSecondDialogButton.setText(R.string.use_password);
+ mFingerprintContent.setVisibility(View.VISIBLE);
+ mBackupContent.setVisibility(View.GONE);
+ break;
+ case NEW_FINGERPRINT_ENROLLED:
+ // Intentional fall through
+ case PASSWORD:
+ mCancelButton.setText(R.string.cancel);
+ mSecondDialogButton.setText(R.string.ok);
+ mFingerprintContent.setVisibility(View.GONE);
+ mBackupContent.setVisibility(View.VISIBLE);
+ if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) {
+ mPasswordDescriptionTextView.setVisibility(View.GONE);
+ mNewFingerprintEnrolledTextView.setVisibility(View.VISIBLE);
+ mUseFingerprintFutureCheckBox.setVisibility(View.VISIBLE);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_GO) {
+ verifyPassword();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onAuthenticated() {
+ // Callback from FingerprintUiHelper. Let the activity know that authentication was
+ // successful.
+ mPassword.setText("");
+ Signature signature = mCryptoObject.getSignature();
+ // Include a client nonce in the transaction so that the nonce is also signed by the private
+ // key and the backend can verify that the same nonce can't be used to prevent replay
+ // attacks.
+ Transaction transaction = new Transaction("user", 1, new SecureRandom().nextLong());
+ try {
+ signature.update(transaction.toByteArray());
+ byte[] sigBytes = signature.sign();
+ if (mStoreBackend.verify(transaction, sigBytes)) {
+ mActivity.onPurchased(sigBytes);
+ dismiss();
+ } else {
+ mActivity.onPurchaseFailed();
+ dismiss();
+ }
+ } catch (SignatureException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void onError() {
+ goToBackup();
+ }
+
+ /**
+ * Enumeration to indicate which authentication method the user is trying to authenticate with.
+ */
+ public enum Stage {
+ FINGERPRINT,
+ NEW_FINGERPRINT_ENROLLED,
+ PASSWORD
+ }
+}
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintModule.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintModule.java
new file mode 100644
index 0000000..ae3acf8
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintModule.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 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.asymmetricfingerprintdialog;
+
+import com.example.android.asymmetricfingerprintdialog.server.StoreBackend;
+import com.example.android.asymmetricfingerprintdialog.server.StoreBackendImpl;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.hardware.fingerprint.FingerprintManager;
+import android.preference.PreferenceManager;
+import android.security.keystore.KeyProperties;
+import android.view.inputmethod.InputMethodManager;
+
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Signature;
+
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Dagger module for Fingerprint APIs.
+ */
+@Module(
+ library = true,
+ injects = {MainActivity.class}
+)
+public class FingerprintModule {
+
+ private final Context mContext;
+
+ public FingerprintModule(Context context) {
+ mContext = context;
+ }
+
+ @Provides
+ public Context providesContext() {
+ return mContext;
+ }
+
+ @Provides
+ public FingerprintManager providesFingerprintManager(Context context) {
+ return context.getSystemService(FingerprintManager.class);
+ }
+
+ @Provides
+ public KeyguardManager providesKeyguardManager(Context context) {
+ return context.getSystemService(KeyguardManager.class);
+ }
+
+ @Provides
+ public KeyStore providesKeystore() {
+ try {
+ return KeyStore.getInstance("AndroidKeyStore");
+ } catch (KeyStoreException e) {
+ throw new RuntimeException("Failed to get an instance of KeyStore", e);
+ }
+ }
+
+ @Provides
+ public KeyPairGenerator providesKeyPairGenerator() {
+ try {
+ return KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+ throw new RuntimeException("Failed to get an instance of KeyPairGenerator", e);
+ }
+ }
+
+ @Provides
+ public Signature providesSignature(KeyStore keyStore) {
+ try {
+ return Signature.getInstance("SHA256withECDSA");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("Failed to get an instance of Signature", e);
+ }
+ }
+
+ @Provides
+ public InputMethodManager providesInputMethodManager(Context context) {
+ return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+
+ @Provides
+ public SharedPreferences providesSharedPreferences(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ @Provides
+ public StoreBackend providesStoreBackend() {
+ return new StoreBackendImpl();
+ }
+}
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintUiHelper.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintUiHelper.java
new file mode 100644
index 0000000..f654811
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintUiHelper.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2015 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.asymmetricfingerprintdialog;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.CancellationSignal;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import javax.inject.Inject;
+
+/**
+ * Small helper class to manage text/icon around fingerprint authentication UI.
+ */
+public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
+
+ @VisibleForTesting static final long ERROR_TIMEOUT_MILLIS = 1600;
+ @VisibleForTesting static final long SUCCESS_DELAY_MILLIS = 1300;
+
+ private final FingerprintManager mFingerprintManager;
+ private final ImageView mIcon;
+ private final TextView mErrorTextView;
+ private final Callback mCallback;
+ private CancellationSignal mCancellationSignal;
+
+ @VisibleForTesting boolean mSelfCancelled;
+
+ /**
+ * Builder class for {@link FingerprintUiHelper} in which injected fields from Dagger
+ * holds its fields and takes other arguments in the {@link #build} method.
+ */
+ public static class FingerprintUiHelperBuilder {
+ private final FingerprintManager mFingerPrintManager;
+
+ @Inject
+ public FingerprintUiHelperBuilder(FingerprintManager fingerprintManager) {
+ mFingerPrintManager = fingerprintManager;
+ }
+
+ public FingerprintUiHelper build(ImageView icon, TextView errorTextView, Callback callback) {
+ return new FingerprintUiHelper(mFingerPrintManager, icon, errorTextView,
+ callback);
+ }
+ }
+
+ /**
+ * Constructor for {@link FingerprintUiHelper}. This method is expected to be called from
+ * only the {@link FingerprintUiHelperBuilder} class.
+ */
+ private FingerprintUiHelper(FingerprintManager fingerprintManager,
+ ImageView icon, TextView errorTextView, Callback callback) {
+ mFingerprintManager = fingerprintManager;
+ mIcon = icon;
+ mErrorTextView = errorTextView;
+ mCallback = callback;
+ }
+
+ public boolean isFingerprintAuthAvailable() {
+ return mFingerprintManager.isHardwareDetected()
+ && mFingerprintManager.hasEnrolledFingerprints();
+ }
+
+ public void startListening(FingerprintManager.CryptoObject cryptoObject) {
+ if (!isFingerprintAuthAvailable()) {
+ return;
+ }
+ mCancellationSignal = new CancellationSignal();
+ mSelfCancelled = false;
+ mFingerprintManager
+ .authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
+ mIcon.setImageResource(R.drawable.ic_fp_40px);
+ }
+
+ public void stopListening() {
+ if (mCancellationSignal != null) {
+ mSelfCancelled = true;
+ mCancellationSignal.cancel();
+ mCancellationSignal = null;
+ }
+ }
+
+ @Override
+ public void onAuthenticationError(int errMsgId, CharSequence errString) {
+ if (!mSelfCancelled) {
+ showError(errString);
+ mIcon.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onError();
+ }
+ }, ERROR_TIMEOUT_MILLIS);
+ }
+ }
+
+ @Override
+ public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
+ showError(helpString);
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ showError(mIcon.getResources().getString(
+ R.string.fingerprint_not_recognized));
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
+ mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
+ mIcon.setImageResource(R.drawable.ic_fingerprint_success);
+ mErrorTextView.setTextColor(
+ mErrorTextView.getResources().getColor(R.color.success_color, null));
+ mErrorTextView.setText(
+ mErrorTextView.getResources().getString(R.string.fingerprint_success));
+ mIcon.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onAuthenticated();
+ }
+ }, SUCCESS_DELAY_MILLIS);
+ }
+
+ private void showError(CharSequence error) {
+ mIcon.setImageResource(R.drawable.ic_fingerprint_error);
+ mErrorTextView.setText(error);
+ mErrorTextView.setTextColor(
+ mErrorTextView.getResources().getColor(R.color.warning_color, null));
+ mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
+ mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
+ }
+
+ @VisibleForTesting
+ Runnable mResetErrorTextRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mErrorTextView.setTextColor(
+ mErrorTextView.getResources().getColor(R.color.hint_color, null));
+ mErrorTextView.setText(
+ mErrorTextView.getResources().getString(R.string.fingerprint_hint));
+ mIcon.setImageResource(R.drawable.ic_fp_40px);
+ }
+ };
+
+ public interface Callback {
+
+ void onAuthenticated();
+
+ void onError();
+ }
+}
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/InjectedApplication.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/InjectedApplication.java
new file mode 100644
index 0000000..1c3ed7e
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/InjectedApplication.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 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.asymmetricfingerprintdialog;
+
+import android.app.Application;
+import android.util.Log;
+
+import dagger.ObjectGraph;
+
+/**
+ * The Application class of the sample which holds the ObjectGraph in Dagger and enables
+ * dependency injection.
+ */
+public class InjectedApplication extends Application {
+
+ private static final String TAG = InjectedApplication.class.getSimpleName();
+
+ private ObjectGraph mObjectGraph;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ initObjectGraph(new FingerprintModule(this));
+ }
+
+ /**
+ * Initialize the Dagger module. Passing null or mock modules can be used for testing.
+ *
+ * @param module for Dagger
+ */
+ public void initObjectGraph(Object module) {
+ mObjectGraph = module != null ? ObjectGraph.create(module) : null;
+ }
+
+ public void inject(Object object) {
+ if (mObjectGraph == null) {
+ // This usually happens during tests.
+ Log.i(TAG, "Object graph is not initialized.");
+ return;
+ }
+ mObjectGraph.inject(object);
+ }
+
+}
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
new file mode 100644
index 0000000..26832f2
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/MainActivity.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2015 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.asymmetricfingerprintdialog;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.security.keystore.KeyProperties;
+import android.util.Base64;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.spec.ECGenParameterSpec;
+
+import javax.inject.Inject;
+
+/**
+ * Main entry point for the sample, showing a backpack and "Purchase" button.
+ */
+public class MainActivity extends Activity {
+
+ private static final String DIALOG_FRAGMENT_TAG = "myFragment";
+ /** Alias for our key in the Android Key Store */
+ public static final String KEY_NAME = "my_key";
+
+ @Inject KeyguardManager mKeyguardManager;
+ @Inject FingerprintManager mFingerprintManager;
+ @Inject FingerprintAuthenticationDialogFragment mFragment;
+ @Inject KeyStore mKeyStore;
+ @Inject KeyPairGenerator mKeyPairGenerator;
+ @Inject Signature mSignature;
+ @Inject SharedPreferences mSharedPreferences;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ((InjectedApplication) getApplication()).inject(this);
+
+ setContentView(R.layout.activity_main);
+ Button purchaseButton = (Button) findViewById(R.id.purchase_button);
+ if (!mKeyguardManager.isKeyguardSecure()) {
+ // Show a message that the user hasn't set up a fingerprint or lock screen.
+ Toast.makeText(this,
+ "Secure lock screen hasn't set up.\n"
+ + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
+ Toast.LENGTH_LONG).show();
+ purchaseButton.setEnabled(false);
+ return;
+ }
+ //noinspection ResourceType
+ if (!mFingerprintManager.hasEnrolledFingerprints()) {
+ purchaseButton.setEnabled(false);
+ // This happens when no fingerprints are registered.
+ Toast.makeText(this,
+ "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ createKeyPair();
+ purchaseButton.setEnabled(true);
+ purchaseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ findViewById(R.id.confirmation_message).setVisibility(View.GONE);
+ findViewById(R.id.encrypted_message).setVisibility(View.GONE);
+
+ // Set up the crypto object for later. The object will be authenticated by use
+ // of the fingerprint.
+ if (initSignature()) {
+
+ // Show the fingerprint dialog. The user has the option to use the fingerprint with
+ // crypto, or you can fall back to using a server-side verified password.
+ mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature));
+ boolean useFingerprintPreference = mSharedPreferences
+ .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
+ true);
+ if (useFingerprintPreference) {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
+ } else {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
+ }
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ } else {
+ // This happens if the lock screen has been disabled or or a fingerprint got
+ // enrolled. Thus show the dialog to authenticate with their password first
+ // and ask the user if they want to authenticate with fingerprints in the
+ // future
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ }
+ }
+ });
+ }
+
+ /**
+ * Initialize the {@link Signature} instance with the created key in the
+ * {@link #createKeyPair()} method.
+ *
+ * @return {@code true} if initialization is successful, {@code false} if the lock screen has
+ * been disabled or reset after the key was generated, or if a fingerprint got enrolled after
+ * the key was generated.
+ */
+ private boolean initSignature() {
+ try {
+ mKeyStore.load(null);
+ PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null);
+ mSignature.initSign(key);
+ return true;
+ } catch (KeyPermanentlyInvalidatedException e) {
+ return false;
+ } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException
+ | NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new RuntimeException("Failed to init Cipher", e);
+ }
+ }
+
+ public void onPurchased(byte[] signature) {
+ showConfirmation(signature);
+ }
+
+ public void onPurchaseFailed() {
+ Toast.makeText(this, R.string.purchase_fail, Toast.LENGTH_SHORT).show();
+ }
+
+ // Show confirmation, if fingerprint was used show crypto information.
+ private void showConfirmation(byte[] encrypted) {
+ findViewById(R.id.confirmation_message).setVisibility(View.VISIBLE);
+ if (encrypted != null) {
+ TextView v = (TextView) findViewById(R.id.encrypted_message);
+ v.setVisibility(View.VISIBLE);
+ v.setText(Base64.encodeToString(encrypted, 0 /* flags */));
+ }
+ }
+
+ /**
+ * Generates an asymmetric key pair in the Android Keystore. Every use of the private key must
+ * be authorized by the user authenticating with fingerprint. Public key use is unrestricted.
+ */
+ public void createKeyPair() {
+ // The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint
+ // for your flow. Use of keys is necessary if you need to know if the set of
+ // enrolled fingerprints has changed.
+ try {
+ // Set the alias of the entry in Android KeyStore where the key will appear
+ // and the constrains (purposes) in the constructor of the Builder
+ mKeyPairGenerator.initialize(
+ new KeyGenParameterSpec.Builder(KEY_NAME,
+ KeyProperties.PURPOSE_SIGN)
+ .setDigests(KeyProperties.DIGEST_SHA256)
+ .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
+ // Require the user to authenticate with a fingerprint to authorize
+ // every use of the private key
+ .setUserAuthenticationRequired(true)
+ .build());
+ mKeyPairGenerator.generateKeyPair();
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == R.id.action_settings) {
+ Intent intent = new Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/SettingsActivity.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/SettingsActivity.java
new file mode 100644
index 0000000..acc5e37
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/SettingsActivity.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2015 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.asymmetricfingerprintdialog;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+
+public class SettingsActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Display the fragment as the main content.
+ getFragmentManager().beginTransaction().replace(android.R.id.content,
+ new SettingsFragment()).commit();
+ }
+
+ /**
+ * Fragment for settings.
+ */
+ public static class SettingsFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.preferences);
+ }
+ }
+}
+
+
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackend.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackend.java
new file mode 100644
index 0000000..87921ae
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackend.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 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.asymmetricfingerprintdialog.server;
+
+import java.security.PublicKey;
+
+/**
+ * An interface that defines the methods required for the store backend.
+ */
+public interface StoreBackend {
+
+ /**
+ * Verifies the authenticity of the provided transaction by confirming that it was signed with
+ * the private key enrolled for the userId.
+ *
+ * @param transaction the contents of the purchase transaction, its contents are
+ * signed
+ * by the
+ * private key in the client side.
+ * @param transactionSignature the signature of the transaction's contents.
+ * @return true if the signedSignature was verified, false otherwise. If this method returns
+ * true, the server can consider the transaction is successful.
+ */
+ boolean verify(Transaction transaction, byte[] transactionSignature);
+
+ /**
+ * Verifies the authenticity of the provided transaction by password.
+ *
+ * @param transaction the contents of the purchase transaction, its contents are signed by the
+ * private key in the client side.
+ * @param password the password for the user associated with the {@code transaction}.
+ * @return true if the password is verified.
+ */
+ boolean verify(Transaction transaction, String password);
+
+ /**
+ * Enrolls a public key associated with the userId
+ *
+ * @param userId the unique ID of the user within the app including server side
+ * implementation
+ * @param password the password for the user for the server side
+ * @param publicKey the public key object to verify the signature from the user
+ * @return true if the enrollment was successful, false otherwise
+ */
+ boolean enroll(String userId, String password, PublicKey publicKey);
+}
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackendImpl.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackendImpl.java
new file mode 100644
index 0000000..b28dce4
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackendImpl.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 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.asymmetricfingerprintdialog.server;
+
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A fake backend implementation of {@link StoreBackend}.
+ */
+public class StoreBackendImpl implements StoreBackend {
+
+ private final Map<String, PublicKey> mPublicKeys = new HashMap<>();
+ private final Set<Transaction> mReceivedTransactions = new HashSet<>();
+
+ @Override
+ public boolean verify(Transaction transaction, byte[] transactionSignature) {
+ try {
+ if (mReceivedTransactions.contains(transaction)) {
+ // It verifies the equality of the transaction including the client nonce
+ // So attackers can't do replay attacks.
+ return false;
+ }
+ mReceivedTransactions.add(transaction);
+ PublicKey publicKey = mPublicKeys.get(transaction.getUserId());
+ Signature verificationFunction = Signature.getInstance("SHA256withECDSA");
+ verificationFunction.initVerify(publicKey);
+ verificationFunction.update(transaction.toByteArray());
+ if (verificationFunction.verify(transactionSignature)) {
+ // Transaction is verified with the public key associated with the user
+ // Do some post purchase processing in the server
+ return true;
+ }
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
+ // In a real world, better to send some error message to the user
+ }
+ return false;
+ }
+
+ @Override
+ public boolean verify(Transaction transaction, String password) {
+ // As this is just a sample, we always assume that the password is right.
+ return true;
+ }
+
+ @Override
+ public boolean enroll(String userId, String password, PublicKey publicKey) {
+ if (publicKey != null) {
+ mPublicKeys.put(userId, publicKey);
+ }
+ // We just ignore the provided password here, but in real life, it is registered to the
+ // backend.
+ return true;
+ }
+}
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/Transaction.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/Transaction.java
new file mode 100644
index 0000000..789cc0e
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/Transaction.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 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.asymmetricfingerprintdialog.server;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * An entity that represents a single transaction (purchase) of an item.
+ */
+public class Transaction {
+
+ /** The unique ID of the item of the purchase */
+ private final Long mItemId;
+
+ /** The unique user ID who made the transaction */
+ private final String mUserId;
+
+ /**
+ * The random long value that will be also signed by the private key and verified in the server
+ * that the same nonce can't be reused to prevent replay attacks.
+ */
+ private final Long mClientNonce;
+
+ public Transaction(String userId, long itemId, long clientNonce) {
+ mItemId = itemId;
+ mUserId = userId;
+ mClientNonce = clientNonce;
+ }
+
+ public String getUserId() {
+ return mUserId;
+ }
+
+ public byte[] toByteArray() {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ DataOutputStream dataOutputStream = null;
+ try {
+ dataOutputStream = new DataOutputStream(byteArrayOutputStream);
+ dataOutputStream.writeLong(mItemId);
+ dataOutputStream.writeUTF(mUserId);
+ dataOutputStream.writeLong(mClientNonce);
+ return byteArrayOutputStream.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ try {
+ if (dataOutputStream != null) {
+ dataOutputStream.close();
+ }
+ } catch (IOException ignore) {
+ }
+ try {
+ byteArrayOutputStream.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Transaction that = (Transaction) o;
+ return Objects.equals(mItemId, that.mItemId) && Objects.equals(mUserId, that.mUserId) &&
+ Objects.equals(mClientNonce, that.mClientNonce);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mItemId, mUserId, mClientNonce);
+ }
+}
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-hdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-hdpi/ic_fp_40px.png
new file mode 100644
index 0000000..48ebd8a
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-hdpi/ic_fp_40px.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-mdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-mdpi/ic_fp_40px.png
new file mode 100644
index 0000000..122f442
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-mdpi/ic_fp_40px.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-nodpi/android_robot.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-nodpi/android_robot.png
new file mode 100644
index 0000000..40bf934
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-nodpi/android_robot.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xhdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xhdpi/ic_fp_40px.png
new file mode 100644
index 0000000..e1c9590
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xhdpi/ic_fp_40px.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxhdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxhdpi/ic_fp_40px.png
new file mode 100644
index 0000000..f7e8724
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxhdpi/ic_fp_40px.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxxhdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxxhdpi/ic_fp_40px.png
new file mode 100644
index 0000000..0fb8545
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxxhdpi/ic_fp_40px.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/card.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/card.xml
new file mode 100644
index 0000000..691a4c5
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/card.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <solid
+ android:color="#fefefe"/>
+
+ <corners
+ android:radius="2dp" />
+</shape>
\ No newline at end of file
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_error.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_error.xml
new file mode 100644
index 0000000..be46116
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_error.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40.0dp"
+ android:height="40.0dp"
+ android:viewportWidth="40.0"
+ android:viewportHeight="40.0">
+ <path
+ android:pathData="M20.0,0.0C8.96,0.0 0.0,8.95 0.0,20.0s8.96,20.0 20.0,20.0c11.04,0.0 20.0,-8.95 20.0,-20.0S31.04,0.0 20.0,0.0z"
+ android:fillColor="#F4511E"/>
+ <path
+ android:pathData="M21.33,29.33l-2.67,0.0l0.0,-2.67l2.67,0.0L21.33,29.33zM21.33,22.67l-2.67,0.0l0.0,-12.0l2.67,0.0L21.33,22.67z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_success.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_success.xml
new file mode 100644
index 0000000..261f3e7
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_success.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40.0dp"
+ android:height="40.0dp"
+ android:viewportWidth="40.0"
+ android:viewportHeight="40.0">
+ <path
+ android:pathData="M20.0,20.0m-20.0,0.0a20.0,20.0 0.0,1.0 1.0,40.0 0.0a20.0,20.0 0.0,1.0 1.0,-40.0 0.0"
+ android:fillColor="#009688"/>
+ <path
+ android:pathData="M11.2,21.41l1.63,-1.619999 4.17,4.169998 10.59,-10.589999 1.619999,1.63 -12.209999,12.209999z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/activity_main.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..8f30557
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/activity_main.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <ImageView
+ android:layout_width="150dp"
+ android:layout_height="150dp"
+ android:layout_marginTop="32dp"
+ android:layout_marginBottom="32dp"
+ android:layout_gravity="center_horizontal"
+ android:scaleType="fitCenter"
+ android:src="@drawable/android_robot"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:orientation="vertical"
+ android:background="@drawable/card"
+ android:elevation="4dp"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@android:style/TextAppearance.Material.Headline"
+ android:text="@string/item_title"/>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@android:style/TextAppearance.Material.Body2"
+ android:textColor="?android:attr/colorAccent"
+ android:text="@string/item_price"/>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:textAppearance="@android:style/TextAppearance.Material.Body1"
+ android:textColor="?android:attr/textColorSecondary"
+ android:text="@string/item_description"/>
+
+ </LinearLayout>
+ <Button style="@android:style/Widget.Material.Button.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_marginEnd="4dp"
+ android:layout_gravity="end"
+ android:textColor="?android:attr/textColorPrimaryInverse"
+ android:text="@string/purchase"
+ android:id="@+id/purchase_button"
+ android:layout_alignParentEnd="true"/>
+
+ <TextView
+ android:id="@+id/confirmation_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:textAppearance="@android:style/TextAppearance.Material.Body2"
+ android:textColor="?android:attr/colorAccent"
+ android:text="@string/purchase_done"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/encrypted_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:textAppearance="@android:style/TextAppearance.Material.Body2"
+ android:textColor="?android:attr/colorAccent"
+ android:text="@string/purchase_done"
+ android:visibility="gone"/>
+ </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_backup.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_backup.xml
new file mode 100644
index 0000000..2be05b1
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_backup.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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"
+ android:id="@+id/backup_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingBottom="8dp">
+
+ <FrameLayout
+ android:id="@+id/description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
+ >
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@android:style/TextAppearance.Material.Subhead"
+ android:text="@string/password_description"
+ android:id="@+id/password_description"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@android:style/TextAppearance.Material.Subhead"
+ android:text="@string/new_fingerprint_enrolled_description"
+ android:id="@+id/new_fingerprint_enrolled_description"
+ android:visibility="gone"
+ android:textColor="?android:attr/textColorSecondary" />
+ </FrameLayout>
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:inputType="textPassword"
+ android:ems="10"
+ android:hint="@string/password"
+ android:imeOptions="actionGo"
+ android:id="@+id/password"
+ android:layout_below="@+id/description"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="20dp"
+ android:layout_marginEnd="20dp"
+ android:layout_alignParentStart="true" />
+
+ <CheckBox
+ android:id="@+id/use_fingerprint_in_future_check"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/password"
+ android:layout_alignParentStart="true"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="20dp"
+ android:layout_marginEnd="20dp"
+ android:checked="true"
+ android:visibility="gone"
+ android:text="@string/use_fingerprint_in_future" />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_container.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_container.xml
new file mode 100644
index 0000000..08bb1bb
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_container.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <include layout="@layout/fingerprint_dialog_content" />
+
+ <include
+ layout="@layout/fingerprint_dialog_backup"
+ android:visibility="gone" />
+
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/buttonPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp"
+ android:gravity="bottom"
+ style="?android:attr/buttonBarStyle">
+
+ <Space
+ android:id="@+id/spacer"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:visibility="invisible" />
+ <Button
+ android:id="@+id/cancel_button"
+ style="?android:attr/buttonBarNegativeButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <Button
+ android:id="@+id/second_dialog_button"
+ style="?android:attr/buttonBarPositiveButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_content.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_content.xml
new file mode 100644
index 0000000..3929eba
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_content.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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"
+ android:id="@+id/fingerprint_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="8dp"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:paddingTop="16dp">
+
+ <TextView
+ android:id="@+id/fingerprint_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:text="@string/fingerprint_description"
+ android:textAppearance="@android:style/TextAppearance.Material.Subhead"
+ android:textColor="?android:attr/textColorSecondary"/>
+
+
+ <ImageView
+ android:id="@+id/fingerprint_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_below="@+id/fingerprint_description"
+ android:layout_marginTop="20dp"
+ android:src="@drawable/ic_fp_40px" />
+
+ <TextView
+ android:id="@+id/fingerprint_status"
+ style="@android:style/TextAppearance.Material.Body1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/fingerprint_icon"
+ android:layout_alignTop="@+id/fingerprint_icon"
+ android:layout_marginStart="16dp"
+ android:layout_toEndOf="@+id/fingerprint_icon"
+ android:gravity="center_vertical"
+ android:text="@string/fingerprint_hint"
+ android:textColor="@color/hint_color" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/menu/menu_main.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..73f5e89
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/menu/menu_main.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
+ <item android:id="@+id/action_settings" android:title="@string/action_settings"
+ android:orderInCategory="100" android:showAsAction="never" />
+</menu>
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-hdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..b82d7af
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-mdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..b0d26ba
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xhdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..135858d
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..02960da
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d425459
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/values/colors.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/values/colors.xml
new file mode 100644
index 0000000..a24f3c8
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <color name="warning_color">#f4511e</color>
+ <color name="hint_color">#42000000</color>
+ <color name="success_color">#009688</color>
+</resources>
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/values/strings.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/values/strings.xml
new file mode 100644
index 0000000..f44c06d
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/values/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <string name="action_settings">Settings</string>
+ <string name="cancel">Cancel</string>
+ <string name="use_password">Use password</string>
+ <string name="sign_in">Sign in</string>
+ <string name="ok">Ok</string>
+ <string name="password">Password</string>
+ <string name="fingerprint_description">Confirm fingerprint to continue</string>
+ <string name="fingerprint_hint">Touch sensor</string>
+ <string name="password_description">Enter your store password to continue</string>
+ <string name="purchase">Purchase</string>
+ <string name="fingerprint_not_recognized">Fingerprint not recognized. Try again</string>
+ <string name="fingerprint_success">Fingerprint recognized</string>
+ <string name="item_title">White Mesh Pluto Backpack</string>
+ <string name="item_price">$62.68</string>
+ <string name="item_description">Mesh backpack in white. Black textile trim throughout.</string>
+ <string name="purchase_done">Purchase successful</string>
+ <string name="purchase_fail">Purchase failed</string>
+ <string name="new_fingerprint_enrolled_description">A new fingerprint was added to this device, so your password is required.</string>
+ <string name="use_fingerprint_in_future">Use fingerprint in the future</string>
+ <string name="use_fingerprint_to_authenticate_title">Use fingerprint to authenticate</string>
+ <string name="use_fingerprint_to_authenticate_key" >use_fingerprint_to_authenticate_key</string>
+</resources>
diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/xml/preferences.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000..761391d
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/xml/preferences.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+ <CheckBoxPreference
+ android:key="@string/use_fingerprint_to_authenticate_key"
+ android:title="@string/use_fingerprint_to_authenticate_title"
+ android:persistent="true"
+ android:defaultValue="true" />
+</PreferenceScreen>
\ No newline at end of file
diff --git a/security/AsymmetricFingerprintDialog/Application/src/test/java/com/example/android/fingerprintdialog/FingerprintUiHelperTest.java b/security/AsymmetricFingerprintDialog/Application/src/test/java/com/example/android/fingerprintdialog/FingerprintUiHelperTest.java
new file mode 100644
index 0000000..a5a9108
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/Application/src/test/java/com/example/android/fingerprintdialog/FingerprintUiHelperTest.java
@@ -0,0 +1,111 @@
+package com.example.android.asymmetricfingerprintdialog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import android.content.res.Resources;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link FingerprintUiHelper}.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class FingerprintUiHelperTest {
+
+ private static final int ERROR_MSG_ID = 1;
+ private static final CharSequence ERR_STRING = "ERROR_STRING";
+ private static final int HINT_COLOR = 10;
+
+ @Mock private FingerprintManager mockFingerprintManager;
+ @Mock private ImageView mockIcon;
+ @Mock private TextView mockTextView;
+ @Mock private FingerprintUiHelper.Callback mockCallback;
+ @Mock private FingerprintManager.CryptoObject mockCryptoObject;
+ @Mock private Resources mockResources;
+
+ @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
+
+ @InjectMocks private FingerprintUiHelper.FingerprintUiHelperBuilder mockBuilder;
+
+ private FingerprintUiHelper mFingerprintUiHelper;
+
+ @Before
+ public void setUp() {
+ mFingerprintUiHelper = mockBuilder.build(mockIcon, mockTextView, mockCallback);
+
+ when(mockFingerprintManager.isHardwareDetected()).thenReturn(true);
+ when(mockFingerprintManager.hasEnrolledFingerprints()).thenReturn(true);
+ when(mockTextView.getResources()).thenReturn(mockResources);
+ when(mockResources.getColor(R.color.hint_color, null)).thenReturn(HINT_COLOR);
+ }
+
+ @Test
+ public void testStartListening_fingerprintAuthAvailable() {
+ mFingerprintUiHelper.startListening(mockCryptoObject);
+
+ verify(mockFingerprintManager).authenticate(eq(mockCryptoObject),
+ isA(CancellationSignal.class), eq(0), eq(mFingerprintUiHelper),
+ any(Handler.class));
+ verify(mockIcon).setImageResource(R.drawable.ic_fp_40px);
+ }
+
+ @Test
+ public void testStartListening_fingerprintAuthNotAvailable() {
+ when(mockFingerprintManager.isHardwareDetected()).thenReturn(false);
+
+ mFingerprintUiHelper.startListening(mockCryptoObject);
+
+ verify(mockFingerprintManager, never()).authenticate(
+ any(FingerprintManager.CryptoObject.class), any(CancellationSignal.class), eq(0),
+ any(FingerprintUiHelper.class), any(Handler.class));
+ }
+
+ @Test
+ public void testOnAuthenticationError() {
+ mFingerprintUiHelper.mSelfCancelled = false;
+
+ mFingerprintUiHelper.onAuthenticationError(ERROR_MSG_ID, ERR_STRING);
+
+ verify(mockIcon).setImageResource(R.drawable.ic_fingerprint_error);
+ verify(mockTextView).removeCallbacks(mFingerprintUiHelper.mResetErrorTextRunnable);
+ verify(mockTextView).postDelayed(mFingerprintUiHelper.mResetErrorTextRunnable,
+ FingerprintUiHelper.ERROR_TIMEOUT_MILLIS);
+ verify(mockIcon).postDelayed(mRunnableArgumentCaptor.capture(),
+ eq(FingerprintUiHelper.ERROR_TIMEOUT_MILLIS));
+
+ mRunnableArgumentCaptor.getValue().run();
+
+ verify(mockCallback).onError();
+ }
+
+ @Test
+ public void testOnAuthenticationSucceeded() {
+ mFingerprintUiHelper.onAuthenticationSucceeded(null);
+
+ verify(mockIcon).setImageResource(R.drawable.ic_fingerprint_success);
+ verify(mockTextView).removeCallbacks(mFingerprintUiHelper.mResetErrorTextRunnable);
+ verify(mockIcon).postDelayed(mRunnableArgumentCaptor.capture(),
+ eq(FingerprintUiHelper.SUCCESS_DELAY_MILLIS));
+
+ mRunnableArgumentCaptor.getValue().run();
+
+ verify(mockCallback).onAuthenticated();
+ }
+}
diff --git a/security/AsymmetricFingerprintDialog/build.gradle b/security/AsymmetricFingerprintDialog/build.gradle
new file mode 100644
index 0000000..2b8d1ef
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/build.gradle
@@ -0,0 +1,11 @@
+
+// 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/security/AsymmetricFingerprintDialog/buildSrc/build.gradle b/security/AsymmetricFingerprintDialog/buildSrc/build.gradle
new file mode 100644
index 0000000..8c294c2
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/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/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.jar b/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.properties b/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.properties
similarity index 79%
rename from ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.properties
rename to security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.properties
index a51db8c..203da3a 100644
--- a/ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.properties
+++ b/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue May 20 13:33:02 BST 2014
+#Mon Apr 27 11:28:32 JST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradlew b/security/AsymmetricFingerprintDialog/gradlew
similarity index 100%
copy from ui/views/Elevation/ElevationDrag/gradle/gradlew
copy to security/AsymmetricFingerprintDialog/gradlew
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradlew.bat b/security/AsymmetricFingerprintDialog/gradlew.bat
similarity index 100%
copy from ui/views/Elevation/ElevationDrag/gradle/gradlew.bat
copy to security/AsymmetricFingerprintDialog/gradlew.bat
diff --git a/security/AsymmetricFingerprintDialog/screenshots/1-purchase-screen.png b/security/AsymmetricFingerprintDialog/screenshots/1-purchase-screen.png
new file mode 100644
index 0000000..0bf03bd
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/screenshots/1-purchase-screen.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/screenshots/2-fingerprint-dialog.png b/security/AsymmetricFingerprintDialog/screenshots/2-fingerprint-dialog.png
new file mode 100644
index 0000000..5e681f9
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/screenshots/2-fingerprint-dialog.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/screenshots/3-fingerprint-authenticated.png b/security/AsymmetricFingerprintDialog/screenshots/3-fingerprint-authenticated.png
new file mode 100644
index 0000000..d485b1d
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/screenshots/3-fingerprint-authenticated.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/screenshots/4-new-fingerprint-enrolled.png b/security/AsymmetricFingerprintDialog/screenshots/4-new-fingerprint-enrolled.png
new file mode 100644
index 0000000..7dcf080
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/screenshots/4-new-fingerprint-enrolled.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/screenshots/big-icon.png b/security/AsymmetricFingerprintDialog/screenshots/big-icon.png
new file mode 100644
index 0000000..1063d0b
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/screenshots/big-icon.png
Binary files differ
diff --git a/security/AsymmetricFingerprintDialog/settings.gradle b/security/AsymmetricFingerprintDialog/settings.gradle
new file mode 100644
index 0000000..9464a35
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/settings.gradle
@@ -0,0 +1 @@
+include 'Application'
diff --git a/security/AsymmetricFingerprintDialog/template-params.xml b/security/AsymmetricFingerprintDialog/template-params.xml
new file mode 100644
index 0000000..019a132
--- /dev/null
+++ b/security/AsymmetricFingerprintDialog/template-params.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2015 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.
+-->
+<!-- TODO(thagikura) Add tests for Activity and Fragment once InstrumentationTests can be run
+ on an emulator or a device.
+ At this moment, due to the different API between the image and the SDK, they can't be launched.
+ E.g. Skipping device 'Nexus 5 - MNC', due to different API preview 'MNC' and 'android-MNC'
+ -->
+<sample>
+ <name>AsymmetricFingerprintDialog</name>
+ <group>Security</group>
+ <package>com.example.android.asymmetricfingerprintdialog</package>
+
+ <minSdk>23</minSdk>
+ <targetSdkVersion>23</targetSdkVersion>
+ <compileSdkVersion>23</compileSdkVersion>
+
+ <dependency>com.squareup.dagger:dagger:1.2.2</dependency>
+ <dependency>com.squareup.dagger:dagger-compiler:1.2.2</dependency>
+
+ <!-- TODO(thagikura) These dependencies should be created as testCompile instead of compile but
+ the template system doesn't allow androidTestCompile dependencies. Change it once fixed.
+ -->
+ <dependency>junit:junit:4.12</dependency>
+ <dependency>org.mockito:mockito-core:1.10.19</dependency>
+
+ <strings>
+ <intro>
+ <![CDATA[
+This sample demonstrates how you can use registered fingerprints to authenticate the user
+before proceeding some actions such as purchasing an item. This version uses asymmetric keys.
+ ]]>
+ </intro>
+ </strings>
+
+ <template src="base" />
+
+ <metadata>
+ <!-- Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED} -->
+ <status>DRAFT</status>
+ <categories>security</categories>
+ <technologies>Android</technologies>
+ <languages>Java</languages>
+ <solutions>Mobile</solutions>
+ <level>INTERMEDIATE</level>
+ <icon>screenshots/big-icon.png</icon>
+ <screenshots>
+ <img>screenshots/1-purchase-screen.png</img>
+ <img>screenshots/2-fingerprint-dialog.png</img>
+ <img>screenshots/3-fingerprint-authenticated.png</img>
+ <img>screenshots/4-new-fingerprint-enrolled.png</img>
+ </screenshots>
+
+ <api_refs>
+ <android>android.hardware.fingerprint.FingerprintManager</android>
+ <android>android.hardware.fingerprint.FingerprintManager.AuthenticationCallback</android>
+ <android>android.hardware.fingerprint.FingerprintManager.CryptoObject</android>
+ <android>android.security.KeyGenParameterSpec</android>
+ <android>java.security.KeyStore</android>
+ <android>java.security.Signature</android>
+ <android>java.security.KeyPairGenerator</android>
+ </api_refs>
+
+ <description>
+<![CDATA[
+A sample that demonstrates to use registered fingerprints to authenticate the user in your app
+]]>
+ </description>
+
+ <intro>
+<![CDATA[
+This sample demonstrates how you can use registered fingerprints in your app to authenticate the
+user before proceeding some actions such as purchasing an item.
+
+First you need to create an asymmetric key pair in the Android Key Store using [KeyPairGenerator][1]
+in the way that its private key can only be used after the user has authenticated with fingerprint
+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 [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.
+
+Then start listening to a fingerprint on the fingerprint sensor by calling
+[FingerprintManager.authenticate][3] with a [Signature][4] initialized with the asymmetric key pair
+created. Or alternatively you can fall back to server-side verified password as an authenticator.
+
+Once the fingerprint (or password) is verified, the
+[FingerprintManager.AuthenticationCallback#onAuthenticationSucceeded()][5] callback is called.
+
+Then you can verify the purchase transaction on server side with the public key passed from the
+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/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/fingerprint/FingerprintManager.AuthenticationCallback.html#onAuthenticationSucceeded%28android.hardware.fingerprint.FingerprintManager.AuthenticationResult%29
+]]>
+ </intro>
+ </metadata>
+</sample>
diff --git a/security/ConfirmCredential/gradle/wrapper/gradle-wrapper.properties b/security/ConfirmCredential/gradle/wrapper/gradle-wrapper.properties
index afb3296..07fc193 100644
--- a/security/ConfirmCredential/gradle/wrapper/gradle-wrapper.properties
+++ b/security/ConfirmCredential/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/security/ConfirmCredential/template-params.xml b/security/ConfirmCredential/template-params.xml
index fe142c9..d803a89 100644
--- a/security/ConfirmCredential/template-params.xml
+++ b/security/ConfirmCredential/template-params.xml
@@ -21,7 +21,7 @@
-->
<sample>
<name>Confirm Credential</name>
- <group>security</group>
+ <group>Security</group>
<package>com.example.android.confirmcredential</package>
<minSdk>23</minSdk>
@@ -81,7 +81,7 @@
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 @@
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 c954bfa..7caf9e6 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 @@
/** 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 @@
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;
- }
- createKey();
- purchaseButton.setEnabled(true);
- purchaseButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- findViewById(R.id.confirmation_message).setVisibility(View.GONE);
- findViewById(R.id.encrypted_message).setVisibility(View.GONE);
-
- // Set up the crypto object for later. The object will be authenticated by use
- // of the fingerprint.
- if (initCipher()) {
-
- // Show the fingerprint dialog. The user has the option to use the fingerprint with
- // crypto, or you can fall back to using a server-side verified password.
- mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
- boolean useFingerprintPreference = mSharedPreferences
- .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
- true);
- if (useFingerprintPreference) {
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
- } else {
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
- }
- mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
- } else {
- // This happens if the lock screen has been disabled or or a fingerprint got
- // enrolled. Thus show the dialog to authenticate with their password first
- // and ask the user if they want to authenticate with fingerprints in the
- // future
- mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
- mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
- }
- }
- });
+ setContentView(R.layout.activity_main);
+ Button purchaseButton = (Button) findViewById(R.id.purchase_button);
+ if (!mKeyguardManager.isKeyguardSecure()) {
+ // Show a message that the user hasn't set up a fingerprint or lock screen.
+ Toast.makeText(this,
+ "Secure lock screen hasn't set up.\n"
+ + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
+ Toast.LENGTH_LONG).show();
+ purchaseButton.setEnabled(false);
+ return;
}
+
+ //noinspection ResourceType
+ if (!mFingerprintManager.hasEnrolledFingerprints()) {
+ purchaseButton.setEnabled(false);
+ // This happens when no fingerprints are registered.
+ Toast.makeText(this,
+ "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ createKey();
+ purchaseButton.setEnabled(true);
+ purchaseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ findViewById(R.id.confirmation_message).setVisibility(View.GONE);
+ findViewById(R.id.encrypted_message).setVisibility(View.GONE);
+
+ // Set up the crypto object for later. The object will be authenticated by use
+ // of the fingerprint.
+ if (initCipher()) {
+
+ // Show the fingerprint dialog. The user has the option to use the fingerprint with
+ // crypto, or you can fall back to using a server-side verified password.
+ mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
+ boolean useFingerprintPreference = mSharedPreferences
+ .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
+ true);
+ if (useFingerprintPreference) {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
+ } else {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
+ }
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ } else {
+ // This happens if the lock screen has been disabled or or a fingerprint got
+ // enrolled. Thus show the dialog to authenticate with their password first
+ // and ask the user if they want to authenticate with fingerprints in the
+ // future
+ mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ }
+ }
+ });
}
/**
diff --git a/security/FingerprintDialog/gradle/wrapper/gradle-wrapper.properties b/security/FingerprintDialog/gradle/wrapper/gradle-wrapper.properties
index f943b1e..203da3a 100644
--- a/security/FingerprintDialog/gradle/wrapper/gradle-wrapper.properties
+++ b/security/FingerprintDialog/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/security/FingerprintDialog/template-params.xml b/security/FingerprintDialog/template-params.xml
index ab12696..33db81c 100644
--- a/security/FingerprintDialog/template-params.xml
+++ b/security/FingerprintDialog/template-params.xml
@@ -20,8 +20,8 @@
E.g. Skipping device 'Nexus 5 - MNC', due to different API preview 'MNC' and 'android-MNC'
-->
<sample>
- <name>Fingerprint Dialog Sample</name>
- <group>security</group>
+ <name>FingerprintDialog</name>
+ <group>Security</group>
<package>com.example.android.fingerprintdialog</package>
<minSdk>23</minSdk>
@@ -87,9 +87,9 @@
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 @@
[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 12873e8..e6244bf 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 @@
// 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 @@
// 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/security/keystore/BasicAndroidKeyStore/gradle/wrapper/gradle-wrapper.properties b/security/keystore/BasicAndroidKeyStore/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/security/keystore/BasicAndroidKeyStore/gradle/wrapper/gradle-wrapper.properties
+++ b/security/keystore/BasicAndroidKeyStore/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/sensors/BatchStepSensor/gradle/wrapper/gradle-wrapper.properties b/sensors/BatchStepSensor/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/sensors/BatchStepSensor/gradle/wrapper/gradle-wrapper.properties
+++ b/sensors/BatchStepSensor/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/system/AppUsageStatistics/Application/src/main/java/com/example/android/appusagestatistics/AppUsageStatisticsFragment.java b/system/AppUsageStatistics/Application/src/main/java/com/example/android/appusagestatistics/AppUsageStatisticsFragment.java
index 9f54d02..9a3fd5e 100644
--- a/system/AppUsageStatistics/Application/src/main/java/com/example/android/appusagestatistics/AppUsageStatisticsFragment.java
+++ b/system/AppUsageStatistics/Application/src/main/java/com/example/android/appusagestatistics/AppUsageStatisticsFragment.java
@@ -24,7 +24,6 @@
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.Fragment;
-import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
@@ -91,10 +90,9 @@
public void onViewCreated(View rootView, Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
- mLayoutManager = new LinearLayoutManager(getActivity());
mUsageListAdapter = new UsageListAdapter();
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_app_usage);
- mRecyclerView.setLayoutManager(mLayoutManager);
+ mLayoutManager = mRecyclerView.getLayoutManager();
mRecyclerView.scrollToPosition(0);
mRecyclerView.setAdapter(mUsageListAdapter);
mOpenUsageSettingButton = (Button) rootView.findViewById(R.id.button_open_usage_setting);
@@ -197,7 +195,7 @@
@Override
public int compare(UsageStats left, UsageStats right) {
- return (int) (right.getLastTimeUsed() - left.getLastTimeUsed());
+ return Long.compare(right.getLastTimeUsed(), left.getLastTimeUsed());
}
}
diff --git a/system/AppUsageStatistics/Application/src/main/res/layout/fragment_app_usage_statistics.xml b/system/AppUsageStatistics/Application/src/main/res/layout/fragment_app_usage_statistics.xml
index 1d567b7..297bf1e 100644
--- a/system/AppUsageStatistics/Application/src/main/res/layout/fragment_app_usage_statistics.xml
+++ b/system/AppUsageStatistics/Application/src/main/res/layout/fragment_app_usage_statistics.xml
@@ -16,11 +16,13 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- android:padding="@dimen/margin_medium">
+ android:padding="@dimen/margin_medium"
+ >
<Button android:id="@+id/button_open_usage_setting"
android:layout_width="wrap_content"
@@ -50,6 +52,8 @@
android:scrollbars="vertical"
android:drawSelectorOnTop="true"
android:layout_width="match_parent"
- android:layout_height="match_parent"/>
+ android:layout_height="match_parent"
+ app:layoutManager="LinearLayoutManager"
+ />
</LinearLayout>
diff --git a/system/AppUsageStatistics/gradle/wrapper/gradle-wrapper.properties b/system/AppUsageStatistics/gradle/wrapper/gradle-wrapper.properties
index 8a27ebf..cd50297 100644
--- a/system/AppUsageStatistics/gradle/wrapper/gradle-wrapper.properties
+++ b/system/AppUsageStatistics/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/system/RuntimePermissions/gradle/wrapper/gradle-wrapper.properties b/system/RuntimePermissions/gradle/wrapper/gradle-wrapper.properties
index 8a27ebf..cd50297 100644
--- a/system/RuntimePermissions/gradle/wrapper/gradle-wrapper.properties
+++ b/system/RuntimePermissions/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/system/RuntimePermissionsBasic/gradle/wrapper/gradle-wrapper.properties b/system/RuntimePermissionsBasic/gradle/wrapper/gradle-wrapper.properties
index 8a27ebf..cd50297 100644
--- a/system/RuntimePermissionsBasic/gradle/wrapper/gradle-wrapper.properties
+++ b/system/RuntimePermissionsBasic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/testing/ActivityInstrumentation/gradle/wrapper/gradle-wrapper.properties b/testing/ActivityInstrumentation/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/testing/ActivityInstrumentation/gradle/wrapper/gradle-wrapper.properties
+++ b/testing/ActivityInstrumentation/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/DrawableTinting/gradle/wrapper/gradle-wrapper.properties b/ui/DrawableTinting/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/DrawableTinting/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/DrawableTinting/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/Interpolator/gradle/wrapper/gradle-wrapper.properties b/ui/Interpolator/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/Interpolator/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/Interpolator/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/accessibility/BasicAccessibility/gradle/wrapper/gradle-wrapper.properties b/ui/accessibility/BasicAccessibility/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/accessibility/BasicAccessibility/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/accessibility/BasicAccessibility/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/actionbar/DoneBar/gradle/wrapper/gradle-wrapper.properties b/ui/actionbar/DoneBar/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/actionbar/DoneBar/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/actionbar/DoneBar/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/actionbarcompat/ActionBarCompat-Basic/gradle/wrapper/gradle-wrapper.properties b/ui/actionbarcompat/ActionBarCompat-Basic/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/actionbarcompat/ActionBarCompat-Basic/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/actionbarcompat/ActionBarCompat-Basic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/actionbarcompat/ActionBarCompat-ListPopupMenu/gradle/wrapper/gradle-wrapper.properties b/ui/actionbarcompat/ActionBarCompat-ListPopupMenu/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/actionbarcompat/ActionBarCompat-ListPopupMenu/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/actionbarcompat/ActionBarCompat-ListPopupMenu/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/actionbarcompat/ActionBarCompat-ListViewModalSelect/gradle/wrapper/gradle-wrapper.properties b/ui/actionbarcompat/ActionBarCompat-ListViewModalSelect/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/actionbarcompat/ActionBarCompat-ListViewModalSelect/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/actionbarcompat/ActionBarCompat-ListViewModalSelect/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/actionbarcompat/ActionBarCompat-ShareActionProvider/gradle/wrapper/gradle-wrapper.properties b/ui/actionbarcompat/ActionBarCompat-ShareActionProvider/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/actionbarcompat/ActionBarCompat-ShareActionProvider/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/actionbarcompat/ActionBarCompat-ShareActionProvider/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/actionbarcompat/ActionBarCompat-ShareActionProvider/template-params.xml b/ui/actionbarcompat/ActionBarCompat-ShareActionProvider/template-params.xml
index 177f448..c039113 100644
--- a/ui/actionbarcompat/ActionBarCompat-ShareActionProvider/template-params.xml
+++ b/ui/actionbarcompat/ActionBarCompat-ShareActionProvider/template-params.xml
@@ -44,8 +44,8 @@
<level>ADVANCED</level>
<icon>screenshots/icon-web.png</icon>
<screenshots>
- <img>screenshots/1-gridview.png</img>
- <img>screenshots/2-detail.png</img>
+ <img>screenshots/1-image.png</img>
+ <img>screenshots/2-text.png</img>
</screenshots>
<api_refs>
<android>android.support.v7.widget.ShareActionProvider</android>
diff --git a/ui/actionbarcompat/ActionBarCompat-Styled/gradle/wrapper/gradle-wrapper.properties b/ui/actionbarcompat/ActionBarCompat-Styled/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/actionbarcompat/ActionBarCompat-Styled/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/actionbarcompat/ActionBarCompat-Styled/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/actionbarcompat/ActionBarCompat-Styled/screenshots/1-activity.png.png b/ui/actionbarcompat/ActionBarCompat-Styled/screenshots/1-activity.png
similarity index 100%
rename from ui/actionbarcompat/ActionBarCompat-Styled/screenshots/1-activity.png.png
rename to ui/actionbarcompat/ActionBarCompat-Styled/screenshots/1-activity.png
Binary files differ
diff --git a/ui/activityscenetransition/ActivitySceneTransitionBasic/gradle/wrapper/gradle-wrapper.properties b/ui/activityscenetransition/ActivitySceneTransitionBasic/gradle/wrapper/gradle-wrapper.properties
index 46b1748..2ea35fd 100644
--- a/ui/activityscenetransition/ActivitySceneTransitionBasic/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/activityscenetransition/ActivitySceneTransitionBasic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/activitytasks/DocumentCentricApps/gradle/wrapper/gradle-wrapper.properties b/ui/activitytasks/DocumentCentricApps/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/activitytasks/DocumentCentricApps/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/activitytasks/DocumentCentricApps/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/activitytasks/DocumentCentricRelinquishIdentity/gradle/wrapper/gradle-wrapper.properties b/ui/activitytasks/DocumentCentricRelinquishIdentity/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/activitytasks/DocumentCentricRelinquishIdentity/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/activitytasks/DocumentCentricRelinquishIdentity/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/graphics/DisplayingBitmaps/Application/src/main/java/com/example/android/displayingbitmaps/ui/ImageDetailFragment.java b/ui/graphics/DisplayingBitmaps/Application/src/main/java/com/example/android/displayingbitmaps/ui/ImageDetailFragment.java
index 506729a..75d16c1 100644
--- a/ui/graphics/DisplayingBitmaps/Application/src/main/java/com/example/android/displayingbitmaps/ui/ImageDetailFragment.java
+++ b/ui/graphics/DisplayingBitmaps/Application/src/main/java/com/example/android/displayingbitmaps/ui/ImageDetailFragment.java
@@ -23,6 +23,7 @@
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
+import android.widget.ProgressBar;
import com.example.android.displayingbitmaps.R;
import com.example.android.displayingbitmaps.util.ImageFetcher;
@@ -32,10 +33,11 @@
/**
* This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}.
*/
-public class ImageDetailFragment extends Fragment {
+public class ImageDetailFragment extends Fragment implements ImageWorker.OnImageLoadedListener {
private static final String IMAGE_DATA_EXTRA = "extra_image_data";
private String mImageUrl;
private ImageView mImageView;
+ private ProgressBar mProgressBar;
private ImageFetcher mImageFetcher;
/**
@@ -75,6 +77,7 @@
// Inflate and locate the main ImageView
final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
mImageView = (ImageView) v.findViewById(R.id.imageView);
+ mProgressBar = (ProgressBar) v.findViewById(R.id.progressbar);
return v;
}
@@ -86,7 +89,7 @@
// cache can be used over all pages in the ViewPager
if (ImageDetailActivity.class.isInstance(getActivity())) {
mImageFetcher = ((ImageDetailActivity) getActivity()).getImageFetcher();
- mImageFetcher.loadImage(mImageUrl, mImageView);
+ mImageFetcher.loadImage(mImageUrl, mImageView, this);
}
// Pass clicks on the ImageView to the parent activity to handle
@@ -104,4 +107,11 @@
mImageView.setImageDrawable(null);
}
}
+
+ @Override
+ public void onImageLoaded(boolean success) {
+ // Set loading spinner to gone once image has loaded. Cloud also show
+ // an error view here if needed.
+ mProgressBar.setVisibility(View.GONE);
+ }
}
diff --git a/ui/graphics/DisplayingBitmaps/Application/src/main/java/com/example/android/displayingbitmaps/util/ImageWorker.java b/ui/graphics/DisplayingBitmaps/Application/src/main/java/com/example/android/displayingbitmaps/util/ImageWorker.java
index f44d00d..d42d3c5 100644
--- a/ui/graphics/DisplayingBitmaps/Application/src/main/java/com/example/android/displayingbitmaps/util/ImageWorker.java
+++ b/ui/graphics/DisplayingBitmaps/Application/src/main/java/com/example/android/displayingbitmaps/util/ImageWorker.java
@@ -71,8 +71,9 @@
*
* @param data The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
+ * @param listener A listener that will be called back once the image has been loaded.
*/
- public void loadImage(Object data, ImageView imageView) {
+ public void loadImage(Object data, ImageView imageView, OnImageLoadedListener listener) {
if (data == null) {
return;
}
@@ -86,9 +87,12 @@
if (value != null) {
// Bitmap found in memory cache
imageView.setImageDrawable(value);
+ if (listener != null) {
+ listener.onImageLoaded(true);
+ }
} else if (cancelPotentialWork(data, imageView)) {
//BEGIN_INCLUDE(execute_background_task)
- final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
+ final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView, listener);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(mResources, mLoadingBitmap, task);
imageView.setImageDrawable(asyncDrawable);
@@ -102,6 +106,21 @@
}
/**
+ * Load an image specified by the data parameter into an ImageView (override
+ * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and
+ * disk cache will be used if an {@link ImageCache} has been added using
+ * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the
+ * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask}
+ * will be created to asynchronously load the bitmap.
+ *
+ * @param data The URL of the image to download.
+ * @param imageView The ImageView to bind the downloaded image to.
+ */
+ public void loadImage(Object data, ImageView imageView) {
+ loadImage(data, imageView, null);
+ }
+
+ /**
* Set placeholder bitmap that shows when the the background thread is running.
*
* @param bitmap
@@ -238,10 +257,18 @@
private class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable> {
private Object mData;
private final WeakReference<ImageView> imageViewReference;
+ private final OnImageLoadedListener mOnImageLoadedListener;
public BitmapWorkerTask(Object data, ImageView imageView) {
mData = data;
imageViewReference = new WeakReference<ImageView>(imageView);
+ mOnImageLoadedListener = null;
+ }
+
+ public BitmapWorkerTask(Object data, ImageView imageView, OnImageLoadedListener listener) {
+ mData = data;
+ imageViewReference = new WeakReference<ImageView>(imageView);
+ mOnImageLoadedListener = listener;
}
/**
@@ -318,6 +345,7 @@
@Override
protected void onPostExecute(BitmapDrawable value) {
//BEGIN_INCLUDE(complete_background_work)
+ boolean success = false;
// if cancel was called on this task or the "exit early" flag is set then we're done
if (isCancelled() || mExitTasksEarly) {
value = null;
@@ -328,8 +356,12 @@
if (BuildConfig.DEBUG) {
Log.d(TAG, "onPostExecute - setting bitmap");
}
+ success = true;
setImageDrawable(imageView, value);
}
+ if (mOnImageLoadedListener != null) {
+ mOnImageLoadedListener.onImageLoaded(success);
+ }
//END_INCLUDE(complete_background_work)
}
@@ -358,6 +390,19 @@
}
/**
+ * Interface definition for callback on image loaded successfully.
+ */
+ public interface OnImageLoadedListener {
+
+ /**
+ * Called once the image has been loaded.
+ * @param success True if the image was loaded successfully, false if
+ * there was an error.
+ */
+ void onImageLoaded(boolean success);
+ }
+
+ /**
* A custom Drawable that will be attached to the imageView while the work is in progress.
* Contains a reference to the actual worker task, so that it can be stopped if a new binding is
* required, and makes sure that only the last started worker process can bind its result,
diff --git a/ui/graphics/DisplayingBitmaps/Application/src/main/res/layout/image_detail_fragment.xml b/ui/graphics/DisplayingBitmaps/Application/src/main/res/layout/image_detail_fragment.xml
index 97ac520..d8bd2f7 100644
--- a/ui/graphics/DisplayingBitmaps/Application/src/main/res/layout/image_detail_fragment.xml
+++ b/ui/graphics/DisplayingBitmaps/Application/src/main/res/layout/image_detail_fragment.xml
@@ -20,6 +20,7 @@
android:layout_height="fill_parent" >
<ProgressBar
+ android:id="@+id/progressbar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/ui/graphics/DisplayingBitmaps/README.md b/ui/graphics/DisplayingBitmaps/README.md
index c0269d8..e47b105 100644
--- a/ui/graphics/DisplayingBitmaps/README.md
+++ b/ui/graphics/DisplayingBitmaps/README.md
@@ -21,7 +21,7 @@
--------------
- Android SDK v23
-- Android Build Tools v23.0.0
+- Android Build Tools v23.0.2
- Android Support Repository
Screenshots
diff --git a/ui/graphics/DisplayingBitmaps/gradle/wrapper/gradle-wrapper.properties b/ui/graphics/DisplayingBitmaps/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/graphics/DisplayingBitmaps/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/graphics/DisplayingBitmaps/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/graphics/PdfRendererBasic/gradle/wrapper/gradle-wrapper.properties b/ui/graphics/PdfRendererBasic/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/graphics/PdfRendererBasic/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/graphics/PdfRendererBasic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/holo/BorderlessButtons/gradle/wrapper/gradle-wrapper.properties b/ui/holo/BorderlessButtons/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/holo/BorderlessButtons/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/holo/BorderlessButtons/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/lists/CustomChoiceList/gradle/wrapper/gradle-wrapper.properties b/ui/lists/CustomChoiceList/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/lists/CustomChoiceList/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/lists/CustomChoiceList/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/transition/AdapterTransition/gradle/wrapper/gradle-wrapper.properties b/ui/transition/AdapterTransition/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/transition/AdapterTransition/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/transition/AdapterTransition/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/transition/BasicTransition/gradle/wrapper/gradle-wrapper.properties b/ui/transition/BasicTransition/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/transition/BasicTransition/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/transition/BasicTransition/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/transition/CustomTransition/gradle/wrapper/gradle-wrapper.properties b/ui/transition/CustomTransition/gradle/wrapper/gradle-wrapper.properties
index 4cd80b2..fb05029 100644
--- a/ui/transition/CustomTransition/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/transition/CustomTransition/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/transition/FragmentTransition/gradle/wrapper/gradle-wrapper.properties b/ui/transition/FragmentTransition/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/transition/FragmentTransition/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/transition/FragmentTransition/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/CardView/gradle/wrapper/gradle-wrapper.properties b/ui/views/CardView/gradle/wrapper/gradle-wrapper.properties
index 78c4ddd..5fd1493 100644
--- a/ui/views/CardView/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/CardView/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/Clipping/ClippingBasic/gradle/wrapper/gradle-wrapper.properties b/ui/views/Clipping/ClippingBasic/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/views/Clipping/ClippingBasic/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/Clipping/ClippingBasic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/Elevation/ElevationBasic/gradle/wrapper/gradle-wrapper.properties b/ui/views/Elevation/ElevationBasic/gradle/wrapper/gradle-wrapper.properties
index 2aebf9c..f1b6ded 100644
--- a/ui/views/Elevation/ElevationBasic/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/Elevation/ElevationBasic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.jar b/ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 5838598..0000000
--- a/ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.jar
+++ /dev/null
Binary files differ
diff --git a/ui/views/Elevation/ElevationDrag/gradle/wrapper/gradle-wrapper.properties b/ui/views/Elevation/ElevationDrag/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/views/Elevation/ElevationDrag/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/Elevation/ElevationDrag/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradle/wrapper/gradle-wrapper.jar b/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 0087cd3..0000000
--- a/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradle/wrapper/gradle-wrapper.jar
+++ /dev/null
Binary files differ
diff --git a/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradle/wrapper/gradle-wrapper.properties b/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 12dab4c..0000000
--- a/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Wed Oct 15 14:12:11 BST 2014
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
diff --git a/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradlew b/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradlew
deleted file mode 100755
index 91a7e26..0000000
--- a/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradlew
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/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/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradlew.bat b/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradlew.bat
deleted file mode 100644
index 8a0b282..0000000
--- a/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/gradlew.bat
+++ /dev/null
@@ -1,90 +0,0 @@
-@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/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/wrapper/gradle-wrapper.properties b/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/wrapper/gradle-wrapper.properties
index a4c577a..f15b32c 100644
--- a/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/FloatingActionButton/FloatingActionButtonBasic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/HorizontalPaging/gradle/wrapper/gradle-wrapper.properties b/ui/views/HorizontalPaging/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/views/HorizontalPaging/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/HorizontalPaging/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/NavigationDrawer/Application/src/main/java/com/example/android/navigationdrawer/NavigationDrawerActivity.java b/ui/views/NavigationDrawer/Application/src/main/java/com/example/android/navigationdrawer/NavigationDrawerActivity.java
index 1176757..26d677b 100644
--- a/ui/views/NavigationDrawer/Application/src/main/java/com/example/android/navigationdrawer/NavigationDrawerActivity.java
+++ b/ui/views/NavigationDrawer/Application/src/main/java/com/example/android/navigationdrawer/NavigationDrawerActivity.java
@@ -27,7 +27,6 @@
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
-import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -89,7 +88,6 @@
mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
// improve performance by indicating the list if fixed size.
mDrawerList.setHasFixedSize(true);
- mDrawerList.setLayoutManager(new LinearLayoutManager(this));
// set up the drawer's list view with items and click listener
mDrawerList.setAdapter(new PlanetAdapter(mPlanetTitles, this));
diff --git a/ui/views/NavigationDrawer/Application/src/main/res/layout/activity_navigation_drawer.xml b/ui/views/NavigationDrawer/Application/src/main/res/layout/activity_navigation_drawer.xml
index 4e61639..673ef63 100644
--- a/ui/views/NavigationDrawer/Application/src/main/res/layout/activity_navigation_drawer.xml
+++ b/ui/views/NavigationDrawer/Application/src/main/res/layout/activity_navigation_drawer.xml
@@ -19,9 +19,12 @@
<!-- A DrawerLayout is intended to be used as the top-level content view using match_parent for both width and height to consume the full space available. -->
<android.support.v4.widget.DrawerLayout
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:id="@+id/drawer_layout"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ tools:openDrawer="start" >
<!-- As the main content view, the view below consumes the entire
space available using match_parent in both dimensions. -->
@@ -44,5 +47,6 @@
android:layout_gravity="left|start"
android:choiceMode="singleChoice"
android:divider="@null"
+ app:layoutManager="LinearLayoutManager"
/>
-</android.support.v4.widget.DrawerLayout>
\ No newline at end of file
+</android.support.v4.widget.DrawerLayout>
diff --git a/ui/views/NavigationDrawer/README.md b/ui/views/NavigationDrawer/README.md
index 093ef66..ce9cfe2 100644
--- a/ui/views/NavigationDrawer/README.md
+++ b/ui/views/NavigationDrawer/README.md
@@ -8,8 +8,8 @@
Pre-requisites
--------------
-- Android SDK v21
-- Android Build Tools v23.0.0
+- Android SDK v23
+- Android Build Tools v23.0.2
- Android Support Repository
Getting Started
diff --git a/ui/views/NavigationDrawer/gradle/wrapper/gradle-wrapper.properties b/ui/views/NavigationDrawer/gradle/wrapper/gradle-wrapper.properties
index 5f735f1..5288c6d 100644
--- a/ui/views/NavigationDrawer/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/NavigationDrawer/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/NavigationDrawer/template-params.xml b/ui/views/NavigationDrawer/template-params.xml
index 559f32e..e85b0dd 100644
--- a/ui/views/NavigationDrawer/template-params.xml
+++ b/ui/views/NavigationDrawer/template-params.xml
@@ -26,13 +26,13 @@
<!-- change minSdk if needed-->
<minSdk>21</minSdk>
- <compileSdkVersion>21</compileSdkVersion>
+ <compileSdkVersion>23</compileSdkVersion>
- <dependency>com.android.support:support-v13:21.0.2</dependency>
- <dependency>com.android.support:appcompat-v7:21.0.2</dependency>
- <dependency>com.android.support:recyclerview-v7:21.0.2</dependency>
- <dependency>com.android.support:cardview-v7:21.0.2</dependency>
+ <dependency>com.android.support:support-v13:23.1.1</dependency>
+ <dependency>com.android.support:appcompat-v7:23.1.1</dependency>
+ <dependency>com.android.support:recyclerview-v7:23.1.1</dependency>
+ <dependency>com.android.support:cardview-v7:23.1.1</dependency>
<strings>
<intro>
diff --git a/ui/views/RecyclerView/gradle/wrapper/gradle-wrapper.properties b/ui/views/RecyclerView/gradle/wrapper/gradle-wrapper.properties
index 0e0523e..8505d8d 100644
--- a/ui/views/RecyclerView/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/RecyclerView/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/RevealEffect/RevealEffectBasic/gradle/gradle/wrapper/gradle-wrapper.jar b/ui/views/RevealEffect/RevealEffectBasic/gradle/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 5838598..0000000
--- a/ui/views/RevealEffect/RevealEffectBasic/gradle/gradle/wrapper/gradle-wrapper.jar
+++ /dev/null
Binary files differ
diff --git a/ui/views/RevealEffect/RevealEffectBasic/gradle/gradle/wrapper/gradle-wrapper.properties b/ui/views/RevealEffect/RevealEffectBasic/gradle/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 62a8a6c..0000000
--- a/ui/views/RevealEffect/RevealEffectBasic/gradle/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Fri May 23 13:44:29 BST 2014
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
diff --git a/ui/views/RevealEffect/RevealEffectBasic/gradle/gradlew b/ui/views/RevealEffect/RevealEffectBasic/gradle/gradlew
deleted file mode 100755
index 91a7e26..0000000
--- a/ui/views/RevealEffect/RevealEffectBasic/gradle/gradlew
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/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/ui/views/RevealEffect/RevealEffectBasic/gradle/gradlew.bat b/ui/views/RevealEffect/RevealEffectBasic/gradle/gradlew.bat
deleted file mode 100644
index aec9973..0000000
--- a/ui/views/RevealEffect/RevealEffectBasic/gradle/gradlew.bat
+++ /dev/null
@@ -1,90 +0,0 @@
-@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/ui/views/RevealEffect/RevealEffectBasic/gradle/wrapper/gradle-wrapper.properties b/ui/views/RevealEffect/RevealEffectBasic/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/views/RevealEffect/RevealEffectBasic/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/RevealEffect/RevealEffectBasic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/SlidingTabs/SlidingTabsBasic/gradle/wrapper/gradle-wrapper.properties b/ui/views/SlidingTabs/SlidingTabsBasic/gradle/wrapper/gradle-wrapper.properties
index c25449e..fb05029 100644
--- a/ui/views/SlidingTabs/SlidingTabsBasic/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/SlidingTabs/SlidingTabsBasic/gradle/wrapper/gradle-wrapper.properties
@@ -3,5 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
-
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/SlidingTabs/SlidingTabsColors/gradle/wrapper/gradle-wrapper.properties b/ui/views/SlidingTabs/SlidingTabsColors/gradle/wrapper/gradle-wrapper.properties
index 0c8edb4..fac5adc 100644
--- a/ui/views/SlidingTabs/SlidingTabsColors/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/SlidingTabs/SlidingTabsColors/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/SwipeRefreshLayout/SwipeRefreshLayoutBasic/gradle/wrapper/gradle-wrapper.properties b/ui/views/SwipeRefreshLayout/SwipeRefreshLayoutBasic/gradle/wrapper/gradle-wrapper.properties
index 2727e36..653fca1 100644
--- a/ui/views/SwipeRefreshLayout/SwipeRefreshLayoutBasic/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/SwipeRefreshLayout/SwipeRefreshLayoutBasic/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/SwipeRefreshLayout/SwipeRefreshListFragment/gradle/wrapper/gradle-wrapper.properties b/ui/views/SwipeRefreshLayout/SwipeRefreshListFragment/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/views/SwipeRefreshLayout/SwipeRefreshListFragment/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/SwipeRefreshLayout/SwipeRefreshListFragment/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/SwipeRefreshLayout/SwipeRefreshMultipleViews/gradle/wrapper/gradle-wrapper.properties b/ui/views/SwipeRefreshLayout/SwipeRefreshMultipleViews/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/views/SwipeRefreshLayout/SwipeRefreshMultipleViews/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/SwipeRefreshLayout/SwipeRefreshMultipleViews/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/TextSwitcher/gradle/wrapper/gradle-wrapper.properties b/ui/views/TextSwitcher/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/views/TextSwitcher/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/views/TextSwitcher/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/window/AdvancedImmersiveMode/gradle/wrapper/gradle-wrapper.properties b/ui/window/AdvancedImmersiveMode/gradle/wrapper/gradle-wrapper.properties
index faba5b7..699a491 100644
--- a/ui/window/AdvancedImmersiveMode/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/window/AdvancedImmersiveMode/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/window/BasicImmersiveMode/gradle/wrapper/gradle-wrapper.properties b/ui/window/BasicImmersiveMode/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/window/BasicImmersiveMode/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/window/BasicImmersiveMode/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/window/ImmersiveMode/gradle/wrapper/gradle-wrapper.properties b/ui/window/ImmersiveMode/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/ui/window/ImmersiveMode/gradle/wrapper/gradle-wrapper.properties
+++ b/ui/window/ImmersiveMode/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/views/TextLinkify/gradle/wrapper/gradle-wrapper.properties b/views/TextLinkify/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/views/TextLinkify/gradle/wrapper/gradle-wrapper.properties
+++ b/views/TextLinkify/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/AgendaData/Application/src/main/AndroidManifest.xml b/wearable/wear/AgendaData/Application/src/main/AndroidManifest.xml
index aa8a14a..ad6cccd 100644
--- a/wearable/wear/AgendaData/Application/src/main/AndroidManifest.xml
+++ b/wearable/wear/AgendaData/Application/src/main/AndroidManifest.xml
@@ -18,16 +18,22 @@
package="com.example.android.wearable.agendadata">
<uses-sdk android:minSdkVersion="18"
- android:targetSdkVersion="21" />
+ 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 these permissions is only requested at run time. -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
+ <!-- END_INCLUDE(manifest) -->
+
<application
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name"
- android:theme="@android:style/Theme.Holo.Light"
- >
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppCompat.Light">
<meta-data
android:name="com.google.android.gms.version"
diff --git a/wearable/wear/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/CalendarQueryService.java b/wearable/wear/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/CalendarQueryService.java
index c39a5ed..9d65b7e 100644
--- a/wearable/wear/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/CalendarQueryService.java
+++ b/wearable/wear/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/CalendarQueryService.java
@@ -250,6 +250,11 @@
public PutDataMapRequest toPutDataMapRequest(){
final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(
makeDataItemPath(eventId, begin));
+ /* In most cases (as in this one), you don't need your DataItem appear instantly. By
+ default, delivery of normal DataItems to the Wear network might be delayed in order to
+ improve battery life for user devices. However, if you can't tolerate a delay in the
+ sync of your DataItems, you can mark them as urgent via setUrgent().
+ */
DataMap data = putDataMapRequest.getDataMap();
data.putString(DATA_ITEM_URI, putDataMapRequest.getUri().toString());
data.putLong(ID, id);
diff --git a/wearable/wear/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/MainActivity.java b/wearable/wear/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/MainActivity.java
index 34e327b..6a9678a 100644
--- a/wearable/wear/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/MainActivity.java
+++ b/wearable/wear/AgendaData/Application/src/main/java/com/example/android/wearable/agendadata/MainActivity.java
@@ -18,11 +18,16 @@
import static com.example.android.wearable.agendadata.Constants.TAG;
-import android.app.Activity;
+import android.Manifest;
import android.content.Intent;
import android.content.IntentSender;
+import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ScrollView;
@@ -40,27 +45,42 @@
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable;
-import java.util.List;
+/**
+ * Syncs or deletes calendar events (event time, description, and background image) to your
+ * Wearable via the Wearable DataApi at the click of a button. Includes code to handle dynamic M+
+ * permissions as well.
+ */
+public class MainActivity extends AppCompatActivity implements
+ NodeApi.NodeListener,
+ ConnectionCallbacks,
+ OnConnectionFailedListener,
+ ActivityCompat.OnRequestPermissionsResultCallback {
-public class MainActivity extends Activity implements NodeApi.NodeListener, ConnectionCallbacks,
- OnConnectionFailedListener {
-
- /** Request code for launching the Intent to resolve Google Play services errors. */
+ /* Request code for launching the Intent to resolve Google Play services errors. */
private static final int REQUEST_RESOLVE_ERROR = 1000;
+ /* Id to identify calendar and contact permissions request. */
+ private static final int REQUEST_CALENDAR_AND_CONTACTS = 0;
+
+
private GoogleApiClient mGoogleApiClient;
private boolean mResolvingError = false;
private TextView mLogTextView;
ScrollView mScroller;
+ private View mLayout;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
+ mLayout = findViewById(R.id.main_layout);
+
mLogTextView = (TextView) findViewById(R.id.log);
mScroller = (ScrollView) findViewById(R.id.scroller);
+
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
@@ -85,11 +105,94 @@
super.onStop();
}
- public void onGetEventsClicked(View v) {
+ public void onGetEventsClicked(View view) {
+
+ Log.i(TAG, "onGetEventsClicked(): Checking permission.");
+
+ // BEGIN_INCLUDE(calendar_and_contact_permissions)
+ // Check if the Calendar permission is already available.
+ boolean calendarApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR)
+ == PackageManager.PERMISSION_GRANTED;
+
+ boolean contactsApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (!calendarApproved || !contactsApproved) {
+ // Calendar and/or Contact permissions have not been granted.
+ requestCalendarAndContactPermissions();
+
+ } else {
+ // Calendar permissions is already available, start service
+ Log.i(TAG, "Permissions already granted. Starting service.");
+ pushCalendarToWear();
+ }
+ // END_INCLUDE(calendar_and_contact_permissions)
+
+ }
+
+ private void pushCalendarToWear() {
startService(new Intent(this, CalendarQueryService.class));
}
- public void onDeleteEventsClicked(View v) {
+ /*
+ * Requests Calendar and Contact permissions.
+ * If the permission has been denied previously, a SnackBar will prompt the user to grant the
+ * permission, otherwise it is requested directly.
+ */
+ private void requestCalendarAndContactPermissions() {
+ Log.i(TAG, "CALENDAR permission has NOT been granted. Requesting permission.");
+
+ // BEGIN_INCLUDE(calendar_and_contact_permissions_request)
+
+ boolean showCalendarPermissionRationale =
+ ActivityCompat.shouldShowRequestPermissionRationale(this,
+ Manifest.permission.READ_CALENDAR);
+ boolean showContactsPermissionRationale =
+ ActivityCompat.shouldShowRequestPermissionRationale(this,
+ Manifest.permission.READ_CONTACTS);
+
+ if (showCalendarPermissionRationale || showContactsPermissionRationale) {
+ /*
+ * Provide an additional rationale to the user if the permission was not granted and
+ * the user would benefit from additional context for the use of the permission. For
+ * example, if the user has previously denied the permission.
+ */
+ Log.i(TAG, "Display calendar & contact permissions rationale for additional context.");
+
+ Snackbar.make(mLayout, R.string.permissions_rationale,
+ Snackbar.LENGTH_INDEFINITE)
+ .setAction(R.string.ok, new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ActivityCompat.requestPermissions(MainActivity.this,
+ new String[] {
+ Manifest.permission.READ_CALENDAR,
+ Manifest.permission.READ_CONTACTS},
+ REQUEST_CALENDAR_AND_CONTACTS);
+ }
+ })
+ .show();
+
+
+ } else {
+
+ // Calendar/Contact permissions have not been granted yet. Request it directly.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{
+ Manifest.permission.READ_CALENDAR,
+ Manifest.permission.READ_CONTACTS
+ },
+ REQUEST_CALENDAR_AND_CONTACTS);
+ }
+ // END_INCLUDE(calendar_and_contact_permissions_request)
+ }
+
+
+
+ public void onDeleteEventsClicked(View view) {
if (mGoogleApiClient.isConnected()) {
Wearable.DataApi.getDataItems(mGoogleApiClient)
.setResultCallback(new ResultCallback<DataItemBuffer>() {
@@ -100,9 +203,8 @@
deleteDataItems(result);
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG,"onDeleteEventsClicked(): failed to get Data "
+ Log.d(TAG, "onDeleteEventsClicked(): failed to get Data "
+ "Items");
-
}
}
} finally {
@@ -120,9 +222,11 @@
if (mGoogleApiClient.isConnected()) {
for (final DataItem dataItem : dataItemList) {
final Uri dataItemUri = dataItem.getUri();
- // In a real calendar application, this might delete the corresponding calendar
- // event from the calendar data provider. In this sample, we simply delete the
- // DataItem, but leave the phone's calendar data intact.
+ /*
+ * In a real calendar application, this might delete the corresponding calendar
+ * events from the calendar data provider. However, we simply delete the DataItem,
+ * but leave the phone's calendar data intact for this simple sample.
+ */
Wearable.DataApi.deleteDataItems(mGoogleApiClient, dataItemUri)
.setResultCallback(new ResultCallback<DataApi.DeleteDataItemsResult>() {
@Override
@@ -141,17 +245,6 @@
}
}
- private void appendLog(final String s) {
- mLogTextView.post(new Runnable() {
- @Override
- public void run() {
- mLogTextView.append(s);
- mLogTextView.append("\n");
- mScroller.fullScroll(View.FOCUS_DOWN);
- }
- });
- }
-
@Override
public void onPeerConnected(Node peer) {
appendLog("Device connected");
@@ -165,7 +258,7 @@
@Override
public void onConnected(Bundle connectionHint) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Connected to Google Api Service");
+ Log.d(TAG, "Connected to Google Api Service.");
}
mResolvingError = false;
Wearable.NodeApi.addListener(mGoogleApiClient, this);
@@ -193,10 +286,77 @@
result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
} catch (IntentSender.SendIntentException e) {
// There was an error with the resolution intent. Try again.
+ mResolvingError = false;
mGoogleApiClient.connect();
}
} else {
mResolvingError = false;
}
}
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onActivityResult request/result codes: " + requestCode + "/" + resultCode);
+ }
+
+ if (requestCode == REQUEST_RESOLVE_ERROR) {
+ mResolvingError = false;
+ if (resultCode == RESULT_OK) {
+ // Make sure the app is not already connected or attempting to connect
+ if (!mGoogleApiClient.isConnecting() && !mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.connect();
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onRequestPermissionsResult(): " + permissions);
+ }
+
+ if (requestCode == REQUEST_CALENDAR_AND_CONTACTS) {
+ // BEGIN_INCLUDE(permissions_result)
+ // Received permission result for calendar permission.
+ Log.i(TAG, "Received response for Calendar permission request.");
+
+ // Check if all required permissions have been granted.
+ if ((grantResults.length == 2)
+ && (grantResults[0] == PackageManager.PERMISSION_GRANTED)
+ && (grantResults[1] == PackageManager.PERMISSION_GRANTED)) {
+ // Calendar/Contact permissions have been granted, pull all calendar events
+ Log.i(TAG, "All permission has now been granted. Showing preview.");
+ Snackbar.make(mLayout, R.string.permisions_granted, Snackbar.LENGTH_SHORT).show();
+
+ pushCalendarToWear();
+
+ } else {
+ Log.i(TAG, "CALENDAR and/or CONTACT permissions were NOT granted.");
+ Snackbar.make(mLayout, R.string.permissions_denied, Snackbar.LENGTH_SHORT).show();
+ }
+ // END_INCLUDE(permissions_result)
+
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void appendLog(final String s) {
+ mLogTextView.post(new Runnable() {
+ @Override
+ public void run() {
+ mLogTextView.append(s);
+ mLogTextView.append("\n");
+ mScroller.fullScroll(View.FOCUS_DOWN);
+ }
+ });
+ }
}
diff --git a/wearable/wear/AgendaData/Application/src/main/res/layout/main.xml b/wearable/wear/AgendaData/Application/src/main/res/layout/main.xml
index 8e82cdd..57fc99a 100644
--- a/wearable/wear/AgendaData/Application/src/main/res/layout/main.xml
+++ b/wearable/wear/AgendaData/Application/src/main/res/layout/main.xml
@@ -15,10 +15,17 @@
-->
<LinearLayout
+ android:id="@+id/main_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/vertical_page_margin"
+ android:paddingLeft="@dimen/horizontal_page_margin"
+ android:paddingRight="@dimen/horizontal_page_margin"
+ android:paddingTop="@dimen/vertical_page_margin"
+ tools:context=".MainActivity">
<Button
android:layout_height="wrap_content"
@@ -37,7 +44,7 @@
android:textAppearance="?android:textAppearanceLarge"
android:layout_marginTop="6dp"
android:padding="6dp"
- android:text="@string/log"
+ android:text="@string/log_label"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:background="@android:color/black"/>
diff --git a/wearable/wear/AgendaData/Application/src/main/res/values/strings.xml b/wearable/wear/AgendaData/Application/src/main/res/values/strings.xml
index 9969f4f..84cb60d 100644
--- a/wearable/wear/AgendaData/Application/src/main/res/values/strings.xml
+++ b/wearable/wear/AgendaData/Application/src/main/res/values/strings.xml
@@ -17,5 +17,9 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="get_events">Sync calendar events to wearable</string>
<string name="delete_events">Delete calendar events from wearable</string>
- <string name="log">Log</string>
+ <string name="log_label">Deletion Log:</string>
+ <string name="permisions_granted">Permissions granted. Send Calendar events to Wear device.</string>
+ <string name="permissions_denied">Permission requests were denied. Can\'t send calendar events.</string>
+ <string name="permissions_rationale"><![CDATA[Calendar & Contact permissions are required to push calendar to Wear device.]]></string>
+ <string name="ok">OK</string>
</resources>
diff --git a/wearable/wear/AgendaData/Wearable/src/main/AndroidManifest.xml b/wearable/wear/AgendaData/Wearable/src/main/AndroidManifest.xml
index dcab622..e6dbab7 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/Wearable/src/main/java/com/example/android/wearable/agendadata/HomeListenerService.java b/wearable/wear/AgendaData/Wearable/src/main/java/com/example/android/wearable/agendadata/HomeListenerService.java
index 0cbda71..02a3861 100644
--- a/wearable/wear/AgendaData/Wearable/src/main/java/com/example/android/wearable/agendadata/HomeListenerService.java
+++ b/wearable/wear/AgendaData/Wearable/src/main/java/com/example/android/wearable/agendadata/HomeListenerService.java
@@ -72,7 +72,7 @@
if (event.getType() == DataEvent.TYPE_DELETED) {
deleteDataItem(event.getDataItem());
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
- UpdateNotificationForDataItem(event.getDataItem());
+ updateNotificationForDataItem(event.getDataItem());
}
}
}
@@ -89,7 +89,7 @@
/**
* Posts a local notification to show calendar card.
*/
- private void UpdateNotificationForDataItem(DataItem dataItem) {
+ private void updateNotificationForDataItem(DataItem dataItem) {
DataMapItem mapDataItem = DataMapItem.fromDataItem(dataItem);
DataMap data = mapDataItem.getDataMap();
diff --git a/wearable/wear/AgendaData/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/AgendaData/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/AgendaData/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/AgendaData/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/AgendaData/template-params.xml b/wearable/wear/AgendaData/template-params.xml
index 3331da2..a6b4882 100644
--- a/wearable/wear/AgendaData/template-params.xml
+++ b/wearable/wear/AgendaData/template-params.xml
@@ -23,7 +23,10 @@
<package>com.example.android.wearable.agendadata</package>
<minSdk>18</minSdk>
- <targetSdkVersion>22</targetSdkVersion>
+ <targetSdkVersion>23</targetSdkVersion>
+ <targetSdkVersionWear>22</targetSdkVersionWear>
+
+ <dependency>com.android.support:design:23.1.1</dependency>
<wearable>
<has_handheld_app>true</has_handheld_app>
@@ -33,10 +36,10 @@
<intro>
<![CDATA[
Syncs calendar events to your wearable at the press of a button, using the Wearable
- DataApi to transmit data such as event time, description, and background image. The DataItems can be
- deleted individually via an action on the event notifications, or all at once via a button on the
- companion. When deleted using the notification action, a ConfirmationActivity is used to indicate
- success or failure.
+ DataApi to transmit data such as event time, description, and background image. The
+ DataItems can be deleted individually via an action on the event notifications, or all
+ at once via a button on the companion. When deleted using the notification action, a
+ ConfirmationActivity is used to indicate success or failure.
]]>
</intro>
</strings>
@@ -57,8 +60,8 @@
</screenshots>
<api_refs>
<android>android.app.IntentService</android>
- <ext>com.google.android.gms.wearable.DataApi</ext>
- <ext>com.google.android.gms.wearable.Node</ext>
+ <ext>gms:com.google.android.gms.wearable.DataApi</ext>
+ <ext>gms:com.google.android.gms.wearable.Node</ext>
</api_refs>
<description>
<![CDATA[
diff --git a/wearable/wear/AlwaysOn/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/AlwaysOn/gradle/wrapper/gradle-wrapper.properties
index 7d3b483..4364027 100644
--- a/wearable/wear/AlwaysOn/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/AlwaysOn/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/AlwaysOn/template-params.xml b/wearable/wear/AlwaysOn/template-params.xml
index e5794ee..b3aac9a 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 @@
]]>
</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 e80846d..ed1cec3 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/Application/src/main/java/com/example/android/wearable/datalayer/MainActivity.java b/wearable/wear/DataLayer/Application/src/main/java/com/example/android/wearable/datalayer/MainActivity.java
index 1c67c0e..4afeec9 100644
--- a/wearable/wear/DataLayer/Application/src/main/java/com/example/android/wearable/datalayer/MainActivity.java
+++ b/wearable/wear/DataLayer/Application/src/main/java/com/example/android/wearable/datalayer/MainActivity.java
@@ -381,7 +381,9 @@
public void run() {
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(COUNT_PATH);
putDataMapRequest.getDataMap().putInt(COUNT_KEY, count++);
+
PutDataRequest request = putDataMapRequest.asPutDataRequest();
+ request.setUrgent();
LOGD(TAG, "Generating DataItem: " + request);
if (!mGoogleApiClient.isConnected()) {
@@ -442,6 +444,8 @@
dataMap.getDataMap().putAsset(IMAGE_KEY, asset);
dataMap.getDataMap().putLong("time", new Date().getTime());
PutDataRequest request = dataMap.asPutDataRequest();
+ request.setUrgent();
+
Wearable.DataApi.putDataItem(mGoogleApiClient, request)
.setResultCallback(new ResultCallback<DataItemResult>() {
@Override
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 678e428..b3cb253 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.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.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 @@
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 @@
@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 @@
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 @@
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 @@
* 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 @@
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 @@
});
}
- /**
- * 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 @@
}
}
+
+ /*
+ * 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/DataLayer/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/DataLayer/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/DataLayer/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/DataLayer/template-params.xml b/wearable/wear/DataLayer/template-params.xml
index f40b9d6..6df31f5 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>
@@ -50,9 +51,9 @@
<img>screenshots/wearable_background_image.png</img>
</screenshots>
<api_refs>
- <ext>com.google.android.gms.wearable.DataApi</ext>
- <ext>com.google.android.gms.wearable.DataEvent</ext>
- <ext>com.google.android.gms.wearable.WearableListenerService</ext>
+ <ext>gms:com.google.android.gms.wearable.DataApi</ext>
+ <ext>gms:com.google.android.gms.wearable.DataEvent</ext>
+ <ext>gms:com.google.android.gms.wearable.WearableListenerService</ext>
</api_refs>
<description>
<![CDATA[
diff --git a/wearable/wear/DelayedConfirmation/Application/src/main/AndroidManifest.xml b/wearable/wear/DelayedConfirmation/Application/src/main/AndroidManifest.xml
index d8060a8..e3e6de1 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/DelayedConfirmation/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/DelayedConfirmation/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/DelayedConfirmation/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/DelayedConfirmation/template-params.xml b/wearable/wear/DelayedConfirmation/template-params.xml
index 5f77d65..cfc3957 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>
@@ -45,4 +46,9 @@
<template src="base"/>
<template src="Wear"/>
+ <metadata>
+ <status>DEPRECATED</status>
+ <categories>Wearable</categories>
+ </metadata>
+
</sample>
diff --git a/wearable/wear/ElizaChat/Application/src/main/AndroidManifest.xml b/wearable/wear/ElizaChat/Application/src/main/AndroidManifest.xml
index 8f35c56..b544ed0 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 3bef19c..2406668 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 @@
.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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/ElizaChat/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/ElizaChat/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/ElizaChat/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/ElizaChat/template-params.xml b/wearable/wear/ElizaChat/template-params.xml
index ea26b5c..19c8129 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>
@@ -39,7 +39,7 @@
<template src="base"/>
<metadata>
- <status>PUBLISHED</status>
+ <status>DEPRECATED</status>
<categories>Wearable</categories>
<technologies>Android</technologies>
<languages>Java</languages>
diff --git a/wearable/wear/EmbeddedApp/LICENSE b/wearable/wear/EmbeddedApp/LICENSE
index 1af981f..4f22946 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 4863d66..aab1348 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/EmbeddedApp/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/EmbeddedApp/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/EmbeddedApp/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/EmbeddedApp/template-params.xml b/wearable/wear/EmbeddedApp/template-params.xml
index 13186e5..8f7f7c7 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>
@@ -41,7 +42,7 @@
<template src="Wear"/>
<metadata>
- <status>PUBLISHED</status>
+ <status>DEPRECATED</status>
<categories>Wearable</categories>
<technologies>Android</technologies>
<languages>Java</languages>
diff --git a/wearable/wear/FindMyPhone/Application/src/main/AndroidManifest.xml b/wearable/wear/FindMyPhone/Application/src/main/AndroidManifest.xml
index af108af..a59cd7d 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/Wearable/src/main/java/com/example/android/wearable/findphone/FindPhoneService.java b/wearable/wear/FindMyPhone/Wearable/src/main/java/com/example/android/wearable/findphone/FindPhoneService.java
index c6c6d67..a51a9b2 100644
--- a/wearable/wear/FindMyPhone/Wearable/src/main/java/com/example/android/wearable/findphone/FindPhoneService.java
+++ b/wearable/wear/FindMyPhone/Wearable/src/main/java/com/example/android/wearable/findphone/FindPhoneService.java
@@ -100,6 +100,7 @@
// when it receives the change.
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(PATH_SOUND_ALARM);
putDataMapRequest.getDataMap().putBoolean(FIELD_ALARM_ON, alarmOn);
+ putDataMapRequest.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, putDataMapRequest.asPutDataRequest())
.await();
} else {
diff --git a/wearable/wear/FindMyPhone/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/FindMyPhone/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/FindMyPhone/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/FindMyPhone/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/FindMyPhone/template-params.xml b/wearable/wear/FindMyPhone/template-params.xml
index e8d71c6..8443b77 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>
@@ -43,4 +44,9 @@
<template src="base"/>
<template src="Wear"/>
+ <metadata>
+ <status>DEPRECATED</status>
+ <categories>Wearable</categories>
+ </metadata>
+
</sample>
diff --git a/wearable/wear/Flashlight/Wearable/src/main/AndroidManifest.xml b/wearable/wear/Flashlight/Wearable/src/main/AndroidManifest.xml
index 738ba9d..1eb15d0 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/Flashlight/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/Flashlight/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/Flashlight/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/Flashlight/template-params.xml b/wearable/wear/Flashlight/template-params.xml
index e1ec869..9af6eb4 100644
--- a/wearable/wear/Flashlight/template-params.xml
+++ b/wearable/wear/Flashlight/template-params.xml
@@ -22,21 +22,21 @@
<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>
<metadata>
- <status>PUBLISHED</status>
+ <status>DEPRECATED</status>
<categories>Wearable</categories>
<technologies>Android</technologies>
<language>Java</language>
diff --git a/wearable/wear/Geofencing/Application/src/main/AndroidManifest.xml b/wearable/wear/Geofencing/Application/src/main/AndroidManifest.xml
index d07a265..d1eabc3 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/Application/src/main/java/com/example/android/wearable/geofencing/GeofenceTransitionsIntentService.java b/wearable/wear/Geofencing/Application/src/main/java/com/example/android/wearable/geofencing/GeofenceTransitionsIntentService.java
index 53117e3..e3f6139 100644
--- a/wearable/wear/Geofencing/Application/src/main/java/com/example/android/wearable/geofencing/GeofenceTransitionsIntentService.java
+++ b/wearable/wear/Geofencing/Application/src/main/java/com/example/android/wearable/geofencing/GeofenceTransitionsIntentService.java
@@ -89,6 +89,7 @@
final PutDataMapRequest putDataMapRequest =
PutDataMapRequest.create(GEOFENCE_DATA_ITEM_PATH);
putDataMapRequest.getDataMap().putString(KEY_GEOFENCE_ID, triggeredGeoFenceId);
+ putDataMapRequest.setUrgent();
if (mGoogleApiClient.isConnected()) {
Wearable.DataApi.putDataItem(
mGoogleApiClient, putDataMapRequest.asPutDataRequest()).await();
diff --git a/wearable/wear/Geofencing/Wearable/src/main/AndroidManifest.xml b/wearable/wear/Geofencing/Wearable/src/main/AndroidManifest.xml
index 082f396..f25cc44 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/Geofencing/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/Geofencing/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/Geofencing/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/Geofencing/template-params.xml b/wearable/wear/Geofencing/template-params.xml
index 00fd3b3..26f0b64 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</dependency>
<wearable>
<has_handheld_app>true</has_handheld_app>
@@ -46,7 +47,7 @@
<template src="Wear"/>
<metadata>
- <status>PUBLISHED</status>
+ <status>DEPRECATED</status>
<categories>Wearable, Sensors</categories>
<technologies>Android</technologies>
<languages>Java</languages>
diff --git a/wearable/wear/GridViewPager/Wearable/src/main/AndroidManifest.xml b/wearable/wear/GridViewPager/Wearable/src/main/AndroidManifest.xml
index 5c362dc..e25cd63 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/GridViewPager/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/GridViewPager/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/GridViewPager/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/GridViewPager/template-params.xml b/wearable/wear/GridViewPager/template-params.xml
index f6dc567..66e4bf4 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 02b7a4f..f6cf220 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/JumpingJack/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/JumpingJack/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/JumpingJack/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/JumpingJack/template-params.xml b/wearable/wear/JumpingJack/template-params.xml
index 692a6a5..9512f32 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 @@
[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 3f1274d..6a17ad8 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 34a29ff..a446fd9 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/Notifications/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/Notifications/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/Notifications/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/Notifications/template-params.xml b/wearable/wear/Notifications/template-params.xml
index c4936ea..64d2e5b 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 801a473..8fabd42 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/Application/src/main/java/com/example/android/wearable/quiz/MainActivity.java b/wearable/wear/Quiz/Application/src/main/java/com/example/android/wearable/quiz/MainActivity.java
index de8eb74..d1e2d73 100644
--- a/wearable/wear/Quiz/Application/src/main/java/com/example/android/wearable/quiz/MainActivity.java
+++ b/wearable/wear/Quiz/Application/src/main/java/com/example/android/wearable/quiz/MainActivity.java
@@ -248,7 +248,9 @@
dataMap.putInt(QUESTION_INDEX, questionIndex);
dataMap.putStringArray(ANSWERS, answers);
dataMap.putInt(CORRECT_ANSWER_INDEX, correctAnswerIndex);
- return request.asPutDataRequest();
+ PutDataRequest putDataRequest = request.asPutDataRequest();
+ putDataRequest.setUrgent();
+ return putDataRequest;
}
}
@@ -496,7 +498,10 @@
dataMap.putBoolean(QUESTION_WAS_DELETED, false);
if (!mHasQuestionBeenAsked && dataMap.getInt(QUESTION_INDEX) == 0) {
// Ask the first question now.
- Wearable.DataApi.putDataItem(mGoogleApiClient, request.asPutDataRequest());
+ PutDataRequest putDataRequest = request.asPutDataRequest();
+ // Set to high priority in case it isn't already.
+ putDataRequest.setUrgent();
+ Wearable.DataApi.putDataItem(mGoogleApiClient, putDataRequest);
setHasQuestionBeenAsked(true);
} else {
// Enqueue future questions.
diff --git a/wearable/wear/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/DeleteQuestionService.java b/wearable/wear/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/DeleteQuestionService.java
index 353903c..d715411 100644
--- a/wearable/wear/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/DeleteQuestionService.java
+++ b/wearable/wear/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/DeleteQuestionService.java
@@ -76,6 +76,7 @@
DataMap dataMap = putDataMapRequest.getDataMap();
dataMap.putBoolean(QUESTION_WAS_DELETED, true);
PutDataRequest request = putDataMapRequest.asPutDataRequest();
+ request.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, request).await();
mGoogleApiClient.disconnect();
}
diff --git a/wearable/wear/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/UpdateQuestionService.java b/wearable/wear/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/UpdateQuestionService.java
index 7b8f730..50425b0 100644
--- a/wearable/wear/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/UpdateQuestionService.java
+++ b/wearable/wear/Quiz/Wearable/src/main/java/com/example/android/wearable/quiz/UpdateQuestionService.java
@@ -88,6 +88,7 @@
dataMap.putBoolean(CHOSEN_ANSWER_CORRECT, chosenAnswerCorrect);
dataMap.putBoolean(QUESTION_WAS_ANSWERED, true);
PutDataRequest request = putDataMapRequest.asPutDataRequest();
+ request.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, request).await();
// Remove this question notification.
diff --git a/wearable/wear/Quiz/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/Quiz/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/Quiz/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/Quiz/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/Quiz/template-params.xml b/wearable/wear/Quiz/template-params.xml
index 297bf19..7be9593 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>
@@ -45,5 +46,9 @@
<template src="base"/>
<template src="Wear"/>
+ <metadata>
+ <status>DEPRECATED</status>
+ <categories>Wearable</categories>
+ </metadata>
</sample>
diff --git a/wearable/wear/RecipeAssistant/Application/src/main/AndroidManifest.xml b/wearable/wear/RecipeAssistant/Application/src/main/AndroidManifest.xml
index 1786d27..141da9a 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/RecipeAssistant/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/RecipeAssistant/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/RecipeAssistant/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/RecipeAssistant/template-params.xml b/wearable/wear/RecipeAssistant/template-params.xml
index 943b368..c261aa1 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>
@@ -42,4 +42,9 @@
<common src="logger"/>
<common src="activities"/>
+ <metadata>
+ <status>DEPRECATED</status>
+ <categories>Wearable</categories>
+ </metadata>
+
</sample>
diff --git a/wearable/wear/RuntimePermissionsWear/Application/.gitignore b/wearable/wear/RuntimePermissionsWear/Application/.gitignore
new file mode 100644
index 0000000..6eb878d
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/.gitignore
@@ -0,0 +1,16 @@
+# Copyright 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+src/template/
+src/common/
+build.gradle
diff --git a/wearable/wear/RuntimePermissionsWear/Application/README-singleview.txt b/wearable/wear/RuntimePermissionsWear/Application/README-singleview.txt
new file mode 100644
index 0000000..0cacd46
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/README-singleview.txt
@@ -0,0 +1,47 @@
+<#--
+ Copyright 2013 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.
+-->
+
+Steps to implement SingleView template:
+-in template-params.xml.ftl:
+ -add the following line to common imports
+ <common src="activities"/>
+
+ -add a string for the action button's text using the element name "sample_action".
+ This element should be a child of <strings>:
+ <strings>
+ ...
+ <sample_action>ButtonText</sample_action>
+ ...
+ </strings>
+
+
+
+-Add a Fragment to handle behavior. In your MainActivity.java class, it will reference a Fragment
+ called (yourProjectName)Fragment.java. Create that file in your project, using the "main" source
+ folder instead of "common" or "templates".
+ For instance, if your package name is com.example.foo, create the file
+ src/main/java/com/example/foo/FooFragment.java
+
+
+-Within this fragment, make sure that the onCreate method has the line
+ "setHasOptionsMenu(true);", to enable the fragment to handle menu events.
+
+-In order to override menu events, override onOptionsItemSelected.
+
+-refer to sampleSamples/singleViewSample for a reference implementation of a
+project built on this template.
+
+
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/AndroidManifest.xml b/wearable/wear/RuntimePermissionsWear/Application/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..861cad3
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/AndroidManifest.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.wearable.runtimepermissions"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk
+ android:minSdkVersion="18"
+ android:targetSdkVersion="23" />
+
+ <!-- Permissions for phone. -->
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+ <!-- Permissions for wearable:
+ Earlier watches require their permissions to be a subset of the phone apps permission in order
+ for the wear app to be installed. Therefore, you must include the permissions here as well as in
+ the wear manifest.
+ -->
+ <uses-permission android:name="android.permission.BODY_SENSORS" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+ <application android:allowBackup="true"
+ android:label="@string/app_name"
+ android:icon="@mipmap/ic_launcher"
+ android:theme="@style/Theme.AppCompat.Light">
+ <activity
+ android:name=".MainPhoneActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".PhonePermissionRequestActivity"
+ android:label="@string/title_activity_phone_permission_request"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar" >
+ </activity>
+
+ <activity
+ android:name=".WearPermissionRequestActivity"
+ android:label="@string/title_activity_wear_permission_request"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar" >
+ </activity>
+
+ <service
+ android:name=".IncomingRequestPhoneService"
+ android:enabled="true"
+ android:exported="true" >
+ <intent-filter>
+ <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
+ </intent-filter>
+ </service>
+ </application>
+
+
+</manifest>
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/IncomingRequestPhoneService.java b/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/IncomingRequestPhoneService.java
new file mode 100644
index 0000000..4cad2fa
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/IncomingRequestPhoneService.java
@@ -0,0 +1,176 @@
+/*
+ * 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.runtimepermissions;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.support.v4.app.ActivityCompat;
+import android.util.Log;
+
+import com.example.android.wearable.runtimepermissions.common.Constants;
+
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.MessageApi;
+import com.google.android.gms.wearable.MessageEvent;
+import com.google.android.gms.wearable.Wearable;
+import com.google.android.gms.wearable.WearableListenerService;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handles all incoming requests for phone data (and permissions) from wear devices.
+ */
+public class IncomingRequestPhoneService extends WearableListenerService {
+
+ private static final String TAG = "IncomingRequestService";
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "onCreate()");
+ }
+
+ @Override
+ public void onMessageReceived(MessageEvent messageEvent) {
+ super.onMessageReceived(messageEvent);
+ Log.d(TAG, "onMessageReceived(): " + messageEvent);
+
+ String messagePath = messageEvent.getPath();
+
+ if (messagePath.equals(Constants.MESSAGE_PATH_PHONE)) {
+
+ DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
+ int requestType = dataMap.getInt(Constants.KEY_COMM_TYPE, 0);
+
+ if (requestType == Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION) {
+ promptUserForStoragePermission(messageEvent.getSourceNodeId());
+
+ } else if (requestType == Constants.COMM_TYPE_REQUEST_DATA) {
+ respondWithStorageInformation(messageEvent.getSourceNodeId());
+ }
+ }
+ }
+
+ private void promptUserForStoragePermission(String nodeId) {
+ boolean storagePermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (storagePermissionApproved) {
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
+ sendMessage(nodeId, dataMap);
+ } else {
+ // Launch Phone Activity to grant storage permissions.
+ Intent startIntent = new Intent(this, PhonePermissionRequestActivity.class);
+ startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ /* This extra is included to alert MainPhoneActivity to send back the permission
+ * results after the user has made their decision in PhonePermissionRequestActivity
+ * and it finishes.
+ */
+ startIntent.putExtra(MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR, true);
+ startActivity(startIntent);
+ }
+ }
+
+ private void respondWithStorageInformation(String nodeId) {
+
+ boolean storagePermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (!storagePermissionApproved) {
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED);
+ sendMessage(nodeId, dataMap);
+ } else {
+ /* To keep the sample simple, we are only displaying the top level list of directories.
+ * Otherwise, it will return a message that the media wasn't available.
+ */
+ StringBuilder stringBuilder = new StringBuilder();
+
+ if (isExternalStorageReadable()) {
+ File externalStorageDirectory = Environment.getExternalStorageDirectory();
+ String[] fileList = externalStorageDirectory.list();
+
+ if (fileList.length > 0) {
+ stringBuilder.append("List of directories on phone:\n");
+ for (String file : fileList) {
+ stringBuilder.append(" - " + file + "\n");
+ }
+ } else {
+ stringBuilder.append("No files in external storage.");
+ }
+ } else {
+ stringBuilder.append("No external media is available.");
+ }
+
+ // Send valid results
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_DATA);
+ dataMap.putString(Constants.KEY_PAYLOAD, stringBuilder.toString());
+ sendMessage(nodeId, dataMap);
+
+ }
+ }
+
+ private void sendMessage(String nodeId, DataMap dataMap) {
+ Log.d(TAG, "sendMessage() Node: " + nodeId);
+
+ GoogleApiClient client = new GoogleApiClient.Builder(this)
+ .addApi(Wearable.API)
+ .build();
+ client.blockingConnect(Constants.CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS);
+
+
+ PendingResult<MessageApi.SendMessageResult> pendingMessageResult =
+ Wearable.MessageApi.sendMessage(
+ client,
+ nodeId,
+ Constants.MESSAGE_PATH_WEAR,
+ dataMap.toByteArray());
+
+ MessageApi.SendMessageResult sendMessageResult =
+ pendingMessageResult.await(
+ Constants.CONNECTION_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS);
+
+ if (!sendMessageResult.getStatus().isSuccess()) {
+ Log.d(TAG, "Sending message failed, status: "
+ + sendMessageResult.getStatus());
+ } else {
+ Log.d(TAG, "Message sent successfully");
+ }
+ client.disconnect();
+ }
+
+ private boolean isExternalStorageReadable() {
+ String state = Environment.getExternalStorageState();
+
+ return Environment.MEDIA_MOUNTED.equals(state)
+ || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
+ }
+}
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/MainPhoneActivity.java b/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/MainPhoneActivity.java
new file mode 100644
index 0000000..196b03b
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/MainPhoneActivity.java
@@ -0,0 +1,440 @@
+/*
+ * 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.runtimepermissions;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.os.Looper;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.example.android.wearable.runtimepermissions.common.Constants;
+
+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.wearable.CapabilityApi;
+import com.google.android.gms.wearable.CapabilityInfo;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.MessageApi;
+import com.google.android.gms.wearable.MessageEvent;
+import com.google.android.gms.wearable.Node;
+import com.google.android.gms.wearable.Wearable;
+
+import java.io.File;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Displays data that requires runtime permissions both locally (READ_EXTERNAL_STORAGE) and
+ * remotely on wear (BODY_SENSORS).
+ *
+ * The class also handles sending back the results of a permission request from a remote wear device
+ * when the permission has not been approved yet on the phone (uses EXTRA as trigger). In that case,
+ * the IncomingRequestPhoneService launches the splash Activity (PhonePermissionRequestActivity) to
+ * inform user of permission request. After the user decides what to do, it falls back to this
+ * Activity (which has all the GoogleApiClient code) to handle sending data across and keeps user
+ * in app experience.
+ */
+public class MainPhoneActivity extends AppCompatActivity implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ CapabilityApi.CapabilityListener,
+ MessageApi.MessageListener,
+ ResultCallback<MessageApi.SendMessageResult> {
+
+ private static final String TAG = "MainPhoneActivity";
+
+ /*
+ * Alerts Activity that the initial request for permissions came from wear, and the Activity
+ * needs to send back the results (data or permission rejection).
+ */
+ public static final String EXTRA_PROMPT_PERMISSION_FROM_WEAR =
+ "com.example.android.wearable.runtimepermissions.extra.PROMPT_PERMISSION_FROM_WEAR";
+
+ private static final int REQUEST_WEAR_PERMISSION_RATIONALE = 1;
+
+ private boolean mWearBodySensorsPermissionApproved;
+ private boolean mPhoneStoragePermissionApproved;
+
+ private boolean mWearRequestingPhoneStoragePermission;
+
+ private Button mWearBodySensorsPermissionButton;
+ private Button mPhoneStoragePermissionButton;
+ private TextView mOutputTextView;
+
+ private Set<Node> mWearNodeIds;
+
+ private GoogleApiClient mGoogleApiClient;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.d(TAG, "onCreate()");
+ super.onCreate(savedInstanceState);
+
+ /*
+ * Since this is a remote permission, we initialize it to false and then check the remote
+ * permission once the GoogleApiClient is connected.
+ */
+ mWearBodySensorsPermissionApproved = false;
+
+ setContentView(R.layout.activity_main);
+
+ // Checks if wear app requested phone permission (permission request opens later if true).
+ mWearRequestingPhoneStoragePermission =
+ getIntent().getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_WEAR, false);
+
+ mPhoneStoragePermissionButton =
+ (Button) findViewById(R.id.phoneStoragePermissionButton);
+
+ mWearBodySensorsPermissionButton =
+ (Button) findViewById(R.id.wearBodySensorsPermissionButton);
+
+ mOutputTextView = (TextView) findViewById(R.id.output);
+
+ mGoogleApiClient = new GoogleApiClient.Builder(this)
+ .addApi(Wearable.API)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+ }
+
+ public void onClickWearBodySensors(View view) {
+
+ logToUi("Requested info from wear device(s). New approval may be required.");
+
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE, Constants.COMM_TYPE_REQUEST_DATA);
+ sendMessage(dataMap);
+ }
+
+ public void onClickPhoneStorage(View view) {
+
+ if (mPhoneStoragePermissionApproved) {
+ logToUi(getPhoneStorageInformation());
+
+ } else {
+ // On 23+ (M+) devices, Storage permission not granted. Request permission.
+ Intent startIntent = new Intent(this, PhonePermissionRequestActivity.class);
+ startActivity(startIntent);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ Log.d(TAG, "onPause()");
+ super.onPause();
+ if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) {
+ Wearable.CapabilityApi.removeCapabilityListener(
+ mGoogleApiClient,
+ this,
+ Constants.CAPABILITY_WEAR_APP);
+ Wearable.MessageApi.removeListener(mGoogleApiClient, this);
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ Log.d(TAG, "onResume()");
+ super.onResume();
+
+ /* Enables app to handle 23+ (M+) style permissions. It also covers user changing
+ * permission in settings and coming back to the app.
+ */
+ mPhoneStoragePermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (mPhoneStoragePermissionApproved) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ }
+
+ if (mGoogleApiClient != null) {
+ mGoogleApiClient.connect();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.d(TAG, "onActivityResult()");
+ if (requestCode == REQUEST_WEAR_PERMISSION_RATIONALE) {
+
+ if (resultCode == Activity.RESULT_OK) {
+ logToUi("Requested permission on wear device(s).");
+
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION);
+ sendMessage(dataMap);
+ }
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle bundle) {
+ Log.d(TAG, "onConnected()");
+
+ // Set up listeners for capability and message changes.
+ Wearable.CapabilityApi.addCapabilityListener(
+ mGoogleApiClient,
+ this,
+ Constants.CAPABILITY_WEAR_APP);
+ Wearable.MessageApi.addListener(mGoogleApiClient, this);
+
+ // Initial check of capabilities to find the wear nodes.
+ PendingResult<CapabilityApi.GetCapabilityResult> pendingResult =
+ Wearable.CapabilityApi.getCapability(
+ mGoogleApiClient,
+ Constants.CAPABILITY_WEAR_APP,
+ CapabilityApi.FILTER_REACHABLE);
+
+ pendingResult.setResultCallback(new ResultCallback<CapabilityApi.GetCapabilityResult>() {
+ @Override
+ public void onResult(CapabilityApi.GetCapabilityResult getCapabilityResult) {
+
+ CapabilityInfo capabilityInfo = getCapabilityResult.getCapability();
+ String capabilityName = capabilityInfo.getName();
+
+ boolean wearSupportsSampleApp =
+ capabilityName.equals(Constants.CAPABILITY_WEAR_APP);
+
+ if (wearSupportsSampleApp) {
+ mWearNodeIds = capabilityInfo.getNodes();
+
+ /*
+ * Upon getting all wear nodes, we now need to check if the original request to
+ * launch this activity (and PhonePermissionRequestActivity) was initiated by
+ * a wear device. If it was, we need to send back the permission results (data
+ * or rejection of permission) to the wear device.
+ *
+ * Also, note we set variable to false, this enables the user to continue
+ * changing permissions without sending updates to the wear every time.
+ */
+ if (mWearRequestingPhoneStoragePermission) {
+ mWearRequestingPhoneStoragePermission = false;
+ sendWearPermissionResults();
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult connectionResult) {
+ Log.e(TAG, "onConnectionFailed(): connection to location client failed");
+ }
+
+
+ public void onCapabilityChanged(CapabilityInfo capabilityInfo) {
+ Log.d(TAG, "onCapabilityChanged(): " + capabilityInfo);
+
+ mWearNodeIds = capabilityInfo.getNodes();
+ }
+
+ public void onMessageReceived(MessageEvent messageEvent) {
+ Log.d(TAG, "onMessageReceived(): " + messageEvent);
+
+ String messagePath = messageEvent.getPath();
+
+ if (messagePath.equals(Constants.MESSAGE_PATH_PHONE)) {
+ DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
+
+ int commType = dataMap.getInt(Constants.KEY_COMM_TYPE, 0);
+
+ if (commType == Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED) {
+ mWearBodySensorsPermissionApproved = false;
+ updateWearButtonOnUiThread();
+
+ /* Because our request for remote data requires a remote permission, we now launch
+ * a splash activity informing the user we need those permissions (along with
+ * other helpful information to approve).
+ */
+ Intent wearPermissionRationale =
+ new Intent(this, WearPermissionRequestActivity.class);
+ startActivityForResult(wearPermissionRationale, REQUEST_WEAR_PERMISSION_RATIONALE);
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION) {
+ mWearBodySensorsPermissionApproved = true;
+ updateWearButtonOnUiThread();
+ logToUi("User approved permission on remote device, requesting data again.");
+ DataMap outgoingDataRequestDataMap = new DataMap();
+ outgoingDataRequestDataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_DATA);
+ sendMessage(outgoingDataRequestDataMap);
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION) {
+ mWearBodySensorsPermissionApproved = false;
+ updateWearButtonOnUiThread();
+ logToUi("User denied permission on remote device.");
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_DATA) {
+ mWearBodySensorsPermissionApproved = true;
+ String storageDetails = dataMap.getString(Constants.KEY_PAYLOAD);
+ updateWearButtonOnUiThread();
+ logToUi(storageDetails);
+
+ } else {
+ Log.d(TAG, "Unrecognized communication type received.");
+ }
+ }
+ }
+
+ @Override
+ public void onResult(MessageApi.SendMessageResult sendMessageResult) {
+ if (!sendMessageResult.getStatus().isSuccess()) {
+ Log.d(TAG, "Sending message failed, onResult: " + sendMessageResult);
+ updateWearButtonOnUiThread();
+ logToUi("Sending message failed.");
+
+ } else {
+ Log.d(TAG, "Message sent.");
+ }
+ }
+
+ private void sendMessage(DataMap dataMap) {
+ Log.d(TAG, "sendMessage(): " + mWearNodeIds);
+
+ if ((mWearNodeIds != null) && (!mWearNodeIds.isEmpty())) {
+
+ PendingResult<MessageApi.SendMessageResult> pendingResult;
+
+ for (Node node : mWearNodeIds) {
+
+ pendingResult = Wearable.MessageApi.sendMessage(
+ mGoogleApiClient,
+ node.getId(),
+ Constants.MESSAGE_PATH_WEAR,
+ dataMap.toByteArray());
+
+ pendingResult.setResultCallback(this, Constants.CONNECTION_TIME_OUT_MS,
+ TimeUnit.SECONDS);
+ }
+ } else {
+ // Unable to retrieve node with proper capability
+ mWearBodySensorsPermissionApproved = false;
+ updateWearButtonOnUiThread();
+ logToUi("Wear devices not available to send message.");
+ }
+ }
+
+ private void updateWearButtonOnUiThread() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mWearBodySensorsPermissionApproved) {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ } else {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+ }
+ }
+ });
+ }
+
+ /*
+ * Handles all messages for the UI coming on and off the main thread. Not all callbacks happen
+ * on the main thread.
+ */
+ private void logToUi(final String message) {
+
+ boolean mainUiThread = (Looper.myLooper() == Looper.getMainLooper());
+
+ if (mainUiThread) {
+
+ if (!message.isEmpty()) {
+ Log.d(TAG, message);
+ mOutputTextView.setText(message);
+ }
+
+ } else {
+ if (!message.isEmpty()) {
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ Log.d(TAG, message);
+ mOutputTextView.setText(message);
+ }
+ });
+ }
+ }
+ }
+
+ private String getPhoneStorageInformation() {
+
+ StringBuilder stringBuilder = new StringBuilder();
+
+ String state = Environment.getExternalStorageState();
+ boolean isExternalStorageReadable = Environment.MEDIA_MOUNTED.equals(state)
+ || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
+
+ if (isExternalStorageReadable) {
+ File externalStorageDirectory = Environment.getExternalStorageDirectory();
+ String[] fileList = externalStorageDirectory.list();
+
+ if (fileList.length > 0) {
+
+ stringBuilder.append("List of files\n");
+ for (String file : fileList) {
+ stringBuilder.append(" - " + file + "\n");
+ }
+
+ } else {
+ stringBuilder.append("No files in external storage.");
+ }
+
+ } else {
+ stringBuilder.append("No external media is available.");
+ }
+
+ return stringBuilder.toString();
+ }
+
+ private void sendWearPermissionResults() {
+
+ Log.d(TAG, "sendWearPermissionResults()");
+
+ DataMap dataMap = new DataMap();
+
+ if (mPhoneStoragePermissionApproved) {
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
+ } else {
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION);
+ }
+ sendMessage(dataMap);
+ }
+}
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/PhonePermissionRequestActivity.java b/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/PhonePermissionRequestActivity.java
new file mode 100644
index 0000000..0b10e35
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/PhonePermissionRequestActivity.java
@@ -0,0 +1,114 @@
+/*
+ * 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.runtimepermissions;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+
+/**
+ * This is a simple splash screen (activity) for giving more details on why the user should approve
+ * phone permissions for storage. If they choose to move forward, the permission screen
+ * is brought up. Either way (approve or disapprove), this will exit to the MainPhoneActivity after
+ * they are finished with their final decision.
+ *
+ * If this activity is started by our service (IncomingRequestPhoneService) it is marked via an
+ * extra (MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR). That service only starts
+ * this activity if the phone permission hasn't been approved for the data wear is trying to access.
+ * When the user decides within this Activity what to do with the permission request, it closes and
+ * opens the MainPhoneActivity (to maintain the app experience). It also again passes along the same
+ * extra (MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR) to alert MainPhoneActivity to
+ * send the results of the user's decision to the wear device.
+ */
+public class PhonePermissionRequestActivity extends AppCompatActivity implements
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final String TAG = "PhoneRationale";
+
+ /* Id to identify Location permission request. */
+ private static final int PERMISSION_REQUEST_READ_STORAGE = 1;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // If permissions granted, we start the main activity (shut this activity down).
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED) {
+ startMainActivity();
+ }
+
+ setContentView(R.layout.activity_phone_permission_request);
+ }
+
+ public void onClickApprovePermissionRequest(View view) {
+ Log.d(TAG, "onClickApprovePermissionRequest()");
+
+ // On 23+ (M+) devices, External storage permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
+ PERMISSION_REQUEST_READ_STORAGE);
+ }
+
+ public void onClickDenyPermissionRequest(View view) {
+ Log.d(TAG, "onClickDenyPermissionRequest()");
+ startMainActivity();
+ }
+
+ /*
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+
+ String permissionResult = "Request code: " + requestCode + ", Permissions: " + permissions
+ + ", Results: " + grantResults;
+ Log.d(TAG, "onRequestPermissionsResult(): " + permissionResult);
+
+ if (requestCode == PERMISSION_REQUEST_READ_STORAGE) {
+ // Close activity regardless of user's decision (decision picked up in main activity).
+ startMainActivity();
+ }
+ }
+
+ private void startMainActivity() {
+
+ Intent mainActivityIntent = new Intent(this, MainPhoneActivity.class);
+
+ /*
+ * If service started this Activity (b/c wear requested data where permissions were not
+ * approved), tells MainPhoneActivity to send results to wear device (via this extra).
+ */
+ boolean serviceStartedActivity = getIntent().getBooleanExtra(
+ MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR, false);
+
+ if (serviceStartedActivity) {
+ mainActivityIntent.putExtra(
+ MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR, true);
+ }
+
+ startActivity(mainActivityIntent);
+ }
+}
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/WearPermissionRequestActivity.java b/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/WearPermissionRequestActivity.java
new file mode 100644
index 0000000..3340ef6
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/java/com/example/android/wearable/runtimepermissions/WearPermissionRequestActivity.java
@@ -0,0 +1,62 @@
+/*
+ * 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.runtimepermissions;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+
+/**
+ * This is a simple splash screen (activity) for giving more details on why the user should approve
+ * phone permissions for storage. If they choose to move forward, the permission screen
+ * is brought up. Either way (approve or disapprove), this will exit to the MainPhoneActivity after
+ * they are finished with their final decision.
+ *
+ * If this activity is started by our service (IncomingRequestPhoneService) it is marked via an
+ * extra (MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR). That service only starts
+ * this activity if the phone permission hasn't been approved for the data wear is trying to access.
+ * When the user decides within this Activity what to do with the permission request, it closes and
+ * opens the MainPhoneActivity (to maintain the app experience). It also again passes along the same
+ * extra (MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR) to alert MainPhoneActivity to
+ * send the results of the user's decision to the wear device.
+ */
+public class WearPermissionRequestActivity extends AppCompatActivity implements
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final String TAG = "WearRationale";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_wear_permission_request);
+ }
+
+ public void onClickApprovePermissionRequest(View view) {
+ Log.d(TAG, "onClickApprovePermissionRequest()");
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+
+ public void onClickDenyPermissionRequest(View view) {
+ Log.d(TAG, "onClickDenyPermissionRequest()");
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
+}
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_file_folder.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_file_folder.png
new file mode 100644
index 0000000..8fb69a5
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_file_folder.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_hardware_watch.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_hardware_watch.png
new file mode 100644
index 0000000..e05cb6a
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_hardware_watch.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_permission_approved.png
new file mode 100644
index 0000000..7989330
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_permission_denied.png
new file mode 100644
index 0000000..814bb63
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-hdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_file_folder.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_file_folder.png
new file mode 100644
index 0000000..ef11a06
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_file_folder.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_hardware_watch.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_hardware_watch.png
new file mode 100644
index 0000000..5f5900e
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_hardware_watch.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_permission_approved.png
new file mode 100644
index 0000000..1e63d37
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_permission_denied.png
new file mode 100644
index 0000000..45a0d87
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-mdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_file_folder.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_file_folder.png
new file mode 100644
index 0000000..6877103
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_file_folder.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_hardware_watch.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_hardware_watch.png
new file mode 100644
index 0000000..7c6773c
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_hardware_watch.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_permission_approved.png
new file mode 100644
index 0000000..24d1efb
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_permission_denied.png
new file mode 100644
index 0000000..17f093d
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xhdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_file_folder.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_file_folder.png
new file mode 100644
index 0000000..3f2db91
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_file_folder.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_hardware_watch.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_hardware_watch.png
new file mode 100644
index 0000000..e8a5f74
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_hardware_watch.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_permission_approved.png
new file mode 100644
index 0000000..f29c5a3
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_permission_denied.png
new file mode 100644
index 0000000..52b0671
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxhdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_file_folder.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_file_folder.png
new file mode 100644
index 0000000..de3c50f
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_file_folder.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_hardware_watch.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_hardware_watch.png
new file mode 100644
index 0000000..8daad4f
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_hardware_watch.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_permission_approved.png
new file mode 100644
index 0000000..ec642b5
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_permission_denied.png
new file mode 100644
index 0000000..35d6c4f
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/drawable-xxxhdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/layout/activity_main.xml b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..d35cb0c
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/layout/activity_main.xml
@@ -0,0 +1,58 @@
+<?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.
+ -->
+
+<LinearLayout
+ 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:orientation="vertical"
+ tools:context=".MainActivity"
+ tools:deviceIds="wear_square"
+ android:padding="12dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/wearBodySensorsPermissionButton"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:drawableLeft="@drawable/ic_permission_denied"
+ android:text="@string/button_wear_label_activity_main"
+ android:onClick="onClickWearBodySensors" />
+
+ <Button
+ android:id="@+id/phoneStoragePermissionButton"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:drawableLeft="@drawable/ic_permission_denied"
+ android:text="@string/button_phone_label_activity_main"
+ android:onClick="onClickPhoneStorage" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/output"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="3"
+ android:text="@string/hello_phone_activity_main"
+ android:padding="8dp"
+ android:textSize="16sp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/layout/activity_phone_permission_request.xml b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/layout/activity_phone_permission_request.xml
new file mode 100644
index 0000000..f6c5720
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/layout/activity_phone_permission_request.xml
@@ -0,0 +1,85 @@
+<?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.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:background="#4c9699">
+
+<Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:stateListAnimator="@null"
+ android:text="@string/no_thanks_activity_phone_permission_request"
+ android:id="@+id/deny_permission_request"
+ android:onClick="onClickDenyPermissionRequest"
+ android:textColor="#ffffff"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+<Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:stateListAnimator="@null"
+ android:text="@string/continue_activity_phone_permission_request"
+ android:id="@+id/approve_permission_request"
+ android:onClick="onClickApprovePermissionRequest"
+ android:layout_alignTop="@+id/deny_permission_request"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:textStyle="bold"
+ android:textColor="#ffffff" />
+
+<TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:text="@string/main_message_activity_phone_permission_request"
+ android:id="@+id/mainMessageTextView"
+ android:textColor="#ffffff"
+ android:textStyle="bold"
+ android:layout_below="@+id/imageView"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginTop="117dp" />
+
+<TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/details_message_activity_phone_permission_request"
+ android:id="@+id/detailsTextView"
+ android:textColor="#ffffff"
+ android:layout_below="@+id/mainMessageTextView"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+<ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView"
+ android:src="@drawable/ic_file_folder"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="60dp" />
+
+</RelativeLayout>
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/layout/activity_wear_permission_request.xml b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/layout/activity_wear_permission_request.xml
new file mode 100644
index 0000000..b656cf5
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/layout/activity_wear_permission_request.xml
@@ -0,0 +1,85 @@
+<?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.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:background="#4c9699">
+
+<Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:stateListAnimator="@null"
+ android:text="@string/no_thanks_activity_wear_permission_request"
+ android:id="@+id/deny_permission_request"
+ android:onClick="onClickDenyPermissionRequest"
+ android:textColor="#ffffff"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+<Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:stateListAnimator="@null"
+ android:text="@string/continue_activity_wear_permission_request"
+ android:id="@+id/approve_permission_request"
+ android:onClick="onClickApprovePermissionRequest"
+ android:layout_alignTop="@+id/deny_permission_request"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:textStyle="bold"
+ android:textColor="#ffffff" />
+
+<TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:text="@string/main_message_activity_wear_permission_request"
+ android:id="@+id/mainMessageTextView"
+ android:textColor="#ffffff"
+ android:textStyle="bold"
+ android:layout_below="@+id/imageView"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginTop="117dp" />
+<!--TODO: R.string.dialog_message_activity_main -->
+<TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/details_message_activity_wear_permission_request"
+ android:id="@+id/detailsTextView"
+ android:textColor="#ffffff"
+ android:layout_below="@+id/mainMessageTextView"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true" />
+
+<ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/imageView"
+ android:src="@drawable/ic_hardware_watch"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="60dp" />
+
+</RelativeLayout>
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-hdpi/ic_launcher.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-mdpi/ic_launcher.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-xhdpi/ic_launcher.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values-w820dp/dimens.xml b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..74184fc
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,20 @@
+<?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>
+ <!-- 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/RuntimePermissionsWear/Application/src/main/res/values/dimens.xml b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..e9366a9
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?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>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values/strings.xml b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values/strings.xml
new file mode 100644
index 0000000..95a2c83
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values/strings.xml
@@ -0,0 +1,34 @@
+<?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="hello_phone_activity_main">Happy equals approved, sad equals denied.\n\nTo see results or request permissions, click on the buttons above.</string>
+ <string name="denied_permission_activity_main">You do not have the correct permissions. Tap sad face to bring up permission dialog again.</string>
+ <string name="button_wear_label_activity_main">Wear Sensors</string>
+ <string name="button_phone_label_activity_main">Phone Storage</string>
+
+ <string name="title_activity_phone_permission_request">PhonePermissionRequestActivity</string>
+ <string name="main_message_activity_phone_permission_request">See your directory structure by letting us read your phone\'s storage.</string>
+ <string name="details_message_activity_phone_permission_request">Your phone and watch experience need access to your phone\'s storage to show your top level directories.</string>
+ <string name="no_thanks_activity_phone_permission_request">No Thanks</string>
+ <string name="continue_activity_phone_permission_request">Continue</string>
+
+ <string name="title_activity_wear_permission_request">WearPermissionRequestActivity</string>
+ <string name="main_message_activity_wear_permission_request">See your total sensor count by letting us read your wear\'s sensors.</string>
+ <string name="details_message_activity_wear_permission_request">Your phone and watch experience need access to your wear\'s sensors to show sensor count.</string>
+ <string name="no_thanks_activity_wear_permission_request">No Thanks</string>
+ <string name="continue_activity_wear_permission_request">Open on Watch</string>
+
+</resources>
diff --git a/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values/wear.xml b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values/wear.xml
new file mode 100644
index 0000000..2787972
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Application/src/main/res/values/wear.xml
@@ -0,0 +1,19 @@
+<?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-array name="android_wear_capabilities">
+ <item>phone_app_runtime_permissions</item>
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Shared/.gitignore b/wearable/wear/RuntimePermissionsWear/Shared/.gitignore
new file mode 100644
index 0000000..6eb878d
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Shared/.gitignore
@@ -0,0 +1,16 @@
+# Copyright 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+src/template/
+src/common/
+build.gradle
diff --git a/wearable/wear/RuntimePermissionsWear/Shared/src/main/AndroidManifest.xml b/wearable/wear/RuntimePermissionsWear/Shared/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..fa262fa
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Shared/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?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.runtimepermissions.common">
+
+ <application android:allowBackup="true" android:label="@string/app_name">
+
+ </application>
+
+</manifest>
diff --git a/wearable/wear/RuntimePermissionsWear/Shared/src/main/java/com/example/android/wearable/runtimepermissions/common/Constants.java b/wearable/wear/RuntimePermissionsWear/Shared/src/main/java/com/example/android/wearable/runtimepermissions/common/Constants.java
new file mode 100644
index 0000000..d124400
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Shared/src/main/java/com/example/android/wearable/runtimepermissions/common/Constants.java
@@ -0,0 +1,51 @@
+/*
+ * 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.runtimepermissions.common;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A collection of constants that is shared between the wearable and handset apps.
+ */
+public class Constants {
+
+ // Shared
+ public static final long CONNECTION_TIME_OUT_MS = TimeUnit.SECONDS.toMillis(5);
+
+ public static final String KEY_COMM_TYPE = "communicationType";
+ public static final String KEY_PAYLOAD = "payload";
+
+ // Requests
+ public static final int COMM_TYPE_REQUEST_PROMPT_PERMISSION = 1;
+ public static final int COMM_TYPE_REQUEST_DATA = 2;
+
+ // Responses
+ public static final int COMM_TYPE_RESPONSE_PERMISSION_REQUIRED = 1001;
+ public static final int COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION = 1002;
+ public static final int COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION = 1003;
+ public static final int COMM_TYPE_RESPONSE_DATA = 1004;
+
+ // Phone
+ public static final String CAPABILITY_PHONE_APP = "phone_app_runtime_permissions";
+ public static final String MESSAGE_PATH_PHONE = "/phone_message_path";
+
+ // Wear
+ public static final String CAPABILITY_WEAR_APP = "wear_app_runtime_permissions";
+ public static final String MESSAGE_PATH_WEAR = "/wear_message_path";
+
+ private Constants() {}
+}
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Shared/src/main/res/values/strings.xml b/wearable/wear/RuntimePermissionsWear/Shared/src/main/res/values/strings.xml
new file mode 100644
index 0000000..cc0aaa9
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Shared/src/main/res/values/strings.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.
+ -->
+
+<resources>
+ <string name="app_name">Shared</string>
+</resources>
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/.gitignore b/wearable/wear/RuntimePermissionsWear/Wearable/.gitignore
new file mode 100644
index 0000000..6eb878d
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/.gitignore
@@ -0,0 +1,16 @@
+# Copyright 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+src/template/
+src/common/
+build.gradle
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/AndroidManifest.xml b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..43218d7
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/AndroidManifest.xml
@@ -0,0 +1,68 @@
+<?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.runtimepermissions" >
+
+ <uses-feature android:name="android.hardware.type.watch" />
+
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.BODY_SENSORS" />
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="23" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@android:style/Theme.DeviceDefault" >
+
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version" />
+
+ <!-- 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" />
+
+ <activity
+ android:name=".MainWearActivity"
+ android:label="@string/app_name"
+ android:launchMode="singleInstance">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".RequestPermissionOnPhoneActivity"
+ android:label="@string/title_activity_request_permission_on_phone"
+ android:theme="@android:style/Theme.DeviceDefault.Light">
+ </activity>
+
+ <service
+ android:name=".IncomingRequestWearService"
+ android:enabled="true"
+ android:exported="true" >
+ <intent-filter>
+ <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/java/com/example/android/wearable/runtimepermissions/IncomingRequestWearService.java b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/java/com/example/android/wearable/runtimepermissions/IncomingRequestWearService.java
new file mode 100644
index 0000000..5dc2467
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/java/com/example/android/wearable/runtimepermissions/IncomingRequestWearService.java
@@ -0,0 +1,209 @@
+/*
+ * 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.runtimepermissions;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.support.v4.app.ActivityCompat;
+import android.util.Log;
+
+import com.example.android.wearable.runtimepermissions.common.Constants;
+
+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.wearable.CapabilityApi;
+import com.google.android.gms.wearable.CapabilityInfo;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.MessageApi;
+import com.google.android.gms.wearable.MessageEvent;
+import com.google.android.gms.wearable.Node;
+import com.google.android.gms.wearable.Wearable;
+import com.google.android.gms.wearable.WearableListenerService;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handles all incoming requests for wear data (and permissions) from phone devices.
+ */
+public class IncomingRequestWearService extends WearableListenerService {
+
+ private static final String TAG = "IncomingRequestService";
+
+ public IncomingRequestWearService() {
+ Log.d(TAG, "IncomingRequestWearService()");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "onCreate()");
+ }
+
+ @Override
+ public void onMessageReceived(MessageEvent messageEvent) {
+ Log.d(TAG, "onMessageReceived(): " + messageEvent);
+
+ String messagePath = messageEvent.getPath();
+
+ if (messagePath.equals(Constants.MESSAGE_PATH_WEAR)) {
+ DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
+
+ int requestType = dataMap.getInt(Constants.KEY_COMM_TYPE);
+
+ if (requestType == Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION) {
+ promptUserForSensorPermission();
+
+ } else if (requestType == Constants.COMM_TYPE_REQUEST_DATA) {
+ respondWithSensorInformation();
+ }
+ }
+ }
+
+ private void promptUserForSensorPermission() {
+ Log.d(TAG, "promptUserForSensorPermission()");
+
+ boolean sensorPermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (sensorPermissionApproved) {
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
+ sendMessage(dataMap);
+ } else {
+ // Launch Activity to grant sensor permissions.
+ Intent startIntent = new Intent(this, MainWearActivity.class);
+ startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startIntent.putExtra(MainWearActivity.EXTRA_PROMPT_PERMISSION_FROM_PHONE, true);
+ startActivity(startIntent);
+ }
+ }
+
+ private void respondWithSensorInformation() {
+ Log.d(TAG, "respondWithSensorInformation()");
+
+ boolean sensorPermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (!sensorPermissionApproved) {
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED);
+ sendMessage(dataMap);
+ } else {
+ /* To keep the sample simple, we are only displaying the number of sensors. You could do
+ * something much more complicated.
+ */
+ SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
+ List<Sensor> sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
+ int numberOfSensorsOnDevice = sensorList.size();
+
+ String sensorSummary = numberOfSensorsOnDevice + " sensors on wear device(s)!";
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_DATA);
+ dataMap.putString(Constants.KEY_PAYLOAD, sensorSummary);
+ sendMessage(dataMap);
+ }
+ }
+
+ private void sendMessage(DataMap dataMap) {
+
+ Log.d(TAG, "sendMessage(): " + dataMap);
+
+ GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
+ .addApi(Wearable.API)
+ .build();
+ ConnectionResult connectionResult =
+ googleApiClient.blockingConnect(
+ Constants.CONNECTION_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS);
+
+ if (!connectionResult.isSuccess()) {
+ Log.d(TAG, "Google API Client failed to connect.");
+ return;
+ }
+
+ PendingResult<CapabilityApi.GetCapabilityResult> pendingCapabilityResult =
+ Wearable.CapabilityApi.getCapability(
+ googleApiClient,
+ Constants.CAPABILITY_PHONE_APP,
+ CapabilityApi.FILTER_REACHABLE);
+
+ CapabilityApi.GetCapabilityResult getCapabilityResult =
+ pendingCapabilityResult.await(
+ Constants.CONNECTION_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS);
+
+ if (!getCapabilityResult.getStatus().isSuccess()) {
+ Log.d(TAG, "CapabilityApi failed to return any results.");
+ googleApiClient.disconnect();
+ return;
+ }
+
+ CapabilityInfo capabilityInfo = getCapabilityResult.getCapability();
+ String phoneNodeId = pickBestNodeId(capabilityInfo.getNodes());
+
+ PendingResult<MessageApi.SendMessageResult> pendingMessageResult =
+ Wearable.MessageApi.sendMessage(
+ googleApiClient,
+ phoneNodeId,
+ Constants.MESSAGE_PATH_PHONE,
+ dataMap.toByteArray());
+
+ MessageApi.SendMessageResult sendMessageResult =
+ pendingMessageResult.await(Constants.CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS);
+
+ if (!sendMessageResult.getStatus().isSuccess()) {
+ Log.d(TAG, "Sending message failed, onResult: " + sendMessageResult.getStatus());
+ } else {
+ Log.d(TAG, "Message sent successfully");
+ }
+
+ googleApiClient.disconnect();
+ }
+
+ /*
+ * There should only ever be one phone in a node set (much less w/ the correct capability), so
+ * I am just grabbing the first one (which should be the only one).
+ */
+ private String pickBestNodeId(Set<Node> nodes) {
+
+ Log.d(TAG, "pickBestNodeId: " + nodes);
+
+
+ String bestNodeId = null;
+ /* Find a nearby node or pick one arbitrarily. There should be only one phone connected
+ * that supports this sample.
+ */
+ for (Node node : nodes) {
+ if (node.isNearby()) {
+ return node.getId();
+ }
+ bestNodeId = node.getId();
+ }
+ return bestNodeId;
+ }
+}
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/java/com/example/android/wearable/runtimepermissions/MainWearActivity.java b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/java/com/example/android/wearable/runtimepermissions/MainWearActivity.java
new file mode 100644
index 0000000..b2a2595
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/java/com/example/android/wearable/runtimepermissions/MainWearActivity.java
@@ -0,0 +1,556 @@
+/*
+ * 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.runtimepermissions;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.os.Bundle;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.wearable.activity.WearableActivity;
+import android.support.wearable.view.WatchViewStub;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.example.android.wearable.runtimepermissions.common.Constants;
+
+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.wearable.CapabilityApi;
+import com.google.android.gms.wearable.CapabilityInfo;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.MessageApi;
+import com.google.android.gms.wearable.MessageEvent;
+import com.google.android.gms.wearable.Node;
+import com.google.android.gms.wearable.Wearable;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Displays data that requires runtime permissions both locally (BODY_SENSORS) and remotely on
+ * the phone (READ_EXTERNAL_STORAGE).
+ *
+ * The class is also launched by IncomingRequestWearService when the permission for the data the
+ * phone is trying to access hasn't been granted (wear's sensors). If granted in that scenario,
+ * this Activity also sends back the results of the permission request to the phone device (and
+ * the sensor data if approved).
+ */
+public class MainWearActivity extends WearableActivity implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ CapabilityApi.CapabilityListener,
+ MessageApi.MessageListener,
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final String TAG = "MainWearActivity";
+
+ /* Id to identify local permission request for body sensors. */
+ private static final int PERMISSION_REQUEST_READ_BODY_SENSORS = 1;
+
+ /* Id to identify starting/closing RequestPermissionOnPhoneActivity (startActivityForResult). */
+ private static final int REQUEST_PHONE_PERMISSION = 1;
+
+ public static final String EXTRA_PROMPT_PERMISSION_FROM_PHONE =
+ "com.example.android.wearable.runtimepermissions.extra.PROMPT_PERMISSION_FROM_PHONE";
+
+ private boolean mWearBodySensorsPermissionApproved;
+ private boolean mPhoneStoragePermissionApproved;
+
+ private boolean mPhoneRequestingWearSensorPermission;
+
+ private Button mWearBodySensorsPermissionButton;
+ private Button mPhoneStoragePermissionButton;
+ private TextView mOutputTextView;
+
+ private String mPhoneNodeId;
+
+ private GoogleApiClient mGoogleApiClient;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.d(TAG, "onCreate()");
+ super.onCreate(savedInstanceState);;
+
+ /*
+ * Since this is a remote permission, we initialize it to false and then check the remote
+ * permission once the GoogleApiClient is connected.
+ */
+ mPhoneStoragePermissionApproved = false;
+
+ setContentView(R.layout.activity_main);
+ setAmbientEnabled();
+
+ // Checks if phone app requested wear permission (permission request opens later if true).
+ mPhoneRequestingWearSensorPermission =
+ getIntent().getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_PHONE, false);
+
+ final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
+ stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
+ @Override
+ public void onLayoutInflated(WatchViewStub stub) {
+
+ mWearBodySensorsPermissionButton =
+ (Button) stub.findViewById(R.id.wearBodySensorsPermissionButton);
+
+ if (mWearBodySensorsPermissionApproved) {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ }
+
+ mPhoneStoragePermissionButton =
+ (Button) stub.findViewById(R.id.phoneStoragePermissionButton);
+
+ mOutputTextView = (TextView) stub.findViewById(R.id.output);
+
+ if (mPhoneRequestingWearSensorPermission) {
+ launchPermissionDialogForPhone();
+ }
+
+ }
+ });
+
+ mGoogleApiClient = new GoogleApiClient.Builder(this)
+ .addApi(Wearable.API)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+ }
+
+ public void onClickWearBodySensors(View view) {
+
+ if (mWearBodySensorsPermissionApproved) {
+
+ // To keep the sample simple, we are only displaying the number of sensors.
+ SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
+ List<Sensor> sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
+ int numberOfSensorsOnDevice = sensorList.size();
+
+ logToUi(numberOfSensorsOnDevice + " sensors on device(s)!");
+
+ } else {
+ logToUi("Requested local permission.");
+ // On 23+ (M+) devices, GPS permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{Manifest.permission.BODY_SENSORS},
+ PERMISSION_REQUEST_READ_BODY_SENSORS);
+ }
+ }
+
+ public void onClickPhoneStorage(View view) {
+
+ logToUi("Requested info from phone. New approval may be required.");
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_DATA);
+ sendMessage(dataMap);
+ }
+
+ @Override
+ protected void onPause() {
+ Log.d(TAG, "onPause()");
+ super.onPause();
+ if ((mGoogleApiClient != null) && mGoogleApiClient.isConnected()) {
+ Wearable.CapabilityApi.removeCapabilityListener(
+ mGoogleApiClient,
+ this,
+ Constants.CAPABILITY_PHONE_APP);
+ Wearable.MessageApi.removeListener(mGoogleApiClient, this);
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ Log.d(TAG, "onResume()");
+ super.onResume();
+ if (mGoogleApiClient != null) {
+ mGoogleApiClient.connect();
+ }
+
+ // Enables app to handle 23+ (M+) style permissions.
+ mWearBodySensorsPermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /*
+ * Because this wear activity is marked "android:launchMode='singleInstance'" in the manifest,
+ * we need to allow the permissions dialog to be opened up from the phone even if the wear app
+ * is in the foreground. By overriding onNewIntent, we can cover that use case.
+ */
+ @Override
+ protected void onNewIntent (Intent intent) {
+ Log.d(TAG, "onNewIntent()");
+ super.onNewIntent(intent);
+
+ // Checks if phone app requested wear permissions (opens up permission request if true).
+ mPhoneRequestingWearSensorPermission =
+ intent.getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_PHONE, false);
+
+ if (mPhoneRequestingWearSensorPermission) {
+ launchPermissionDialogForPhone();
+ }
+ }
+
+ @Override
+ public void onEnterAmbient(Bundle ambientDetails) {
+ Log.d(TAG, "onEnterAmbient() " + ambientDetails);
+
+ if (mWearBodySensorsPermissionApproved) {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved_bw, 0, 0, 0);
+ } else {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied_bw, 0, 0, 0);
+ }
+
+ if (mPhoneStoragePermissionApproved) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved_bw, 0, 0, 0);
+ } else {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied_bw, 0, 0, 0);
+ }
+ super.onEnterAmbient(ambientDetails);
+ }
+
+ @Override
+ public void onExitAmbient() {
+ Log.d(TAG, "onExitAmbient()");
+
+ if (mWearBodySensorsPermissionApproved) {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ } else {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+ }
+
+ if (mPhoneStoragePermissionApproved) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ } else {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+ }
+ super.onExitAmbient();
+ }
+
+ @Override
+ public void onConnected(Bundle bundle) {
+ Log.d(TAG, "onConnected()");
+
+ // Set up listeners for capability and message changes.
+ Wearable.CapabilityApi.addCapabilityListener(
+ mGoogleApiClient,
+ this,
+ Constants.CAPABILITY_PHONE_APP);
+ Wearable.MessageApi.addListener(mGoogleApiClient, this);
+
+ // Initial check of capabilities to find the phone.
+ PendingResult<CapabilityApi.GetCapabilityResult> pendingResult =
+ Wearable.CapabilityApi.getCapability(
+ mGoogleApiClient,
+ Constants.CAPABILITY_PHONE_APP,
+ CapabilityApi.FILTER_REACHABLE);
+
+ pendingResult.setResultCallback(new ResultCallback<CapabilityApi.GetCapabilityResult>() {
+ @Override
+ public void onResult(CapabilityApi.GetCapabilityResult getCapabilityResult) {
+
+ if (getCapabilityResult.getStatus().isSuccess()) {
+ CapabilityInfo capabilityInfo = getCapabilityResult.getCapability();
+ mPhoneNodeId = pickBestNodeId(capabilityInfo.getNodes());
+
+ } else {
+ Log.d(TAG, "Failed CapabilityApi result: "
+ + getCapabilityResult.getStatus());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult connectionResult) {
+ Log.e(TAG, "onConnectionFailed(): connection to location client failed");
+ }
+
+ public void onCapabilityChanged(CapabilityInfo capabilityInfo) {
+ Log.d(TAG, "onCapabilityChanged(): " + capabilityInfo);
+
+ mPhoneNodeId = pickBestNodeId(capabilityInfo.getNodes());
+ }
+
+ /*
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+
+ String permissionResult = "Request code: " + requestCode + ", Permissions: " + permissions
+ + ", Results: " + grantResults;
+ Log.d(TAG, "onRequestPermissionsResult(): " + permissionResult);
+
+
+ if (requestCode == PERMISSION_REQUEST_READ_BODY_SENSORS) {
+
+ if ((grantResults.length == 1)
+ && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
+
+ mWearBodySensorsPermissionApproved = true;
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+
+ // To keep the sample simple, we are only displaying the number of sensors.
+ SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
+ List<Sensor> sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
+ int numberOfSensorsOnDevice = sensorList.size();
+
+ String sensorSummary = numberOfSensorsOnDevice + " sensors on this device!";
+ logToUi(sensorSummary);
+
+ if (mPhoneRequestingWearSensorPermission) {
+ // Resets so this isn't triggered every time permission is changed in app.
+ mPhoneRequestingWearSensorPermission = false;
+
+ // Send 'approved' message to remote phone since it started Activity.
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
+ sendMessage(dataMap);
+ }
+
+ } else {
+
+ mWearBodySensorsPermissionApproved = false;
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+
+ if (mPhoneRequestingWearSensorPermission) {
+ // Resets so this isn't triggered every time permission is changed in app.
+ mPhoneRequestingWearSensorPermission = false;
+ // Send 'denied' message to remote phone since it started Activity.
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION);
+ sendMessage(dataMap);
+ }
+ }
+ }
+ }
+
+ public void onMessageReceived(MessageEvent messageEvent) {
+ Log.d(TAG, "onMessageReceived(): " + messageEvent);
+
+ String messagePath = messageEvent.getPath();
+
+ if (messagePath.equals(Constants.MESSAGE_PATH_WEAR)) {
+
+ DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
+ int commType = dataMap.getInt(Constants.KEY_COMM_TYPE, 0);
+
+ if (commType == Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED) {
+ mPhoneStoragePermissionApproved = false;
+ updatePhoneButtonOnUiThread();
+
+ /* Because our request for remote data requires a remote permission, we now launch
+ * a splash activity informing the user we need those permissions (along with
+ * other helpful information to approve).
+ */
+ Intent phonePermissionRationaleIntent =
+ new Intent(this, RequestPermissionOnPhoneActivity.class);
+ startActivityForResult(phonePermissionRationaleIntent, REQUEST_PHONE_PERMISSION);
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION) {
+ mPhoneStoragePermissionApproved = true;
+ updatePhoneButtonOnUiThread();
+ logToUi("User approved permission on remote device, requesting data again.");
+ DataMap outgoingDataRequestDataMap = new DataMap();
+ outgoingDataRequestDataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_DATA);
+ sendMessage(outgoingDataRequestDataMap);
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION) {
+ mPhoneStoragePermissionApproved = false;
+ updatePhoneButtonOnUiThread();
+ logToUi("User denied permission on remote device.");
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_DATA) {
+ mPhoneStoragePermissionApproved = true;
+ String storageDetails = dataMap.getString(Constants.KEY_PAYLOAD);
+ updatePhoneButtonOnUiThread();
+ logToUi(storageDetails);
+ }
+ }
+ }
+
+ private void sendMessage(DataMap dataMap) {
+ Log.d(TAG, "sendMessage(): " + mPhoneNodeId);
+
+ if (mPhoneNodeId != null) {
+
+ PendingResult<MessageApi.SendMessageResult> pendingResult =
+ Wearable.MessageApi.sendMessage(
+ mGoogleApiClient,
+ mPhoneNodeId,
+ Constants.MESSAGE_PATH_PHONE,
+ dataMap.toByteArray());
+
+ pendingResult.setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {
+ @Override
+ public void onResult(MessageApi.SendMessageResult sendMessageResult) {
+
+ if (!sendMessageResult.getStatus().isSuccess()) {
+ updatePhoneButtonOnUiThread();
+ logToUi("Sending message failed.");
+
+ } else {
+ Log.d(TAG, "Message sent successfully.");
+ }
+ }
+ }, Constants.CONNECTION_TIME_OUT_MS, TimeUnit.SECONDS);
+
+ } else {
+ // Unable to retrieve node with proper capability
+ mPhoneStoragePermissionApproved = false;
+ updatePhoneButtonOnUiThread();
+ logToUi("Phone not available to send message.");
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // Check which request we're responding to
+ if (requestCode == REQUEST_PHONE_PERMISSION) {
+ // Make sure the request was successful
+ if (resultCode == RESULT_OK) {
+ logToUi("Requested permission on phone.");
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION);
+ sendMessage(dataMap);
+ }
+ }
+ }
+
+ /*
+ * There should only ever be one phone in a node set (much less w/ the correct capability), so
+ * I am just grabbing the first one (which should be the only one).
+ */
+ private String pickBestNodeId(Set<Node> nodes) {
+
+ String bestNodeId = null;
+ // Find a nearby node or pick one arbitrarily.
+ for (Node node : nodes) {
+ if (node.isNearby()) {
+ return node.getId();
+ }
+ bestNodeId = node.getId();
+ }
+ return bestNodeId;
+ }
+
+ /*
+ * If Phone triggered the wear app for permissions, we open up the permission
+ * dialog after inflation.
+ */
+ private void launchPermissionDialogForPhone() {
+ Log.d(TAG, "launchPermissionDialogForPhone()");
+
+ if (!mWearBodySensorsPermissionApproved) {
+ // On 23+ (M+) devices, GPS permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ MainWearActivity.this,
+ new String[]{Manifest.permission.BODY_SENSORS},
+ PERMISSION_REQUEST_READ_BODY_SENSORS);
+ }
+ }
+
+ private void updatePhoneButtonOnUiThread() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ if (mPhoneStoragePermissionApproved) {
+
+ if (isAmbient()) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved_bw, 0, 0, 0);
+ } else {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ }
+
+ } else {
+
+ if (isAmbient()) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied_bw, 0, 0, 0);
+ } else {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+ }
+ }
+ }
+ });
+ }
+
+ /*
+ * Handles all messages for the UI coming on and off the main thread. Not all callbacks happen
+ * on the main thread.
+ */
+ private void logToUi(final String message) {
+
+ boolean mainUiThread = (Looper.myLooper() == Looper.getMainLooper());
+
+ if (mainUiThread) {
+
+ if (!message.isEmpty()) {
+ Log.d(TAG, message);
+ mOutputTextView.setText(message);
+ }
+
+ } else {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (!message.isEmpty()) {
+ Log.d(TAG, message);
+ mOutputTextView.setText(message);
+ }
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/java/com/example/android/wearable/runtimepermissions/RequestPermissionOnPhoneActivity.java b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/java/com/example/android/wearable/runtimepermissions/RequestPermissionOnPhoneActivity.java
new file mode 100644
index 0000000..f4ee7fc
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/java/com/example/android/wearable/runtimepermissions/RequestPermissionOnPhoneActivity.java
@@ -0,0 +1,39 @@
+/*
+ * 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.runtimepermissions;
+
+import android.os.Bundle;
+import android.support.wearable.activity.WearableActivity;
+import android.view.View;
+
+/**
+ * Asks user if they want to open permission screen on their remote device (phone).
+ */
+public class RequestPermissionOnPhoneActivity extends WearableActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_request_permission_on_phone);
+ setAmbientEnabled();
+ }
+
+ public void onClickPermissionPhoneStorage(View view) {
+ setResult(RESULT_OK);
+ finish();
+ }
+}
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_cc_open_on_phone.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_cc_open_on_phone.png
new file mode 100644
index 0000000..618a44f
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_cc_open_on_phone.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_approved.png
new file mode 100644
index 0000000..7989330
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_approved_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_approved_bw.png
new file mode 100644
index 0000000..bbd7e8a
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_approved_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_denied.png
new file mode 100644
index 0000000..814bb63
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_denied_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_denied_bw.png
new file mode 100644
index 0000000..accd6c0
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-hdpi/ic_permission_denied_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_cc_open_on_phone.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_cc_open_on_phone.png
new file mode 100644
index 0000000..e66ba6b
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_cc_open_on_phone.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_approved.png
new file mode 100644
index 0000000..1e63d37
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_approved_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_approved_bw.png
new file mode 100644
index 0000000..16050cb
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_approved_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_denied.png
new file mode 100644
index 0000000..45a0d87
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_denied_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_denied_bw.png
new file mode 100644
index 0000000..376d471
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-mdpi/ic_permission_denied_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_cc_open_on_phone.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_cc_open_on_phone.png
new file mode 100644
index 0000000..5522d6c
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_cc_open_on_phone.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_approved.png
new file mode 100644
index 0000000..24d1efb
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_approved_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_approved_bw.png
new file mode 100644
index 0000000..2682e30
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_approved_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_denied.png
new file mode 100644
index 0000000..17f093d
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_denied_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_denied_bw.png
new file mode 100644
index 0000000..cd9d000
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xhdpi/ic_permission_denied_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_approved.png
new file mode 100644
index 0000000..f29c5a3
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_approved_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_approved_bw.png
new file mode 100644
index 0000000..1bcf27a
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_approved_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_denied.png
new file mode 100644
index 0000000..52b0671
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_denied_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_denied_bw.png
new file mode 100644
index 0000000..2292033
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxhdpi/ic_permission_denied_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_approved.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_approved.png
new file mode 100644
index 0000000..ec642b5
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_approved.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_approved_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_approved_bw.png
new file mode 100644
index 0000000..c9eab85
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_approved_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_denied.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_denied.png
new file mode 100644
index 0000000..35d6c4f
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_denied.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_denied_bw.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_denied_bw.png
new file mode 100644
index 0000000..81e5355
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/drawable-xxxhdpi/ic_permission_denied_bw.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/activity_main.xml b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..588ff9a
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/activity_main.xml
@@ -0,0 +1,26 @@
+<?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.
+ -->
+
+<android.support.wearable.view.WatchViewStub
+ 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:id="@+id/watch_view_stub"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:rectLayout="@layout/rect_activity_main"
+ app:roundLayout="@layout/round_activity_main"
+ tools:context=".MainActivity"
+ tools:deviceIds="wear">
+</android.support.wearable.view.WatchViewStub>
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/activity_request_permission_on_phone.xml b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/activity_request_permission_on_phone.xml
new file mode 100644
index 0000000..c8a5d05
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/activity_request_permission_on_phone.xml
@@ -0,0 +1,74 @@
+<?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.
+ -->
+
+<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"
+ tools:deviceIds="wear"
+ android:background="@color/white"
+ tools:context="com.example.android.wearable.runtimepermissions.RequestPermissionOnPhoneActivity"
+ android:paddingStart="30dp"
+ android:paddingTop="18dp"
+ android:paddingRight="18dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:onClick="onClickPermissionPhoneStorage"
+ android:orientation="vertical"
+ app:layout_box="all">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/permission_message_activity_request_permission_on_phone"
+ android:textSize="16sp"
+ android:paddingRight="6dp"
+ android:textColor="#000000"/>
+
+ <android.support.v4.widget.Space
+ android:layout_width="18dp"
+ android:layout_height="18dp"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center">
+
+ <android.support.wearable.view.CircledImageView
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ app:circle_radius="20dp"
+ app:circle_color="#0086D4"
+ android:src="@drawable/ic_cc_open_on_phone"/>
+
+ <android.support.v4.widget.Space
+ android:layout_width="8dp"
+ android:layout_height="8dp"/>
+
+ <TextView
+ android:id="@+id/openOnPhone"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:textSize="16sp"
+ android:text="@string/open_on_phone_message_activity_request_permission_on_phone"
+ android:textColor="#0086D4"/>
+ </LinearLayout>
+ </LinearLayout>
+</android.support.wearable.view.BoxInsetLayout>
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/rect_activity_main.xml b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/rect_activity_main.xml
new file mode 100644
index 0000000..5a44894
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/rect_activity_main.xml
@@ -0,0 +1,57 @@
+<?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.
+ -->
+
+<LinearLayout
+ 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:orientation="vertical"
+ tools:context=".MainActivity"
+ tools:deviceIds="wear_square">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/wearBodySensorsPermissionButton"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:drawableLeft="@drawable/ic_permission_denied"
+ android:textSize="8sp"
+ android:text="@string/button_wear_label_activity_main"
+ android:onClick="onClickWearBodySensors" />
+
+ <Button
+ android:id="@+id/phoneStoragePermissionButton"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:drawableLeft="@drawable/ic_permission_denied"
+ android:textSize="8sp"
+ android:text="@string/button_phone_label_activity_main"
+ android:onClick="onClickPhoneStorage" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/output"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="2"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:text="@string/hello_wear_activity_main" />
+</LinearLayout>
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/round_activity_main.xml b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/round_activity_main.xml
new file mode 100644
index 0000000..d35012d
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/layout/round_activity_main.xml
@@ -0,0 +1,61 @@
+<?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.
+ -->
+
+<LinearLayout
+ 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:orientation="vertical"
+ tools:context=".MainActivity"
+ tools:deviceIds="wear_round"
+ android:paddingTop="24dp"
+ android:paddingLeft="10dp"
+ android:paddingRight="10dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/wearBodySensorsPermissionButton"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:drawableLeft="@drawable/ic_permission_denied"
+ android:textSize="8sp"
+ android:text="@string/button_wear_label_activity_main"
+ android:onClick="onClickWearBodySensors" />
+
+ <Button
+ android:id="@+id/phoneStoragePermissionButton"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:drawableLeft="@drawable/ic_permission_denied"
+ android:textSize="8sp"
+ android:text="@string/button_phone_label_activity_main"
+ android:onClick="onClickPhoneStorage" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/output"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="2"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:text="@string/hello_wear_activity_main" />
+
+</LinearLayout>
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-hdpi/ic_launcher.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-mdpi/ic_launcher.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-xhdpi/ic_launcher.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/values/dimens.xml b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..2f2eb2a
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <dimen name="pair_button_diameter">40dp</dimen>
+ <dimen name="circle_border_normal_width">10dp</dimen>
+ <dimen name="circle_padding">5dp</dimen>
+ <dimen name="circle_radius">35dp</dimen>
+ <dimen name="circle_radius_pressed">40dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/values/strings.xml b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/values/strings.xml
new file mode 100644
index 0000000..67f460a
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?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">Runtime Permissions</string>
+
+ <string name="hello_wear_activity_main">Happy equals approved, sad equals denied.\n\nTo see results or request permissions, click on the buttons above.</string>
+ <string name="button_wear_label_activity_main">Wear Sensors</string>
+ <string name="button_phone_label_activity_main">Phone Storage</string>
+
+ <string name="title_activity_request_permission_on_phone">PhonePermissionRationale</string>
+ <string name="permission_message_activity_request_permission_on_phone">App requires access to your phone\'s storage.</string>
+ <string name="open_on_phone_message_activity_request_permission_on_phone">Open on phone</string>
+</resources>
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/values/wear.xml b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/values/wear.xml
new file mode 100644
index 0000000..42e922f
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/Wearable/src/main/res/values/wear.xml
@@ -0,0 +1,19 @@
+<?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-array name="android_wear_capabilities">
+ <item>wear_app_runtime_permissions</item>
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/wearable/wear/RuntimePermissionsWear/build.gradle b/wearable/wear/RuntimePermissionsWear/build.gradle
new file mode 100644
index 0000000..b95a860
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/build.gradle
@@ -0,0 +1,12 @@
+
+
+// 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/RuntimePermissionsWear/buildSrc/build.gradle b/wearable/wear/RuntimePermissionsWear/buildSrc/build.gradle
new file mode 100644
index 0000000..7c150e4
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/buildSrc/build.gradle
@@ -0,0 +1,16 @@
+
+repositories {
+ jcenter()
+}
+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/RuntimePermissionsWear/gradle/wrapper/gradle-wrapper.jar b/wearable/wear/RuntimePermissionsWear/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/RuntimePermissionsWear/gradle/wrapper/gradle-wrapper.properties
similarity index 79%
copy from ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.properties
copy to wearable/wear/RuntimePermissionsWear/gradle/wrapper/gradle-wrapper.properties
index a51db8c..07fc193 100644
--- a/ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/RuntimePermissionsWear/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue May 20 13:33:02 BST 2014
+#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradlew b/wearable/wear/RuntimePermissionsWear/gradlew
similarity index 100%
copy from ui/views/Elevation/ElevationDrag/gradle/gradlew
copy to wearable/wear/RuntimePermissionsWear/gradlew
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradlew.bat b/wearable/wear/RuntimePermissionsWear/gradlew.bat
similarity index 100%
copy from ui/views/Elevation/ElevationDrag/gradle/gradlew.bat
copy to wearable/wear/RuntimePermissionsWear/gradlew.bat
diff --git a/wearable/wear/RuntimePermissionsWear/screenshots/screenshot-phone.png b/wearable/wear/RuntimePermissionsWear/screenshots/screenshot-phone.png
new file mode 100644
index 0000000..080a3c8
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/screenshots/screenshot-phone.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/screenshots/screenshot-wear.png b/wearable/wear/RuntimePermissionsWear/screenshots/screenshot-wear.png
new file mode 100644
index 0000000..2228d69
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/screenshots/screenshot-wear.png
Binary files differ
diff --git a/wearable/wear/RuntimePermissionsWear/settings.gradle b/wearable/wear/RuntimePermissionsWear/settings.gradle
new file mode 100644
index 0000000..8c3a205
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/settings.gradle
@@ -0,0 +1,2 @@
+
+include ':Application', ':Wearable', ':Shared'
diff --git a/wearable/wear/RuntimePermissionsWear/template-params.xml b/wearable/wear/RuntimePermissionsWear/template-params.xml
new file mode 100644
index 0000000..9c2d4f6
--- /dev/null
+++ b/wearable/wear/RuntimePermissionsWear/template-params.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<sample>
+ <name>RuntimePermissionsWear</name>
+ <group>Wearable</group>
+ <package>com.example.android.wearable.runtimepermissions</package>
+
+ <minSdk>18</minSdk>
+ <targetSdkVersion>23</targetSdkVersion>
+ <targetSdkVersionWear>23</targetSdkVersionWear>
+
+ <wearable>
+ <has_handheld_app>true</has_handheld_app>
+ </wearable>
+
+ <dependency>com.android.support:appcompat-v7:23.1.1</dependency>
+ <dependency>com.android.support:design:23.1.1</dependency>
+ <provided_dependency_wearable>com.google.android.wearable:wearable:1.0.0</provided_dependency_wearable>
+
+ <strings>
+ <intro>
+<![CDATA[
+A sample that shows how you can handle remote data that requires permissions both on
+a wearable device and a mobile device.
+]]>
+ </intro>
+ </strings>
+
+ <!-- The basic templates have already been enabled. Uncomment more as desired. -->
+ <template src="base" />
+ <template src="WearPlusShared"/>
+
+ <metadata>
+ <!-- Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED} -->
+ <status>PUBLISHED</status>
+ <!-- See http://go/sample-categories for details on the next 4 fields. -->
+ <categories>Wearable, Permissions</categories>
+ <technologies>Android</technologies>
+ <languages>Java</languages>
+ <solutions>Mobile</solutions>
+ <level>INTERMEDIATE</level>
+ <!-- Dimensions: 512x512, PNG fomrat -->
+ <icon>screenshots/icon-web.png</icon>
+ <!-- Path to screenshots. Use <img> tags for each. -->
+ <screenshots>
+ <img>screenshots/screenshot-wear.png</img>
+ <img>screenshots/screenshot-phone.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.support.v4.app.ActivityCompat</android>
+ <android>android.support.v7.app.AppCompatActivity</android>
+ <android>android.support.wearable.activity.WearableActivity</android>
+ <android>android.support.wearable.view.WatchViewStub</android>
+ <android>com.google.android.gms.common.api.GoogleApiClient</android>
+ <android>com.google.android.gms.wearable.CapabilityApi</android>
+ <android>com.google.android.gms.wearable.CapabilityInfo</android>
+ <android>com.google.android.gms.wearable.DataMap</android>
+ <android>com.google.android.gms.wearable.MessageApi</android>
+ <android>com.google.android.gms.wearable.MessageEvent</android>
+ <android>com.google.android.gms.wearable.Node</android>
+ <android>com.google.android.gms.wearable.Wearable</android>
+ <android>com.google.android.gms.wearable.WearableListenerService</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>
+A sample that shows how you can handle remote data that requires permissions both on
+a wearable device and a mobile device.
+ </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[
+Steps for trying out this sample:
+* Compile and install the mobile app onto your mobile device or emulator.
+* Compile and install the wearable app onto your Wear device or emulator.
+(**Note:** wearable apps are not automatically pushed from your mobile device
+unless you build a production release, see [here][3] for more info).
+* Start the mobile or wear app. Each app contains two buttons: one for showing
+local data and another for showing remote data.
+* Click either button to view the data. Both local and remote data require
+[dangerous permissions][4] to be approved before displaying the data for
+devices running 23 or above. You will be asked to approve the access if you
+do not have the proper permissions.
+* The happy icon signifies you have access to the data while the sad icon
+signifies you do or may not have access (and may be asked to approve access).
+
+This sample demonstrates how to access data and trigger permission approval
+on remote devices. It uses [Services][5] and the [Wearable MessageApi][2] to
+communicate between devices.
+
+To find out more about wear, visit our [developer Wear page][1].
+
+[1]: http://developer.android.com/wear/
+[2]: https://developer.android.com/reference/com/google/android/gms/wearable/MessageApi.html
+[3]: https://developer.android.com/training/wearables/apps/creating.html#Install
+[4]: http://developer.android.com/guide/topics/security/permissions.html#normal-dangerous
+[5]: http://developer.android.com/guide/components/services.html
+]]>
+ </intro>
+ </metadata>
+</sample>
diff --git a/wearable/wear/SkeletonWearableApp/Wearable/src/main/AndroidManifest.xml b/wearable/wear/SkeletonWearableApp/Wearable/src/main/AndroidManifest.xml
index f99d785..f9e8978 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/SkeletonWearableApp/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/SkeletonWearableApp/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/SkeletonWearableApp/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/SkeletonWearableApp/template-params.xml b/wearable/wear/SkeletonWearableApp/template-params.xml
index b0f4b36..1441c1f 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>
@@ -35,7 +34,7 @@
<template src="base-build"/>
<template src="Wear"/>
<metadata>
- <status>PUBLISHED</status>
+ <status>DEPRECATED</status>
<categories>Getting Started, Wearable</categories>
<technologies>Android</technologies>
<languages>Java</languages>
diff --git a/wearable/wear/SpeedTracker/Application/src/main/AndroidManifest.xml b/wearable/wear/SpeedTracker/Application/src/main/AndroidManifest.xml
index 44284d4..be88f6d 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 76f609b..c645bdd 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.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 @@
* 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 a18c644..17a8f6a 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 ab19d5e..c9cbad1 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 d55d7df..d178891 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 @@
*/
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 @@
@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 f3015bf..25f424c 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.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.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 @@
* 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;
- public static final String PREFS_SPEED_LIMIT_KEY = "speed_limit";
- public static final int SPEED_LIMIT_DEFAULT_MPH = 45;
+ private static final int SPEED_LIMIT_DEFAULT_MPH = 45;
+
private static final long INDICATOR_DOT_FADE_AWAY_MS = 500L;
- private GoogleApiClient mGoogleApiClient;
- private TextView mSpeedLimitText;
- private TextView mCurrentSpeedText;
- private ImageView mSaveImageView;
- private TextView mAcquiringGps;
- private TextView mCurrentSpeedMphText;
+ // Request codes for changing speed limit and location permissions.
+ private static final int REQUEST_PICK_SPEED_LIMIT = 0;
- private int mCurrentSpeedLimit;
- private float mCurrentSpeed;
- private View mDot;
- private Handler mHandler = new Handler();
+ // 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 boolean mSaveGpsLocation;
+
+ private TextView mSpeedLimitTextView;
+ private TextView mSpeedTextView;
+ private ImageView mGpsPermissionImageView;
+ private TextView mCurrentSpeedMphTextView;
+ private TextView mGpsIssueTextView;
+ private View mBlinkingGpsStatusDotView;
+
+ private String mGpsPermissionNeededMessage;
+ private String mAcquiringGpsMessage;
+
+ private int mSpeedLimit;
+ private float mSpeed;
+
+ private boolean mGpsPermissionApproved;
+
+ private boolean mWaitingForGpsSignal;
+
+ private GoogleApiClient mGoogleApiClient;
+
+ private Handler mHandler = new Handler();
private enum SpeedState {
BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above);
@@ -104,20 +126,53 @@
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 @@
@Override
public void onDismiss(DialogInterface dialog) {
dialog.cancel();
- finish();
}
})
.setCancelable(false)
@@ -133,164 +187,216 @@
.show();
}
+
setupViews();
- updateSpeedVisibility(false);
- setSpeedLimit();
+
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
- mGoogleApiClient.connect();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected()) &&
+ (mGoogleApiClient.isConnecting())) {
+ LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
+ mGoogleApiClient.disconnect();
+ }
+
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mGoogleApiClient != null) {
+ mGoogleApiClient.connect();
+ }
}
private void setupViews() {
- mSpeedLimitText = (TextView) findViewById(R.id.max_speed_text);
- mCurrentSpeedText = (TextView) findViewById(R.id.current_speed_text);
- mSaveImageView = (ImageView) findViewById(R.id.saving);
- ImageButton settingButton = (ImageButton) findViewById(R.id.settings);
- mAcquiringGps = (TextView) findViewById(R.id.acquiring_gps);
- mCurrentSpeedMphText = (TextView) findViewById(R.id.current_speed_mph);
- mDot = findViewById(R.id.dot);
+ mSpeedLimitTextView = (TextView) findViewById(R.id.max_speed_text);
+ mSpeedTextView = (TextView) findViewById(R.id.current_speed_text);
+ mCurrentSpeedMphTextView = (TextView) findViewById(R.id.current_speed_mph);
- settingButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent speedIntent = new Intent(WearableMainActivity.this,
- SpeedPickerActivity.class);
- startActivity(speedIntent);
- }
- });
+ mGpsPermissionImageView = (ImageView) findViewById(R.id.gps_permission);
+ mGpsIssueTextView = (TextView) findViewById(R.id.gps_issue_text);
+ mBlinkingGpsStatusDotView = findViewById(R.id.dot);
- mSaveImageView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent savingIntent = new Intent(WearableMainActivity.this,
- LocationSettingActivity.class);
- startActivity(savingIntent);
- }
- });
+ updateActivityViewsBasedOnLocationPermissions();
}
- private void setSpeedLimit(int speedLimit) {
- mSpeedLimitText.setText(getString(R.string.speed_limit, speedLimit));
+ public void onSpeedLimitClick(View view) {
+ Intent speedIntent = new Intent(WearableMainActivity.this,
+ SpeedPickerActivity.class);
+ startActivityForResult(speedIntent, REQUEST_PICK_SPEED_LIMIT);
}
- private void setSpeedLimit() {
- SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
- mCurrentSpeedLimit = pref.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH);
- setSpeedLimit(mCurrentSpeedLimit);
- }
+ public void onGpsPermissionClick(View view) {
- private void setCurrentSpeed(float speed) {
- mCurrentSpeed = speed;
- mCurrentSpeedText.setText(String.format(getString(R.string.speed_format), speed));
- adjustColor();
+ if (!mGpsPermissionApproved) {
+
+ Log.i(TAG, "Location permission has NOT been granted. Requesting permission.");
+
+ // On 23+ (M+) devices, GPS permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
+ REQUEST_GPS_PERMISSION);
+ }
}
/**
- * Adjusts the color of the speed based on its value relative to the speed limit.
+ * Adjusts the visibility of views based on location permissions.
*/
- private void adjustColor() {
- SpeedState state = SpeedState.ABOVE;
- if (mCurrentSpeed <= mCurrentSpeedLimit - 5) {
- state = SpeedState.BELOW;
- } else if (mCurrentSpeed <= mCurrentSpeedLimit) {
- state = SpeedState.CLOSE;
- }
+ private void updateActivityViewsBasedOnLocationPermissions() {
- mCurrentSpeedText.setTextColor(getResources().getColor(state.getColor()));
+ /*
+ * If the user has approved location but we don't have a signal yet, we let the user know
+ * we are waiting on the GPS signal (this sometimes takes a little while). Otherwise, the
+ * user might think something is wrong.
+ */
+ if (mGpsPermissionApproved && mWaitingForGpsSignal) {
+
+ // We are getting a GPS signal w/ user permission.
+ mGpsIssueTextView.setText(mAcquiringGpsMessage);
+ mGpsIssueTextView.setVisibility(View.VISIBLE);
+ mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
+
+ mSpeedTextView.setVisibility(View.GONE);
+ mSpeedLimitTextView.setVisibility(View.GONE);
+ mCurrentSpeedMphTextView.setVisibility(View.GONE);
+
+ } else if (mGpsPermissionApproved) {
+
+ mGpsIssueTextView.setVisibility(View.GONE);
+
+ mSpeedTextView.setVisibility(View.VISIBLE);
+ mSpeedLimitTextView.setVisibility(View.VISIBLE);
+ mCurrentSpeedMphTextView.setVisibility(View.VISIBLE);
+ mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
+
+ } else {
+
+ // User needs to enable location for the app to work.
+ mGpsIssueTextView.setVisibility(View.VISIBLE);
+ mGpsIssueTextView.setText(mGpsPermissionNeededMessage);
+ mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_not_saving_grey600_96dp);
+
+ mSpeedTextView.setVisibility(View.GONE);
+ mSpeedLimitTextView.setVisibility(View.GONE);
+ mCurrentSpeedMphTextView.setVisibility(View.GONE);
+ }
+ }
+
+ private void updateSpeedInViews() {
+
+ if (mGpsPermissionApproved) {
+
+ mSpeedLimitTextView.setText(getString(R.string.speed_limit, mSpeedLimit));
+ mSpeedTextView.setText(String.format(getString(R.string.speed_format), mSpeed));
+
+ // Adjusts the color of the speed based on its value relative to the speed limit.
+ SpeedState state = SpeedState.ABOVE;
+ if (mSpeed <= mSpeedLimit - 5) {
+ state = SpeedState.BELOW;
+ } else if (mSpeed <= mSpeedLimit) {
+ state = SpeedState.CLOSE;
+ }
+
+ mSpeedTextView.setTextColor(getResources().getColor(state.getColor()));
+
+ // Causes the (green) dot blinks when new GPS location data is acquired.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
+ }
+ });
+ mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mBlinkingGpsStatusDotView.setVisibility(View.INVISIBLE);
+ }
+ }, INDICATOR_DOT_FADE_AWAY_MS);
+ }
}
@Override
public void onConnected(Bundle bundle) {
- LocationRequest locationRequest = LocationRequest.create()
- .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
- .setInterval(UPDATE_INTERVAL_MS)
- .setFastestInterval(FASTEST_INTERVAL_MS);
- LocationServices.FusedLocationApi
- .requestLocationUpdates(mGoogleApiClient, locationRequest, this)
- .setResultCallback(new ResultCallback<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();
+ Log.d(TAG, "onLocationChanged() : " + location);
+
+
+ if (mWaitingForGpsSignal) {
+ mWaitingForGpsSignal = false;
+ updateActivityViewsBasedOnLocationPermissions();
+ }
+
+ mSpeed = location.getSpeed() * MPH_IN_METERS_PER_SECOND;
+ updateSpeedInViews();
addLocationEntry(location.getLatitude(), location.getLongitude());
}
- /**
- * Causes the (green) dot blinks when new GPS location data is acquired.
- */
- private void flashDot() {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mDot.setVisibility(View.VISIBLE);
- }
- });
- mDot.setVisibility(View.VISIBLE);
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- mDot.setVisibility(View.INVISIBLE);
- }
- }, INDICATOR_DOT_FADE_AWAY_MS);
- }
-
- /**
- * Adjusts the visibility of speed indicator based on the arrival of GPS data.
- */
- private void updateSpeedVisibility(boolean speedVisible) {
- if (speedVisible) {
- mAcquiringGps.setVisibility(View.GONE);
- mCurrentSpeedText.setVisibility(View.VISIBLE);
- mCurrentSpeedMphText.setVisibility(View.VISIBLE);
- } else {
- mAcquiringGps.setVisibility(View.VISIBLE);
- mCurrentSpeedText.setVisibility(View.GONE);
- mCurrentSpeedMphText.setVisibility(View.GONE);
- }
- }
-
- /**
- * Adds a data item to the data Layer storage
+ /*
+ * Adds a data item to the data Layer storage.
*/
private void addLocationEntry(double latitude, double longitude) {
- if (!mSaveGpsLocation || !mGoogleApiClient.isConnected()) {
+ if (!mGpsPermissionApproved || !mGoogleApiClient.isConnected()) {
return;
}
mCalendar.setTimeInMillis(System.currentTimeMillis());
@@ -302,6 +408,7 @@
putDataMapRequest.getDataMap()
.putLong(Constants.KEY_TIME, entry.calendar.getTimeInMillis());
PutDataRequest request = putDataMapRequest.asPutDataRequest();
+ request.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, request)
.setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
@Override
@@ -315,29 +422,56 @@
});
}
+ /**
+ * Handles user choices for both speed limit and location permissions (GPS tracking).
+ */
@Override
- protected void onStop() {
- super.onStop();
- if (mGoogleApiClient.isConnected()) {
- LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+
+ if (requestCode == REQUEST_PICK_SPEED_LIMIT) {
+ if (resultCode == RESULT_OK) {
+ // The user updated the speed limit.
+ int newSpeedLimit =
+ data.getIntExtra(SpeedPickerActivity.EXTRA_NEW_SPEED_LIMIT, mSpeedLimit);
+
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(this);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY, newSpeedLimit);
+ editor.apply();
+
+ mSpeedLimit = newSpeedLimit;
+
+ updateSpeedInViews();
+ }
}
- mGoogleApiClient.disconnect();
}
+ /**
+ * Callback received when a permissions request has been completed.
+ */
@Override
- protected void onResume() {
- super.onResume();
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- mCalendar = Calendar.getInstance();
- setSpeedLimit();
- adjustColor();
- updateRecordingIcon();
- }
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- private void updateRecordingIcon() {
- mSaveGpsLocation = LocationSettingActivity.getGpsRecordingStatusFromPreferences(this);
- mSaveImageView.setImageResource(mSaveGpsLocation ? R.drawable.ic_gps_saving_grey600_96dp
- : R.drawable.ic_gps_not_saving_grey600_96dp);
+ Log.d(TAG, "onRequestPermissionsResult(): " + permissions);
+
+
+ if (requestCode == REQUEST_GPS_PERMISSION) {
+ Log.i(TAG, "Received response for GPS permission request.");
+
+ if ((grantResults.length == 1)
+ && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
+ Log.i(TAG, "GPS permission granted.");
+ mGpsPermissionApproved = true;
+ } else {
+ Log.i(TAG, "GPS permission NOT granted.");
+ mGpsPermissionApproved = false;
+ }
+
+ updateActivityViewsBasedOnLocationPermissions();
+
+ }
}
/**
@@ -346,4 +480,4 @@
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 1f8be71..0000000
--- 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 e3b284b..df25a6a 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 @@
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 a1b9081..a2b678e 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 c37d959..0000000
--- 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 dda3ecd..b0c3747 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/SpeedTracker/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/SpeedTracker/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/SpeedTracker/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/SpeedTracker/template-params.xml b/wearable/wear/SpeedTracker/template-params.xml
index 1cf3161..982e1b0 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.1.1</dependency>
+ <dependency>com.google.android.gms:play-services-location</dependency>
+ <dependency_wearable>com.google.android.gms:play-services-location</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 1737c7d..04a69e0 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/Application/src/main/java/com/example/android/wearable/synchronizednotifications/DismissListener.java b/wearable/wear/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/DismissListener.java
index 8d5cca4..4621879 100644
--- a/wearable/wear/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/DismissListener.java
+++ b/wearable/wear/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/DismissListener.java
@@ -31,7 +31,6 @@
import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer;
-import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;
diff --git a/wearable/wear/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/SynchronizedNotificationsFragment.java b/wearable/wear/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/SynchronizedNotificationsFragment.java
index 240af9b..837c7ad 100644
--- a/wearable/wear/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/SynchronizedNotificationsFragment.java
+++ b/wearable/wear/SynchronizedNotifications/Application/src/main/java/com/example/android/wearable/synchronizednotifications/SynchronizedNotificationsFragment.java
@@ -19,17 +19,11 @@
import android.app.PendingIntent;
import android.content.Intent;
import android.support.v4.app.Fragment;
-import android.app.Activity;
-import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
-import android.view.LayoutInflater;
import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Toast;
import com.example.android.wearable.synchronizednotifications.common.Constants;
import com.google.android.gms.common.ConnectionResult;
@@ -61,7 +55,7 @@
public class SynchronizedNotificationsFragment extends Fragment
implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
- private static final String TAG = "SynchronizedNotificationsFragment";
+ private static final String TAG = "SynchronizedFragment";
private GoogleApiClient mGoogleApiClient;
@Override
@@ -87,7 +81,8 @@
Constants.WATCH_ONLY_PATH);
return true;
case R.id.btn_different:
- buildMirroredNotifications(getString(R.string.phone_both), getString(R.string.watch_both), now());
+ buildMirroredNotifications(
+ getString(R.string.phone_both), getString(R.string.watch_both), now());
return true;
}
return false;
@@ -110,8 +105,12 @@
if (withDismissal) {
Intent dismissIntent = new Intent(Constants.ACTION_DISMISS);
dismissIntent.putExtra(Constants.KEY_NOTIFICATION_ID, Constants.BOTH_ID);
- PendingIntent pendingIntent = PendingIntent
- .getService(this.getActivity(), 0, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent pendingIntent =
+ PendingIntent.getService(
+ this.getActivity(),
+ 0,
+ dismissIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(pendingIntent);
}
NotificationManagerCompat.from(this.getActivity()).notify(notificationId, builder.build());
@@ -127,6 +126,7 @@
putDataMapRequest.getDataMap().putString(Constants.KEY_CONTENT, content);
putDataMapRequest.getDataMap().putString(Constants.KEY_TITLE, title);
PutDataRequest request = putDataMapRequest.asPutDataRequest();
+ request.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, request)
.setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
@Override
diff --git a/wearable/wear/SynchronizedNotifications/Wearable/src/main/AndroidManifest.xml b/wearable/wear/SynchronizedNotifications/Wearable/src/main/AndroidManifest.xml
index f9b0d9c..5c4d259 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/Wearable/src/main/java/com/example/android/wearable/synchronizednotifications/NotificationUpdateService.java b/wearable/wear/SynchronizedNotifications/Wearable/src/main/java/com/example/android/wearable/synchronizednotifications/NotificationUpdateService.java
index 8b46bf3..b5040d4 100644
--- a/wearable/wear/SynchronizedNotifications/Wearable/src/main/java/com/example/android/wearable/synchronizednotifications/NotificationUpdateService.java
+++ b/wearable/wear/SynchronizedNotifications/Wearable/src/main/java/com/example/android/wearable/synchronizednotifications/NotificationUpdateService.java
@@ -35,7 +35,6 @@
import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.DataMap;
import com.google.android.gms.wearable.DataMapItem;
-import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;
diff --git a/wearable/wear/SynchronizedNotifications/Wearable/src/main/java/com/example/android/wearable/synchronizednotifications/WearableActivity.java b/wearable/wear/SynchronizedNotifications/Wearable/src/main/java/com/example/android/wearable/synchronizednotifications/WearableActivity.java
index 6ef2f1b..9d1eff4 100644
--- a/wearable/wear/SynchronizedNotifications/Wearable/src/main/java/com/example/android/wearable/synchronizednotifications/WearableActivity.java
+++ b/wearable/wear/SynchronizedNotifications/Wearable/src/main/java/com/example/android/wearable/synchronizednotifications/WearableActivity.java
@@ -18,6 +18,9 @@
import android.app.Activity;
import android.os.Bundle;
+/**
+ * Empty Activity.
+ */
public class WearableActivity extends Activity {
@Override
diff --git a/wearable/wear/SynchronizedNotifications/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/SynchronizedNotifications/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/SynchronizedNotifications/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/SynchronizedNotifications/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/SynchronizedNotifications/template-params.xml b/wearable/wear/SynchronizedNotifications/template-params.xml
index 799b226..97c2244 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 364fb5a..59634fc 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/Timer/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/Timer/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/Timer/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/Timer/template-params.xml b/wearable/wear/Timer/template-params.xml
index 1188402..5f9d767 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>
@@ -37,4 +36,9 @@
<template src="base-build"/>
<template src="Wear"/>
+ <metadata>
+ <status>DEPRECATED</status>
+ <categories>Wearable</categories>
+ </metadata>
+
</sample>
diff --git a/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml b/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml
index 5433c94..d946cdb 100644
--- a/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml
+++ b/wearable/wear/WatchFace/Application/src/main/AndroidManifest.xml
@@ -18,13 +18,18 @@
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 +60,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 0000000..1d8e4c9
--- /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 0000000..73d1489
--- /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 0000000..63fc816
--- /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 0000000..56dca87
--- /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 6c6834f..275dcd3 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 c96d730..ce042e4 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,118 +14,128 @@
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" />
<!-- Required to act as a custom watch face. -->
- <uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Calendar permission used by CalendarWatchFaceService -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
+ <!-- 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" />
- <intent-filter>
- <action android:name="android.service.wallpaper.WallpaperService" />
- <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
- </intent-filter>
- </service>
-
- <service
- android:name=".SweepWatchFaceService"
- android:label="@string/sweep_name"
- android:permission="android.permission.BIND_WALLPAPER" >
- <meta-data
- android:name="android.service.wallpaper"
- android:resource="@xml/watch_face" />
- <meta-data
- android:name="com.google.android.wearable.watchface.preview"
- android:resource="@drawable/preview_analog" />
- <meta-data
- android:name="com.google.android.wearable.watchface.preview_circular"
- android:resource="@drawable/preview_analog_circular" />
- <intent-filter>
- <action android:name="android.service.wallpaper.WallpaperService" />
- <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
- </intent-filter>
- </service>
-
- <service
- android:name=".OpenGLWatchFaceService"
- android:label="@string/opengl_name"
- android:permission="android.permission.BIND_WALLPAPER" >
- <meta-data
- android:name="android.service.wallpaper"
- android:resource="@xml/watch_face" />
- <meta-data
- android:name="com.google.android.wearable.watchface.preview"
- android:resource="@drawable/preview_opengl" />
- <meta-data
- android:name="com.google.android.wearable.watchface.preview_circular"
- android:resource="@drawable/preview_opengl_circular" />
- <meta-data
- android:name="com.google.android.wearable.watchface.companionConfigurationAction"
- android:value="com.example.android.wearable.watchface.CONFIG_OPENGL" />
+ 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=".CardBoundsWatchFaceService"
- android:label="@string/card_bounds_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_card_bounds" />
+ 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_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.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=".InteractiveWatchFaceService"
- android:label="@string/interactive_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" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_opengl" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_opengl_circular" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_OPENGL" />
+
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
+ </service>
+ <service
+ android:name=".CardBoundsWatchFaceService"
+ android:label="@string/card_bounds_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_card_bounds" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_card_bounds_circular" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_CARD_BOUNDS" />
+
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
+ </service>
+ <service
+ android:name=".InteractiveWatchFaceService"
+ android:label="@string/interactive_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face" />
@@ -134,81 +145,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/AnalogWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/AnalogWatchFaceService.java
index fb86ac7..9fd73a5 100644
--- a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/AnalogWatchFaceService.java
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/AnalogWatchFaceService.java
@@ -87,7 +87,7 @@
/* Colors for all hands (hour, minute, seconds, ticks) based on photo loaded. */
private int mWatchHandColor;
- private int mWatchHandHightlightColor;
+ private int mWatchHandHighlightColor;
private int mWatchHandShadowColor;
private Paint mHourPaint;
@@ -151,7 +151,7 @@
/* Set defaults for colors */
mWatchHandColor = Color.WHITE;
- mWatchHandHightlightColor = Color.RED;
+ mWatchHandHighlightColor = Color.RED;
mWatchHandShadowColor = Color.BLACK;
mHourPaint = new Paint();
@@ -169,7 +169,7 @@
mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mSecondPaint = new Paint();
- mSecondPaint.setColor(mWatchHandHightlightColor);
+ mSecondPaint.setColor(mWatchHandHighlightColor);
mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
mSecondPaint.setAntiAlias(true);
mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
@@ -193,7 +193,7 @@
Log.d(TAG, "Palette: " + palette);
}
- mWatchHandHightlightColor = palette.getVibrantColor(Color.RED);
+ mWatchHandHighlightColor = palette.getVibrantColor(Color.RED);
mWatchHandColor = palette.getLightVibrantColor(Color.WHITE);
mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK);
updateWatchHandStyle();
@@ -261,7 +261,7 @@
} else {
mHourPaint.setColor(mWatchHandColor);
mMinutePaint.setColor(mWatchHandColor);
- mSecondPaint.setColor(mWatchHandHightlightColor);
+ mSecondPaint.setColor(mWatchHandHighlightColor);
mTickAndCirclePaint.setColor(mWatchHandColor);
mHourPaint.setAntiAlias(true);
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 0000000..7effd33
--- /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 a8ab955..98a251c 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.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 @@
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 @@
@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 @@
// 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 @@
@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 @@
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/DigitalWatchFaceUtil.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceUtil.java
index 1c4af70..e13440d 100644
--- a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceUtil.java
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/DigitalWatchFaceUtil.java
@@ -165,6 +165,7 @@
*/
public static void putConfigDataItem(GoogleApiClient googleApiClient, DataMap newConfig) {
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(PATH_WITH_FEATURE);
+ putDataMapRequest.setUrgent();
DataMap configToPut = putDataMapRequest.getDataMap();
configToPut.putAll(newConfig);
Wearable.DataApi.putDataItem(googleApiClient, putDataMapRequest.asPutDataRequest())
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 0000000..6dee04e
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/FitDistanceWatchFaceService.java
@@ -0,0 +1,536 @@
+/*
+ * 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). To authenticate and communicate with Google Fit, you must create a
+ * project in the Google Developers Console, activate the Fitness API, create an OAuth 2.0
+ * client ID, and register the public certificate from your app's signed APK. More details can be
+ * found here: https://developers.google.com/fit/android/get-started#step_3_enable_the_fitness_api
+ *
+ * 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 0000000..1f7b298
--- /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/java/com/example/android/wearable/watchface/InteractiveWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/InteractiveWatchFaceService.java
index 1a6f25b..7a50208 100644
--- a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/InteractiveWatchFaceService.java
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/InteractiveWatchFaceService.java
@@ -30,7 +30,7 @@
import android.view.WindowInsets;
/**
- * Demostrates interactive watch face capabilities, i.e., touching the display and registering
+ * Demonstrates interactive watch face capabilities, i.e., touching the display and registering
* three different events: touch, touch-cancel and tap. The watch face UI will show the count of
* these events as they occur. See the {@code onTapCommand} below.
*/
diff --git a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/SweepWatchFaceService.java b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/SweepWatchFaceService.java
index 0ba2ab9..a94097f 100644
--- a/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/SweepWatchFaceService.java
+++ b/wearable/wear/WatchFace/Wearable/src/main/java/com/example/android/wearable/watchface/SweepWatchFaceService.java
@@ -78,7 +78,7 @@
/* Colors for all hands (hour, minute, seconds, ticks) based on photo loaded. */
private int mWatchHandColor;
- private int mWatchHandHightlightColor;
+ private int mWatchHandHighlightColor;
private int mWatchHandShadowColor;
private Paint mHourPaint;
@@ -123,7 +123,7 @@
/* Set defaults for colors */
mWatchHandColor = Color.WHITE;
- mWatchHandHightlightColor = Color.RED;
+ mWatchHandHighlightColor = Color.RED;
mWatchHandShadowColor = Color.BLACK;
mHourPaint = new Paint();
@@ -141,7 +141,7 @@
mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mSecondPaint = new Paint();
- mSecondPaint.setColor(mWatchHandHightlightColor);
+ mSecondPaint.setColor(mWatchHandHighlightColor);
mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
mSecondPaint.setAntiAlias(true);
mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
@@ -165,7 +165,7 @@
Log.d(TAG, "Palette: " + palette);
}
- mWatchHandHightlightColor = palette.getVibrantColor(Color.RED);
+ mWatchHandHighlightColor = palette.getVibrantColor(Color.RED);
mWatchHandColor = palette.getLightVibrantColor(Color.WHITE);
mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK);
updateWatchHandStyle();
@@ -226,7 +226,7 @@
} else {
mHourPaint.setColor(mWatchHandColor);
mMinutePaint.setColor(mWatchHandColor);
- mSecondPaint.setColor(mWatchHandHightlightColor);
+ mSecondPaint.setColor(mWatchHandHighlightColor);
mTickAndCirclePaint.setColor(mWatchHandColor);
mHourPaint.setAntiAlias(true);
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
new file mode 100644
index 0000000..6bae68f
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/ic_lock_open_white_24dp.png
Binary files differ
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
new file mode 100644
index 0000000..a96f355
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_distance.png
Binary files differ
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
new file mode 100644
index 0000000..912d85b
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_distance_circular.png
Binary files differ
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
new file mode 100644
index 0000000..04b8b5e
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_fit.png
Binary files differ
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
new file mode 100644
index 0000000..b421e28
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-hdpi/preview_fit_circular.png
Binary files differ
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
new file mode 100644
index 0000000..3f47b54
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-mdpi/ic_lock_open_white_24dp.png
Binary files differ
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
new file mode 100644
index 0000000..cbe9e1c
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png
Binary files differ
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
new file mode 100644
index 0000000..1d1b0f4
--- /dev/null
+++ b/wearable/wear/WatchFace/Wearable/src/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png
Binary files differ
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 0000000..bf0e3f6
--- /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 4973466..0b0672b 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 19bc3e7..4090995 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/WatchFace/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/WatchFace/template-params.xml b/wearable/wear/WatchFace/template-params.xml
index cedf8db..e339fe2 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.1</dependency_wearable>
<dependency>com.google.android.support:wearable:1.3.0</dependency>
+ <dependency>com.google.android.gms:play-services-fitness</dependency>
+ <dependency_wearable>com.google.android.gms:play-services-fitness</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 33a266d..774817b 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/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/WatchViewStub/gradle/wrapper/gradle-wrapper.properties
index 0c71e76..07fc193 100644
--- a/wearable/wear/WatchViewStub/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/WatchViewStub/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/WatchViewStub/template-params.xml b/wearable/wear/WatchViewStub/template-params.xml
index ff7e7f8..39568aa 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 0000000..fe1f588
--- /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 0000000..25ba12a
--- /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 0000000..7cebf71
--- /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 0000000..1d3591c
--- /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
new file mode 100644
index 0000000..8c0fb64
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.properties
similarity index 79%
copy from ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.properties
copy to wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.properties
index a51db8c..08a6fd5 100644
--- a/ui/views/Elevation/ElevationDrag/gradle/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/WearSpeakerSample/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue May 20 13:33:02 BST 2014
+#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.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradlew b/wearable/wear/WearSpeakerSample/gradlew
similarity index 100%
rename from ui/views/Elevation/ElevationDrag/gradle/gradlew
rename to wearable/wear/WearSpeakerSample/gradlew
diff --git a/ui/views/Elevation/ElevationDrag/gradle/gradlew.bat b/wearable/wear/WearSpeakerSample/gradlew.bat
similarity index 100%
rename from ui/views/Elevation/ElevationDrag/gradle/gradlew.bat
rename to wearable/wear/WearSpeakerSample/gradlew.bat
diff --git a/wearable/wear/WearSpeakerSample/screenshots/1.png b/wearable/wear/WearSpeakerSample/screenshots/1.png
new file mode 100644
index 0000000..d98e6fa
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/screenshots/1.png
Binary files differ
diff --git a/wearable/wear/WearSpeakerSample/screenshots/2.png b/wearable/wear/WearSpeakerSample/screenshots/2.png
new file mode 100644
index 0000000..226da1f
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/screenshots/2.png
Binary files differ
diff --git a/wearable/wear/WearSpeakerSample/screenshots/3.png b/wearable/wear/WearSpeakerSample/screenshots/3.png
new file mode 100644
index 0000000..d7467d7
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/screenshots/3.png
Binary files differ
diff --git a/wearable/wear/WearSpeakerSample/screenshots/4.png b/wearable/wear/WearSpeakerSample/screenshots/4.png
new file mode 100644
index 0000000..1044ee1
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/screenshots/4.png
Binary files differ
diff --git a/wearable/wear/WearSpeakerSample/settings.gradle b/wearable/wear/WearSpeakerSample/settings.gradle
new file mode 100644
index 0000000..8d97c99
--- /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 0000000..f07d4c5
--- /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 0000000..796b96d
--- /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 0000000..8d3e550
--- /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 0000000..002bc05
--- /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 0000000..135d3e0
--- /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 0000000..e7a4870
--- /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 0000000..a45bdd2
--- /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 0000000..7ce2fd5
--- /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 0000000..df4abe5
--- /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 0000000..0971d96
--- /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 0000000..70de799
--- /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 0000000..15e798a
--- /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 0000000..c9417dd
--- /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 0000000..e87660d
--- /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 0000000..9dd8678
--- /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 0000000..7e004ad
--- /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
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
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
new file mode 100644
index 0000000..c133a0c
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
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
new file mode 100644
index 0000000..bfa42f0
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
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
new file mode 100644
index 0000000..324e72c
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/wearable/wear/WearSpeakerSample/wear/src/main/res/raw/sound.mp3 b/wearable/wear/WearSpeakerSample/wear/src/main/res/raw/sound.mp3
new file mode 100644
index 0000000..94e3d0e
--- /dev/null
+++ b/wearable/wear/WearSpeakerSample/wear/src/main/res/raw/sound.mp3
Binary files differ
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 0000000..e9b8605
--- /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 0000000..cc342b5
--- /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/Application/src/main/AndroidManifest.xml b/wearable/wear/XYZTouristAttractions/Application/src/main/AndroidManifest.xml
index 76f0198..9d88b39 100644
--- a/wearable/wear/XYZTouristAttractions/Application/src/main/AndroidManifest.xml
+++ b/wearable/wear/XYZTouristAttractions/Application/src/main/AndroidManifest.xml
@@ -16,17 +16,21 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.example.android.xyztouristattractions" >
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.android.xyztouristattractions">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:theme="@style/XYZAppTheme" >
+ android:theme="@style/XYZAppTheme"
+ android:fullBackupContent="true">
<activity
android:name=".ui.AttractionListActivity"
diff --git a/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/provider/TouristAttractions.java b/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/provider/TouristAttractions.java
index 50be362..62ddbf9 100644
--- a/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/provider/TouristAttractions.java
+++ b/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/provider/TouristAttractions.java
@@ -18,7 +18,6 @@
import android.net.Uri;
-import com.example.android.xyztouristattractions.BuildConfig;
import com.example.android.xyztouristattractions.common.Attraction;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.maps.model.LatLng;
@@ -126,11 +125,8 @@
public static String getClosestCity(LatLng curLatLng) {
if (curLatLng == null) {
- // In debug build still return a city so some data is displayed
- if (BuildConfig.DEBUG) {
- return TEST_CITY;
- }
- return null;
+ // If location is unknown return test city so some data is shown
+ return TEST_CITY;
}
double minDistance = 0;
diff --git a/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/service/UtilityService.java b/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/service/UtilityService.java
index 4112a65..3122d56 100644
--- a/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/service/UtilityService.java
+++ b/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/service/UtilityService.java
@@ -150,6 +150,11 @@
*/
private void addGeofencesInternal() {
Log.v(TAG, ACTION_ADD_GEOFENCES);
+
+ if (!Utils.checkFineLocationPermission(this)) {
+ return;
+ }
+
GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.build();
@@ -202,6 +207,11 @@
*/
private void requestLocationInternal() {
Log.v(TAG, ACTION_REQUEST_LOCATION);
+
+ if (!Utils.checkFineLocationPermission(this)) {
+ return;
+ }
+
GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.build();
@@ -358,7 +368,7 @@
.setSmallIcon(R.drawable.ic_stat_maps_pin_drop)
.setContentIntent(pendingIntent)
.setDeleteIntent(deletePendingIntent)
- .setColor(getResources().getColor(R.color.colorPrimary))
+ .setColor(getResources().getColor(R.color.colorPrimary, getTheme()))
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setAutoCancel(true);
@@ -466,6 +476,7 @@
dataMap.getDataMap().putDataMapArrayList(Constants.EXTRA_ATTRACTIONS, attractionsData);
dataMap.getDataMap().putLong(Constants.EXTRA_TIMESTAMP, new Date().getTime());
PutDataRequest request = dataMap.asPutDataRequest();
+ request.setUrgent();
// Send the data over
DataApi.DataItemResult result =
diff --git a/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/ui/AttractionListActivity.java b/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/ui/AttractionListActivity.java
index 8d2908c..8c23f3d 100644
--- a/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/ui/AttractionListActivity.java
+++ b/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/ui/AttractionListActivity.java
@@ -16,11 +16,16 @@
package com.example.android.xyztouristattractions.ui;
+import android.Manifest;
+import android.content.pm.PackageManager;
import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
+import android.view.View;
import android.widget.Toast;
import com.example.android.xyztouristattractions.R;
@@ -31,7 +36,10 @@
* The main tourist attraction activity screen which contains a list of
* attractions sorted by distance.
*/
-public class AttractionListActivity extends AppCompatActivity {
+public class AttractionListActivity extends AppCompatActivity implements
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final int PERMISSION_REQ = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -44,7 +52,23 @@
.commit();
}
- UtilityService.addGeofences(this);
+ // Check fine location permission has been granted
+ if (!Utils.checkFineLocationPermission(this)) {
+ // See if user has denied permission in the past
+ if (ActivityCompat.shouldShowRequestPermissionRationale(
+ this, Manifest.permission.ACCESS_FINE_LOCATION)) {
+ // Show a simple snackbar explaining the request instead
+ showPermissionSnackbar();
+ } else {
+ // Otherwise request permission from user
+ if (savedInstanceState == null) {
+ requestFineLocationPermission();
+ }
+ }
+ } else {
+ // Otherwise permission is granted (which is always the case on pre-M devices)
+ fineLocationPermissionGranted();
+ }
}
@Override
@@ -88,6 +112,51 @@
}
/**
+ * Permissions request result callback
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ switch (requestCode) {
+ case PERMISSION_REQ:
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ fineLocationPermissionGranted();
+ }
+ }
+ }
+
+ /**
+ * Request the fine location permission from the user
+ */
+ private void requestFineLocationPermission() {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQ);
+ }
+
+ /**
+ * Run when fine location permission has been granted
+ */
+ private void fineLocationPermissionGranted() {
+ UtilityService.addGeofences(this);
+ UtilityService.requestLocation(this);
+ }
+
+ /**
+ * Show a permission explanation snackbar
+ */
+ private void showPermissionSnackbar() {
+ Snackbar.make(
+ findViewById(R.id.container), R.string.permission_explanation, Snackbar.LENGTH_LONG)
+ .setAction(R.string.permission_explanation_action, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ requestFineLocationPermission();
+ }
+ })
+ .show();
+ }
+
+ /**
* Show a basic debug dialog to provide more info on the built-in debug
* options.
*/
diff --git a/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/ui/AttractionListFragment.java b/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/ui/AttractionListFragment.java
index 28f9127..71439b1 100644
--- a/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/ui/AttractionListFragment.java
+++ b/wearable/wear/XYZTouristAttractions/Application/src/main/java/com/example/android/xyztouristattractions/ui/AttractionListFragment.java
@@ -23,7 +23,6 @@
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
-import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -60,6 +59,7 @@
private AttractionAdapter mAdapter;
private LatLng mLatestLocation;
private int mImageSize;
+ private boolean mItemClicked;
public AttractionListFragment() {}
@@ -79,8 +79,6 @@
(AttractionsRecyclerView) view.findViewById(android.R.id.list);
recyclerView.setEmptyView(view.findViewById(android.R.id.empty));
recyclerView.setHasFixedSize(true);
- recyclerView.setLayoutManager(new GridLayoutManager(
- getActivity(), getResources().getInteger(R.integer.list_columns)));
recyclerView.setAdapter(mAdapter);
return view;
@@ -89,6 +87,7 @@
@Override
public void onResume() {
super.onResume();
+ mItemClicked = false;
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
mBroadcastReceiver, UtilityService.getLocationUpdatedIntentFilter());
}
@@ -189,9 +188,12 @@
@Override
public void onItemClick(View view, int position) {
- View heroView = view.findViewById(android.R.id.icon);
- DetailActivity.launch(
- getActivity(), mAdapter.mAttractionList.get(position).name, heroView);
+ if (!mItemClicked) {
+ mItemClicked = true;
+ View heroView = view.findViewById(android.R.id.icon);
+ DetailActivity.launch(
+ getActivity(), mAdapter.mAttractionList.get(position).name, heroView);
+ }
}
}
diff --git a/wearable/wear/XYZTouristAttractions/Application/src/main/res/layout/activity_main.xml b/wearable/wear/XYZTouristAttractions/Application/src/main/res/layout/activity_main.xml
index 8661610..17c9f66 100644
--- a/wearable/wear/XYZTouristAttractions/Application/src/main/res/layout/activity_main.xml
+++ b/wearable/wear/XYZTouristAttractions/Application/src/main/res/layout/activity_main.xml
@@ -14,12 +14,13 @@
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.design.widget.CoordinatorLayout
+
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
tools:context=".MainActivity">
-</LinearLayout>
\ No newline at end of file
+</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/wearable/wear/XYZTouristAttractions/Application/src/main/res/layout/fragment_main.xml b/wearable/wear/XYZTouristAttractions/Application/src/main/res/layout/fragment_main.xml
index 94d110a..c92a2a8 100644
--- a/wearable/wear/XYZTouristAttractions/Application/src/main/res/layout/fragment_main.xml
+++ b/wearable/wear/XYZTouristAttractions/Application/src/main/res/layout/fragment_main.xml
@@ -16,6 +16,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@@ -25,7 +26,9 @@
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:scrollbars="vertical" />
+ android:scrollbars="vertical"
+ app:layoutManager="GridLayoutManager"
+ app:spanCount="@integer/list_columns" />
<TextView
android:id="@android:id/empty"
diff --git a/wearable/wear/XYZTouristAttractions/Application/src/main/res/values/strings.xml b/wearable/wear/XYZTouristAttractions/Application/src/main/res/values/strings.xml
index 5f3ee14..fede01e 100644
--- a/wearable/wear/XYZTouristAttractions/Application/src/main/res/values/strings.xml
+++ b/wearable/wear/XYZTouristAttractions/Application/src/main/res/values/strings.xml
@@ -45,5 +45,7 @@
</string>
<string name="action_test_toggle_geofence">Toggle Geofence Trigger</string>
<string name="action_map">Show on Map</string>
+ <string name="permission_explanation">Allow this app to use your location to show distance to attractions?</string>
+ <string name="permission_explanation_action">Let\'s do it!</string>
</resources>
diff --git a/wearable/wear/XYZTouristAttractions/Shared/src/main/java/com/example/android/xyztouristattractions/common/Utils.java b/wearable/wear/XYZTouristAttractions/Shared/src/main/java/com/example/android/xyztouristattractions/common/Utils.java
index 70e05bf..6fa5129 100644
--- a/wearable/wear/XYZTouristAttractions/Shared/src/main/java/com/example/android/xyztouristattractions/common/Utils.java
+++ b/wearable/wear/XYZTouristAttractions/Shared/src/main/java/com/example/android/xyztouristattractions/common/Utils.java
@@ -16,13 +16,16 @@
package com.example.android.xyztouristattractions.common;
+import android.Manifest;
import android.content.Context;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.graphics.Rect;
import android.preference.PreferenceManager;
+import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.Display;
@@ -54,6 +57,15 @@
private static final String DISTANCE_M_POSTFIX = "m";
/**
+ * Check if the app has access to fine location permission. On pre-M
+ * devices this will always return true.
+ */
+ public static boolean checkFineLocationPermission(Context context) {
+ return PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(
+ context, Manifest.permission.ACCESS_FINE_LOCATION);
+ }
+
+ /**
* Calculate distance between two LatLng points and format it nicely for
* display. As this is a sample, it only statically supports metric units.
* A production app should check locale and support the correct units.
@@ -90,6 +102,10 @@
* Fetch the location from app preferences.
*/
public static LatLng getLocation(Context context) {
+ if (!checkFineLocationPermission(context)) {
+ return null;
+ }
+
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
Long lat = prefs.getLong(PREFERENCES_LAT, Long.MAX_VALUE);
Long lng = prefs.getLong(PREFERENCES_LNG, Long.MAX_VALUE);
diff --git a/wearable/wear/XYZTouristAttractions/Wearable/src/main/AndroidManifest.xml b/wearable/wear/XYZTouristAttractions/Wearable/src/main/AndroidManifest.xml
index 80d0c92..d353b29 100644
--- a/wearable/wear/XYZTouristAttractions/Wearable/src/main/AndroidManifest.xml
+++ b/wearable/wear/XYZTouristAttractions/Wearable/src/main/AndroidManifest.xml
@@ -16,10 +16,14 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.example.android.xyztouristattractions" >
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.android.xyztouristattractions">
<uses-feature android:name="android.hardware.type.watch" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
+
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="22" />
diff --git a/wearable/wear/XYZTouristAttractions/Wearable/src/main/java/com/example/android/xyztouristattractions/service/ListenerService.java b/wearable/wear/XYZTouristAttractions/Wearable/src/main/java/com/example/android/xyztouristattractions/service/ListenerService.java
index d228251..ea071f0 100644
--- a/wearable/wear/XYZTouristAttractions/Wearable/src/main/java/com/example/android/xyztouristattractions/service/ListenerService.java
+++ b/wearable/wear/XYZTouristAttractions/Wearable/src/main/java/com/example/android/xyztouristattractions/service/ListenerService.java
@@ -17,11 +17,12 @@
package com.example.android.xyztouristattractions.service;
import android.app.Notification;
-import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import com.example.android.xyztouristattractions.R;
@@ -39,7 +40,6 @@
import com.google.android.gms.wearable.WearableListenerService;
import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@@ -106,21 +106,20 @@
PendingIntent deletePendingIntent = PendingIntent.getService(
this, 0, UtilityService.getClearRemoteNotificationsIntent(this), 0);
- Notification notification = new Notification.Builder(this)
+ Notification notification = new NotificationCompat.Builder(this)
.setContentText(getResources().getQuantityString(
R.plurals.attractions_found, count, count))
.setSmallIcon(R.mipmap.ic_launcher)
.setDeleteIntent(deletePendingIntent)
- .addAction(R.drawable.ic_full_explore,
+ .addAction(new NotificationCompat.Action.Builder(R.drawable.ic_full_explore,
getString(R.string.action_explore),
- pendingIntent)
- .extend(new Notification.WearableExtender()
+ pendingIntent).build())
+ .extend(new NotificationCompat.WearableExtender()
.setBackground(bitmap)
)
.build();
- NotificationManager notificationManager =
- (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(Constants.WEAR_NOTIFICATION_ID, notification);
googleApiClient.disconnect();
diff --git a/wearable/wear/XYZTouristAttractions/Wearable/src/main/res/layout/gridpager_action.xml b/wearable/wear/XYZTouristAttractions/Wearable/src/main/res/layout/gridpager_action.xml
index ac01509..45495ad 100644
--- a/wearable/wear/XYZTouristAttractions/Wearable/src/main/res/layout/gridpager_action.xml
+++ b/wearable/wear/XYZTouristAttractions/Wearable/src/main/res/layout/gridpager_action.xml
@@ -26,4 +26,4 @@
android:text="@string/action_open"
android:maxLines="1"
android:color="@color/colorPrimary"
- app:rippleColor="@color/colorAccent" />
+ app:buttonRippleColor="@color/colorAccent" />
diff --git a/wearable/wear/XYZTouristAttractions/gradle/wrapper/gradle-wrapper.properties b/wearable/wear/XYZTouristAttractions/gradle/wrapper/gradle-wrapper.properties
index 7d3b483..4364027 100644
--- a/wearable/wear/XYZTouristAttractions/gradle/wrapper/gradle-wrapper.properties
+++ b/wearable/wear/XYZTouristAttractions/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
diff --git a/wearable/wear/XYZTouristAttractions/template-params.xml b/wearable/wear/XYZTouristAttractions/template-params.xml
index cfa368f..69f2f77 100644
--- a/wearable/wear/XYZTouristAttractions/template-params.xml
+++ b/wearable/wear/XYZTouristAttractions/template-params.xml
@@ -19,7 +19,8 @@
<group>Wearable</group>
<package>com.example.android.xyztouristattractions</package>
<minSdk>18</minSdk>
- <targetSdkVersion>22</targetSdkVersion>
+ <targetSdkVersion>23</targetSdkVersion>
+ <targetSdkVersionWear>22</targetSdkVersionWear>
<wearable>
<has_handheld_app>true</has_handheld_app>
@@ -27,11 +28,12 @@
<dependency>com.google.android.gms:play-services-location</dependency>
<dependency>com.google.maps.android:android-maps-utils:0.3.4</dependency>
- <dependency>com.github.bumptech.glide:glide:3.6.0</dependency>
- <dependency>com.android.support:appcompat-v7:22.2.0</dependency>
- <dependency>com.android.support:recyclerview-v7:22.2.0</dependency>
- <dependency>com.android.support:design:22.2.0</dependency>
+ <dependency>com.github.bumptech.glide:glide:3.6.1</dependency>
+ <dependency>com.android.support:appcompat-v7:23.0.0</dependency>
+ <dependency>com.android.support:recyclerview-v7:23.0.0</dependency>
+ <dependency>com.android.support:design:23.0.0</dependency>
<dependency_wearable>com.google.android.gms:play-services-location</dependency_wearable>
+ <dependency_shared>com.android.support:support-v13:23.0.0</dependency_shared>
<dependency_shared>com.google.android.gms:play-services-wearable</dependency_shared>
<dependency_shared>com.google.android.gms:play-services-location</dependency_shared>
<dependency_shared>com.google.maps.android:android-maps-utils:0.3.4</dependency_shared>
@@ -83,6 +85,8 @@
<android>android.support.v7.appcompat</android>
<android>android.support.v7.widget.RecyclerView</android>
<android>android.support.v7.widget.GridLayoutManager</android>
+ <android>android.support.v4.content.ContextCompat</android>
+ <android>android.support.v4.app.ActivityCompat</android>
<android>android.support.design.widget.FloatingActionButton</android>
<android>android.support.design.widget.CoordinatorLayout</android>
<android>com.google.android.gms.common.api.GoogleApiClient</android>