Sync sample browser prebuilts for mnc-docs
Synced to //developers/samples/android commit 44c699bf11bae6c9eef1f8c56bd405d547e179e6. Change-Id: I61ceec197e6fa420fc5a6d16e703aa1aab2b0c4e
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.agendadata" >
|
package="com.example.android.wearable.agendadata" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/bundle_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
@@ -147,6 +148,7 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
android:id="@+id/bundle_array_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import android.content.Context;
|
|||||||
import android.content.RestrictionEntry;
|
import android.content.RestrictionEntry;
|
||||||
import android.content.RestrictionsManager;
|
import android.content.RestrictionsManager;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
@@ -105,6 +106,8 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
private static final String DELIMETER = ",";
|
private static final String DELIMETER = ",";
|
||||||
private static final String SEPARATOR = ":";
|
private static final String SEPARATOR = ":";
|
||||||
|
|
||||||
|
private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current status of the restrictions.
|
* Current status of the restrictions.
|
||||||
*/
|
*/
|
||||||
@@ -138,6 +141,15 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
mEditProfileAge = (EditText) view.findViewById(R.id.profile_age);
|
mEditProfileAge = (EditText) view.findViewById(R.id.profile_age);
|
||||||
mLayoutItems = (LinearLayout) view.findViewById(R.id.items);
|
mLayoutItems = (LinearLayout) view.findViewById(R.id.items);
|
||||||
view.findViewById(R.id.item_add).setOnClickListener(this);
|
view.findViewById(R.id.item_add).setOnClickListener(this);
|
||||||
|
View bundleLayout = view.findViewById(R.id.bundle_layout);
|
||||||
|
View bundleArrayLayout = view.findViewById(R.id.bundle_array_layout);
|
||||||
|
if (BUNDLE_SUPPORTED) {
|
||||||
|
bundleLayout.setVisibility(View.VISIBLE);
|
||||||
|
bundleArrayLayout.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
bundleLayout.setVisibility(View.GONE);
|
||||||
|
bundleArrayLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -280,7 +292,7 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
TextUtils.join(DELIMETER,
|
TextUtils.join(DELIMETER,
|
||||||
restriction.getAllSelectedStrings())),
|
restriction.getAllSelectedStrings())),
|
||||||
DELIMETER));
|
DELIMETER));
|
||||||
} else if (RESTRICTION_KEY_PROFILE.equals(key)) {
|
} else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_PROFILE.equals(key)) {
|
||||||
String name = null;
|
String name = null;
|
||||||
int age = 0;
|
int age = 0;
|
||||||
for (RestrictionEntry entry : restriction.getRestrictions()) {
|
for (RestrictionEntry entry : restriction.getRestrictions()) {
|
||||||
@@ -294,7 +306,7 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
name = prefs.getString(RESTRICTION_KEY_PROFILE_NAME, name);
|
name = prefs.getString(RESTRICTION_KEY_PROFILE_NAME, name);
|
||||||
age = prefs.getInt(RESTRICTION_KEY_PROFILE_AGE, age);
|
age = prefs.getInt(RESTRICTION_KEY_PROFILE_AGE, age);
|
||||||
updateProfile(name, age);
|
updateProfile(name, age);
|
||||||
} else if (RESTRICTION_KEY_ITEMS.equals(key)) {
|
} else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_ITEMS.equals(key)) {
|
||||||
String itemsString = prefs.getString(RESTRICTION_KEY_ITEMS, "");
|
String itemsString = prefs.getString(RESTRICTION_KEY_ITEMS, "");
|
||||||
HashMap<String, String> items = new HashMap<>();
|
HashMap<String, String> items = new HashMap<>();
|
||||||
for (String itemString : TextUtils.split(itemsString, DELIMETER)) {
|
for (String itemString : TextUtils.split(itemsString, DELIMETER)) {
|
||||||
@@ -351,6 +363,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateProfile(String name, int age) {
|
private void updateProfile(String name, int age) {
|
||||||
|
if (!BUNDLE_SUPPORTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Bundle profile = new Bundle();
|
Bundle profile = new Bundle();
|
||||||
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
|
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
|
||||||
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
|
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
|
||||||
@@ -364,6 +379,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateItems(Context context, Map<String, String> items) {
|
private void updateItems(Context context, Map<String, String> items) {
|
||||||
|
if (!BUNDLE_SUPPORTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
|
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
|
||||||
LayoutInflater inflater = LayoutInflater.from(context);
|
LayoutInflater inflater = LayoutInflater.from(context);
|
||||||
mLayoutItems.removeAllViews();
|
mLayoutItems.removeAllViews();
|
||||||
@@ -500,6 +518,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
* @param age The value to be set for the "age" field.
|
* @param age The value to be set for the "age" field.
|
||||||
*/
|
*/
|
||||||
private void saveProfile(Activity activity, String name, int age) {
|
private void saveProfile(Activity activity, String name, int age) {
|
||||||
|
if (!BUNDLE_SUPPORTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Bundle profile = new Bundle();
|
Bundle profile = new Bundle();
|
||||||
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
|
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
|
||||||
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
|
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
|
||||||
@@ -515,6 +536,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
* @param items The values.
|
* @param items The values.
|
||||||
*/
|
*/
|
||||||
private void saveItems(Activity activity, Map<String, String> items) {
|
private void saveItems(Activity activity, Map<String, String> items) {
|
||||||
|
if (!BUNDLE_SUPPORTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
|
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
|
||||||
saveRestrictions(activity);
|
saveRestrictions(activity);
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ limitations under the License.
|
|||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
tools:text="@string/your_rank"/>
|
tools:text="@string/your_rank"/>
|
||||||
|
|
||||||
<include layout="@layout/separator"/>
|
<include layout="@layout/separator" android:id="@+id/bundle_separator"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/approvals_you_have"
|
android:id="@+id/approvals_you_have"
|
||||||
@@ -77,7 +77,7 @@ limitations under the License.
|
|||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
tools:text="@string/your_profile"/>
|
tools:text="@string/your_profile"/>
|
||||||
|
|
||||||
<include layout="@layout/separator"/>
|
<include layout="@layout/separator" android:id="@+id/bundle_array_separator" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/your_items"
|
android:id="@+id/your_items"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package com.example.android.apprestrictionschema;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.RestrictionEntry;
|
import android.content.RestrictionEntry;
|
||||||
import android.content.RestrictionsManager;
|
import android.content.RestrictionsManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
@@ -57,6 +58,8 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
|
|||||||
private static final String KEY_ITEM_KEY = "key";
|
private static final String KEY_ITEM_KEY = "key";
|
||||||
private static final String KEY_ITEM_VALUE = "value";
|
private static final String KEY_ITEM_VALUE = "value";
|
||||||
|
|
||||||
|
private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
|
||||||
|
|
||||||
// Message to show when the button is clicked (String restriction)
|
// Message to show when the button is clicked (String restriction)
|
||||||
private String mMessage;
|
private String mMessage;
|
||||||
|
|
||||||
@@ -82,9 +85,22 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
|
|||||||
mTextNumber = (TextView) view.findViewById(R.id.your_number);
|
mTextNumber = (TextView) view.findViewById(R.id.your_number);
|
||||||
mTextRank = (TextView) view.findViewById(R.id.your_rank);
|
mTextRank = (TextView) view.findViewById(R.id.your_rank);
|
||||||
mTextApprovals = (TextView) view.findViewById(R.id.approvals_you_have);
|
mTextApprovals = (TextView) view.findViewById(R.id.approvals_you_have);
|
||||||
|
View bundleSeparator = view.findViewById(R.id.bundle_separator);
|
||||||
mTextProfile = (TextView) view.findViewById(R.id.your_profile);
|
mTextProfile = (TextView) view.findViewById(R.id.your_profile);
|
||||||
|
View bundleArraySeparator = view.findViewById(R.id.bundle_array_separator);
|
||||||
mTextItems = (TextView) view.findViewById(R.id.your_items);
|
mTextItems = (TextView) view.findViewById(R.id.your_items);
|
||||||
mButtonSayHello.setOnClickListener(this);
|
mButtonSayHello.setOnClickListener(this);
|
||||||
|
if (BUNDLE_SUPPORTED) {
|
||||||
|
bundleSeparator.setVisibility(View.VISIBLE);
|
||||||
|
mTextProfile.setVisibility(View.VISIBLE);
|
||||||
|
bundleArraySeparator.setVisibility(View.VISIBLE);
|
||||||
|
mTextItems.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
bundleSeparator.setVisibility(View.GONE);
|
||||||
|
mTextProfile.setVisibility(View.GONE);
|
||||||
|
bundleArraySeparator.setVisibility(View.GONE);
|
||||||
|
mTextItems.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -178,6 +194,9 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateProfile(RestrictionEntry entry, Bundle restrictions) {
|
private void updateProfile(RestrictionEntry entry, Bundle restrictions) {
|
||||||
|
if (!BUNDLE_SUPPORTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
String name = null;
|
String name = null;
|
||||||
int age = 0;
|
int age = 0;
|
||||||
if (restrictions == null || !restrictions.containsKey(KEY_PROFILE)) {
|
if (restrictions == null || !restrictions.containsKey(KEY_PROFILE)) {
|
||||||
@@ -201,6 +220,9 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateItems(RestrictionEntry entry, Bundle restrictions) {
|
private void updateItems(RestrictionEntry entry, Bundle restrictions) {
|
||||||
|
if (!BUNDLE_SUPPORTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
if (restrictions != null) {
|
if (restrictions != null) {
|
||||||
Parcelable[] parcelables = restrictions.getParcelableArray(KEY_ITEMS);
|
Parcelable[] parcelables = restrictions.getParcelableArray(KEY_ITEMS);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
page.tags="Asymmetric Fingerprint Dialog Sample"
|
page.tags="AsymmetricFingerprintDialog"
|
||||||
sample.group=Security
|
sample.group=Security
|
||||||
@jd:body
|
@jd:body
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Asymmetric Fingerprint Dialog Sample</string>
|
<string name="app_name">AsymmetricFingerprintDialog</string>
|
||||||
<string name="intro_message">
|
<string name="intro_message">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,10 @@
|
|||||||
|
|
||||||
package com.example.android.asymmetricfingerprintdialog;
|
package com.example.android.asymmetricfingerprintdialog;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.KeyguardManager;
|
import android.app.KeyguardManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.hardware.fingerprint.FingerprintManager;
|
import android.hardware.fingerprint.FingerprintManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.security.keystore.KeyGenParameterSpec;
|
import android.security.keystore.KeyGenParameterSpec;
|
||||||
@@ -59,8 +57,6 @@ public class MainActivity extends Activity {
|
|||||||
/** Alias for our key in the Android Key Store */
|
/** Alias for our key in the Android Key Store */
|
||||||
public static final String KEY_NAME = "my_key";
|
public static final String KEY_NAME = "my_key";
|
||||||
|
|
||||||
private static final int FINGERPRINT_PERMISSION_REQUEST_CODE = 0;
|
|
||||||
|
|
||||||
@Inject KeyguardManager mKeyguardManager;
|
@Inject KeyguardManager mKeyguardManager;
|
||||||
@Inject FingerprintManager mFingerprintManager;
|
@Inject FingerprintManager mFingerprintManager;
|
||||||
@Inject FingerprintAuthenticationDialogFragment mFragment;
|
@Inject FingerprintAuthenticationDialogFragment mFragment;
|
||||||
@@ -74,71 +70,63 @@ public class MainActivity extends Activity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
((InjectedApplication) getApplication()).inject(this);
|
((InjectedApplication) getApplication()).inject(this);
|
||||||
|
|
||||||
requestPermissions(new String[]{Manifest.permission.USE_FINGERPRINT},
|
setContentView(R.layout.activity_main);
|
||||||
FINGERPRINT_PERMISSION_REQUEST_CODE);
|
Button purchaseButton = (Button) findViewById(R.id.purchase_button);
|
||||||
}
|
if (!mKeyguardManager.isKeyguardSecure()) {
|
||||||
|
// Show a message that the user hasn't set up a fingerprint or lock screen.
|
||||||
@Override
|
Toast.makeText(this,
|
||||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) {
|
"Secure lock screen hasn't set up.\n"
|
||||||
if (requestCode == FINGERPRINT_PERMISSION_REQUEST_CODE
|
+ "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
|
||||||
&& state[0] == PackageManager.PERMISSION_GRANTED) {
|
Toast.LENGTH_LONG).show();
|
||||||
setContentView(R.layout.activity_main);
|
purchaseButton.setEnabled(false);
|
||||||
Button purchaseButton = (Button) findViewById(R.id.purchase_button);
|
return;
|
||||||
if (!mKeyguardManager.isKeyguardSecure()) {
|
|
||||||
// Show a message that the user hasn't set up a fingerprint or lock screen.
|
|
||||||
Toast.makeText(this,
|
|
||||||
"Secure lock screen hasn't set up.\n"
|
|
||||||
+ "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
purchaseButton.setEnabled(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!mFingerprintManager.hasEnrolledFingerprints()) {
|
|
||||||
purchaseButton.setEnabled(false);
|
|
||||||
// This happens when no fingerprints are registered.
|
|
||||||
Toast.makeText(this,
|
|
||||||
"Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
createKeyPair();
|
|
||||||
purchaseButton.setEnabled(true);
|
|
||||||
purchaseButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
findViewById(R.id.confirmation_message).setVisibility(View.GONE);
|
|
||||||
findViewById(R.id.encrypted_message).setVisibility(View.GONE);
|
|
||||||
|
|
||||||
// Set up the crypto object for later. The object will be authenticated by use
|
|
||||||
// of the fingerprint.
|
|
||||||
if (initSignature()) {
|
|
||||||
|
|
||||||
// Show the fingerprint dialog. The user has the option to use the fingerprint with
|
|
||||||
// crypto, or you can fall back to using a server-side verified password.
|
|
||||||
mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature));
|
|
||||||
boolean useFingerprintPreference = mSharedPreferences
|
|
||||||
.getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
|
|
||||||
true);
|
|
||||||
if (useFingerprintPreference) {
|
|
||||||
mFragment.setStage(
|
|
||||||
FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
|
|
||||||
} else {
|
|
||||||
mFragment.setStage(
|
|
||||||
FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
|
|
||||||
}
|
|
||||||
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
|
|
||||||
} else {
|
|
||||||
// This happens if the lock screen has been disabled or or a fingerprint got
|
|
||||||
// enrolled. Thus show the dialog to authenticate with their password first
|
|
||||||
// and ask the user if they want to authenticate with fingerprints in the
|
|
||||||
// future
|
|
||||||
mFragment.setStage(
|
|
||||||
FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
|
|
||||||
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
//noinspection ResourceType
|
||||||
|
if (!mFingerprintManager.hasEnrolledFingerprints()) {
|
||||||
|
purchaseButton.setEnabled(false);
|
||||||
|
// This happens when no fingerprints are registered.
|
||||||
|
Toast.makeText(this,
|
||||||
|
"Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createKeyPair();
|
||||||
|
purchaseButton.setEnabled(true);
|
||||||
|
purchaseButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
findViewById(R.id.confirmation_message).setVisibility(View.GONE);
|
||||||
|
findViewById(R.id.encrypted_message).setVisibility(View.GONE);
|
||||||
|
|
||||||
|
// Set up the crypto object for later. The object will be authenticated by use
|
||||||
|
// of the fingerprint.
|
||||||
|
if (initSignature()) {
|
||||||
|
|
||||||
|
// Show the fingerprint dialog. The user has the option to use the fingerprint with
|
||||||
|
// crypto, or you can fall back to using a server-side verified password.
|
||||||
|
mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature));
|
||||||
|
boolean useFingerprintPreference = mSharedPreferences
|
||||||
|
.getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
|
||||||
|
true);
|
||||||
|
if (useFingerprintPreference) {
|
||||||
|
mFragment.setStage(
|
||||||
|
FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
|
||||||
|
} else {
|
||||||
|
mFragment.setStage(
|
||||||
|
FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
|
||||||
|
}
|
||||||
|
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
|
||||||
|
} else {
|
||||||
|
// This happens if the lock screen has been disabled or or a fingerprint got
|
||||||
|
// enrolled. Thus show the dialog to authenticate with their password first
|
||||||
|
// and ask the user if they want to authenticate with fingerprints in the
|
||||||
|
// future
|
||||||
|
mFragment.setStage(
|
||||||
|
FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
|
||||||
|
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment {
|
|||||||
// generated.
|
// generated.
|
||||||
Calendar start = new GregorianCalendar();
|
Calendar start = new GregorianCalendar();
|
||||||
Calendar end = new GregorianCalendar();
|
Calendar end = new GregorianCalendar();
|
||||||
end.add(1, Calendar.YEAR);
|
end.add(Calendar.YEAR, 1);
|
||||||
//END_INCLUDE(create_valid_dates)
|
//END_INCLUDE(create_valid_dates)
|
||||||
|
|
||||||
|
|
||||||
@@ -316,8 +316,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment {
|
|||||||
// Verify the data.
|
// Verify the data.
|
||||||
s.initVerify(((KeyStore.PrivateKeyEntry) entry).getCertificate());
|
s.initVerify(((KeyStore.PrivateKeyEntry) entry).getCertificate());
|
||||||
s.update(data);
|
s.update(data);
|
||||||
boolean valid = s.verify(signature);
|
return s.verify(signature);
|
||||||
return valid;
|
|
||||||
// END_INCLUDE(verify_data)
|
// END_INCLUDE(verify_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import android.content.pm.PackageManager;
|
|||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.ImageFormat;
|
import android.graphics.ImageFormat;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Point;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
import android.hardware.camera2.CameraAccessException;
|
import android.hardware.camera2.CameraAccessException;
|
||||||
@@ -116,6 +117,16 @@ public class Camera2BasicFragment extends Fragment
|
|||||||
*/
|
*/
|
||||||
private static final int STATE_PICTURE_TAKEN = 4;
|
private static final int STATE_PICTURE_TAKEN = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max preview width that is guaranteed by Camera2 API
|
||||||
|
*/
|
||||||
|
private static final int MAX_PREVIEW_WIDTH = 1920;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max preview height that is guaranteed by Camera2 API
|
||||||
|
*/
|
||||||
|
private static final int MAX_PREVIEW_HEIGHT = 1080;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a
|
* {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a
|
||||||
* {@link TextureView}.
|
* {@link TextureView}.
|
||||||
@@ -344,31 +355,48 @@ public class Camera2BasicFragment extends Fragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
|
* Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
|
||||||
* width and height are at least as large as the respective requested values, and whose aspect
|
* is at least as large as the respective texture view size, and that is at most as large as the
|
||||||
* ratio matches with the specified value.
|
* respective max size, and whose aspect ratio matches with the specified value. If such size
|
||||||
|
* doesn't exist, choose the largest one that is at most as large as the respective max size,
|
||||||
|
* and whose aspect ratio matches with the specified value.
|
||||||
*
|
*
|
||||||
* @param choices The list of sizes that the camera supports for the intended output class
|
* @param choices The list of sizes that the camera supports for the intended output
|
||||||
* @param width The minimum desired width
|
* class
|
||||||
* @param height The minimum desired height
|
* @param textureViewWidth The width of the texture view relative to sensor coordinate
|
||||||
* @param aspectRatio The aspect ratio
|
* @param textureViewHeight The height of the texture view relative to sensor coordinate
|
||||||
|
* @param maxWidth The maximum width that can be chosen
|
||||||
|
* @param maxHeight The maximum height that can be chosen
|
||||||
|
* @param aspectRatio The aspect ratio
|
||||||
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
|
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
|
||||||
*/
|
*/
|
||||||
private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
|
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
|
||||||
|
int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
|
||||||
|
|
||||||
// Collect the supported resolutions that are at least as big as the preview Surface
|
// Collect the supported resolutions that are at least as big as the preview Surface
|
||||||
List<Size> bigEnough = new ArrayList<>();
|
List<Size> bigEnough = new ArrayList<>();
|
||||||
|
// Collect the supported resolutions that are smaller than the preview Surface
|
||||||
|
List<Size> notBigEnough = new ArrayList<>();
|
||||||
int w = aspectRatio.getWidth();
|
int w = aspectRatio.getWidth();
|
||||||
int h = aspectRatio.getHeight();
|
int h = aspectRatio.getHeight();
|
||||||
for (Size option : choices) {
|
for (Size option : choices) {
|
||||||
if (option.getHeight() == option.getWidth() * h / w &&
|
if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
|
||||||
option.getWidth() >= width && option.getHeight() >= height) {
|
option.getHeight() == option.getWidth() * h / w) {
|
||||||
bigEnough.add(option);
|
if (option.getWidth() >= textureViewWidth &&
|
||||||
|
option.getHeight() >= textureViewHeight) {
|
||||||
|
bigEnough.add(option);
|
||||||
|
} else {
|
||||||
|
notBigEnough.add(option);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick the smallest of those, assuming we found any
|
// Pick the smallest of those big enough. If there is no one big enough, pick the
|
||||||
|
// largest of those not big enough.
|
||||||
if (bigEnough.size() > 0) {
|
if (bigEnough.size() > 0) {
|
||||||
return Collections.min(bigEnough, new CompareSizesByArea());
|
return Collections.min(bigEnough, new CompareSizesByArea());
|
||||||
|
} else if (notBigEnough.size() > 0) {
|
||||||
|
return Collections.max(notBigEnough, new CompareSizesByArea());
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Couldn't find any suitable preview size");
|
Log.e(TAG, "Couldn't find any suitable preview size");
|
||||||
return choices[0];
|
return choices[0];
|
||||||
@@ -478,11 +506,57 @@ public class Camera2BasicFragment extends Fragment
|
|||||||
mImageReader.setOnImageAvailableListener(
|
mImageReader.setOnImageAvailableListener(
|
||||||
mOnImageAvailableListener, mBackgroundHandler);
|
mOnImageAvailableListener, mBackgroundHandler);
|
||||||
|
|
||||||
|
// Find out if we need to swap dimension to get the preview size relative to sensor
|
||||||
|
// coordinate.
|
||||||
|
int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
|
||||||
|
int sensorOrientation =
|
||||||
|
characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
|
||||||
|
boolean swappedDimensions = false;
|
||||||
|
switch (displayRotation) {
|
||||||
|
case Surface.ROTATION_0:
|
||||||
|
case Surface.ROTATION_180:
|
||||||
|
if (sensorOrientation == 90 || sensorOrientation == 270) {
|
||||||
|
swappedDimensions = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_90:
|
||||||
|
case Surface.ROTATION_270:
|
||||||
|
if (sensorOrientation == 0 || sensorOrientation == 180) {
|
||||||
|
swappedDimensions = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.e(TAG, "Display rotation is invalid: " + displayRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
Point displaySize = new Point();
|
||||||
|
activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
|
||||||
|
int rotatedPreviewWidth = width;
|
||||||
|
int rotatedPreviewHeight = height;
|
||||||
|
int maxPreviewWidth = displaySize.x;
|
||||||
|
int maxPreviewHeight = displaySize.y;
|
||||||
|
|
||||||
|
if (swappedDimensions) {
|
||||||
|
rotatedPreviewWidth = height;
|
||||||
|
rotatedPreviewHeight = width;
|
||||||
|
maxPreviewWidth = displaySize.y;
|
||||||
|
maxPreviewHeight = displaySize.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
|
||||||
|
maxPreviewWidth = MAX_PREVIEW_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
|
||||||
|
maxPreviewHeight = MAX_PREVIEW_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
// Danger, W.R.! Attempting to use too large a preview size could exceed the camera
|
// Danger, W.R.! Attempting to use too large a preview size could exceed the camera
|
||||||
// bus' bandwidth limitation, resulting in gorgeous previews but the storage of
|
// bus' bandwidth limitation, resulting in gorgeous previews but the storage of
|
||||||
// garbage capture data.
|
// garbage capture data.
|
||||||
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
|
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
|
||||||
width, height, largest);
|
rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
|
||||||
|
maxPreviewHeight, largest);
|
||||||
|
|
||||||
// We fit the aspect ratio of TextureView to the size of preview we picked.
|
// We fit the aspect ratio of TextureView to the size of preview we picked.
|
||||||
int orientation = getResources().getConfiguration().orientation;
|
int orientation = getResources().getConfiguration().orientation;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.content.DialogInterface;
|
|||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.ImageFormat;
|
import android.graphics.ImageFormat;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Point;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
import android.hardware.SensorManager;
|
import android.hardware.SensorManager;
|
||||||
@@ -156,6 +157,16 @@ public class Camera2RawFragment extends Fragment
|
|||||||
*/
|
*/
|
||||||
private static final double ASPECT_RATIO_TOLERANCE = 0.005;
|
private static final double ASPECT_RATIO_TOLERANCE = 0.005;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max preview width that is guaranteed by Camera2 API
|
||||||
|
*/
|
||||||
|
private static final int MAX_PREVIEW_WIDTH = 1920;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max preview height that is guaranteed by Camera2 API
|
||||||
|
*/
|
||||||
|
private static final int MAX_PREVIEW_HEIGHT = 1080;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag for the {@link Log}.
|
* Tag for the {@link Log}.
|
||||||
*/
|
*/
|
||||||
@@ -1033,6 +1044,8 @@ public class Camera2RawFragment extends Fragment
|
|||||||
|
|
||||||
// Find the rotation of the device relative to the native device orientation.
|
// Find the rotation of the device relative to the native device orientation.
|
||||||
int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
|
int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
|
||||||
|
Point displaySize = new Point();
|
||||||
|
activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
|
||||||
|
|
||||||
// Find the rotation of the device relative to the camera sensor's orientation.
|
// Find the rotation of the device relative to the camera sensor's orientation.
|
||||||
int totalRotation = sensorToDeviceRotation(mCharacteristics, deviceRotation);
|
int totalRotation = sensorToDeviceRotation(mCharacteristics, deviceRotation);
|
||||||
@@ -1042,14 +1055,29 @@ public class Camera2RawFragment extends Fragment
|
|||||||
boolean swappedDimensions = totalRotation == 90 || totalRotation == 270;
|
boolean swappedDimensions = totalRotation == 90 || totalRotation == 270;
|
||||||
int rotatedViewWidth = viewWidth;
|
int rotatedViewWidth = viewWidth;
|
||||||
int rotatedViewHeight = viewHeight;
|
int rotatedViewHeight = viewHeight;
|
||||||
|
int maxPreviewWidth = displaySize.x;
|
||||||
|
int maxPreviewHeight = displaySize.y;
|
||||||
|
|
||||||
if (swappedDimensions) {
|
if (swappedDimensions) {
|
||||||
rotatedViewWidth = viewHeight;
|
rotatedViewWidth = viewHeight;
|
||||||
rotatedViewHeight = viewWidth;
|
rotatedViewHeight = viewWidth;
|
||||||
|
maxPreviewWidth = displaySize.y;
|
||||||
|
maxPreviewHeight = displaySize.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview should not be larger than display size and 1080p.
|
||||||
|
if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
|
||||||
|
maxPreviewWidth = MAX_PREVIEW_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
|
||||||
|
maxPreviewHeight = MAX_PREVIEW_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the best preview size for these view dimensions and configured JPEG size.
|
// Find the best preview size for these view dimensions and configured JPEG size.
|
||||||
Size previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
|
Size previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
|
||||||
rotatedViewWidth, rotatedViewHeight, largestJpeg);
|
rotatedViewWidth, rotatedViewHeight, maxPreviewWidth, maxPreviewHeight,
|
||||||
|
largestJpeg);
|
||||||
|
|
||||||
if (swappedDimensions) {
|
if (swappedDimensions) {
|
||||||
mTextureView.setAspectRatio(
|
mTextureView.setAspectRatio(
|
||||||
@@ -1580,31 +1608,47 @@ public class Camera2RawFragment extends Fragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
|
* Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
|
||||||
* width and height are at least as large as the respective requested values, and whose aspect
|
* is at least as large as the respective texture view size, and that is at most as large as the
|
||||||
* ratio matches with the specified value.
|
* respective max size, and whose aspect ratio matches with the specified value. If such size
|
||||||
|
* doesn't exist, choose the largest one that is at most as large as the respective max size,
|
||||||
|
* and whose aspect ratio matches with the specified value.
|
||||||
*
|
*
|
||||||
* @param choices The list of sizes that the camera supports for the intended output class
|
* @param choices The list of sizes that the camera supports for the intended output
|
||||||
* @param width The minimum desired width
|
* class
|
||||||
* @param height The minimum desired height
|
* @param textureViewWidth The width of the texture view relative to sensor coordinate
|
||||||
* @param aspectRatio The aspect ratio
|
* @param textureViewHeight The height of the texture view relative to sensor coordinate
|
||||||
|
* @param maxWidth The maximum width that can be chosen
|
||||||
|
* @param maxHeight The maximum height that can be chosen
|
||||||
|
* @param aspectRatio The aspect ratio
|
||||||
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
|
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
|
||||||
*/
|
*/
|
||||||
private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
|
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
|
||||||
|
int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
|
||||||
// Collect the supported resolutions that are at least as big as the preview Surface
|
// Collect the supported resolutions that are at least as big as the preview Surface
|
||||||
List<Size> bigEnough = new ArrayList<>();
|
List<Size> bigEnough = new ArrayList<>();
|
||||||
|
// Collect the supported resolutions that are smaller than the preview Surface
|
||||||
|
List<Size> notBigEnough = new ArrayList<>();
|
||||||
int w = aspectRatio.getWidth();
|
int w = aspectRatio.getWidth();
|
||||||
int h = aspectRatio.getHeight();
|
int h = aspectRatio.getHeight();
|
||||||
for (Size option : choices) {
|
for (Size option : choices) {
|
||||||
if (option.getHeight() == option.getWidth() * h / w &&
|
if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
|
||||||
option.getWidth() >= width && option.getHeight() >= height) {
|
option.getHeight() == option.getWidth() * h / w) {
|
||||||
bigEnough.add(option);
|
if (option.getWidth() >= textureViewWidth &&
|
||||||
|
option.getHeight() >= textureViewHeight) {
|
||||||
|
bigEnough.add(option);
|
||||||
|
} else {
|
||||||
|
notBigEnough.add(option);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick the smallest of those, assuming we found any
|
// Pick the smallest of those big enough. If there is no one big enough, pick the
|
||||||
|
// largest of those not big enough.
|
||||||
if (bigEnough.size() > 0) {
|
if (bigEnough.size() > 0) {
|
||||||
return Collections.min(bigEnough, new CompareSizesByArea());
|
return Collections.min(bigEnough, new CompareSizesByArea());
|
||||||
|
} else if (notBigEnough.size() > 0) {
|
||||||
|
return Collections.max(notBigEnough, new CompareSizesByArea());
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Couldn't find any suitable preview size");
|
Log.e(TAG, "Couldn't find any suitable preview size");
|
||||||
return choices[0];
|
return choices[0];
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.datalayer" >
|
package="com.example.android.wearable.datalayer" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="22" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.app.Fragment;
|
|||||||
import android.app.FragmentManager;
|
import android.app.FragmentManager;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.wearable.view.DotsPageIndicator;
|
import android.support.wearable.view.DotsPageIndicator;
|
||||||
@@ -41,6 +42,7 @@ import com.google.android.gms.common.ConnectionResult;
|
|||||||
import com.google.android.gms.common.api.GoogleApiClient;
|
import com.google.android.gms.common.api.GoogleApiClient;
|
||||||
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
|
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
|
||||||
import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
|
import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
|
||||||
|
import com.google.android.gms.common.api.PendingResult;
|
||||||
import com.google.android.gms.common.api.ResultCallback;
|
import com.google.android.gms.common.api.ResultCallback;
|
||||||
import com.google.android.gms.wearable.Asset;
|
import com.google.android.gms.wearable.Asset;
|
||||||
import com.google.android.gms.wearable.CapabilityApi;
|
import com.google.android.gms.wearable.CapabilityApi;
|
||||||
@@ -85,7 +87,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
|||||||
private static final String CAPABILITY_2_NAME = "capability_2";
|
private static final String CAPABILITY_2_NAME = "capability_2";
|
||||||
|
|
||||||
private GoogleApiClient mGoogleApiClient;
|
private GoogleApiClient mGoogleApiClient;
|
||||||
private Handler mHandler;
|
|
||||||
private GridViewPager mPager;
|
private GridViewPager mPager;
|
||||||
private DataFragment mDataFragment;
|
private DataFragment mDataFragment;
|
||||||
private AssetFragment mAssetFragment;
|
private AssetFragment mAssetFragment;
|
||||||
@@ -93,7 +94,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle b) {
|
public void onCreate(Bundle b) {
|
||||||
super.onCreate(b);
|
super.onCreate(b);
|
||||||
mHandler = new Handler();
|
|
||||||
setContentView(R.layout.main_activity);
|
setContentView(R.layout.main_activity);
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
setupViews();
|
setupViews();
|
||||||
@@ -137,15 +137,6 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
|||||||
Log.e(TAG, "onConnectionFailed(): Failed to connect, with result: " + result);
|
Log.e(TAG, "onConnectionFailed(): Failed to connect, with result: " + result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generateEvent(final String title, final String text) {
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mDataFragment.appendItem(title, text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDataChanged(DataEventBuffer dataEvents) {
|
public void onDataChanged(DataEventBuffer dataEvents) {
|
||||||
LOGD(TAG, "onDataChanged(): " + dataEvents);
|
LOGD(TAG, "onDataChanged(): " + dataEvents);
|
||||||
@@ -155,29 +146,22 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
|||||||
String path = event.getDataItem().getUri().getPath();
|
String path = event.getDataItem().getUri().getPath();
|
||||||
if (DataLayerListenerService.IMAGE_PATH.equals(path)) {
|
if (DataLayerListenerService.IMAGE_PATH.equals(path)) {
|
||||||
DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
|
DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
|
||||||
Asset photo = dataMapItem.getDataMap()
|
Asset photoAsset = dataMapItem.getDataMap()
|
||||||
.getAsset(DataLayerListenerService.IMAGE_KEY);
|
.getAsset(DataLayerListenerService.IMAGE_KEY);
|
||||||
final Bitmap bitmap = loadBitmapFromAsset(mGoogleApiClient, photo);
|
// Loads image on background thread.
|
||||||
mHandler.post(new Runnable() {
|
new LoadBitmapAsyncTask().execute(photoAsset);
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Log.d(TAG, "Setting background image on second page..");
|
|
||||||
moveToPage(1);
|
|
||||||
mAssetFragment.setBackgroundImage(bitmap);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} else if (DataLayerListenerService.COUNT_PATH.equals(path)) {
|
} else if (DataLayerListenerService.COUNT_PATH.equals(path)) {
|
||||||
LOGD(TAG, "Data Changed for COUNT_PATH");
|
LOGD(TAG, "Data Changed for COUNT_PATH");
|
||||||
generateEvent("DataItem Changed", event.getDataItem().toString());
|
mDataFragment.appendItem("DataItem Changed", event.getDataItem().toString());
|
||||||
} else {
|
} else {
|
||||||
LOGD(TAG, "Unrecognized path: " + path);
|
LOGD(TAG, "Unrecognized path: " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (event.getType() == DataEvent.TYPE_DELETED) {
|
} else if (event.getType() == DataEvent.TYPE_DELETED) {
|
||||||
generateEvent("DataItem Deleted", event.getDataItem().toString());
|
mDataFragment.appendItem("DataItem Deleted", event.getDataItem().toString());
|
||||||
} else {
|
} else {
|
||||||
generateEvent("Unknown data event type", "Type = " + event.getType());
|
mDataFragment.appendItem("Unknown data event type", "Type = " + event.getType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,20 +183,27 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
|||||||
* Find the connected nodes that provide at least one of the given capabilities
|
* Find the connected nodes that provide at least one of the given capabilities
|
||||||
*/
|
*/
|
||||||
private void showNodes(final String... capabilityNames) {
|
private void showNodes(final String... capabilityNames) {
|
||||||
Wearable.CapabilityApi.getAllCapabilities(mGoogleApiClient,
|
|
||||||
CapabilityApi.FILTER_REACHABLE).setResultCallback(
|
|
||||||
|
|
||||||
|
PendingResult<CapabilityApi.GetAllCapabilitiesResult> pendingCapabilityResult =
|
||||||
|
Wearable.CapabilityApi.getAllCapabilities(
|
||||||
|
mGoogleApiClient,
|
||||||
|
CapabilityApi.FILTER_REACHABLE);
|
||||||
|
|
||||||
|
pendingCapabilityResult.setResultCallback(
|
||||||
new ResultCallback<CapabilityApi.GetAllCapabilitiesResult>() {
|
new ResultCallback<CapabilityApi.GetAllCapabilitiesResult>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResult(
|
public void onResult(
|
||||||
CapabilityApi.GetAllCapabilitiesResult getAllCapabilitiesResult) {
|
CapabilityApi.GetAllCapabilitiesResult getAllCapabilitiesResult) {
|
||||||
|
|
||||||
if (!getAllCapabilitiesResult.getStatus().isSuccess()) {
|
if (!getAllCapabilitiesResult.getStatus().isSuccess()) {
|
||||||
Log.e(TAG, "Failed to get capabilities");
|
Log.e(TAG, "Failed to get capabilities");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Map<String, CapabilityInfo>
|
|
||||||
capabilitiesMap = getAllCapabilitiesResult.getAllCapabilities();
|
Map<String, CapabilityInfo> capabilitiesMap =
|
||||||
|
getAllCapabilitiesResult.getAllCapabilities();
|
||||||
Set<Node> nodes = new HashSet<>();
|
Set<Node> nodes = new HashSet<>();
|
||||||
|
|
||||||
if (capabilitiesMap.isEmpty()) {
|
if (capabilitiesMap.isEmpty()) {
|
||||||
showDiscoveredNodes(nodes);
|
showDiscoveredNodes(nodes);
|
||||||
return;
|
return;
|
||||||
@@ -231,7 +222,7 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
|||||||
for (Node node : nodes) {
|
for (Node node : nodes) {
|
||||||
nodesList.add(node.getDisplayName());
|
nodesList.add(node.getDisplayName());
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Connected Nodes: " + (nodesList.isEmpty()
|
LOGD(TAG, "Connected Nodes: " + (nodesList.isEmpty()
|
||||||
? "No connected device was found for the given capabilities"
|
? "No connected device was found for the given capabilities"
|
||||||
: TextUtils.join(",", nodesList)));
|
: TextUtils.join(",", nodesList)));
|
||||||
String msg;
|
String msg;
|
||||||
@@ -246,39 +237,20 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts {@link android.graphics.Bitmap} data from the
|
|
||||||
* {@link com.google.android.gms.wearable.Asset}
|
|
||||||
*/
|
|
||||||
private Bitmap loadBitmapFromAsset(GoogleApiClient apiClient, Asset asset) {
|
|
||||||
if (asset == null) {
|
|
||||||
throw new IllegalArgumentException("Asset must be non-null");
|
|
||||||
}
|
|
||||||
|
|
||||||
InputStream assetInputStream = Wearable.DataApi.getFdForAsset(
|
|
||||||
apiClient, asset).await().getInputStream();
|
|
||||||
|
|
||||||
if (assetInputStream == null) {
|
|
||||||
Log.w(TAG, "Requested an unknown Asset.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return BitmapFactory.decodeStream(assetInputStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessageReceived(MessageEvent event) {
|
public void onMessageReceived(MessageEvent event) {
|
||||||
LOGD(TAG, "onMessageReceived: " + event);
|
LOGD(TAG, "onMessageReceived: " + event);
|
||||||
generateEvent("Message", event.toString());
|
mDataFragment.appendItem("Message", event.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPeerConnected(Node node) {
|
public void onPeerConnected(Node node) {
|
||||||
generateEvent("Node Connected", node.getId());
|
mDataFragment.appendItem("Node Connected", node.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPeerDisconnected(Node node) {
|
public void onPeerDisconnected(Node node) {
|
||||||
generateEvent("Node Disconnected", node.getId());
|
mDataFragment.appendItem("Node Disconnected", node.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupViews() {
|
private void setupViews() {
|
||||||
@@ -330,4 +302,43 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extracts {@link android.graphics.Bitmap} data from the
|
||||||
|
* {@link com.google.android.gms.wearable.Asset}
|
||||||
|
*/
|
||||||
|
private class LoadBitmapAsyncTask extends AsyncTask<Asset, Void, Bitmap> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Bitmap doInBackground(Asset... params) {
|
||||||
|
|
||||||
|
if(params.length > 0) {
|
||||||
|
|
||||||
|
Asset asset = params[0];
|
||||||
|
|
||||||
|
InputStream assetInputStream = Wearable.DataApi.getFdForAsset(
|
||||||
|
mGoogleApiClient, asset).await().getInputStream();
|
||||||
|
|
||||||
|
if (assetInputStream == null) {
|
||||||
|
Log.w(TAG, "Requested an unknown Asset.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return BitmapFactory.decodeStream(assetInputStream);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Asset must be non-null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap bitmap) {
|
||||||
|
|
||||||
|
if(bitmap != null) {
|
||||||
|
LOGD(TAG, "Setting background image on second page..");
|
||||||
|
moveToPage(1);
|
||||||
|
mAssetFragment.setBackgroundImage(bitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.delayedconfirmation" >
|
package="com.example.android.wearable.delayedconfirmation" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="22" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.elizachat" >
|
package="com.example.android.wearable.elizachat" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ public class ResponderService extends Service {
|
|||||||
.setContentText(mLastResponse)
|
.setContentText(mLastResponse)
|
||||||
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.bg_eliza))
|
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.bg_eliza))
|
||||||
.setSmallIcon(R.drawable.bg_eliza)
|
.setSmallIcon(R.drawable.bg_eliza)
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN);
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
||||||
|
|
||||||
Intent intent = new Intent(ACTION_RESPONSE);
|
Intent intent = new Intent(ACTION_RESPONSE);
|
||||||
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent,
|
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.findphone">
|
package="com.example.android.wearable.findphone">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="22" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<application
|
<application
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
page.tags="Fingerprint Dialog Sample"
|
page.tags="FingerprintDialog"
|
||||||
sample.group=Security
|
sample.group=Security
|
||||||
@jd:body
|
@jd:body
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Fingerprint Dialog Sample</string>
|
<string name="app_name">FingerprintDialog</string>
|
||||||
<string name="intro_message">
|
<string name="intro_message">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,10 @@
|
|||||||
|
|
||||||
package com.example.android.fingerprintdialog;
|
package com.example.android.fingerprintdialog;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.KeyguardManager;
|
import android.app.KeyguardManager;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.hardware.fingerprint.FingerprintManager;
|
import android.hardware.fingerprint.FingerprintManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.security.keystore.KeyGenParameterSpec;
|
import android.security.keystore.KeyGenParameterSpec;
|
||||||
@@ -64,8 +62,6 @@ public class MainActivity extends Activity {
|
|||||||
/** Alias for our key in the Android Key Store */
|
/** Alias for our key in the Android Key Store */
|
||||||
private static final String KEY_NAME = "my_key";
|
private static final String KEY_NAME = "my_key";
|
||||||
|
|
||||||
private static final int FINGERPRINT_PERMISSION_REQUEST_CODE = 0;
|
|
||||||
|
|
||||||
@Inject KeyguardManager mKeyguardManager;
|
@Inject KeyguardManager mKeyguardManager;
|
||||||
@Inject FingerprintManager mFingerprintManager;
|
@Inject FingerprintManager mFingerprintManager;
|
||||||
@Inject FingerprintAuthenticationDialogFragment mFragment;
|
@Inject FingerprintAuthenticationDialogFragment mFragment;
|
||||||
@@ -79,72 +75,65 @@ public class MainActivity extends Activity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
((InjectedApplication) getApplication()).inject(this);
|
((InjectedApplication) getApplication()).inject(this);
|
||||||
|
|
||||||
requestPermissions(new String[]{Manifest.permission.USE_FINGERPRINT},
|
setContentView(R.layout.activity_main);
|
||||||
FINGERPRINT_PERMISSION_REQUEST_CODE);
|
Button purchaseButton = (Button) findViewById(R.id.purchase_button);
|
||||||
}
|
if (!mKeyguardManager.isKeyguardSecure()) {
|
||||||
|
// Show a message that the user hasn't set up a fingerprint or lock screen.
|
||||||
@Override
|
Toast.makeText(this,
|
||||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) {
|
"Secure lock screen hasn't set up.\n"
|
||||||
if (requestCode == FINGERPRINT_PERMISSION_REQUEST_CODE
|
+ "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
|
||||||
&& state[0] == PackageManager.PERMISSION_GRANTED) {
|
Toast.LENGTH_LONG).show();
|
||||||
setContentView(R.layout.activity_main);
|
purchaseButton.setEnabled(false);
|
||||||
Button purchaseButton = (Button) findViewById(R.id.purchase_button);
|
return;
|
||||||
if (!mKeyguardManager.isKeyguardSecure()) {
|
|
||||||
// Show a message that the user hasn't set up a fingerprint or lock screen.
|
|
||||||
Toast.makeText(this,
|
|
||||||
"Secure lock screen hasn't set up.\n"
|
|
||||||
+ "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
purchaseButton.setEnabled(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!mFingerprintManager.hasEnrolledFingerprints()) {
|
|
||||||
purchaseButton.setEnabled(false);
|
|
||||||
// This happens when no fingerprints are registered.
|
|
||||||
Toast.makeText(this,
|
|
||||||
"Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
createKey();
|
|
||||||
purchaseButton.setEnabled(true);
|
|
||||||
purchaseButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
findViewById(R.id.confirmation_message).setVisibility(View.GONE);
|
|
||||||
findViewById(R.id.encrypted_message).setVisibility(View.GONE);
|
|
||||||
|
|
||||||
// Set up the crypto object for later. The object will be authenticated by use
|
|
||||||
// of the fingerprint.
|
|
||||||
if (initCipher()) {
|
|
||||||
|
|
||||||
// Show the fingerprint dialog. The user has the option to use the fingerprint with
|
|
||||||
// crypto, or you can fall back to using a server-side verified password.
|
|
||||||
mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
|
|
||||||
boolean useFingerprintPreference = mSharedPreferences
|
|
||||||
.getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
|
|
||||||
true);
|
|
||||||
if (useFingerprintPreference) {
|
|
||||||
mFragment.setStage(
|
|
||||||
FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
|
|
||||||
} else {
|
|
||||||
mFragment.setStage(
|
|
||||||
FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
|
|
||||||
}
|
|
||||||
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
|
|
||||||
} else {
|
|
||||||
// This happens if the lock screen has been disabled or or a fingerprint got
|
|
||||||
// enrolled. Thus show the dialog to authenticate with their password first
|
|
||||||
// and ask the user if they want to authenticate with fingerprints in the
|
|
||||||
// future
|
|
||||||
mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
|
|
||||||
mFragment.setStage(
|
|
||||||
FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
|
|
||||||
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection ResourceType
|
||||||
|
if (!mFingerprintManager.hasEnrolledFingerprints()) {
|
||||||
|
purchaseButton.setEnabled(false);
|
||||||
|
// This happens when no fingerprints are registered.
|
||||||
|
Toast.makeText(this,
|
||||||
|
"Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createKey();
|
||||||
|
purchaseButton.setEnabled(true);
|
||||||
|
purchaseButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
findViewById(R.id.confirmation_message).setVisibility(View.GONE);
|
||||||
|
findViewById(R.id.encrypted_message).setVisibility(View.GONE);
|
||||||
|
|
||||||
|
// Set up the crypto object for later. The object will be authenticated by use
|
||||||
|
// of the fingerprint.
|
||||||
|
if (initCipher()) {
|
||||||
|
|
||||||
|
// Show the fingerprint dialog. The user has the option to use the fingerprint with
|
||||||
|
// crypto, or you can fall back to using a server-side verified password.
|
||||||
|
mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
|
||||||
|
boolean useFingerprintPreference = mSharedPreferences
|
||||||
|
.getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
|
||||||
|
true);
|
||||||
|
if (useFingerprintPreference) {
|
||||||
|
mFragment.setStage(
|
||||||
|
FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
|
||||||
|
} else {
|
||||||
|
mFragment.setStage(
|
||||||
|
FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
|
||||||
|
}
|
||||||
|
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
|
||||||
|
} else {
|
||||||
|
// This happens if the lock screen has been disabled or or a fingerprint got
|
||||||
|
// enrolled. Thus show the dialog to authenticate with their password first
|
||||||
|
// and ask the user if they want to authenticate with fingerprints in the
|
||||||
|
// future
|
||||||
|
mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
|
||||||
|
mFragment.setStage(
|
||||||
|
FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
|
||||||
|
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.flashlight" >
|
package="com.example.android.wearable.flashlight" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ sample.group=Wearable
|
|||||||
<p>
|
<p>
|
||||||
|
|
||||||
Wearable activity that uses your wearable screen as a flashlight. There is also
|
Wearable activity that uses your wearable screen as a flashlight. There is also
|
||||||
a party-mode option, if you want to make things interesting.
|
a party-mode option (swipe left), if you want to make things interesting.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.geofencing">
|
package="com.example.android.wearable.geofencing">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.geofencing" >
|
package="com.example.android.wearable.geofencing" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.gridviewpager" >
|
package="com.example.android.wearable.gridviewpager" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.jumpingjack">
|
package="com.example.android.wearable.jumpingjack">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.support.wearable.notifications" >
|
package="com.example.android.support.wearable.notifications" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.support.wearable.notifications" >
|
package="com.example.android.support.wearable.notifications" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.quiz" >
|
package="com.example.android.wearable.quiz" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="22" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.recipeassistant" >
|
package="com.example.android.wearable.recipeassistant" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.google.wearable.app" >
|
package="com.example.android.google.wearable.app" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,35 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.example.android.wearable.speedtracker" >
|
package="com.example.android.wearable.speedtracker" >
|
||||||
|
|
||||||
|
|
||||||
|
<uses-sdk
|
||||||
|
android:minSdkVersion="18"
|
||||||
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
|
<!-- BEGIN_INCLUDE(manifest) -->
|
||||||
|
|
||||||
|
<!-- Note that all required permissions are declared here in the Android manifest.
|
||||||
|
On Android M and above, use of permissions not in the normal permission group are
|
||||||
|
requested at run time. -->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="18"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
|
<!-- END_INCLUDE(manifest) -->
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.location.gps" android:required="true" />
|
<uses-feature android:name="android.hardware.location.gps" android:required="true" />
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:glEsVersion="0x00020000" android:required="true"/>
|
android:glEsVersion="0x00020000" android:required="true"/>
|
||||||
<uses-sdk
|
|
||||||
android:minSdkVersion="18"
|
|
||||||
android:targetSdkVersion="21" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".PhoneApplication"
|
android:name=".PhoneApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/AppTheme" >
|
android:theme="@style/Theme.AppCompat.Light" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.maps.v2.API_KEY"
|
android:name="com.google.android.maps.v2.API_KEY"
|
||||||
android:value="@string/map_v2_api_key"/>
|
android:value="@string/map_v2_api_key"/>
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/top_container"
|
android:id="@+id/top_container"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp">
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/date_picker"
|
android:id="@+id/date_picker"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import com.google.android.gms.maps.model.LatLng;
|
|||||||
import com.google.android.gms.maps.model.LatLngBounds;
|
import com.google.android.gms.maps.model.LatLngBounds;
|
||||||
import com.google.android.gms.maps.model.PolylineOptions;
|
import com.google.android.gms.maps.model.PolylineOptions;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.DatePickerDialog;
|
import android.app.DatePickerDialog;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -45,7 +45,8 @@ import java.util.List;
|
|||||||
* a map. This data is then saved into an internal database and the corresponding data items are
|
* a map. This data is then saved into an internal database and the corresponding data items are
|
||||||
* deleted.
|
* deleted.
|
||||||
*/
|
*/
|
||||||
public class PhoneMainActivity extends Activity implements DatePickerDialog.OnDateSetListener {
|
public class PhoneMainActivity extends AppCompatActivity implements
|
||||||
|
DatePickerDialog.OnDateSetListener {
|
||||||
|
|
||||||
private static final String TAG = "PhoneMainActivity";
|
private static final String TAG = "PhoneMainActivity";
|
||||||
private static final int BOUNDING_BOX_PADDING_PX = 50;
|
private static final int BOUNDING_BOX_PADDING_PX = 50;
|
||||||
|
|||||||
@@ -19,18 +19,22 @@
|
|||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch"/>
|
<uses-feature android:name="android.hardware.type.watch"/>
|
||||||
<uses-feature android:name="android.hardware.location.gps" android:required="true" />
|
<uses-feature android:name="android.hardware.location.gps" android:required="true" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>\
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
android:minSdkVersion="20"
|
android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@android:style/Theme.DeviceDefault">
|
android:theme="@android:style/Theme.DeviceDefault">
|
||||||
|
|
||||||
|
<!--If you want your app to run on pre-22, then set required to false -->
|
||||||
|
<uses-library android:name="com.google.android.wearable" android:required="false" />
|
||||||
|
|
||||||
<meta-data android:name="com.google.android.gms.version"
|
<meta-data android:name="com.google.android.gms.version"
|
||||||
android:value="@integer/google_play_services_version"/>
|
android:value="@integer/google_play_services_version"/>
|
||||||
<activity
|
<activity
|
||||||
@@ -38,7 +42,6 @@
|
|||||||
android:label="@string/app_name">
|
android:label="@string/app_name">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
@@ -48,12 +51,6 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".ui.LocationSettingActivity">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -29,11 +29,13 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_centerInParent="true"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
android:fontFamily="sans-serif-light"
|
android:fontFamily="sans-serif-light"
|
||||||
|
android:textAlignment="center"
|
||||||
android:textSize="17sp"
|
android:textSize="17sp"
|
||||||
android:textStyle="italic"
|
android:textStyle="italic"
|
||||||
android:id="@+id/acquiring_gps"
|
android:id="@+id/gps_issue_text"
|
||||||
android:text="@string/acquiring_gps"/>
|
android:text=""/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -84,18 +86,20 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_gps_not_saving_grey600_96dp"
|
android:src="@drawable/ic_gps_not_saving_grey600_96dp"
|
||||||
android:id="@+id/saving"
|
android:id="@+id/gps_permission"
|
||||||
|
android:onClick="onGpsPermissionClick"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_alignParentLeft="true"
|
||||||
android:layout_marginBottom="20dp"
|
android:layout_marginBottom="20dp"
|
||||||
android:layout_marginLeft="60dp" />
|
android:layout_marginLeft="50dp" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:id="@+id/settings"
|
android:id="@+id/speed_limit_setting"
|
||||||
|
android:onClick="onSpeedLimitClick"
|
||||||
android:background="@drawable/settings"
|
android:background="@drawable/settings"
|
||||||
android:layout_alignParentRight="true"
|
android:layout_alignParentRight="true"
|
||||||
android:layout_alignBottom="@+id/saving"
|
android:layout_alignBottom="@+id/gps_permission"
|
||||||
android:layout_marginRight="60dp"/>
|
android:layout_marginRight="50dp"/>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -25,11 +25,16 @@
|
|||||||
<string name="speed_limit">Limit: %1$d mph</string>
|
<string name="speed_limit">Limit: %1$d mph</string>
|
||||||
<string name="acquiring_gps">Acquiring GPS Fix ...</string>
|
<string name="acquiring_gps">Acquiring GPS Fix ...</string>
|
||||||
<string name="speed_for_list">%1$d mph</string>
|
<string name="speed_for_list">%1$d mph</string>
|
||||||
<string name="start_saving_gps">Start Recording GPS?</string>
|
|
||||||
<string name="stop_saving_gps">Stop Recording GPS?</string>
|
<string name="enable_disable_gps_label">Enable Location Permission?</string>
|
||||||
|
|
||||||
<string name="mph">mph</string>
|
<string name="mph">mph</string>
|
||||||
<string name="speed_limit_header">Speed Limit</string>
|
<string name="speed_limit_header">Speed Limit</string>
|
||||||
<string name="gps_not_available">GPS not available.</string>
|
<string name="gps_not_available">No GPS on device. Will use phone GPS when available.</string>
|
||||||
<string name="ok">OK</string>
|
<string name="ok">OK</string>
|
||||||
<string name="speed_format">%.0f</string>
|
<string name="speed_format">%.0f</string>
|
||||||
|
|
||||||
|
<string name="permission_rationale">App requires location permission to function, tap GPS icon.</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -17,9 +17,8 @@
|
|||||||
package com.example.android.wearable.speedtracker;
|
package com.example.android.wearable.speedtracker;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.SharedPreferences;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.wearable.view.WearableListView;
|
import android.support.wearable.view.WearableListView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@@ -31,6 +30,9 @@ import com.example.android.wearable.speedtracker.ui.SpeedPickerListAdapter;
|
|||||||
*/
|
*/
|
||||||
public class SpeedPickerActivity extends Activity implements WearableListView.ClickListener {
|
public class SpeedPickerActivity extends Activity implements WearableListView.ClickListener {
|
||||||
|
|
||||||
|
public static final String EXTRA_NEW_SPEED_LIMIT =
|
||||||
|
"com.example.android.wearable.speedtracker.extra.NEW_SPEED_LIMIT";
|
||||||
|
|
||||||
/* Speeds, in mph, that will be shown on the list */
|
/* Speeds, in mph, that will be shown on the list */
|
||||||
private int[] speeds = {25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75};
|
private int[] speeds = {25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75};
|
||||||
|
|
||||||
@@ -75,9 +77,13 @@ public class SpeedPickerActivity extends Activity implements WearableListView.Cl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(WearableListView.ViewHolder viewHolder) {
|
public void onClick(WearableListView.ViewHolder viewHolder) {
|
||||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
pref.edit().putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY,
|
int newSpeedLimit = speeds[viewHolder.getPosition()];
|
||||||
speeds[viewHolder.getPosition()]).apply();
|
|
||||||
|
Intent resultIntent = new Intent(Intent.ACTION_PICK);
|
||||||
|
resultIntent.putExtra(EXTRA_NEW_SPEED_LIMIT, newSpeedLimit);
|
||||||
|
setResult(RESULT_OK, resultIntent);
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import com.google.android.gms.wearable.PutDataMapRequest;
|
|||||||
import com.google.android.gms.wearable.PutDataRequest;
|
import com.google.android.gms.wearable.PutDataRequest;
|
||||||
import com.google.android.gms.wearable.Wearable;
|
import com.google.android.gms.wearable.Wearable;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.Manifest;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -38,18 +38,19 @@ import android.location.Location;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v4.app.ActivityCompat;
|
||||||
|
import android.support.wearable.activity.WearableActivity;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.example.android.wearable.speedtracker.common.Constants;
|
import com.example.android.wearable.speedtracker.common.Constants;
|
||||||
import com.example.android.wearable.speedtracker.common.LocationEntry;
|
import com.example.android.wearable.speedtracker.common.LocationEntry;
|
||||||
import com.example.android.wearable.speedtracker.ui.LocationSettingActivity;
|
|
||||||
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main activity for the wearable app. User can pick a speed limit, and after this activity
|
* The main activity for the wearable app. User can pick a speed limit, and after this activity
|
||||||
@@ -58,33 +59,54 @@ import java.util.Calendar;
|
|||||||
* and if the user exceeds the speed limit, it will turn red. In order to show the user that GPS
|
* and if the user exceeds the speed limit, it will turn red. In order to show the user that GPS
|
||||||
* location data is coming in, a small green dot keeps on blinking while GPS data is available.
|
* location data is coming in, a small green dot keeps on blinking while GPS data is available.
|
||||||
*/
|
*/
|
||||||
public class WearableMainActivity extends Activity implements GoogleApiClient.ConnectionCallbacks,
|
public class WearableMainActivity extends WearableActivity implements
|
||||||
GoogleApiClient.OnConnectionFailedListener, LocationListener {
|
GoogleApiClient.ConnectionCallbacks,
|
||||||
|
GoogleApiClient.OnConnectionFailedListener,
|
||||||
|
ActivityCompat.OnRequestPermissionsResultCallback,
|
||||||
|
LocationListener {
|
||||||
|
|
||||||
private static final String TAG = "WearableActivity";
|
private static final String TAG = "WearableActivity";
|
||||||
|
|
||||||
private static final long UPDATE_INTERVAL_MS = 5 * 1000;
|
private static final long UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
|
||||||
private static final long FASTEST_INTERVAL_MS = 5 * 1000;
|
private static final long FASTEST_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
|
||||||
|
|
||||||
public static final float MPH_IN_METERS_PER_SECOND = 2.23694f;
|
private static final float MPH_IN_METERS_PER_SECOND = 2.23694f;
|
||||||
|
|
||||||
|
private static final int SPEED_LIMIT_DEFAULT_MPH = 45;
|
||||||
|
|
||||||
public static final String PREFS_SPEED_LIMIT_KEY = "speed_limit";
|
|
||||||
public static final int SPEED_LIMIT_DEFAULT_MPH = 45;
|
|
||||||
private static final long INDICATOR_DOT_FADE_AWAY_MS = 500L;
|
private static final long INDICATOR_DOT_FADE_AWAY_MS = 500L;
|
||||||
|
|
||||||
private GoogleApiClient mGoogleApiClient;
|
// Request codes for changing speed limit and location permissions.
|
||||||
private TextView mSpeedLimitText;
|
private static final int REQUEST_PICK_SPEED_LIMIT = 0;
|
||||||
private TextView mCurrentSpeedText;
|
|
||||||
private ImageView mSaveImageView;
|
// Id to identify Location permission request.
|
||||||
private TextView mAcquiringGps;
|
private static final int REQUEST_GPS_PERMISSION = 1;
|
||||||
private TextView mCurrentSpeedMphText;
|
|
||||||
|
// Shared Preferences for saving speed limit and location permission between app launches.
|
||||||
|
private static final String PREFS_SPEED_LIMIT_KEY = "SpeedLimit";
|
||||||
|
|
||||||
private int mCurrentSpeedLimit;
|
|
||||||
private float mCurrentSpeed;
|
|
||||||
private View mDot;
|
|
||||||
private Handler mHandler = new Handler();
|
|
||||||
private Calendar mCalendar;
|
private Calendar mCalendar;
|
||||||
private boolean mSaveGpsLocation;
|
|
||||||
|
private TextView mSpeedLimitTextView;
|
||||||
|
private TextView mSpeedTextView;
|
||||||
|
private ImageView mGpsPermissionImageView;
|
||||||
|
private TextView mCurrentSpeedMphTextView;
|
||||||
|
private TextView mGpsIssueTextView;
|
||||||
|
private View mBlinkingGpsStatusDotView;
|
||||||
|
|
||||||
|
private String mGpsPermissionNeededMessage;
|
||||||
|
private String mAcquiringGpsMessage;
|
||||||
|
|
||||||
|
private int mSpeedLimit;
|
||||||
|
private float mSpeed;
|
||||||
|
|
||||||
|
private boolean mGpsPermissionApproved;
|
||||||
|
|
||||||
|
private boolean mWaitingForGpsSignal;
|
||||||
|
|
||||||
|
private GoogleApiClient mGoogleApiClient;
|
||||||
|
|
||||||
|
private Handler mHandler = new Handler();
|
||||||
|
|
||||||
private enum SpeedState {
|
private enum SpeedState {
|
||||||
BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above);
|
BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above);
|
||||||
@@ -104,20 +126,53 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
Log.d(TAG, "onCreate()");
|
||||||
|
|
||||||
|
|
||||||
setContentView(R.layout.main_activity);
|
setContentView(R.layout.main_activity);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enables Always-on, so our app doesn't shut down when the watch goes into ambient mode.
|
||||||
|
* Best practice is to override onEnterAmbient(), onUpdateAmbient(), and onExitAmbient() to
|
||||||
|
* optimize the display for ambient mode. However, for brevity, we aren't doing that here
|
||||||
|
* to focus on learning location and permissions. For more information on best practices
|
||||||
|
* in ambient mode, check this page:
|
||||||
|
* https://developer.android.com/training/wearables/apps/always-on.html
|
||||||
|
*/
|
||||||
|
setAmbientEnabled();
|
||||||
|
|
||||||
|
mCalendar = Calendar.getInstance();
|
||||||
|
|
||||||
|
// Enables app to handle 23+ (M+) style permissions.
|
||||||
|
mGpsPermissionApproved =
|
||||||
|
ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
||||||
|
mGpsPermissionNeededMessage = getString(R.string.permission_rationale);
|
||||||
|
mAcquiringGpsMessage = getString(R.string.acquiring_gps);
|
||||||
|
|
||||||
|
|
||||||
|
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
mSpeedLimit = sharedPreferences.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH);
|
||||||
|
|
||||||
|
mSpeed = 0;
|
||||||
|
|
||||||
|
mWaitingForGpsSignal = true;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If this hardware doesn't support GPS, we warn the user. Note that when such device is
|
||||||
|
* connected to a phone with GPS capabilities, the framework automatically routes the
|
||||||
|
* location requests from the phone. However, if the phone becomes disconnected and the
|
||||||
|
* wearable doesn't support GPS, no location is recorded until the phone is reconnected.
|
||||||
|
*/
|
||||||
if (!hasGps()) {
|
if (!hasGps()) {
|
||||||
// If this hardware doesn't support GPS, we prefer to exit.
|
Log.w(TAG, "This hardware doesn't have GPS, so we warn user.");
|
||||||
// Note that when such device is connected to a phone with GPS capabilities, the
|
|
||||||
// framework automatically routes the location requests to the phone. For this
|
|
||||||
// application, this would not be desirable so we exit the app but for some other
|
|
||||||
// applications, that might be a valid scenario.
|
|
||||||
Log.w(TAG, "This hardware doesn't have GPS, so we exit");
|
|
||||||
new AlertDialog.Builder(this)
|
new AlertDialog.Builder(this)
|
||||||
.setMessage(getString(R.string.gps_not_available))
|
.setMessage(getString(R.string.gps_not_available))
|
||||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int id) {
|
public void onClick(DialogInterface dialog, int id) {
|
||||||
finish();
|
|
||||||
dialog.cancel();
|
dialog.cancel();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -125,7 +180,6 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
|
|||||||
@Override
|
@Override
|
||||||
public void onDismiss(DialogInterface dialog) {
|
public void onDismiss(DialogInterface dialog) {
|
||||||
dialog.cancel();
|
dialog.cancel();
|
||||||
finish();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
@@ -133,164 +187,216 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
|
|||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setupViews();
|
setupViews();
|
||||||
updateSpeedVisibility(false);
|
|
||||||
setSpeedLimit();
|
|
||||||
mGoogleApiClient = new GoogleApiClient.Builder(this)
|
mGoogleApiClient = new GoogleApiClient.Builder(this)
|
||||||
.addApi(LocationServices.API)
|
.addApi(LocationServices.API)
|
||||||
.addApi(Wearable.API)
|
.addApi(Wearable.API)
|
||||||
.addConnectionCallbacks(this)
|
.addConnectionCallbacks(this)
|
||||||
.addOnConnectionFailedListener(this)
|
.addOnConnectionFailedListener(this)
|
||||||
.build();
|
.build();
|
||||||
mGoogleApiClient.connect();
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected()) &&
|
||||||
|
(mGoogleApiClient.isConnecting())) {
|
||||||
|
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
|
||||||
|
mGoogleApiClient.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (mGoogleApiClient != null) {
|
||||||
|
mGoogleApiClient.connect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupViews() {
|
private void setupViews() {
|
||||||
mSpeedLimitText = (TextView) findViewById(R.id.max_speed_text);
|
mSpeedLimitTextView = (TextView) findViewById(R.id.max_speed_text);
|
||||||
mCurrentSpeedText = (TextView) findViewById(R.id.current_speed_text);
|
mSpeedTextView = (TextView) findViewById(R.id.current_speed_text);
|
||||||
mSaveImageView = (ImageView) findViewById(R.id.saving);
|
mCurrentSpeedMphTextView = (TextView) findViewById(R.id.current_speed_mph);
|
||||||
ImageButton settingButton = (ImageButton) findViewById(R.id.settings);
|
|
||||||
mAcquiringGps = (TextView) findViewById(R.id.acquiring_gps);
|
|
||||||
mCurrentSpeedMphText = (TextView) findViewById(R.id.current_speed_mph);
|
|
||||||
mDot = findViewById(R.id.dot);
|
|
||||||
|
|
||||||
settingButton.setOnClickListener(new View.OnClickListener() {
|
mGpsPermissionImageView = (ImageView) findViewById(R.id.gps_permission);
|
||||||
@Override
|
mGpsIssueTextView = (TextView) findViewById(R.id.gps_issue_text);
|
||||||
public void onClick(View v) {
|
mBlinkingGpsStatusDotView = findViewById(R.id.dot);
|
||||||
Intent speedIntent = new Intent(WearableMainActivity.this,
|
|
||||||
SpeedPickerActivity.class);
|
|
||||||
startActivity(speedIntent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mSaveImageView.setOnClickListener(new View.OnClickListener() {
|
updateActivityViewsBasedOnLocationPermissions();
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
Intent savingIntent = new Intent(WearableMainActivity.this,
|
|
||||||
LocationSettingActivity.class);
|
|
||||||
startActivity(savingIntent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSpeedLimit(int speedLimit) {
|
public void onSpeedLimitClick(View view) {
|
||||||
mSpeedLimitText.setText(getString(R.string.speed_limit, speedLimit));
|
Intent speedIntent = new Intent(WearableMainActivity.this,
|
||||||
|
SpeedPickerActivity.class);
|
||||||
|
startActivityForResult(speedIntent, REQUEST_PICK_SPEED_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSpeedLimit() {
|
public void onGpsPermissionClick(View view) {
|
||||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
mCurrentSpeedLimit = pref.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH);
|
|
||||||
setSpeedLimit(mCurrentSpeedLimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCurrentSpeed(float speed) {
|
if (!mGpsPermissionApproved) {
|
||||||
mCurrentSpeed = speed;
|
|
||||||
mCurrentSpeedText.setText(String.format(getString(R.string.speed_format), speed));
|
Log.i(TAG, "Location permission has NOT been granted. Requesting permission.");
|
||||||
adjustColor();
|
|
||||||
|
// On 23+ (M+) devices, GPS permission not granted. Request permission.
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
|
||||||
|
REQUEST_GPS_PERMISSION);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adjusts the color of the speed based on its value relative to the speed limit.
|
* Adjusts the visibility of views based on location permissions.
|
||||||
*/
|
*/
|
||||||
private void adjustColor() {
|
private void updateActivityViewsBasedOnLocationPermissions() {
|
||||||
SpeedState state = SpeedState.ABOVE;
|
|
||||||
if (mCurrentSpeed <= mCurrentSpeedLimit - 5) {
|
|
||||||
state = SpeedState.BELOW;
|
|
||||||
} else if (mCurrentSpeed <= mCurrentSpeedLimit) {
|
|
||||||
state = SpeedState.CLOSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
mCurrentSpeedText.setTextColor(getResources().getColor(state.getColor()));
|
/*
|
||||||
|
* If the user has approved location but we don't have a signal yet, we let the user know
|
||||||
|
* we are waiting on the GPS signal (this sometimes takes a little while). Otherwise, the
|
||||||
|
* user might think something is wrong.
|
||||||
|
*/
|
||||||
|
if (mGpsPermissionApproved && mWaitingForGpsSignal) {
|
||||||
|
|
||||||
|
// We are getting a GPS signal w/ user permission.
|
||||||
|
mGpsIssueTextView.setText(mAcquiringGpsMessage);
|
||||||
|
mGpsIssueTextView.setVisibility(View.VISIBLE);
|
||||||
|
mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
|
||||||
|
|
||||||
|
mSpeedTextView.setVisibility(View.GONE);
|
||||||
|
mSpeedLimitTextView.setVisibility(View.GONE);
|
||||||
|
mCurrentSpeedMphTextView.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
} else if (mGpsPermissionApproved) {
|
||||||
|
|
||||||
|
mGpsIssueTextView.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
mSpeedTextView.setVisibility(View.VISIBLE);
|
||||||
|
mSpeedLimitTextView.setVisibility(View.VISIBLE);
|
||||||
|
mCurrentSpeedMphTextView.setVisibility(View.VISIBLE);
|
||||||
|
mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// User needs to enable location for the app to work.
|
||||||
|
mGpsIssueTextView.setVisibility(View.VISIBLE);
|
||||||
|
mGpsIssueTextView.setText(mGpsPermissionNeededMessage);
|
||||||
|
mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_not_saving_grey600_96dp);
|
||||||
|
|
||||||
|
mSpeedTextView.setVisibility(View.GONE);
|
||||||
|
mSpeedLimitTextView.setVisibility(View.GONE);
|
||||||
|
mCurrentSpeedMphTextView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSpeedInViews() {
|
||||||
|
|
||||||
|
if (mGpsPermissionApproved) {
|
||||||
|
|
||||||
|
mSpeedLimitTextView.setText(getString(R.string.speed_limit, mSpeedLimit));
|
||||||
|
mSpeedTextView.setText(String.format(getString(R.string.speed_format), mSpeed));
|
||||||
|
|
||||||
|
// Adjusts the color of the speed based on its value relative to the speed limit.
|
||||||
|
SpeedState state = SpeedState.ABOVE;
|
||||||
|
if (mSpeed <= mSpeedLimit - 5) {
|
||||||
|
state = SpeedState.BELOW;
|
||||||
|
} else if (mSpeed <= mSpeedLimit) {
|
||||||
|
state = SpeedState.CLOSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
mSpeedTextView.setTextColor(getResources().getColor(state.getColor()));
|
||||||
|
|
||||||
|
// Causes the (green) dot blinks when new GPS location data is acquired.
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
|
||||||
|
mHandler.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mBlinkingGpsStatusDotView.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
}, INDICATOR_DOT_FADE_AWAY_MS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConnected(Bundle bundle) {
|
public void onConnected(Bundle bundle) {
|
||||||
LocationRequest locationRequest = LocationRequest.create()
|
|
||||||
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
|
|
||||||
.setInterval(UPDATE_INTERVAL_MS)
|
|
||||||
.setFastestInterval(FASTEST_INTERVAL_MS);
|
|
||||||
|
|
||||||
LocationServices.FusedLocationApi
|
Log.d(TAG, "onConnected()");
|
||||||
.requestLocationUpdates(mGoogleApiClient, locationRequest, this)
|
|
||||||
.setResultCallback(new ResultCallback<Status>() {
|
|
||||||
|
|
||||||
@Override
|
/*
|
||||||
public void onResult(Status status) {
|
* mGpsPermissionApproved covers 23+ (M+) style permissions. If that is already approved or
|
||||||
if (status.getStatus().isSuccess()) {
|
* the device is pre-23, the app uses mSaveGpsLocation to save the user's location
|
||||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
* preference.
|
||||||
Log.d(TAG, "Successfully requested location updates");
|
*/
|
||||||
|
if (mGpsPermissionApproved) {
|
||||||
|
|
||||||
|
LocationRequest locationRequest = LocationRequest.create()
|
||||||
|
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
|
||||||
|
.setInterval(UPDATE_INTERVAL_MS)
|
||||||
|
.setFastestInterval(FASTEST_INTERVAL_MS);
|
||||||
|
|
||||||
|
LocationServices.FusedLocationApi
|
||||||
|
.requestLocationUpdates(mGoogleApiClient, locationRequest, this)
|
||||||
|
.setResultCallback(new ResultCallback<Status>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResult(Status status) {
|
||||||
|
if (status.getStatus().isSuccess()) {
|
||||||
|
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||||
|
Log.d(TAG, "Successfully requested location updates");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG,
|
||||||
|
"Failed in requesting location updates, "
|
||||||
|
+ "status code: "
|
||||||
|
+ status.getStatusCode() + ", message: " + status
|
||||||
|
.getStatusMessage());
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.e(TAG,
|
|
||||||
"Failed in requesting location updates, "
|
|
||||||
+ "status code: "
|
|
||||||
+ status.getStatusCode() + ", message: " + status
|
|
||||||
.getStatusMessage());
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionSuspended(int i) {
|
public void onConnectionSuspended(int i) {
|
||||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
|
||||||
Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
|
|
||||||
}
|
|
||||||
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
|
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionFailed(ConnectionResult connectionResult) {
|
public void onConnectionFailed(ConnectionResult connectionResult) {
|
||||||
Log.e(TAG, "onConnectionFailed(): connection to location client failed");
|
Log.e(TAG, "onConnectionFailed(): " + connectionResult.getErrorMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLocationChanged(Location location) {
|
public void onLocationChanged(Location location) {
|
||||||
updateSpeedVisibility(true);
|
Log.d(TAG, "onLocationChanged() : " + location);
|
||||||
setCurrentSpeed(location.getSpeed() * MPH_IN_METERS_PER_SECOND);
|
|
||||||
flashDot();
|
|
||||||
|
if (mWaitingForGpsSignal) {
|
||||||
|
mWaitingForGpsSignal = false;
|
||||||
|
updateActivityViewsBasedOnLocationPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
mSpeed = location.getSpeed() * MPH_IN_METERS_PER_SECOND;
|
||||||
|
updateSpeedInViews();
|
||||||
addLocationEntry(location.getLatitude(), location.getLongitude());
|
addLocationEntry(location.getLatitude(), location.getLongitude());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Causes the (green) dot blinks when new GPS location data is acquired.
|
* Adds a data item to the data Layer storage.
|
||||||
*/
|
|
||||||
private void flashDot() {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mDot.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mDot.setVisibility(View.VISIBLE);
|
|
||||||
mHandler.postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mDot.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
}, INDICATOR_DOT_FADE_AWAY_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjusts the visibility of speed indicator based on the arrival of GPS data.
|
|
||||||
*/
|
|
||||||
private void updateSpeedVisibility(boolean speedVisible) {
|
|
||||||
if (speedVisible) {
|
|
||||||
mAcquiringGps.setVisibility(View.GONE);
|
|
||||||
mCurrentSpeedText.setVisibility(View.VISIBLE);
|
|
||||||
mCurrentSpeedMphText.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
mAcquiringGps.setVisibility(View.VISIBLE);
|
|
||||||
mCurrentSpeedText.setVisibility(View.GONE);
|
|
||||||
mCurrentSpeedMphText.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a data item to the data Layer storage
|
|
||||||
*/
|
*/
|
||||||
private void addLocationEntry(double latitude, double longitude) {
|
private void addLocationEntry(double latitude, double longitude) {
|
||||||
if (!mSaveGpsLocation || !mGoogleApiClient.isConnected()) {
|
if (!mGpsPermissionApproved || !mGoogleApiClient.isConnected()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mCalendar.setTimeInMillis(System.currentTimeMillis());
|
mCalendar.setTimeInMillis(System.currentTimeMillis());
|
||||||
@@ -315,29 +421,56 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles user choices for both speed limit and location permissions (GPS tracking).
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
super.onStop();
|
|
||||||
if (mGoogleApiClient.isConnected()) {
|
if (requestCode == REQUEST_PICK_SPEED_LIMIT) {
|
||||||
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
|
if (resultCode == RESULT_OK) {
|
||||||
|
// The user updated the speed limit.
|
||||||
|
int newSpeedLimit =
|
||||||
|
data.getIntExtra(SpeedPickerActivity.EXTRA_NEW_SPEED_LIMIT, mSpeedLimit);
|
||||||
|
|
||||||
|
SharedPreferences sharedPreferences =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||||
|
editor.putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY, newSpeedLimit);
|
||||||
|
editor.apply();
|
||||||
|
|
||||||
|
mSpeedLimit = newSpeedLimit;
|
||||||
|
|
||||||
|
updateSpeedInViews();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mGoogleApiClient.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback received when a permissions request has been completed.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
public void onRequestPermissionsResult(
|
||||||
super.onResume();
|
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
||||||
mCalendar = Calendar.getInstance();
|
|
||||||
setSpeedLimit();
|
|
||||||
adjustColor();
|
|
||||||
updateRecordingIcon();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateRecordingIcon() {
|
Log.d(TAG, "onRequestPermissionsResult(): " + permissions);
|
||||||
mSaveGpsLocation = LocationSettingActivity.getGpsRecordingStatusFromPreferences(this);
|
|
||||||
mSaveImageView.setImageResource(mSaveGpsLocation ? R.drawable.ic_gps_saving_grey600_96dp
|
|
||||||
: R.drawable.ic_gps_not_saving_grey600_96dp);
|
if (requestCode == REQUEST_GPS_PERMISSION) {
|
||||||
|
Log.i(TAG, "Received response for GPS permission request.");
|
||||||
|
|
||||||
|
if ((grantResults.length == 1)
|
||||||
|
&& (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
||||||
|
Log.i(TAG, "GPS permission granted.");
|
||||||
|
mGpsPermissionApproved = true;
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "GPS permission NOT granted.");
|
||||||
|
mGpsPermissionApproved = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActivityViewsBasedOnLocationPermissions();
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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();
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,6 +41,9 @@ public class SpeedPickerListAdapter extends WearableListView.Adapter {
|
|||||||
mDataSet = dataset;
|
mDataSet = dataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays all possible speed limit choices.
|
||||||
|
*/
|
||||||
public static class ItemViewHolder extends WearableListView.ViewHolder {
|
public static class ItemViewHolder extends WearableListView.ViewHolder {
|
||||||
|
|
||||||
private TextView mTextView;
|
private TextView mTextView;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import java.util.Set;
|
|||||||
* Manages documents and exposes them to the Android system for sharing.
|
* Manages documents and exposes them to the Android system for sharing.
|
||||||
*/
|
*/
|
||||||
public class MyCloudProvider extends DocumentsProvider {
|
public class MyCloudProvider extends DocumentsProvider {
|
||||||
private static final String TAG = MyCloudProvider.class.getSimpleName();
|
private static final String TAG = "MyCloudProvider";
|
||||||
|
|
||||||
// Use these as the default columns to return information about a root if no specific
|
// Use these as the default columns to return information about a root if no specific
|
||||||
// columns are requested in a query.
|
// columns are requested in a query.
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ import com.example.android.common.logger.Log;
|
|||||||
* Toggles the user's login status via a login menu option, and enables/disables the cloud storage
|
* Toggles the user's login status via a login menu option, and enables/disables the cloud storage
|
||||||
* content provider.
|
* content provider.
|
||||||
*/
|
*/
|
||||||
public class MyCloudFragment extends Fragment {
|
public class StorageProviderFragment extends Fragment {
|
||||||
|
|
||||||
private static final String TAG = "MyCloudFragment";
|
private static final String TAG = "StorageProviderFragment";
|
||||||
private static final String AUTHORITY = "com.example.android.storageprovider.documents";
|
private static final String AUTHORITY = "com.example.android.storageprovider.documents";
|
||||||
private boolean mLoggedIn = false;
|
private boolean mLoggedIn = false;
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
android:versionName="1.0">
|
android:versionName="1.0">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<application android:allowBackup="true"
|
<application android:allowBackup="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
package="com.example.android.wearable.synchronizednotifications">
|
package="com.example.android.wearable.synchronizednotifications">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.wearable.timer" >
|
package="com.example.android.wearable.timer" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,19 @@
|
|||||||
package="com.example.android.wearable.watchface" >
|
package="com.example.android.wearable.watchface" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<uses-sdk android:minSdkVersion="18"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<!-- Permissions required by the wearable app -->
|
<!-- Permissions required by the wearable app -->
|
||||||
<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
|
<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<!-- Requests to calendar are only made on the wear side (CalendarWatchFaceService.java), so
|
||||||
|
no runtime permissions are needed on the phone side. -->
|
||||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||||
|
|
||||||
|
<!-- Location permission used by FitDistanceWatchFaceService -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
<!-- All intent-filters for config actions must include the categories
|
<!-- All intent-filters for config actions must include the categories
|
||||||
com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION and
|
com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION and
|
||||||
android.intent.category.DEFAULT. -->
|
android.intent.category.DEFAULT. -->
|
||||||
@@ -55,6 +61,18 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- This activity is needed to allow the user to authorize Google Fit for the Fit Distance
|
||||||
|
WatchFace (required to view distance). -->
|
||||||
|
<activity
|
||||||
|
android:name=".FitDistanceWatchFaceConfigActivity"
|
||||||
|
android:label="@string/app_name">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.example.android.wearable.watchface.CONFIG_FIT_DISTANCE" />
|
||||||
|
<category android:name="com.google.android.wearable.watchface.category.COMPANION_CONFIGURATION" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".OpenGLWatchFaceConfigActivity"
|
android:name=".OpenGLWatchFaceConfigActivity"
|
||||||
android:label="@string/app_name">
|
android:label="@string/app_name">
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -23,9 +23,14 @@
|
|||||||
|
|
||||||
This sample demonstrates how to create watch faces for android wear and includes a phone app
|
This sample demonstrates how to create watch faces for android wear and includes a phone app
|
||||||
and a wearable app. The wearable app has a variety of watch faces including analog, digital,
|
and a wearable app. The wearable app has a variety of watch faces including analog, digital,
|
||||||
opengl, calendar, interactive, etc. It also includes a watch-side configuration example.
|
opengl, calendar, steps, interactive, etc. It also includes a watch-side configuration example.
|
||||||
The phone app includes a phone-side configuration example.
|
The phone app includes a phone-side configuration example.
|
||||||
|
|
||||||
|
Additional note on Steps WatchFace Sample, if the user has not installed or setup the Google Fit app
|
||||||
|
on their phone and their Wear device has not configured the Google Fit Wear App, then you may get
|
||||||
|
zero steps until one of the two is setup. Please note, many Wear devices configure the Google Fit
|
||||||
|
Wear App beforehand.
|
||||||
|
|
||||||
|
|
||||||
]]>
|
]]>
|
||||||
</string>
|
</string>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
<string name="digital_config_minutes">Minutes</string>
|
<string name="digital_config_minutes">Minutes</string>
|
||||||
<string name="digital_config_seconds">Seconds</string>
|
<string name="digital_config_seconds">Seconds</string>
|
||||||
|
|
||||||
|
<string name="fit_config_switch_text">Google Fit</string>
|
||||||
|
|
||||||
<string name="title_no_device_connected">No wearable device is currently connected.</string>
|
<string name="title_no_device_connected">No wearable device is currently connected.</string>
|
||||||
<string name="ok_no_device_connected">OK</string>
|
<string name="ok_no_device_connected">OK</string>
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
<!--
|
||||||
|
Copyright (C) 2014 The Android Open Source Project
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -13,12 +14,12 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.example.android.wearable.watchface" >
|
package="com.example.android.wearable.watchface" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="21"
|
<uses-sdk
|
||||||
android:targetSdkVersion="21" />
|
android:minSdkVersion="21"
|
||||||
|
android:targetSdkVersion="23" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
@@ -29,102 +30,113 @@
|
|||||||
<!-- Calendar permission used by CalendarWatchFaceService -->
|
<!-- Calendar permission used by CalendarWatchFaceService -->
|
||||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||||
|
|
||||||
|
<!-- Location permission used by FitDistanceWatchFaceService -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name" >
|
android:label="@string/app_name" >
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.version"
|
||||||
|
android:value="@integer/google_play_services_version" />
|
||||||
|
|
||||||
|
<uses-library android:name="com.google.android.wearable" android:required="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".AnalogWatchFaceService"
|
android:name=".AnalogWatchFaceService"
|
||||||
android:label="@string/analog_name"
|
android:label="@string/analog_name"
|
||||||
android:permission="android.permission.BIND_WALLPAPER" >
|
android:permission="android.permission.BIND_WALLPAPER" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.wallpaper"
|
android:name="android.service.wallpaper"
|
||||||
android:resource="@xml/watch_face" />
|
android:resource="@xml/watch_face" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.preview"
|
android:name="com.google.android.wearable.watchface.preview"
|
||||||
android:resource="@drawable/preview_analog" />
|
android:resource="@drawable/preview_analog" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.preview_circular"
|
android:name="com.google.android.wearable.watchface.preview_circular"
|
||||||
android:resource="@drawable/preview_analog_circular" />
|
android:resource="@drawable/preview_analog_circular" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
|
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
|
||||||
android:value="com.example.android.wearable.watchface.CONFIG_ANALOG" />
|
android:value="com.example.android.wearable.watchface.CONFIG_ANALOG" />
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.wallpaper.WallpaperService" />
|
|
||||||
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".SweepWatchFaceService"
|
|
||||||
android:label="@string/sweep_name"
|
|
||||||
android:permission="android.permission.BIND_WALLPAPER" >
|
|
||||||
<meta-data
|
|
||||||
android:name="android.service.wallpaper"
|
|
||||||
android:resource="@xml/watch_face" />
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.wearable.watchface.preview"
|
|
||||||
android:resource="@drawable/preview_analog" />
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.wearable.watchface.preview_circular"
|
|
||||||
android:resource="@drawable/preview_analog_circular" />
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.service.wallpaper.WallpaperService" />
|
|
||||||
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".OpenGLWatchFaceService"
|
|
||||||
android:label="@string/opengl_name"
|
|
||||||
android:permission="android.permission.BIND_WALLPAPER" >
|
|
||||||
<meta-data
|
|
||||||
android:name="android.service.wallpaper"
|
|
||||||
android:resource="@xml/watch_face" />
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.wearable.watchface.preview"
|
|
||||||
android:resource="@drawable/preview_opengl" />
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.wearable.watchface.preview_circular"
|
|
||||||
android:resource="@drawable/preview_opengl_circular" />
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
|
|
||||||
android:value="com.example.android.wearable.watchface.CONFIG_OPENGL" />
|
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.wallpaper.WallpaperService" />
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
|
|
||||||
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".CardBoundsWatchFaceService"
|
android:name=".SweepWatchFaceService"
|
||||||
android:label="@string/card_bounds_name"
|
android:label="@string/sweep_name"
|
||||||
android:permission="android.permission.BIND_WALLPAPER" >
|
android:permission="android.permission.BIND_WALLPAPER" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.wallpaper"
|
android:name="android.service.wallpaper"
|
||||||
android:resource="@xml/watch_face" />
|
android:resource="@xml/watch_face" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.preview"
|
android:name="com.google.android.wearable.watchface.preview"
|
||||||
android:resource="@drawable/preview_card_bounds" />
|
android:resource="@drawable/preview_analog" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.preview_circular"
|
android:name="com.google.android.wearable.watchface.preview_circular"
|
||||||
android:resource="@drawable/preview_card_bounds_circular" />
|
android:resource="@drawable/preview_analog_circular" />
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
|
|
||||||
android:value="com.example.android.wearable.watchface.CONFIG_CARD_BOUNDS" />
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.wallpaper.WallpaperService" />
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
|
|
||||||
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".InteractiveWatchFaceService"
|
android:name=".OpenGLWatchFaceService"
|
||||||
android:label="@string/interactive_name"
|
android:label="@string/opengl_name"
|
||||||
android:permission="android.permission.BIND_WALLPAPER" >
|
android:permission="android.permission.BIND_WALLPAPER" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.wallpaper"
|
||||||
|
android:resource="@xml/watch_face" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.preview"
|
||||||
|
android:resource="@drawable/preview_opengl" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.preview_circular"
|
||||||
|
android:resource="@drawable/preview_opengl_circular" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
|
||||||
|
android:value="com.example.android.wearable.watchface.CONFIG_OPENGL" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
|
|
||||||
|
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".CardBoundsWatchFaceService"
|
||||||
|
android:label="@string/card_bounds_name"
|
||||||
|
android:permission="android.permission.BIND_WALLPAPER" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.wallpaper"
|
||||||
|
android:resource="@xml/watch_face" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.preview"
|
||||||
|
android:resource="@drawable/preview_card_bounds" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.preview_circular"
|
||||||
|
android:resource="@drawable/preview_card_bounds_circular" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
|
||||||
|
android:value="com.example.android.wearable.watchface.CONFIG_CARD_BOUNDS" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
|
|
||||||
|
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".InteractiveWatchFaceService"
|
||||||
|
android:label="@string/interactive_name"
|
||||||
|
android:permission="android.permission.BIND_WALLPAPER" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.wallpaper"
|
android:name="android.service.wallpaper"
|
||||||
android:resource="@xml/watch_face" />
|
android:resource="@xml/watch_face" />
|
||||||
@@ -134,81 +146,130 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.preview_circular"
|
android:name="com.google.android.wearable.watchface.preview_circular"
|
||||||
android:resource="@drawable/preview_interactive_circular" />
|
android:resource="@drawable/preview_interactive_circular" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.wallpaper.WallpaperService" />
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
<category
|
|
||||||
android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".DigitalWatchFaceService"
|
android:name=".DigitalWatchFaceService"
|
||||||
android:label="@string/digital_name"
|
android:label="@string/digital_name"
|
||||||
android:permission="android.permission.BIND_WALLPAPER" >
|
android:permission="android.permission.BIND_WALLPAPER" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.wallpaper"
|
android:name="android.service.wallpaper"
|
||||||
android:resource="@xml/watch_face" />
|
android:resource="@xml/watch_face" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.preview"
|
android:name="com.google.android.wearable.watchface.preview"
|
||||||
android:resource="@drawable/preview_digital" />
|
android:resource="@drawable/preview_digital" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.preview_circular"
|
android:name="com.google.android.wearable.watchface.preview_circular"
|
||||||
android:resource="@drawable/preview_digital_circular" />
|
android:resource="@drawable/preview_digital_circular" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
|
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
|
||||||
android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
|
android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
|
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
|
||||||
android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
|
android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.wallpaper.WallpaperService" />
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
|
|
||||||
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<!-- All intent-filters for config actions must include the categories
|
<!--
|
||||||
|
All intent-filters for config actions must include the categories
|
||||||
com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION
|
com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION
|
||||||
and android.intent.category.DEFAULT. -->
|
and android.intent.category.DEFAULT.
|
||||||
|
-->
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".DigitalWatchFaceWearableConfigActivity"
|
android:name=".DigitalWatchFaceWearableConfigActivity"
|
||||||
android:label="@string/digital_config_name">
|
android:label="@string/digital_config_name" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
|
<action android:name="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
|
||||||
|
|
||||||
<category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
|
<category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".CalendarWatchFaceService"
|
android:name=".CalendarWatchFaceService"
|
||||||
android:label="@string/calendar_name"
|
android:label="@string/calendar_name"
|
||||||
android:permission="android.permission.BIND_WALLPAPER" >
|
android:permission="android.permission.BIND_WALLPAPER" >
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.wallpaper"
|
android:name="android.service.wallpaper"
|
||||||
android:resource="@xml/watch_face" />
|
android:resource="@xml/watch_face" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.preview"
|
android:name="com.google.android.wearable.watchface.preview"
|
||||||
android:resource="@drawable/preview_calendar" />
|
android:resource="@drawable/preview_calendar" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.wearable.watchface.preview_circular"
|
android:name="com.google.android.wearable.watchface.preview_circular"
|
||||||
android:resource="@drawable/preview_calendar_circular" />
|
android:resource="@drawable/preview_calendar_circular" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.wallpaper.WallpaperService" />
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
|
|
||||||
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
<service android:name=".DigitalWatchFaceConfigListenerService" >
|
||||||
<service android:name=".DigitalWatchFaceConfigListenerService">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
|
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".FitDistanceWatchFaceService"
|
||||||
|
android:label="@string/fit_distance_name"
|
||||||
|
android:permission="android.permission.BIND_WALLPAPER" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.wallpaper"
|
||||||
|
android:resource="@xml/watch_face" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.preview"
|
||||||
|
android:resource="@drawable/preview_distance" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.preview_circular"
|
||||||
|
android:resource="@drawable/preview_distance_circular" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.companionConfigurationAction"
|
||||||
|
android:value="com.example.android.wearable.watchface.CONFIG_FIT_DISTANCE" />
|
||||||
|
|
||||||
<meta-data
|
<intent-filter>
|
||||||
android:name="com.google.android.gms.version"
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
android:value="@integer/google_play_services_version" />
|
|
||||||
|
|
||||||
|
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".FitStepsWatchFaceService"
|
||||||
|
android:label="@string/fit_steps_name"
|
||||||
|
android:permission="android.permission.BIND_WALLPAPER" >
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.wallpaper"
|
||||||
|
android:resource="@xml/watch_face" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.preview"
|
||||||
|
android:resource="@drawable/preview_fit" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.wearable.watchface.preview_circular"
|
||||||
|
android:resource="@drawable/preview_fit_circular" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||||
|
|
||||||
|
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".CalendarWatchFacePermissionActivity"
|
||||||
|
android:label="@string/title_activity_calendar_watch_face_permission" >
|
||||||
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 306 B |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 200 B |
|
After Width: | Height: | Size: 354 B |
|
After Width: | Height: | Size: 512 B |
@@ -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>
|
||||||
@@ -32,4 +32,15 @@
|
|||||||
<dimen name="interactive_y_offset">72dp</dimen>
|
<dimen name="interactive_y_offset">72dp</dimen>
|
||||||
<dimen name="interactive_y_offset_round">84dp</dimen>
|
<dimen name="interactive_y_offset_round">84dp</dimen>
|
||||||
<dimen name="interactive_line_height">25dp</dimen>
|
<dimen name="interactive_line_height">25dp</dimen>
|
||||||
|
<dimen name="fit_text_size">40dp</dimen>
|
||||||
|
<dimen name="fit_text_size_round">45dp</dimen>
|
||||||
|
<dimen name="fit_steps_or_distance_text_size">20dp</dimen>
|
||||||
|
<dimen name="fit_am_pm_size">25dp</dimen>
|
||||||
|
<dimen name="fit_am_pm_size_round">30dp</dimen>
|
||||||
|
<dimen name="fit_x_offset">15dp</dimen>
|
||||||
|
<dimen name="fit_x_offset_round">25dp</dimen>
|
||||||
|
<dimen name="fit_steps_or_distance_x_offset">20dp</dimen>
|
||||||
|
<dimen name="fit_steps_or_distance_x_offset_round">30dp</dimen>
|
||||||
|
<dimen name="fit_y_offset">80dp</dimen>
|
||||||
|
<dimen name="fit_line_height">25dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -25,11 +25,22 @@
|
|||||||
<string name="digital_config_name">Digital watch face configuration</string>
|
<string name="digital_config_name">Digital watch face configuration</string>
|
||||||
<string name="digital_am">AM</string>
|
<string name="digital_am">AM</string>
|
||||||
<string name="digital_pm">PM</string>
|
<string name="digital_pm">PM</string>
|
||||||
|
|
||||||
|
<string name="fit_steps_name">Sample Fit Steps</string>
|
||||||
|
<string name="fit_distance_name">Sample Fit Distance</string>
|
||||||
|
<string name="fit_am">AM</string>
|
||||||
|
<string name="fit_pm">PM</string>
|
||||||
|
<string name="fit_steps">%1$d steps</string>
|
||||||
|
<string name="fit_distance">%1$,.2f meters</string>
|
||||||
|
|
||||||
<string name="calendar_name">Sample Calendar</string>
|
<string name="calendar_name">Sample Calendar</string>
|
||||||
|
<string name="calendar_permission_not_approved"><br><br><br>WatchFace requires Calendar permission. Click on this WatchFace or visit Settings > Permissions to approve.</string>
|
||||||
<plurals name="calendar_meetings">
|
<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="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>
|
<item quantity="other"><br><br><br>You have <b>%1$d</b> meetings in the next 24 hours.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="title_activity_calendar_watch_face_permission">Calendar Permission Activity</string>
|
||||||
|
<string name="calendar_permission_text">WatchFace requires Calendar access.</string>
|
||||||
|
|
||||||
<!-- TODO: this should be shared (needs covering all the samples with Gradle build model) -->
|
<!-- TODO: this should be shared (needs covering all the samples with Gradle build model) -->
|
||||||
<string name="color_black">Black</string>
|
<string name="color_black">Black</string>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,13 @@
|
|||||||
|
|
||||||
package com.example.android.wearable.watchface;
|
package com.example.android.wearable.watchface;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@@ -30,8 +32,10 @@ import android.os.AsyncTask;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.wearable.provider.WearableCalendarContract;
|
import android.support.wearable.provider.WearableCalendarContract;
|
||||||
import android.support.wearable.watchface.CanvasWatchFaceService;
|
import android.support.wearable.watchface.CanvasWatchFaceService;
|
||||||
|
import android.support.wearable.watchface.WatchFaceService;
|
||||||
import android.support.wearable.watchface.WatchFaceStyle;
|
import android.support.wearable.watchface.WatchFaceStyle;
|
||||||
import android.text.DynamicLayout;
|
import android.text.DynamicLayout;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
@@ -74,31 +78,37 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
|
|||||||
final TextPaint mTextPaint = new TextPaint();
|
final TextPaint mTextPaint = new TextPaint();
|
||||||
|
|
||||||
int mNumMeetings;
|
int mNumMeetings;
|
||||||
|
private boolean mCalendarPermissionApproved;
|
||||||
|
private String mCalendarNotApprovedMessage;
|
||||||
|
|
||||||
private AsyncTask<Void, Void, Integer> mLoadMeetingsTask;
|
private AsyncTask<Void, Void, Integer> mLoadMeetingsTask;
|
||||||
|
|
||||||
|
private boolean mIsReceiverRegistered;
|
||||||
|
|
||||||
/** Handler to load the meetings once a minute in interactive mode. */
|
/** Handler to load the meetings once a minute in interactive mode. */
|
||||||
final Handler mLoadMeetingsHandler = new Handler() {
|
final Handler mLoadMeetingsHandler = new Handler() {
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(Message message) {
|
public void handleMessage(Message message) {
|
||||||
switch (message.what) {
|
switch (message.what) {
|
||||||
case MSG_LOAD_MEETINGS:
|
case MSG_LOAD_MEETINGS:
|
||||||
|
|
||||||
cancelLoadMeetingTask();
|
cancelLoadMeetingTask();
|
||||||
mLoadMeetingsTask = new LoadMeetingsTask();
|
|
||||||
mLoadMeetingsTask.execute();
|
// Loads meetings.
|
||||||
|
if (mCalendarPermissionApproved) {
|
||||||
|
mLoadMeetingsTask = new LoadMeetingsTask();
|
||||||
|
mLoadMeetingsTask.execute();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private boolean mIsReceiverRegistered;
|
|
||||||
|
|
||||||
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
|
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
if (Intent.ACTION_PROVIDER_CHANGED.equals(intent.getAction())
|
if (Intent.ACTION_PROVIDER_CHANGED.equals(intent.getAction())
|
||||||
&& WearableCalendarContract.CONTENT_URI.equals(intent.getData())) {
|
&& WearableCalendarContract.CONTENT_URI.equals(intent.getData())) {
|
||||||
cancelLoadMeetingTask();
|
|
||||||
mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
|
mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,29 +116,59 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(SurfaceHolder holder) {
|
public void onCreate(SurfaceHolder holder) {
|
||||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
|
||||||
Log.d(TAG, "onCreate");
|
|
||||||
}
|
|
||||||
super.onCreate(holder);
|
super.onCreate(holder);
|
||||||
|
Log.d(TAG, "onCreate");
|
||||||
|
|
||||||
|
mCalendarNotApprovedMessage =
|
||||||
|
getResources().getString(R.string.calendar_permission_not_approved);
|
||||||
|
|
||||||
|
/* Accepts tap events to allow permission changes by user. */
|
||||||
setWatchFaceStyle(new WatchFaceStyle.Builder(CalendarWatchFaceService.this)
|
setWatchFaceStyle(new WatchFaceStyle.Builder(CalendarWatchFaceService.this)
|
||||||
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
|
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
|
||||||
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
|
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
|
||||||
.setShowSystemUiTime(false)
|
.setShowSystemUiTime(false)
|
||||||
|
.setAcceptsTapEvents(true)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
mTextPaint.setColor(FOREGROUND_COLOR);
|
mTextPaint.setColor(FOREGROUND_COLOR);
|
||||||
mTextPaint.setTextSize(TEXT_SIZE);
|
mTextPaint.setTextSize(TEXT_SIZE);
|
||||||
|
|
||||||
mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
|
// Enables app to handle 23+ (M+) style permissions.
|
||||||
|
mCalendarPermissionApproved =
|
||||||
|
ActivityCompat.checkSelfPermission(
|
||||||
|
getApplicationContext(),
|
||||||
|
Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
||||||
|
if (mCalendarPermissionApproved) {
|
||||||
|
mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);
|
mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);
|
||||||
cancelLoadMeetingTask();
|
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Captures tap event (and tap type) and increments correct tap type total.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onTapCommand(int tapType, int x, int y, long eventTime) {
|
||||||
|
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||||
|
Log.d(TAG, "Tap Command: " + tapType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore lint error (fixed in wearable support library 1.4)
|
||||||
|
if (tapType == WatchFaceService.TAP_TYPE_TAP && !mCalendarPermissionApproved) {
|
||||||
|
Intent permissionIntent = new Intent(
|
||||||
|
getApplicationContext(),
|
||||||
|
CalendarWatchFacePermissionActivity.class);
|
||||||
|
permissionIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(permissionIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDraw(Canvas canvas, Rect bounds) {
|
public void onDraw(Canvas canvas, Rect bounds) {
|
||||||
// Create or update mLayout if necessary.
|
// Create or update mLayout if necessary.
|
||||||
@@ -141,8 +181,13 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
|
|||||||
|
|
||||||
// Update the contents of mEditable.
|
// Update the contents of mEditable.
|
||||||
mEditable.clear();
|
mEditable.clear();
|
||||||
mEditable.append(Html.fromHtml(getResources().getQuantityString(
|
|
||||||
R.plurals.calendar_meetings, mNumMeetings, mNumMeetings)));
|
if (mCalendarPermissionApproved) {
|
||||||
|
mEditable.append(Html.fromHtml(getResources().getQuantityString(
|
||||||
|
R.plurals.calendar_meetings, mNumMeetings, mNumMeetings)));
|
||||||
|
} else {
|
||||||
|
mEditable.append(Html.fromHtml(mCalendarNotApprovedMessage));
|
||||||
|
}
|
||||||
|
|
||||||
// Draw the text on a solid background.
|
// Draw the text on a solid background.
|
||||||
canvas.drawColor(BACKGROUND_COLOR);
|
canvas.drawColor(BACKGROUND_COLOR);
|
||||||
@@ -151,15 +196,24 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onVisibilityChanged(boolean visible) {
|
public void onVisibilityChanged(boolean visible) {
|
||||||
|
Log.d(TAG, "onVisibilityChanged()");
|
||||||
super.onVisibilityChanged(visible);
|
super.onVisibilityChanged(visible);
|
||||||
if (visible) {
|
if (visible) {
|
||||||
IntentFilter filter = new IntentFilter(Intent.ACTION_PROVIDER_CHANGED);
|
|
||||||
filter.addDataScheme("content");
|
|
||||||
filter.addDataAuthority(WearableCalendarContract.AUTHORITY, null);
|
|
||||||
registerReceiver(mBroadcastReceiver, filter);
|
|
||||||
mIsReceiverRegistered = true;
|
|
||||||
|
|
||||||
mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
|
// Enables app to handle 23+ (M+) style permissions.
|
||||||
|
mCalendarPermissionApproved = ActivityCompat.checkSelfPermission(
|
||||||
|
getApplicationContext(),
|
||||||
|
Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
||||||
|
if (mCalendarPermissionApproved) {
|
||||||
|
IntentFilter filter = new IntentFilter(Intent.ACTION_PROVIDER_CHANGED);
|
||||||
|
filter.addDataScheme("content");
|
||||||
|
filter.addDataAuthority(WearableCalendarContract.AUTHORITY, null);
|
||||||
|
registerReceiver(mBroadcastReceiver, filter);
|
||||||
|
mIsReceiverRegistered = true;
|
||||||
|
|
||||||
|
mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (mIsReceiverRegistered) {
|
if (mIsReceiverRegistered) {
|
||||||
unregisterReceiver(mBroadcastReceiver);
|
unregisterReceiver(mBroadcastReceiver);
|
||||||
@@ -204,9 +258,9 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
|
|||||||
final Cursor cursor = getContentResolver().query(builder.build(),
|
final Cursor cursor = getContentResolver().query(builder.build(),
|
||||||
null, null, null, null);
|
null, null, null, null);
|
||||||
int numMeetings = cursor.getCount();
|
int numMeetings = cursor.getCount();
|
||||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
|
||||||
Log.v(TAG, "Num meetings: " + numMeetings);
|
Log.d(TAG, "Num meetings: " + numMeetings);
|
||||||
}
|
|
||||||
return numMeetings;
|
return numMeetings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,533 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.example.android.wearable.watchface;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.support.wearable.watchface.CanvasWatchFaceService;
|
||||||
|
import android.support.wearable.watchface.WatchFaceStyle;
|
||||||
|
import android.text.format.DateFormat;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.SurfaceHolder;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.ConnectionResult;
|
||||||
|
import com.google.android.gms.common.Scopes;
|
||||||
|
import com.google.android.gms.common.api.GoogleApiClient;
|
||||||
|
import com.google.android.gms.common.api.PendingResult;
|
||||||
|
import com.google.android.gms.common.api.ResultCallback;
|
||||||
|
import com.google.android.gms.common.api.Scope;
|
||||||
|
import com.google.android.gms.common.api.Status;
|
||||||
|
import com.google.android.gms.fitness.Fitness;
|
||||||
|
import com.google.android.gms.fitness.FitnessStatusCodes;
|
||||||
|
import com.google.android.gms.fitness.data.DataPoint;
|
||||||
|
import com.google.android.gms.fitness.data.DataType;
|
||||||
|
import com.google.android.gms.fitness.data.Field;
|
||||||
|
import com.google.android.gms.fitness.result.DailyTotalResult;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the user's daily distance total via Google Fit. Distance is polled initially when the
|
||||||
|
* Google API Client successfully connects and once a minute after that via the onTimeTick callback.
|
||||||
|
* If you want more frequent updates, you will want to add your own Handler.
|
||||||
|
*
|
||||||
|
* Authentication IS a requirement to request distance from Google Fit on Wear. Otherwise, distance
|
||||||
|
* will always come back as zero (or stay at whatever the distance was prior to you
|
||||||
|
* de-authorizing watchface).
|
||||||
|
*
|
||||||
|
* In ambient mode, the seconds are replaced with an AM/PM indicator.
|
||||||
|
*
|
||||||
|
* On devices with low-bit ambient mode, the text is drawn without anti-aliasing. On devices which
|
||||||
|
* require burn-in protection, the hours are drawn in normal rather than bold.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class FitDistanceWatchFaceService extends CanvasWatchFaceService {
|
||||||
|
|
||||||
|
private static final String TAG = "DistanceWatchFace";
|
||||||
|
|
||||||
|
private static final Typeface BOLD_TYPEFACE =
|
||||||
|
Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
|
||||||
|
private static final Typeface NORMAL_TYPEFACE =
|
||||||
|
Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update rate in milliseconds for active mode (non-ambient).
|
||||||
|
*/
|
||||||
|
private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine onCreateEngine() {
|
||||||
|
return new Engine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Engine extends CanvasWatchFaceService.Engine implements
|
||||||
|
GoogleApiClient.ConnectionCallbacks,
|
||||||
|
GoogleApiClient.OnConnectionFailedListener,
|
||||||
|
ResultCallback<DailyTotalResult> {
|
||||||
|
|
||||||
|
private static final int BACKGROUND_COLOR = Color.BLACK;
|
||||||
|
private static final int TEXT_HOURS_MINS_COLOR = Color.WHITE;
|
||||||
|
private static final int TEXT_SECONDS_COLOR = Color.GRAY;
|
||||||
|
private static final int TEXT_AM_PM_COLOR = Color.GRAY;
|
||||||
|
private static final int TEXT_COLON_COLOR = Color.GRAY;
|
||||||
|
private static final int TEXT_DISTANCE_COUNT_COLOR = Color.GRAY;
|
||||||
|
|
||||||
|
private static final String COLON_STRING = ":";
|
||||||
|
|
||||||
|
private static final int MSG_UPDATE_TIME = 0;
|
||||||
|
|
||||||
|
/* Handler to update the time periodically in interactive mode. */
|
||||||
|
private final Handler mUpdateTimeHandler = new Handler() {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message message) {
|
||||||
|
switch (message.what) {
|
||||||
|
case MSG_UPDATE_TIME:
|
||||||
|
Log.v(TAG, "updating time");
|
||||||
|
invalidate();
|
||||||
|
if (shouldUpdateTimeHandlerBeRunning()) {
|
||||||
|
long timeMs = System.currentTimeMillis();
|
||||||
|
long delayMs =
|
||||||
|
ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
|
||||||
|
mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles time zone and locale changes.
|
||||||
|
*/
|
||||||
|
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
mCalendar.setTimeZone(TimeZone.getDefault());
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregistering an unregistered receiver throws an exception. Keep track of the
|
||||||
|
* registration state to prevent that.
|
||||||
|
*/
|
||||||
|
private boolean mRegisteredReceiver = false;
|
||||||
|
|
||||||
|
private Paint mHourPaint;
|
||||||
|
private Paint mMinutePaint;
|
||||||
|
private Paint mSecondPaint;
|
||||||
|
private Paint mAmPmPaint;
|
||||||
|
private Paint mColonPaint;
|
||||||
|
private Paint mDistanceCountPaint;
|
||||||
|
|
||||||
|
private float mColonWidth;
|
||||||
|
|
||||||
|
private Calendar mCalendar;
|
||||||
|
|
||||||
|
private float mXOffset;
|
||||||
|
private float mXDistanceOffset;
|
||||||
|
private float mYOffset;
|
||||||
|
private float mLineHeight;
|
||||||
|
|
||||||
|
private String mAmString;
|
||||||
|
private String mPmString;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the display supports fewer bits for each color in ambient mode. When true, we
|
||||||
|
* disable anti-aliasing in ambient mode.
|
||||||
|
*/
|
||||||
|
private boolean mLowBitAmbient;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Google API Client used to make Google Fit requests for step data.
|
||||||
|
*/
|
||||||
|
private GoogleApiClient mGoogleApiClient;
|
||||||
|
|
||||||
|
private boolean mDistanceRequested;
|
||||||
|
|
||||||
|
private float mDistanceTotal = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SurfaceHolder holder) {
|
||||||
|
Log.d(TAG, "onCreate");
|
||||||
|
|
||||||
|
super.onCreate(holder);
|
||||||
|
|
||||||
|
mDistanceRequested = false;
|
||||||
|
mGoogleApiClient = new GoogleApiClient.Builder(FitDistanceWatchFaceService.this)
|
||||||
|
.addConnectionCallbacks(this)
|
||||||
|
.addOnConnectionFailedListener(this)
|
||||||
|
.addApi(Fitness.HISTORY_API)
|
||||||
|
.addApi(Fitness.RECORDING_API)
|
||||||
|
.addScope(new Scope(Scopes.FITNESS_LOCATION_READ))
|
||||||
|
// When user has multiple accounts, useDefaultAccount() allows Google Fit to
|
||||||
|
// associated with the main account for steps. It also replaces the need for
|
||||||
|
// a scope request.
|
||||||
|
.useDefaultAccount()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
setWatchFaceStyle(new WatchFaceStyle.Builder(FitDistanceWatchFaceService.this)
|
||||||
|
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
|
||||||
|
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
|
||||||
|
.setShowSystemUiTime(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
Resources resources = getResources();
|
||||||
|
|
||||||
|
mYOffset = resources.getDimension(R.dimen.fit_y_offset);
|
||||||
|
mLineHeight = resources.getDimension(R.dimen.fit_line_height);
|
||||||
|
mAmString = resources.getString(R.string.fit_am);
|
||||||
|
mPmString = resources.getString(R.string.fit_pm);
|
||||||
|
|
||||||
|
mHourPaint = createTextPaint(TEXT_HOURS_MINS_COLOR, BOLD_TYPEFACE);
|
||||||
|
mMinutePaint = createTextPaint(TEXT_HOURS_MINS_COLOR);
|
||||||
|
mSecondPaint = createTextPaint(TEXT_SECONDS_COLOR);
|
||||||
|
mAmPmPaint = createTextPaint(TEXT_AM_PM_COLOR);
|
||||||
|
mColonPaint = createTextPaint(TEXT_COLON_COLOR);
|
||||||
|
mDistanceCountPaint = createTextPaint(TEXT_DISTANCE_COUNT_COLOR);
|
||||||
|
|
||||||
|
mCalendar = Calendar.getInstance();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Paint createTextPaint(int color) {
|
||||||
|
return createTextPaint(color, NORMAL_TYPEFACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Paint createTextPaint(int color, Typeface typeface) {
|
||||||
|
Paint paint = new Paint();
|
||||||
|
paint.setColor(color);
|
||||||
|
paint.setTypeface(typeface);
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
return paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onVisibilityChanged(boolean visible) {
|
||||||
|
Log.d(TAG, "onVisibilityChanged: " + visible);
|
||||||
|
|
||||||
|
super.onVisibilityChanged(visible);
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
mGoogleApiClient.connect();
|
||||||
|
|
||||||
|
registerReceiver();
|
||||||
|
|
||||||
|
// Update time zone and date formats, in case they changed while we weren't visible.
|
||||||
|
mCalendar.setTimeZone(TimeZone.getDefault());
|
||||||
|
} else {
|
||||||
|
unregisterReceiver();
|
||||||
|
|
||||||
|
if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
|
||||||
|
mGoogleApiClient.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether the timer should be running depends on whether we're visible (as well as
|
||||||
|
// whether we're in ambient mode), so we may need to start or stop the timer.
|
||||||
|
updateTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void registerReceiver() {
|
||||||
|
if (mRegisteredReceiver) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mRegisteredReceiver = true;
|
||||||
|
IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
|
||||||
|
FitDistanceWatchFaceService.this.registerReceiver(mReceiver, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unregisterReceiver() {
|
||||||
|
if (!mRegisteredReceiver) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mRegisteredReceiver = false;
|
||||||
|
FitDistanceWatchFaceService.this.unregisterReceiver(mReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onApplyWindowInsets(WindowInsets insets) {
|
||||||
|
Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square"));
|
||||||
|
|
||||||
|
super.onApplyWindowInsets(insets);
|
||||||
|
|
||||||
|
// Load resources that have alternate values for round watches.
|
||||||
|
Resources resources = FitDistanceWatchFaceService.this.getResources();
|
||||||
|
boolean isRound = insets.isRound();
|
||||||
|
mXOffset = resources.getDimension(isRound
|
||||||
|
? R.dimen.fit_x_offset_round : R.dimen.fit_x_offset);
|
||||||
|
mXDistanceOffset =
|
||||||
|
resources.getDimension(
|
||||||
|
isRound ?
|
||||||
|
R.dimen.fit_steps_or_distance_x_offset_round :
|
||||||
|
R.dimen.fit_steps_or_distance_x_offset);
|
||||||
|
float textSize = resources.getDimension(isRound
|
||||||
|
? R.dimen.fit_text_size_round : R.dimen.fit_text_size);
|
||||||
|
float amPmSize = resources.getDimension(isRound
|
||||||
|
? R.dimen.fit_am_pm_size_round : R.dimen.fit_am_pm_size);
|
||||||
|
|
||||||
|
mHourPaint.setTextSize(textSize);
|
||||||
|
mMinutePaint.setTextSize(textSize);
|
||||||
|
mSecondPaint.setTextSize(textSize);
|
||||||
|
mAmPmPaint.setTextSize(amPmSize);
|
||||||
|
mColonPaint.setTextSize(textSize);
|
||||||
|
mDistanceCountPaint.setTextSize(
|
||||||
|
resources.getDimension(R.dimen.fit_steps_or_distance_text_size));
|
||||||
|
|
||||||
|
mColonWidth = mColonPaint.measureText(COLON_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPropertiesChanged(Bundle properties) {
|
||||||
|
super.onPropertiesChanged(properties);
|
||||||
|
|
||||||
|
boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
|
||||||
|
mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : BOLD_TYPEFACE);
|
||||||
|
|
||||||
|
mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
|
||||||
|
|
||||||
|
Log.d(TAG, "onPropertiesChanged: burn-in protection = " + burnInProtection
|
||||||
|
+ ", low-bit ambient = " + mLowBitAmbient);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimeTick() {
|
||||||
|
super.onTimeTick();
|
||||||
|
Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
|
||||||
|
getTotalDistance();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAmbientModeChanged(boolean inAmbientMode) {
|
||||||
|
super.onAmbientModeChanged(inAmbientMode);
|
||||||
|
Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
|
||||||
|
|
||||||
|
if (mLowBitAmbient) {
|
||||||
|
boolean antiAlias = !inAmbientMode;;
|
||||||
|
mHourPaint.setAntiAlias(antiAlias);
|
||||||
|
mMinutePaint.setAntiAlias(antiAlias);
|
||||||
|
mSecondPaint.setAntiAlias(antiAlias);
|
||||||
|
mAmPmPaint.setAntiAlias(antiAlias);
|
||||||
|
mColonPaint.setAntiAlias(antiAlias);
|
||||||
|
mDistanceCountPaint.setAntiAlias(antiAlias);
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
|
||||||
|
// Whether the timer should be running depends on whether we're in ambient mode (as well
|
||||||
|
// as whether we're visible), so we may need to start or stop the timer.
|
||||||
|
updateTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTwoDigitNumber(int hour) {
|
||||||
|
return String.format("%02d", hour);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAmPmString(int amPm) {
|
||||||
|
return amPm == Calendar.AM ? mAmString : mPmString;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDraw(Canvas canvas, Rect bounds) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
mCalendar.setTimeInMillis(now);
|
||||||
|
boolean is24Hour = DateFormat.is24HourFormat(FitDistanceWatchFaceService.this);
|
||||||
|
|
||||||
|
// Draw the background.
|
||||||
|
canvas.drawColor(BACKGROUND_COLOR);
|
||||||
|
|
||||||
|
// Draw the hours.
|
||||||
|
float x = mXOffset;
|
||||||
|
String hourString;
|
||||||
|
if (is24Hour) {
|
||||||
|
hourString = formatTwoDigitNumber(mCalendar.get(Calendar.HOUR_OF_DAY));
|
||||||
|
} else {
|
||||||
|
int hour = mCalendar.get(Calendar.HOUR);
|
||||||
|
if (hour == 0) {
|
||||||
|
hour = 12;
|
||||||
|
}
|
||||||
|
hourString = String.valueOf(hour);
|
||||||
|
}
|
||||||
|
canvas.drawText(hourString, x, mYOffset, mHourPaint);
|
||||||
|
x += mHourPaint.measureText(hourString);
|
||||||
|
|
||||||
|
// Draw first colon (between hour and minute).
|
||||||
|
canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
|
||||||
|
|
||||||
|
x += mColonWidth;
|
||||||
|
|
||||||
|
// Draw the minutes.
|
||||||
|
String minuteString = formatTwoDigitNumber(mCalendar.get(Calendar.MINUTE));
|
||||||
|
canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
|
||||||
|
x += mMinutePaint.measureText(minuteString);
|
||||||
|
|
||||||
|
// In interactive mode, draw a second colon followed by the seconds.
|
||||||
|
// Otherwise, if we're in 12-hour mode, draw AM/PM
|
||||||
|
if (!isInAmbientMode()) {
|
||||||
|
canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
|
||||||
|
|
||||||
|
x += mColonWidth;
|
||||||
|
canvas.drawText(formatTwoDigitNumber(
|
||||||
|
mCalendar.get(Calendar.SECOND)), x, mYOffset, mSecondPaint);
|
||||||
|
} else if (!is24Hour) {
|
||||||
|
x += mColonWidth;
|
||||||
|
canvas.drawText(getAmPmString(
|
||||||
|
mCalendar.get(Calendar.AM_PM)), x, mYOffset, mAmPmPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only render distance if there is no peek card, so they do not bleed into each other
|
||||||
|
// in ambient mode.
|
||||||
|
if (getPeekCardPosition().isEmpty()) {
|
||||||
|
canvas.drawText(
|
||||||
|
getString(R.string.fit_distance, mDistanceTotal),
|
||||||
|
mXDistanceOffset,
|
||||||
|
mYOffset + mLineHeight,
|
||||||
|
mDistanceCountPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
|
||||||
|
* or stops it if it shouldn't be running but currently is.
|
||||||
|
*/
|
||||||
|
private void updateTimer() {
|
||||||
|
Log.d(TAG, "updateTimer");
|
||||||
|
|
||||||
|
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
|
||||||
|
if (shouldUpdateTimeHandlerBeRunning()) {
|
||||||
|
mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
|
||||||
|
* only run when we're visible and in interactive mode.
|
||||||
|
*/
|
||||||
|
private boolean shouldUpdateTimeHandlerBeRunning() {
|
||||||
|
return isVisible() && !isInAmbientMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getTotalDistance() {
|
||||||
|
|
||||||
|
Log.d(TAG, "getTotalDistance()");
|
||||||
|
|
||||||
|
if ((mGoogleApiClient != null)
|
||||||
|
&& (mGoogleApiClient.isConnected())
|
||||||
|
&& (!mDistanceRequested)) {
|
||||||
|
|
||||||
|
mDistanceRequested = true;
|
||||||
|
|
||||||
|
PendingResult<DailyTotalResult> distanceResult =
|
||||||
|
Fitness.HistoryApi.readDailyTotal(
|
||||||
|
mGoogleApiClient,
|
||||||
|
DataType.TYPE_DISTANCE_DELTA);
|
||||||
|
|
||||||
|
distanceResult.setResultCallback(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnected(Bundle connectionHint) {
|
||||||
|
Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnected: " + connectionHint);
|
||||||
|
|
||||||
|
mDistanceRequested = false;
|
||||||
|
|
||||||
|
// Subscribe covers devices that do not have Google Fit installed.
|
||||||
|
subscribeToDistance();
|
||||||
|
|
||||||
|
getTotalDistance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Subscribes to distance.
|
||||||
|
*/
|
||||||
|
private void subscribeToDistance() {
|
||||||
|
|
||||||
|
if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnecting())) {
|
||||||
|
|
||||||
|
Fitness.RecordingApi.subscribe(mGoogleApiClient, DataType.TYPE_DISTANCE_DELTA)
|
||||||
|
.setResultCallback(new ResultCallback<Status>() {
|
||||||
|
@Override
|
||||||
|
public void onResult(Status status) {
|
||||||
|
if (status.isSuccess()) {
|
||||||
|
if (status.getStatusCode()
|
||||||
|
== FitnessStatusCodes.SUCCESS_ALREADY_SUBSCRIBED) {
|
||||||
|
Log.i(TAG, "Existing subscription for activity detected.");
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Successfully subscribed!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "There was a problem subscribing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionSuspended(int cause) {
|
||||||
|
Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionSuspended: " + cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionFailed(ConnectionResult result) {
|
||||||
|
Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionFailed: " + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResult(DailyTotalResult dailyTotalResult) {
|
||||||
|
Log.d(TAG, "mGoogleApiAndFitCallbacks.onResult(): " + dailyTotalResult);
|
||||||
|
|
||||||
|
mDistanceRequested = false;
|
||||||
|
|
||||||
|
if (dailyTotalResult.getStatus().isSuccess()) {
|
||||||
|
|
||||||
|
List<DataPoint> points = dailyTotalResult.getTotal().getDataPoints();
|
||||||
|
|
||||||
|
if (!points.isEmpty()) {
|
||||||
|
mDistanceTotal = points.get(0).getValue(Field.FIELD_DISTANCE).asFloat();
|
||||||
|
Log.d(TAG, "distance updated: " + mDistanceTotal);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "onResult() failed! " + dailyTotalResult.getStatus().getStatusMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,12 @@ sample.group=Wearable
|
|||||||
|
|
||||||
This sample demonstrates how to create watch faces for android wear and includes a phone app
|
This sample demonstrates how to create watch faces for android wear and includes a phone app
|
||||||
and a wearable app. The wearable app has a variety of watch faces including analog, digital,
|
and a wearable app. The wearable app has a variety of watch faces including analog, digital,
|
||||||
opengl, calendar, interactive, etc. It also includes a watch-side configuration example.
|
opengl, calendar, steps, interactive, etc. It also includes a watch-side configuration example.
|
||||||
The phone app includes a phone-side configuration example.
|
The phone app includes a phone-side configuration example.
|
||||||
|
|
||||||
|
Additional note on Steps WatchFace Sample, if the user has not installed or setup the Google Fit app
|
||||||
|
on their phone and their Wear device has not configured the Google Fit Wear App, then you may get
|
||||||
|
zero steps until one of the two is setup. Please note, many Wear devices configure the Google Fit
|
||||||
|
Wear App beforehand.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
package="com.example.android.google.wearable.watchviewstub" >
|
package="com.example.android.google.wearable.watchviewstub" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
41
samples/browseable/WearSpeakerSample/AndroidManifest.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2015 Google Inc. All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.example.android.wearable.speaker" >
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
<!-- the following permission is required to record audio using a microphone -->
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@android:style/Theme.DeviceDefault" >
|
||||||
|
<uses-library android:name="com.google.android.wearable" android:required="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
14
samples/browseable/WearSpeakerSample/_index.jd
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
page.tags="WearSpeakerSample"
|
||||||
|
sample.group=Wearable
|
||||||
|
@jd:body
|
||||||
|
|
||||||
|
<p>
|
||||||
|
|
||||||
|
A sample that shows how you can record voice using the microphone on a wearable and
|
||||||
|
play the recorded voice or an mp3 file, if the wearable device has a built-in speaker.
|
||||||
|
|
||||||
|
This sample doesn't have any companion phone app so you need to install this directly
|
||||||
|
on your watch (using "adb").
|
||||||
|
|
||||||
|
</p>
|
||||||
21
samples/browseable/WearSpeakerSample/res/drawable/circle.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2015 Google Inc. All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
|
||||||
|
<size android:width="100dp"
|
||||||
|
android:height="100dp"/>
|
||||||
|
<stroke
|
||||||
|
android:width="3dp"
|
||||||
|
android:color="@color/circle_color"/>
|
||||||
|
<solid android:color="@color/circle_color"/>
|
||||||
|
</shape>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
BIN
samples/browseable/WearSpeakerSample/res/raw/sound.mp3
Normal file
21
samples/browseable/WearSpeakerSample/res/values/colors.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2015 Google Inc. All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<color name="small_icons_color">#FFF3E0</color>
|
||||||
|
<color name="large_icons_color">#FFF3E0</color>
|
||||||
|
<color name="background_color">#FF9100</color>
|
||||||
|
<color name="circle_color">#E65100</color>
|
||||||
|
<color name="progressbar_tint">#FFD180</color>
|
||||||
|
<color name="progressbar_background_tint">#E65100</color>
|
||||||
|
</resources>
|
||||||
18
samples/browseable/WearSpeakerSample/res/values/strings.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2015 Google Inc. All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Wear Speaker Sample</string>
|
||||||
|
<string name="exiting_for_permissions">Recording Audio permission is required, exiting now!</string>
|
||||||
|
<string name="no_speaker_supported">Speaker is not supported</string>
|
||||||
|
</resources>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||