diff --git a/build/sdk.atree b/build/sdk.atree
index baca11f1d..98bd984eb 100644
--- a/build/sdk.atree
+++ b/build/sdk.atree
@@ -330,6 +330,10 @@ developers/build/prebuilts/gradle/RuntimePermissionsBasic sam
developers/build/prebuilts/gradle/ActiveNotifications samples/${PLATFORM_NAME}/notification/ActiveNotifications
developers/build/prebuilts/gradle/Camera2Raw samples/${PLATFORM_NAME}/media/Camera2Raw
developers/build/prebuilts/gradle/AutoBackupForApps samples/${PLATFORM_NAME}/content/AutoBackupForApps
+developers/build/prebuilts/gradle/DirectShare samples/${PLATFORM_NAME}/content/DirectShare
+developers/build/prebuilts/gradle/MidiScope samples/${PLATFORM_NAME}/media/MidiScope
+developers/build/prebuilts/gradle/MidiSynth samples/${PLATFORM_NAME}/media/MidiSynth
+developers/build/prebuilts/gradle/AsymmetricFingerprintDialog samples/${PLATFORM_NAME}/security/AsymmetricFingerprintDialog
developers/build/prebuilts/androidtv samples/${PLATFORM_NAME}/androidtv
@@ -346,12 +350,14 @@ developers/build/prebuilts/gradle/JumpingJack samples/${PLATFO
developers/build/prebuilts/gradle/Notifications samples/${PLATFORM_NAME}/wearable/Notifications
developers/build/prebuilts/gradle/Quiz samples/${PLATFORM_NAME}/wearable/Quiz
developers/build/prebuilts/gradle/RecipeAssistant samples/${PLATFORM_NAME}/wearable/RecipeAssistant
+developers/build/prebuilts/gradle/RuntimePermissionsWear samples/${PLATFORM_NAME}/wearable/RuntimePermissionsWear
developers/build/prebuilts/gradle/SkeletonWearableApp samples/${PLATFORM_NAME}/wearable/SkeletonWearableApp
developers/build/prebuilts/gradle/SpeedTracker samples/${PLATFORM_NAME}/wearable/SpeedTracker
developers/build/prebuilts/gradle/SynchronizedNotifications samples/${PLATFORM_NAME}/wearable/SynchronizedNotifications
developers/build/prebuilts/gradle/Timer samples/${PLATFORM_NAME}/wearable/Timer
developers/build/prebuilts/gradle/WatchFace samples/${PLATFORM_NAME}/wearable/WatchFace
developers/build/prebuilts/gradle/WatchViewStub samples/${PLATFORM_NAME}/wearable/WatchViewStub
+developers/build/prebuilts/gradle/WearSpeakerSample samples/${PLATFORM_NAME}/wearable/WearSpeakerSample
developers/build/prebuilts/gradle/XYZTouristAttractions samples/${PLATFORM_NAME}/wearable/XYZTouristAttractions
# Old sample tree
diff --git a/samples/browseable/ActionBarCompat-Basic/res/values-v11/template-styles.xml b/samples/browseable/ActionBarCompat-Basic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ActionBarCompat-Basic/res/values-v11/template-styles.xml
+++ b/samples/browseable/ActionBarCompat-Basic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ActionBarCompat-Basic/res/values-v21/base-template-styles.xml b/samples/browseable/ActionBarCompat-Basic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ActionBarCompat-Basic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ActionBarCompat-Basic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ActionBarCompat-Basic/res/values/template-styles.xml b/samples/browseable/ActionBarCompat-Basic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ActionBarCompat-Basic/res/values/template-styles.xml
+++ b/samples/browseable/ActionBarCompat-Basic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ActionBarCompat-ListPopupMenu/res/values-v11/template-styles.xml b/samples/browseable/ActionBarCompat-ListPopupMenu/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ActionBarCompat-ListPopupMenu/res/values-v11/template-styles.xml
+++ b/samples/browseable/ActionBarCompat-ListPopupMenu/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ActionBarCompat-ListPopupMenu/res/values-v21/base-template-styles.xml b/samples/browseable/ActionBarCompat-ListPopupMenu/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ActionBarCompat-ListPopupMenu/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ActionBarCompat-ListPopupMenu/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ActionBarCompat-ListPopupMenu/res/values/template-styles.xml b/samples/browseable/ActionBarCompat-ListPopupMenu/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ActionBarCompat-ListPopupMenu/res/values/template-styles.xml
+++ b/samples/browseable/ActionBarCompat-ListPopupMenu/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ActionBarCompat-ShareActionProvider/res/values-v11/template-styles.xml b/samples/browseable/ActionBarCompat-ShareActionProvider/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ActionBarCompat-ShareActionProvider/res/values-v11/template-styles.xml
+++ b/samples/browseable/ActionBarCompat-ShareActionProvider/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ActionBarCompat-ShareActionProvider/res/values-v21/base-template-styles.xml b/samples/browseable/ActionBarCompat-ShareActionProvider/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ActionBarCompat-ShareActionProvider/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ActionBarCompat-ShareActionProvider/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ActionBarCompat-ShareActionProvider/res/values/template-styles.xml b/samples/browseable/ActionBarCompat-ShareActionProvider/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ActionBarCompat-ShareActionProvider/res/values/template-styles.xml
+++ b/samples/browseable/ActionBarCompat-ShareActionProvider/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ActionBarCompat-Styled/res/values-v11/template-styles.xml b/samples/browseable/ActionBarCompat-Styled/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ActionBarCompat-Styled/res/values-v11/template-styles.xml
+++ b/samples/browseable/ActionBarCompat-Styled/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ActionBarCompat-Styled/res/values-v21/base-template-styles.xml b/samples/browseable/ActionBarCompat-Styled/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ActionBarCompat-Styled/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ActionBarCompat-Styled/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ActionBarCompat-Styled/res/values/template-styles.xml b/samples/browseable/ActionBarCompat-Styled/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ActionBarCompat-Styled/res/values/template-styles.xml
+++ b/samples/browseable/ActionBarCompat-Styled/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ActiveNotifications/res/values-v11/template-styles.xml b/samples/browseable/ActiveNotifications/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ActiveNotifications/res/values-v11/template-styles.xml
+++ b/samples/browseable/ActiveNotifications/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ActiveNotifications/res/values-v21/base-template-styles.xml b/samples/browseable/ActiveNotifications/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ActiveNotifications/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ActiveNotifications/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ActiveNotifications/res/values/template-styles.xml b/samples/browseable/ActiveNotifications/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ActiveNotifications/res/values/template-styles.xml
+++ b/samples/browseable/ActiveNotifications/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ActivityInstrumentation/res/values-v11/template-styles.xml b/samples/browseable/ActivityInstrumentation/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ActivityInstrumentation/res/values-v11/template-styles.xml
+++ b/samples/browseable/ActivityInstrumentation/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ActivityInstrumentation/res/values-v21/base-template-styles.xml b/samples/browseable/ActivityInstrumentation/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ActivityInstrumentation/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ActivityInstrumentation/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ActivityInstrumentation/res/values/template-styles.xml b/samples/browseable/ActivityInstrumentation/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ActivityInstrumentation/res/values/template-styles.xml
+++ b/samples/browseable/ActivityInstrumentation/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ActivitySceneTransitionBasic/res/values-v11/template-styles.xml b/samples/browseable/ActivitySceneTransitionBasic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ActivitySceneTransitionBasic/res/values-v11/template-styles.xml
+++ b/samples/browseable/ActivitySceneTransitionBasic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ActivitySceneTransitionBasic/res/values-v21/base-template-styles.xml b/samples/browseable/ActivitySceneTransitionBasic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ActivitySceneTransitionBasic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ActivitySceneTransitionBasic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ActivitySceneTransitionBasic/res/values/template-styles.xml b/samples/browseable/ActivitySceneTransitionBasic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ActivitySceneTransitionBasic/res/values/template-styles.xml
+++ b/samples/browseable/ActivitySceneTransitionBasic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/AdvancedImmersiveMode/res/values-v11/template-styles.xml b/samples/browseable/AdvancedImmersiveMode/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/AdvancedImmersiveMode/res/values-v11/template-styles.xml
+++ b/samples/browseable/AdvancedImmersiveMode/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/AdvancedImmersiveMode/res/values-v21/base-template-styles.xml b/samples/browseable/AdvancedImmersiveMode/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/AdvancedImmersiveMode/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/AdvancedImmersiveMode/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/AdvancedImmersiveMode/res/values/template-styles.xml b/samples/browseable/AdvancedImmersiveMode/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/AdvancedImmersiveMode/res/values/template-styles.xml
+++ b/samples/browseable/AdvancedImmersiveMode/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/AgendaData/Application/AndroidManifest.xml b/samples/browseable/AgendaData/Application/AndroidManifest.xml
index aa8a14a0a..ad6cccde8 100644
--- a/samples/browseable/AgendaData/Application/AndroidManifest.xml
+++ b/samples/browseable/AgendaData/Application/AndroidManifest.xml
@@ -18,16 +18,22 @@
package="com.example.android.wearable.agendadata">
+ android:targetSdkVersion="23" />
+
+
+
+
+
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppCompat.Light">
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/vertical_page_margin"
+ android:paddingLeft="@dimen/horizontal_page_margin"
+ android:paddingRight="@dimen/horizontal_page_margin"
+ android:paddingTop="@dimen/vertical_page_margin"
+ tools:context=".MainActivity">
diff --git a/samples/browseable/AgendaData/Application/res/values-v11/template-styles.xml b/samples/browseable/AgendaData/Application/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/AgendaData/Application/res/values-v11/template-styles.xml
+++ b/samples/browseable/AgendaData/Application/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/AgendaData/Application/res/values-v21/base-template-styles.xml b/samples/browseable/AgendaData/Application/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/AgendaData/Application/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/AgendaData/Application/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/AgendaData/Application/res/values/base-strings.xml b/samples/browseable/AgendaData/Application/res/values/base-strings.xml
index acc40cf94..3f806d0a4 100644
--- a/samples/browseable/AgendaData/Application/res/values/base-strings.xml
+++ b/samples/browseable/AgendaData/Application/res/values/base-strings.xml
@@ -22,10 +22,10 @@
Syncs calendar events to your wearable at the press of a button, using the Wearable
- DataApi to transmit data such as event time, description, and background image. The DataItems can be
- deleted individually via an action on the event notifications, or all at once via a button on the
- companion. When deleted using the notification action, a ConfirmationActivity is used to indicate
- success or failure.
+ DataApi to transmit data such as event time, description, and background image. The
+ DataItems can be deleted individually via an action on the event notifications, or all
+ at once via a button on the companion. When deleted using the notification action, a
+ ConfirmationActivity is used to indicate success or failure.
]]>
diff --git a/samples/browseable/AgendaData/Application/res/values/strings.xml b/samples/browseable/AgendaData/Application/res/values/strings.xml
index 9969f4f24..84cb60dba 100644
--- a/samples/browseable/AgendaData/Application/res/values/strings.xml
+++ b/samples/browseable/AgendaData/Application/res/values/strings.xml
@@ -17,5 +17,9 @@
Sync calendar events to wearableDelete calendar events from wearable
- Log
+ Deletion Log:
+ Permissions granted. Send Calendar events to Wear device.
+ Permission requests were denied. Can\'t send calendar events.
+
+ OK
diff --git a/samples/browseable/AgendaData/Application/res/values/template-styles.xml b/samples/browseable/AgendaData/Application/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/AgendaData/Application/res/values/template-styles.xml
+++ b/samples/browseable/AgendaData/Application/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.wearable.agendadata/CalendarQueryService.java b/samples/browseable/AgendaData/Application/src/com.example.android.wearable.agendadata/CalendarQueryService.java
index c39a5ed1b..9d65b7e01 100644
--- a/samples/browseable/AgendaData/Application/src/com.example.android.wearable.agendadata/CalendarQueryService.java
+++ b/samples/browseable/AgendaData/Application/src/com.example.android.wearable.agendadata/CalendarQueryService.java
@@ -250,6 +250,11 @@ public class CalendarQueryService extends IntentService
public PutDataMapRequest toPutDataMapRequest(){
final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(
makeDataItemPath(eventId, begin));
+ /* In most cases (as in this one), you don't need your DataItem appear instantly. By
+ default, delivery of normal DataItems to the Wear network might be delayed in order to
+ improve battery life for user devices. However, if you can't tolerate a delay in the
+ sync of your DataItems, you can mark them as urgent via setUrgent().
+ */
DataMap data = putDataMapRequest.getDataMap();
data.putString(DATA_ITEM_URI, putDataMapRequest.getUri().toString());
data.putLong(ID, id);
diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.wearable.agendadata/MainActivity.java b/samples/browseable/AgendaData/Application/src/com.example.android.wearable.agendadata/MainActivity.java
index 34e327b3a..6a9678a66 100644
--- a/samples/browseable/AgendaData/Application/src/com.example.android.wearable.agendadata/MainActivity.java
+++ b/samples/browseable/AgendaData/Application/src/com.example.android.wearable.agendadata/MainActivity.java
@@ -18,11 +18,16 @@ package com.example.android.wearable.agendadata;
import static com.example.android.wearable.agendadata.Constants.TAG;
-import android.app.Activity;
+import android.Manifest;
import android.content.Intent;
import android.content.IntentSender;
+import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ScrollView;
@@ -40,27 +45,42 @@ import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable;
-import java.util.List;
+/**
+ * Syncs or deletes calendar events (event time, description, and background image) to your
+ * Wearable via the Wearable DataApi at the click of a button. Includes code to handle dynamic M+
+ * permissions as well.
+ */
+public class MainActivity extends AppCompatActivity implements
+ NodeApi.NodeListener,
+ ConnectionCallbacks,
+ OnConnectionFailedListener,
+ ActivityCompat.OnRequestPermissionsResultCallback {
-public class MainActivity extends Activity implements NodeApi.NodeListener, ConnectionCallbacks,
- OnConnectionFailedListener {
-
- /** Request code for launching the Intent to resolve Google Play services errors. */
+ /* Request code for launching the Intent to resolve Google Play services errors. */
private static final int REQUEST_RESOLVE_ERROR = 1000;
+ /* Id to identify calendar and contact permissions request. */
+ private static final int REQUEST_CALENDAR_AND_CONTACTS = 0;
+
+
private GoogleApiClient mGoogleApiClient;
private boolean mResolvingError = false;
private TextView mLogTextView;
ScrollView mScroller;
+ private View mLayout;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
+ mLayout = findViewById(R.id.main_layout);
+
mLogTextView = (TextView) findViewById(R.id.log);
mScroller = (ScrollView) findViewById(R.id.scroller);
+
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
@@ -85,11 +105,94 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
super.onStop();
}
- public void onGetEventsClicked(View v) {
+ public void onGetEventsClicked(View view) {
+
+ Log.i(TAG, "onGetEventsClicked(): Checking permission.");
+
+ // BEGIN_INCLUDE(calendar_and_contact_permissions)
+ // Check if the Calendar permission is already available.
+ boolean calendarApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR)
+ == PackageManager.PERMISSION_GRANTED;
+
+ boolean contactsApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (!calendarApproved || !contactsApproved) {
+ // Calendar and/or Contact permissions have not been granted.
+ requestCalendarAndContactPermissions();
+
+ } else {
+ // Calendar permissions is already available, start service
+ Log.i(TAG, "Permissions already granted. Starting service.");
+ pushCalendarToWear();
+ }
+ // END_INCLUDE(calendar_and_contact_permissions)
+
+ }
+
+ private void pushCalendarToWear() {
startService(new Intent(this, CalendarQueryService.class));
}
- public void onDeleteEventsClicked(View v) {
+ /*
+ * Requests Calendar and Contact permissions.
+ * If the permission has been denied previously, a SnackBar will prompt the user to grant the
+ * permission, otherwise it is requested directly.
+ */
+ private void requestCalendarAndContactPermissions() {
+ Log.i(TAG, "CALENDAR permission has NOT been granted. Requesting permission.");
+
+ // BEGIN_INCLUDE(calendar_and_contact_permissions_request)
+
+ boolean showCalendarPermissionRationale =
+ ActivityCompat.shouldShowRequestPermissionRationale(this,
+ Manifest.permission.READ_CALENDAR);
+ boolean showContactsPermissionRationale =
+ ActivityCompat.shouldShowRequestPermissionRationale(this,
+ Manifest.permission.READ_CONTACTS);
+
+ if (showCalendarPermissionRationale || showContactsPermissionRationale) {
+ /*
+ * Provide an additional rationale to the user if the permission was not granted and
+ * the user would benefit from additional context for the use of the permission. For
+ * example, if the user has previously denied the permission.
+ */
+ Log.i(TAG, "Display calendar & contact permissions rationale for additional context.");
+
+ Snackbar.make(mLayout, R.string.permissions_rationale,
+ Snackbar.LENGTH_INDEFINITE)
+ .setAction(R.string.ok, new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ActivityCompat.requestPermissions(MainActivity.this,
+ new String[] {
+ Manifest.permission.READ_CALENDAR,
+ Manifest.permission.READ_CONTACTS},
+ REQUEST_CALENDAR_AND_CONTACTS);
+ }
+ })
+ .show();
+
+
+ } else {
+
+ // Calendar/Contact permissions have not been granted yet. Request it directly.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{
+ Manifest.permission.READ_CALENDAR,
+ Manifest.permission.READ_CONTACTS
+ },
+ REQUEST_CALENDAR_AND_CONTACTS);
+ }
+ // END_INCLUDE(calendar_and_contact_permissions_request)
+ }
+
+
+
+ public void onDeleteEventsClicked(View view) {
if (mGoogleApiClient.isConnected()) {
Wearable.DataApi.getDataItems(mGoogleApiClient)
.setResultCallback(new ResultCallback() {
@@ -100,9 +203,8 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
deleteDataItems(result);
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG,"onDeleteEventsClicked(): failed to get Data "
+ Log.d(TAG, "onDeleteEventsClicked(): failed to get Data "
+ "Items");
-
}
}
} finally {
@@ -120,9 +222,11 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
if (mGoogleApiClient.isConnected()) {
for (final DataItem dataItem : dataItemList) {
final Uri dataItemUri = dataItem.getUri();
- // In a real calendar application, this might delete the corresponding calendar
- // event from the calendar data provider. In this sample, we simply delete the
- // DataItem, but leave the phone's calendar data intact.
+ /*
+ * In a real calendar application, this might delete the corresponding calendar
+ * events from the calendar data provider. However, we simply delete the DataItem,
+ * but leave the phone's calendar data intact for this simple sample.
+ */
Wearable.DataApi.deleteDataItems(mGoogleApiClient, dataItemUri)
.setResultCallback(new ResultCallback() {
@Override
@@ -141,17 +245,6 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
}
}
- private void appendLog(final String s) {
- mLogTextView.post(new Runnable() {
- @Override
- public void run() {
- mLogTextView.append(s);
- mLogTextView.append("\n");
- mScroller.fullScroll(View.FOCUS_DOWN);
- }
- });
- }
-
@Override
public void onPeerConnected(Node peer) {
appendLog("Device connected");
@@ -165,7 +258,7 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
@Override
public void onConnected(Bundle connectionHint) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Connected to Google Api Service");
+ Log.d(TAG, "Connected to Google Api Service.");
}
mResolvingError = false;
Wearable.NodeApi.addListener(mGoogleApiClient, this);
@@ -193,10 +286,77 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
} catch (IntentSender.SendIntentException e) {
// There was an error with the resolution intent. Try again.
+ mResolvingError = false;
mGoogleApiClient.connect();
}
} else {
mResolvingError = false;
}
}
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onActivityResult request/result codes: " + requestCode + "/" + resultCode);
+ }
+
+ if (requestCode == REQUEST_RESOLVE_ERROR) {
+ mResolvingError = false;
+ if (resultCode == RESULT_OK) {
+ // Make sure the app is not already connected or attempting to connect
+ if (!mGoogleApiClient.isConnecting() && !mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.connect();
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onRequestPermissionsResult(): " + permissions);
+ }
+
+ if (requestCode == REQUEST_CALENDAR_AND_CONTACTS) {
+ // BEGIN_INCLUDE(permissions_result)
+ // Received permission result for calendar permission.
+ Log.i(TAG, "Received response for Calendar permission request.");
+
+ // Check if all required permissions have been granted.
+ if ((grantResults.length == 2)
+ && (grantResults[0] == PackageManager.PERMISSION_GRANTED)
+ && (grantResults[1] == PackageManager.PERMISSION_GRANTED)) {
+ // Calendar/Contact permissions have been granted, pull all calendar events
+ Log.i(TAG, "All permission has now been granted. Showing preview.");
+ Snackbar.make(mLayout, R.string.permisions_granted, Snackbar.LENGTH_SHORT).show();
+
+ pushCalendarToWear();
+
+ } else {
+ Log.i(TAG, "CALENDAR and/or CONTACT permissions were NOT granted.");
+ Snackbar.make(mLayout, R.string.permissions_denied, Snackbar.LENGTH_SHORT).show();
+ }
+ // END_INCLUDE(permissions_result)
+
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private void appendLog(final String s) {
+ mLogTextView.post(new Runnable() {
+ @Override
+ public void run() {
+ mLogTextView.append(s);
+ mLogTextView.append("\n");
+ mScroller.fullScroll(View.FOCUS_DOWN);
+ }
+ });
+ }
}
diff --git a/samples/browseable/AgendaData/Wearable/AndroidManifest.xml b/samples/browseable/AgendaData/Wearable/AndroidManifest.xml
index dcab6227a..e6dbab705 100644
--- a/samples/browseable/AgendaData/Wearable/AndroidManifest.xml
+++ b/samples/browseable/AgendaData/Wearable/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.agendadata" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/AgendaData/Wearable/src/com.example.android.wearable.agendadata/HomeListenerService.java b/samples/browseable/AgendaData/Wearable/src/com.example.android.wearable.agendadata/HomeListenerService.java
index 0cbda71e8..02a386116 100644
--- a/samples/browseable/AgendaData/Wearable/src/com.example.android.wearable.agendadata/HomeListenerService.java
+++ b/samples/browseable/AgendaData/Wearable/src/com.example.android.wearable.agendadata/HomeListenerService.java
@@ -72,7 +72,7 @@ public class HomeListenerService extends WearableListenerService {
if (event.getType() == DataEvent.TYPE_DELETED) {
deleteDataItem(event.getDataItem());
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
- UpdateNotificationForDataItem(event.getDataItem());
+ updateNotificationForDataItem(event.getDataItem());
}
}
}
@@ -89,7 +89,7 @@ public class HomeListenerService extends WearableListenerService {
/**
* Posts a local notification to show calendar card.
*/
- private void UpdateNotificationForDataItem(DataItem dataItem) {
+ private void updateNotificationForDataItem(DataItem dataItem) {
DataMapItem mapDataItem = DataMapItem.fromDataItem(dataItem);
DataMap data = mapDataItem.getDataMap();
diff --git a/samples/browseable/AgendaData/_index.jd b/samples/browseable/AgendaData/_index.jd
index 9ae7d1007..4a94bcb30 100644
--- a/samples/browseable/AgendaData/_index.jd
+++ b/samples/browseable/AgendaData/_index.jd
@@ -6,9 +6,9 @@ sample.group=Wearable
Syncs calendar events to your wearable at the press of a button, using the Wearable
- DataApi to transmit data such as event time, description, and background image. The DataItems can be
- deleted individually via an action on the event notifications, or all at once via a button on the
- companion. When deleted using the notification action, a ConfirmationActivity is used to indicate
- success or failure.
+ DataApi to transmit data such as event time, description, and background image. The
+ DataItems can be deleted individually via an action on the event notifications, or all
+ at once via a button on the companion. When deleted using the notification action, a
+ ConfirmationActivity is used to indicate success or failure.
diff --git a/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_app_restriction_enforcer.xml b/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_app_restriction_enforcer.xml
index 011819169..b68398974 100644
--- a/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_app_restriction_enforcer.xml
+++ b/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_app_restriction_enforcer.xml
@@ -119,6 +119,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_item_add.xml b/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_item_add.xml
new file mode 100644
index 000000000..f60bb15a0
--- /dev/null
+++ b/samples/browseable/AppRestrictionEnforcer/res/layout/fragment_item_add.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/AppRestrictionEnforcer/res/layout/item.xml b/samples/browseable/AppRestrictionEnforcer/res/layout/item.xml
new file mode 100644
index 000000000..66e6b3dd6
--- /dev/null
+++ b/samples/browseable/AppRestrictionEnforcer/res/layout/item.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/AppRestrictionEnforcer/res/values-v11/template-styles.xml b/samples/browseable/AppRestrictionEnforcer/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/AppRestrictionEnforcer/res/values-v11/template-styles.xml
+++ b/samples/browseable/AppRestrictionEnforcer/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/AppRestrictionEnforcer/res/values-v21/base-template-styles.xml b/samples/browseable/AppRestrictionEnforcer/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/AppRestrictionEnforcer/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/AppRestrictionEnforcer/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/AppRestrictionEnforcer/res/values/strings.xml b/samples/browseable/AppRestrictionEnforcer/res/values/strings.xml
index e35daee3a..ead415275 100644
--- a/samples/browseable/AppRestrictionEnforcer/res/values/strings.xml
+++ b/samples/browseable/AppRestrictionEnforcer/res/values/strings.xml
@@ -29,4 +29,14 @@
Number: Rank: Approvals:
+ Profile:
+ Name
+ Age
+ Items:
+ Add
+ Key
+ Value
+ Remove
+ %1$s: %2$s
+ Add a new item
diff --git a/samples/browseable/AppRestrictionEnforcer/res/values/template-styles.xml b/samples/browseable/AppRestrictionEnforcer/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/AppRestrictionEnforcer/res/values/template-styles.xml
+++ b/samples/browseable/AppRestrictionEnforcer/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/AppRestrictionEnforcerFragment.java b/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/AppRestrictionEnforcerFragment.java
index 8b0620fb8..361c4ac33 100644
--- a/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/AppRestrictionEnforcerFragment.java
+++ b/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/AppRestrictionEnforcerFragment.java
@@ -22,7 +22,10 @@ import android.content.Context;
import android.content.RestrictionEntry;
import android.content.RestrictionsManager;
import android.content.SharedPreferences;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.Editable;
@@ -33,23 +36,28 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
+import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.Switch;
+import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* This fragment provides UI and functionality to set restrictions on the AppRestrictionSchema
* sample.
*/
public class AppRestrictionEnforcerFragment extends Fragment implements
- CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
+ CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener,
+ View.OnClickListener, ItemAddFragment.OnItemAddedListener {
/**
* Key for {@link SharedPreferences}
@@ -81,7 +89,24 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
*/
private static final String RESTRICTION_KEY_APPROVALS = "approvals";
+ /**
+ * Key for the bundle restriction in AppRestrictionSchema.
+ */
+ private static final String RESTRICTION_KEY_PROFILE = "profile";
+ private static final String RESTRICTION_KEY_PROFILE_NAME = "name";
+ private static final String RESTRICTION_KEY_PROFILE_AGE = "age";
+
+ /**
+ * Key for the bundle array restriction in AppRestrictionSchema.
+ */
+ private static final String RESTRICTION_KEY_ITEMS = "items";
+ private static final String RESTRICTION_KEY_ITEM_KEY = "key";
+ private static final String RESTRICTION_KEY_ITEM_VALUE = "value";
+
private static final String DELIMETER = ",";
+ private static final String SEPARATOR = ":";
+
+ private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
/**
* Current status of the restrictions.
@@ -94,6 +119,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
private EditText mEditNumber;
private Spinner mSpinnerRank;
private LinearLayout mLayoutApprovals;
+ private EditText mEditProfileName;
+ private EditText mEditProfileAge;
+ private LinearLayout mLayoutItems;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -109,6 +137,19 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
mEditNumber = (EditText) view.findViewById(R.id.number);
mSpinnerRank = (Spinner) view.findViewById(R.id.rank);
mLayoutApprovals = (LinearLayout) view.findViewById(R.id.approvals);
+ mEditProfileName = (EditText) view.findViewById(R.id.profile_name);
+ mEditProfileAge = (EditText) view.findViewById(R.id.profile_age);
+ mLayoutItems = (LinearLayout) view.findViewById(R.id.items);
+ view.findViewById(R.id.item_add).setOnClickListener(this);
+ View bundleLayout = view.findViewById(R.id.bundle_layout);
+ View bundleArrayLayout = view.findViewById(R.id.bundle_array_layout);
+ if (BUNDLE_SUPPORTED) {
+ bundleLayout.setVisibility(View.VISIBLE);
+ bundleArrayLayout.setVisibility(View.VISIBLE);
+ } else {
+ bundleLayout.setVisibility(View.GONE);
+ bundleArrayLayout.setVisibility(View.GONE);
+ }
}
@Override
@@ -156,6 +197,21 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
}
};
+ private TextWatcher mWatcherProfile = new EasyTextWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ try {
+ String name = mEditProfileName.getText().toString();
+ String ageString = mEditProfileAge.getText().toString();
+ if (!TextUtils.isEmpty(ageString)) {
+ saveProfile(getActivity(), name, Integer.parseInt(ageString));
+ }
+ } catch (NumberFormatException e) {
+ Toast.makeText(getActivity(), "Not an integer!", Toast.LENGTH_SHORT).show();
+ }
+ }
+ };
+
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
switch (parent.getId()) {
@@ -171,9 +227,42 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
// Nothing to do
}
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.item_add:
+ new ItemAddFragment().show(getChildFragmentManager(), "dialog");
+ break;
+ case R.id.item_remove:
+ String key = (String) v.getTag();
+ removeItem(key);
+ mLayoutItems.removeView((View) v.getParent());
+ break;
+ }
+ }
+
+ @Override
+ public void onItemAdded(String key, String value) {
+ key = TextUtils.replace(key,
+ new String[]{DELIMETER, SEPARATOR}, new String[]{"", ""}).toString();
+ value = TextUtils.replace(value,
+ new String[]{DELIMETER, SEPARATOR}, new String[]{"", ""}).toString();
+ Parcelable[] parcelables = mCurrentRestrictions.getParcelableArray(RESTRICTION_KEY_ITEMS);
+ Map items = new HashMap<>();
+ if (parcelables != null) {
+ for (Parcelable parcelable : parcelables) {
+ Bundle bundle = (Bundle) parcelable;
+ items.put(bundle.getString(RESTRICTION_KEY_ITEM_KEY),
+ bundle.getString(RESTRICTION_KEY_ITEM_VALUE));
+ }
+ }
+ items.put(key, value);
+ insertItemRow(LayoutInflater.from(getActivity()), key, value);
+ saveItems(getActivity(), items);
+ }
+
/**
- * Loads the restrictions for the AppRestrictionSchema sample. In this implementation, we just
- * read the default value for the "can_say_hello" restriction.
+ * Loads the restrictions for the AppRestrictionSchema sample.
*
* @param activity The activity
*/
@@ -203,6 +292,28 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
TextUtils.join(DELIMETER,
restriction.getAllSelectedStrings())),
DELIMETER));
+ } else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_PROFILE.equals(key)) {
+ String name = null;
+ int age = 0;
+ for (RestrictionEntry entry : restriction.getRestrictions()) {
+ String profileKey = entry.getKey();
+ if (RESTRICTION_KEY_PROFILE_NAME.equals(profileKey)) {
+ name = entry.getSelectedString();
+ } else if (RESTRICTION_KEY_PROFILE_AGE.equals(profileKey)) {
+ age = entry.getIntValue();
+ }
+ }
+ name = prefs.getString(RESTRICTION_KEY_PROFILE_NAME, name);
+ age = prefs.getInt(RESTRICTION_KEY_PROFILE_AGE, age);
+ updateProfile(name, age);
+ } else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_ITEMS.equals(key)) {
+ String itemsString = prefs.getString(RESTRICTION_KEY_ITEMS, "");
+ HashMap items = new HashMap<>();
+ for (String itemString : TextUtils.split(itemsString, DELIMETER)) {
+ String[] strings = itemString.split(SEPARATOR, 2);
+ items.put(strings[0], strings[1]);
+ }
+ updateItems(activity, items);
}
}
}
@@ -251,6 +362,72 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
}
}
+ private void updateProfile(String name, int age) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
+ Bundle profile = new Bundle();
+ profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
+ profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
+ mCurrentRestrictions.putBundle(RESTRICTION_KEY_PROFILE, profile);
+ mEditProfileName.removeTextChangedListener(mWatcherProfile);
+ mEditProfileName.setText(name);
+ mEditProfileName.addTextChangedListener(mWatcherProfile);
+ mEditProfileAge.removeTextChangedListener(mWatcherProfile);
+ mEditProfileAge.setText(String.valueOf(age));
+ mEditProfileAge.addTextChangedListener((mWatcherProfile));
+ }
+
+ private void updateItems(Context context, Map items) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
+ mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
+ LayoutInflater inflater = LayoutInflater.from(context);
+ mLayoutItems.removeAllViews();
+ for (String key : items.keySet()) {
+ insertItemRow(inflater, key, items.get(key));
+ }
+ }
+
+ private void insertItemRow(LayoutInflater inflater, String key, String value) {
+ View view = inflater.inflate(R.layout.item, mLayoutItems, false);
+ TextView textView = (TextView) view.findViewById(R.id.item_text);
+ textView.setText(getString(R.string.item, key, value));
+ Button remove = (Button) view.findViewById(R.id.item_remove);
+ remove.setTag(key);
+ remove.setOnClickListener(this);
+ mLayoutItems.addView(view);
+ }
+
+ @NonNull
+ private Bundle[] convertToBundles(Map items) {
+ Bundle[] bundles = new Bundle[items.size()];
+ int i = 0;
+ for (String key : items.keySet()) {
+ Bundle bundle = new Bundle();
+ bundle.putString(RESTRICTION_KEY_ITEM_KEY, key);
+ bundle.putString(RESTRICTION_KEY_ITEM_VALUE, items.get(key));
+ bundles[i++] = bundle;
+ }
+ return bundles;
+ }
+
+ private void removeItem(String key) {
+ Parcelable[] parcelables = mCurrentRestrictions.getParcelableArray(RESTRICTION_KEY_ITEMS);
+ if (parcelables != null) {
+ Map items = new HashMap<>();
+ for (Parcelable parcelable : parcelables) {
+ Bundle bundle = (Bundle) parcelable;
+ if (!key.equals(bundle.getString(RESTRICTION_KEY_ITEM_KEY))) {
+ items.put(bundle.getString(RESTRICTION_KEY_ITEM_KEY),
+ bundle.getString(RESTRICTION_KEY_ITEM_VALUE));
+ }
+ }
+ saveItems(getActivity(), items);
+ }
+ }
+
/**
* Saves the value for the "cay_say_hello" restriction of AppRestrictionSchema.
*
@@ -333,6 +510,57 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
TextUtils.join(DELIMETER, approvals)).apply();
}
+ /**
+ * Saves the value for the "profile" restriction of AppRestrictionSchema.
+ *
+ * @param activity The activity
+ * @param name The value to be set for the "name" field.
+ * @param age The value to be set for the "age" field.
+ */
+ private void saveProfile(Activity activity, String name, int age) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
+ Bundle profile = new Bundle();
+ profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
+ profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
+ mCurrentRestrictions.putBundle(RESTRICTION_KEY_PROFILE, profile);
+ saveRestrictions(activity);
+ editPreferences(activity).putString(RESTRICTION_KEY_PROFILE_NAME, name).apply();
+ }
+
+ /**
+ * Saves the value for the "items" restriction of AppRestrictionSchema.
+ *
+ * @param activity The activity.
+ * @param items The values.
+ */
+ private void saveItems(Activity activity, Map items) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
+ mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
+ saveRestrictions(activity);
+ StringBuilder builder = new StringBuilder();
+ boolean first = true;
+ for (String key : items.keySet()) {
+ if (first) {
+ first = false;
+ } else {
+ builder.append(DELIMETER);
+ }
+ builder.append(key);
+ builder.append(SEPARATOR);
+ builder.append(items.get(key));
+ }
+ editPreferences(activity).putString(RESTRICTION_KEY_ITEMS, builder.toString()).apply();
+ }
+
+ /**
+ * Saves all the restrictions.
+ *
+ * @param activity The activity.
+ */
private void saveRestrictions(Activity activity) {
DevicePolicyManager devicePolicyManager
= (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
diff --git a/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/ItemAddFragment.java b/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/ItemAddFragment.java
new file mode 100644
index 000000000..cda2726bd
--- /dev/null
+++ b/samples/browseable/AppRestrictionEnforcer/src/com.example.android.apprestrictionenforcer/ItemAddFragment.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.apprestrictionenforcer;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.Toast;
+
+/**
+ * Provides a dialog to create a new restriction item for the sample bundle array.
+ */
+public class ItemAddFragment extends DialogFragment implements View.OnClickListener {
+
+ public interface OnItemAddedListener {
+ void onItemAdded(String key, String value);
+ }
+
+ private OnItemAddedListener mListener;
+ private EditText mEditKey;
+ private EditText mEditValue;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ Fragment parentFragment = getParentFragment();
+ mListener = (OnItemAddedListener) (parentFragment == null ? activity : parentFragment);
+ }
+
+ @Override
+ public void onDetach() {
+ mListener = null;
+ super.onDetach();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getDialog().setTitle(R.string.add_item);
+ return inflater.inflate(R.layout.fragment_item_add, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ mEditKey = (EditText) view.findViewById(R.id.key);
+ mEditValue = (EditText) view.findViewById(R.id.value);
+ view.findViewById(R.id.ok).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.ok:
+ if (addItem()) {
+ dismiss();
+ }
+ break;
+ }
+ }
+
+ private boolean addItem() {
+ String key = mEditKey.getText().toString();
+ if (TextUtils.isEmpty(key)) {
+ Toast.makeText(getActivity(), "Input the key.", Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ String value = mEditValue.getText().toString();
+ if (TextUtils.isEmpty(value)) {
+ Toast.makeText(getActivity(), "Input the value.", Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ if (mListener != null) {
+ mListener.onItemAdded(key, value);
+ }
+ return true;
+ }
+
+}
diff --git a/samples/browseable/AppRestrictionSchema/res/layout/fragment_app_restriction_schema.xml b/samples/browseable/AppRestrictionSchema/res/layout/fragment_app_restriction_schema.xml
index 18ca0a4d5..02d83e615 100644
--- a/samples/browseable/AppRestrictionSchema/res/layout/fragment_app_restriction_schema.xml
+++ b/samples/browseable/AppRestrictionSchema/res/layout/fragment_app_restriction_schema.xml
@@ -59,7 +59,7 @@ limitations under the License.
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="@string/your_rank"/>
-
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/AppRestrictionSchema/res/values-v11/template-styles.xml b/samples/browseable/AppRestrictionSchema/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/AppRestrictionSchema/res/values-v11/template-styles.xml
+++ b/samples/browseable/AppRestrictionSchema/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/AppRestrictionSchema/res/values-v21/base-template-styles.xml b/samples/browseable/AppRestrictionSchema/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/AppRestrictionSchema/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/AppRestrictionSchema/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/AppRestrictionSchema/res/values/restriction_values.xml b/samples/browseable/AppRestrictionSchema/res/values/restriction_values.xml
index 558d097ff..53be74669 100644
--- a/samples/browseable/AppRestrictionSchema/res/values/restriction_values.xml
+++ b/samples/browseable/AppRestrictionSchema/res/values/restriction_values.xml
@@ -74,4 +74,23 @@ limitations under the License.
This restriction is hidden and will not be shown to the administrator.(Hidden restriction must have some default value)
+
+ Sample profile
+ Profile
+
+ John
+ The name of this person
+ Name
+
+ 25
+ The age of this person
+ Age
+
+
+ Sample items
+ Items
+ Item
+ Key
+ Value
+
diff --git a/samples/browseable/AppRestrictionSchema/res/values/strings.xml b/samples/browseable/AppRestrictionSchema/res/values/strings.xml
index 6dce123f5..1ec68d54c 100644
--- a/samples/browseable/AppRestrictionSchema/res/values/strings.xml
+++ b/samples/browseable/AppRestrictionSchema/res/values/strings.xml
@@ -25,5 +25,7 @@ limitations under the License.
Your rank: %sApprovals you have: %snone
+ Your profile: %1$s (%2$d)
+ Your items: %s
diff --git a/samples/browseable/AppRestrictionSchema/res/values/template-styles.xml b/samples/browseable/AppRestrictionSchema/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/AppRestrictionSchema/res/values/template-styles.xml
+++ b/samples/browseable/AppRestrictionSchema/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/AppRestrictionSchema/res/xml/app_restrictions.xml b/samples/browseable/AppRestrictionSchema/res/xml/app_restrictions.xml
index 9e47f458f..1e2ea457a 100644
--- a/samples/browseable/AppRestrictionSchema/res/xml/app_restrictions.xml
+++ b/samples/browseable/AppRestrictionSchema/res/xml/app_restrictions.xml
@@ -1,5 +1,4 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/AppRestrictionSchema/src/com.example.android.apprestrictionschema/AppRestrictionSchemaFragment.java b/samples/browseable/AppRestrictionSchema/src/com.example.android.apprestrictionschema/AppRestrictionSchemaFragment.java
index 7b8dba831..bbb1ef86e 100644
--- a/samples/browseable/AppRestrictionSchema/src/com.example.android.apprestrictionschema/AppRestrictionSchemaFragment.java
+++ b/samples/browseable/AppRestrictionSchema/src/com.example.android.apprestrictionschema/AppRestrictionSchemaFragment.java
@@ -19,7 +19,9 @@ package com.example.android.apprestrictionschema;
import android.content.Context;
import android.content.RestrictionEntry;
import android.content.RestrictionsManager;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
@@ -49,6 +51,14 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
private static final String KEY_NUMBER = "number";
private static final String KEY_RANK = "rank";
private static final String KEY_APPROVALS = "approvals";
+ private static final String KEY_PROFILE = "profile";
+ private static final String KEY_PROFILE_NAME = "name";
+ private static final String KEY_PROFILE_AGE = "age";
+ private static final String KEY_ITEMS = "items";
+ private static final String KEY_ITEM_KEY = "key";
+ private static final String KEY_ITEM_VALUE = "value";
+
+ private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
// Message to show when the button is clicked (String restriction)
private String mMessage;
@@ -59,6 +69,8 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
private TextView mTextNumber;
private TextView mTextRank;
private TextView mTextApprovals;
+ private TextView mTextProfile;
+ private TextView mTextItems;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -73,7 +85,22 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
mTextNumber = (TextView) view.findViewById(R.id.your_number);
mTextRank = (TextView) view.findViewById(R.id.your_rank);
mTextApprovals = (TextView) view.findViewById(R.id.approvals_you_have);
+ View bundleSeparator = view.findViewById(R.id.bundle_separator);
+ mTextProfile = (TextView) view.findViewById(R.id.your_profile);
+ View bundleArraySeparator = view.findViewById(R.id.bundle_array_separator);
+ mTextItems = (TextView) view.findViewById(R.id.your_items);
mButtonSayHello.setOnClickListener(this);
+ if (BUNDLE_SUPPORTED) {
+ bundleSeparator.setVisibility(View.VISIBLE);
+ mTextProfile.setVisibility(View.VISIBLE);
+ bundleArraySeparator.setVisibility(View.VISIBLE);
+ mTextItems.setVisibility(View.VISIBLE);
+ } else {
+ bundleSeparator.setVisibility(View.GONE);
+ mTextProfile.setVisibility(View.GONE);
+ bundleArraySeparator.setVisibility(View.GONE);
+ mTextItems.setVisibility(View.GONE);
+ }
}
@Override
@@ -86,7 +113,8 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
RestrictionsManager manager =
(RestrictionsManager) getActivity().getSystemService(Context.RESTRICTIONS_SERVICE);
Bundle restrictions = manager.getApplicationRestrictions();
- List entries = manager.getManifestRestrictions(getActivity().getApplicationContext().getPackageName());
+ List entries = manager.getManifestRestrictions(
+ getActivity().getApplicationContext().getPackageName());
for (RestrictionEntry entry : entries) {
String key = entry.getKey();
Log.d(TAG, "key: " + key);
@@ -100,6 +128,10 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
updateRank(entry, restrictions);
} else if (key.equals(KEY_APPROVALS)) {
updateApprovals(entry, restrictions);
+ } else if (key.equals(KEY_PROFILE)) {
+ updateProfile(entry, restrictions);
+ } else if (key.equals(KEY_ITEMS)) {
+ updateItems(entry, restrictions);
}
}
}
@@ -161,6 +193,67 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
mTextApprovals.setText(getString(R.string.approvals_you_have, text));
}
+ private void updateProfile(RestrictionEntry entry, Bundle restrictions) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
+ String name = null;
+ int age = 0;
+ if (restrictions == null || !restrictions.containsKey(KEY_PROFILE)) {
+ RestrictionEntry[] entries = entry.getRestrictions();
+ for (RestrictionEntry profileEntry : entries) {
+ String key = profileEntry.getKey();
+ if (key.equals(KEY_PROFILE_NAME)) {
+ name = profileEntry.getSelectedString();
+ } else if (key.equals(KEY_PROFILE_AGE)) {
+ age = profileEntry.getIntValue();
+ }
+ }
+ } else {
+ Bundle profile = restrictions.getBundle(KEY_PROFILE);
+ if (profile != null) {
+ name = profile.getString(KEY_PROFILE_NAME);
+ age = profile.getInt(KEY_PROFILE_AGE);
+ }
+ }
+ mTextProfile.setText(getString(R.string.your_profile, name, age));
+ }
+
+ private void updateItems(RestrictionEntry entry, Bundle restrictions) {
+ if (!BUNDLE_SUPPORTED) {
+ return;
+ }
+ StringBuilder builder = new StringBuilder();
+ if (restrictions != null) {
+ Parcelable[] parcelables = restrictions.getParcelableArray(KEY_ITEMS);
+ if (parcelables != null && parcelables.length > 0) {
+ Bundle[] items = new Bundle[parcelables.length];
+ for (int i = 0; i < parcelables.length; i++) {
+ items[i] = (Bundle) parcelables[i];
+ }
+ boolean first = true;
+ for (Bundle item : items) {
+ if (!item.containsKey(KEY_ITEM_KEY) || !item.containsKey(KEY_ITEM_VALUE)) {
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ builder.append(", ");
+ }
+ builder.append(item.getString(KEY_ITEM_KEY));
+ builder.append(":");
+ builder.append(item.getString(KEY_ITEM_VALUE));
+ }
+ } else {
+ builder.append(getString(R.string.none));
+ }
+ } else {
+ builder.append(getString(R.string.none));
+ }
+ mTextItems.setText(getString(R.string.your_items, builder));
+ }
+
@Override
public void onClick(View view) {
switch (view.getId()) {
diff --git a/samples/browseable/AppRestrictions/res/values-v11/template-styles.xml b/samples/browseable/AppRestrictions/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/AppRestrictions/res/values-v11/template-styles.xml
+++ b/samples/browseable/AppRestrictions/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/AppRestrictions/res/values-v21/base-template-styles.xml b/samples/browseable/AppRestrictions/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/AppRestrictions/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/AppRestrictions/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/AppRestrictions/res/values/template-styles.xml b/samples/browseable/AppRestrictions/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/AppRestrictions/res/values/template-styles.xml
+++ b/samples/browseable/AppRestrictions/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/AppUsageStatistics/res/layout/fragment_app_usage_statistics.xml b/samples/browseable/AppUsageStatistics/res/layout/fragment_app_usage_statistics.xml
index 1d567b76e..297bf1e37 100644
--- a/samples/browseable/AppUsageStatistics/res/layout/fragment_app_usage_statistics.xml
+++ b/samples/browseable/AppUsageStatistics/res/layout/fragment_app_usage_statistics.xml
@@ -16,11 +16,13 @@
-->
+ android:padding="@dimen/margin_medium"
+ >
+ android:layout_height="match_parent"
+ app:layoutManager="LinearLayoutManager"
+ />
diff --git a/samples/browseable/AppUsageStatistics/res/values-v11/template-styles.xml b/samples/browseable/AppUsageStatistics/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/AppUsageStatistics/res/values-v11/template-styles.xml
+++ b/samples/browseable/AppUsageStatistics/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/AppUsageStatistics/res/values-v21/base-template-styles.xml b/samples/browseable/AppUsageStatistics/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/AppUsageStatistics/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/AppUsageStatistics/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/AppUsageStatistics/res/values/template-styles.xml b/samples/browseable/AppUsageStatistics/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/AppUsageStatistics/res/values/template-styles.xml
+++ b/samples/browseable/AppUsageStatistics/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/AppUsageStatistics/src/com.example.android.appusagestatistics/AppUsageStatisticsFragment.java b/samples/browseable/AppUsageStatistics/src/com.example.android.appusagestatistics/AppUsageStatisticsFragment.java
index 9f54d02cf..9a3fd5e9d 100644
--- a/samples/browseable/AppUsageStatistics/src/com.example.android.appusagestatistics/AppUsageStatisticsFragment.java
+++ b/samples/browseable/AppUsageStatistics/src/com.example.android.appusagestatistics/AppUsageStatisticsFragment.java
@@ -24,7 +24,6 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.Fragment;
-import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
@@ -91,10 +90,9 @@ public class AppUsageStatisticsFragment extends Fragment {
public void onViewCreated(View rootView, Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
- mLayoutManager = new LinearLayoutManager(getActivity());
mUsageListAdapter = new UsageListAdapter();
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_app_usage);
- mRecyclerView.setLayoutManager(mLayoutManager);
+ mLayoutManager = mRecyclerView.getLayoutManager();
mRecyclerView.scrollToPosition(0);
mRecyclerView.setAdapter(mUsageListAdapter);
mOpenUsageSettingButton = (Button) rootView.findViewById(R.id.button_open_usage_setting);
@@ -197,7 +195,7 @@ public class AppUsageStatisticsFragment extends Fragment {
@Override
public int compare(UsageStats left, UsageStats right) {
- return (int) (right.getLastTimeUsed() - left.getLastTimeUsed());
+ return Long.compare(right.getLastTimeUsed(), left.getLastTimeUsed());
}
}
diff --git a/samples/browseable/AsymmetricFingerprintDialog/AndroidManifest.xml b/samples/browseable/AsymmetricFingerprintDialog/AndroidManifest.xml
new file mode 100644
index 000000000..d1cf9f870
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/AndroidManifest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/_index.jd b/samples/browseable/AsymmetricFingerprintDialog/_index.jd
new file mode 100644
index 000000000..4e5987e7e
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/_index.jd
@@ -0,0 +1,11 @@
+
+page.tags="AsymmetricFingerprintDialog"
+sample.group=Security
+@jd:body
+
+
+
+This sample demonstrates how you can use registered fingerprints to authenticate the user
+before proceeding some actions such as purchasing an item. This version uses asymmetric keys.
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable-hdpi/ic_fp_40px.png b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-hdpi/ic_fp_40px.png
new file mode 100644
index 000000000..48ebd8ad7
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-hdpi/ic_fp_40px.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable-hdpi/tile.9.png b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-hdpi/tile.9.png
new file mode 100644
index 000000000..135862883
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-hdpi/tile.9.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable-mdpi/ic_fp_40px.png b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-mdpi/ic_fp_40px.png
new file mode 100644
index 000000000..122f44257
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-mdpi/ic_fp_40px.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable-nodpi/android_robot.png b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-nodpi/android_robot.png
new file mode 100644
index 000000000..40bf934bb
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-nodpi/android_robot.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable-xhdpi/ic_fp_40px.png b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-xhdpi/ic_fp_40px.png
new file mode 100644
index 000000000..e1c9590bb
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-xhdpi/ic_fp_40px.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable-xxhdpi/ic_fp_40px.png b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-xxhdpi/ic_fp_40px.png
new file mode 100644
index 000000000..f7e87240e
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-xxhdpi/ic_fp_40px.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable-xxxhdpi/ic_fp_40px.png b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-xxxhdpi/ic_fp_40px.png
new file mode 100644
index 000000000..0fb854524
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/drawable-xxxhdpi/ic_fp_40px.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable/card.xml b/samples/browseable/AsymmetricFingerprintDialog/res/drawable/card.xml
new file mode 100644
index 000000000..691a4c556
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/drawable/card.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable/ic_fingerprint_error.xml b/samples/browseable/AsymmetricFingerprintDialog/res/drawable/ic_fingerprint_error.xml
new file mode 100644
index 000000000..be46116da
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/drawable/ic_fingerprint_error.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/drawable/ic_fingerprint_success.xml b/samples/browseable/AsymmetricFingerprintDialog/res/drawable/ic_fingerprint_success.xml
new file mode 100644
index 000000000..261f3e7fb
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/drawable/ic_fingerprint_success.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/layout/activity_main.xml b/samples/browseable/AsymmetricFingerprintDialog/res/layout/activity_main.xml
new file mode 100644
index 000000000..8f30557b9
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/layout/activity_main.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/layout/fingerprint_dialog_backup.xml b/samples/browseable/AsymmetricFingerprintDialog/res/layout/fingerprint_dialog_backup.xml
new file mode 100644
index 000000000..2be05b11b
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/layout/fingerprint_dialog_backup.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/layout/fingerprint_dialog_container.xml b/samples/browseable/AsymmetricFingerprintDialog/res/layout/fingerprint_dialog_container.xml
new file mode 100644
index 000000000..08bb1bbb9
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/layout/fingerprint_dialog_container.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/layout/fingerprint_dialog_content.xml b/samples/browseable/AsymmetricFingerprintDialog/res/layout/fingerprint_dialog_content.xml
new file mode 100644
index 000000000..3929ebae6
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/layout/fingerprint_dialog_content.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/menu/menu_main.xml b/samples/browseable/AsymmetricFingerprintDialog/res/menu/menu_main.xml
new file mode 100644
index 000000000..73f5e89a0
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/menu/menu_main.xml
@@ -0,0 +1,21 @@
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..b82d7af35
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..b0d26baed
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..135858d17
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..02960da8b
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-xxxhdpi/ic_launcher.png b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d4254591e
Binary files /dev/null and b/samples/browseable/AsymmetricFingerprintDialog/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values-sw600dp/template-dimens.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values-sw600dp/template-dimens.xml
new file mode 100644
index 000000000..22074a2bd
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values-sw600dp/template-dimens.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ @dimen/margin_huge
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values-sw600dp/template-styles.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values-sw600dp/template-styles.xml
new file mode 100644
index 000000000..03d197418
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values-sw600dp/template-styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values-v11/template-styles.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values-v11/template-styles.xml
new file mode 100644
index 000000000..8c1ea66f2
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values-v11/template-styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values-v21/base-colors.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values-v21/base-colors.xml
new file mode 100644
index 000000000..8b6ec3f85
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values-v21/base-colors.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values-v21/base-template-styles.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values-v21/base-template-styles.xml
new file mode 100644
index 000000000..c778e4f98
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values-v21/base-template-styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values/base-strings.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values/base-strings.xml
new file mode 100644
index 000000000..72dabc1d2
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values/base-strings.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ AsymmetricFingerprintDialog
+
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values/colors.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values/colors.xml
new file mode 100644
index 000000000..a24f3c8fc
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values/colors.xml
@@ -0,0 +1,21 @@
+
+
+
+ #f4511e
+ #42000000
+ #009688
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values/strings.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values/strings.xml
new file mode 100644
index 000000000..f44c06d68
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values/strings.xml
@@ -0,0 +1,39 @@
+
+
+
+ Settings
+ Cancel
+ Use password
+ Sign in
+ Ok
+ Password
+ Confirm fingerprint to continue
+ Touch sensor
+ Enter your store password to continue
+ Purchase
+ Fingerprint not recognized. Try again
+ Fingerprint recognized
+ White Mesh Pluto Backpack
+ $62.68
+ Mesh backpack in white. Black textile trim throughout.
+ Purchase successful
+ Purchase failed
+ A new fingerprint was added to this device, so your password is required.
+ Use fingerprint in the future
+ Use fingerprint to authenticate
+ use_fingerprint_to_authenticate_key
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values/template-dimens.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values/template-dimens.xml
new file mode 100644
index 000000000..39e710b5c
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values/template-dimens.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ 4dp
+ 8dp
+ 16dp
+ 32dp
+ 64dp
+
+
+
+ @dimen/margin_medium
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/values/template-styles.xml b/samples/browseable/AsymmetricFingerprintDialog/res/values/template-styles.xml
new file mode 100644
index 000000000..6e7d593dd
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/values/template-styles.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/res/xml/preferences.xml b/samples/browseable/AsymmetricFingerprintDialog/res/xml/preferences.xml
new file mode 100644
index 000000000..761391d5a
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/res/xml/preferences.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/FingerprintAuthenticationDialogFragment.java b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/FingerprintAuthenticationDialogFragment.java
new file mode 100644
index 000000000..a56556f13
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/FingerprintAuthenticationDialogFragment.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.asymmetricfingerprintdialog;
+
+import com.example.android.asymmetricfingerprintdialog.server.StoreBackend;
+import com.example.android.asymmetricfingerprintdialog.server.Transaction;
+
+import android.app.Activity;
+import android.app.DialogFragment;
+import android.content.SharedPreferences;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+import javax.inject.Inject;
+
+/**
+ * A dialog which uses fingerprint APIs to authenticate the user, and falls back to password
+ * authentication if fingerprint is not available.
+ */
+public class FingerprintAuthenticationDialogFragment extends DialogFragment
+ implements TextView.OnEditorActionListener, FingerprintUiHelper.Callback {
+
+ private Button mCancelButton;
+ private Button mSecondDialogButton;
+ private View mFingerprintContent;
+ private View mBackupContent;
+ private EditText mPassword;
+ private CheckBox mUseFingerprintFutureCheckBox;
+ private TextView mPasswordDescriptionTextView;
+ private TextView mNewFingerprintEnrolledTextView;
+
+ private Stage mStage = Stage.FINGERPRINT;
+
+ private FingerprintManager.CryptoObject mCryptoObject;
+ private FingerprintUiHelper mFingerprintUiHelper;
+ private MainActivity mActivity;
+
+ @Inject FingerprintUiHelper.FingerprintUiHelperBuilder mFingerprintUiHelperBuilder;
+ @Inject InputMethodManager mInputMethodManager;
+ @Inject SharedPreferences mSharedPreferences;
+ @Inject StoreBackend mStoreBackend;
+
+ @Inject
+ public FingerprintAuthenticationDialogFragment() {}
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Do not create a new Fragment when the Activity is re-created such as orientation changes.
+ setRetainInstance(true);
+ setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog);
+
+ // We register a new user account here. Real apps should do this with proper UIs.
+ enroll();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getDialog().setTitle(getString(R.string.sign_in));
+ View v = inflater.inflate(R.layout.fingerprint_dialog_container, container, false);
+ mCancelButton = (Button) v.findViewById(R.id.cancel_button);
+ mCancelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dismiss();
+ }
+ });
+
+ mSecondDialogButton = (Button) v.findViewById(R.id.second_dialog_button);
+ mSecondDialogButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mStage == Stage.FINGERPRINT) {
+ goToBackup();
+ } else {
+ verifyPassword();
+ }
+ }
+ });
+ mFingerprintContent = v.findViewById(R.id.fingerprint_container);
+ mBackupContent = v.findViewById(R.id.backup_container);
+ mPassword = (EditText) v.findViewById(R.id.password);
+ mPassword.setOnEditorActionListener(this);
+ mPasswordDescriptionTextView = (TextView) v.findViewById(R.id.password_description);
+ mUseFingerprintFutureCheckBox = (CheckBox)
+ v.findViewById(R.id.use_fingerprint_in_future_check);
+ mNewFingerprintEnrolledTextView = (TextView)
+ v.findViewById(R.id.new_fingerprint_enrolled_description);
+ mFingerprintUiHelper = mFingerprintUiHelperBuilder.build(
+ (ImageView) v.findViewById(R.id.fingerprint_icon),
+ (TextView) v.findViewById(R.id.fingerprint_status), this);
+ updateStage();
+
+ // If fingerprint authentication is not available, switch immediately to the backup
+ // (password) screen.
+ if (!mFingerprintUiHelper.isFingerprintAuthAvailable()) {
+ goToBackup();
+ }
+ return v;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mStage == Stage.FINGERPRINT) {
+ mFingerprintUiHelper.startListening(mCryptoObject);
+ }
+ }
+
+ public void setStage(Stage stage) {
+ mStage = stage;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mFingerprintUiHelper.stopListening();
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mActivity = (MainActivity) activity;
+ }
+
+ /**
+ * Sets the crypto object to be passed in when authenticating with fingerprint.
+ */
+ public void setCryptoObject(FingerprintManager.CryptoObject cryptoObject) {
+ mCryptoObject = cryptoObject;
+ }
+
+ /**
+ * Switches to backup (password) screen. This either can happen when fingerprint is not
+ * available or the user chooses to use the password authentication method by pressing the
+ * button. This can also happen when the user had too many fingerprint attempts.
+ */
+ private void goToBackup() {
+ mStage = Stage.PASSWORD;
+ updateStage();
+ mPassword.requestFocus();
+
+ // Show the keyboard.
+ mPassword.postDelayed(mShowKeyboardRunnable, 500);
+
+ // Fingerprint is not used anymore. Stop listening for it.
+ mFingerprintUiHelper.stopListening();
+ }
+
+ /**
+ * Enrolls a user to the fake backend.
+ */
+ private void enroll() {
+ try {
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ PublicKey publicKey = keyStore.getCertificate(MainActivity.KEY_NAME).getPublicKey();
+ // Provide the public key to the backend. In most cases, the key needs to be transmitted
+ // to the backend over the network, for which Key.getEncoded provides a suitable wire
+ // format (X.509 DER-encoded). The backend can then create a PublicKey instance from the
+ // X.509 encoded form using KeyFactory.generatePublic. This conversion is also currently
+ // needed on API Level 23 (Android M) due to a platform bug which prevents the use of
+ // Android Keystore public keys when their private keys require user authentication.
+ // This conversion creates a new public key which is not backed by Android Keystore and
+ // thus is not affected by the bug.
+ KeyFactory factory = KeyFactory.getInstance(publicKey.getAlgorithm());
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey.getEncoded());
+ PublicKey verificationKey = factory.generatePublic(spec);
+ mStoreBackend.enroll("user", "password", verificationKey);
+ } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException |
+ IOException | InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Checks whether the current entered password is correct, and dismisses the the dialog and lets
+ * the activity know about the result.
+ */
+ private void verifyPassword() {
+ Transaction transaction = new Transaction("user", 1, new SecureRandom().nextLong());
+ if (!mStoreBackend.verify(transaction, mPassword.getText().toString())) {
+ return;
+ }
+ if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
+ mUseFingerprintFutureCheckBox.isChecked());
+ editor.apply();
+
+ if (mUseFingerprintFutureCheckBox.isChecked()) {
+ // Re-create the key so that fingerprints including new ones are validated.
+ mActivity.createKeyPair();
+ mStage = Stage.FINGERPRINT;
+ }
+ }
+ mPassword.setText("");
+ mActivity.onPurchased(null);
+ dismiss();
+ }
+
+ private final Runnable mShowKeyboardRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mInputMethodManager.showSoftInput(mPassword, 0);
+ }
+ };
+
+ private void updateStage() {
+ switch (mStage) {
+ case FINGERPRINT:
+ mCancelButton.setText(R.string.cancel);
+ mSecondDialogButton.setText(R.string.use_password);
+ mFingerprintContent.setVisibility(View.VISIBLE);
+ mBackupContent.setVisibility(View.GONE);
+ break;
+ case NEW_FINGERPRINT_ENROLLED:
+ // Intentional fall through
+ case PASSWORD:
+ mCancelButton.setText(R.string.cancel);
+ mSecondDialogButton.setText(R.string.ok);
+ mFingerprintContent.setVisibility(View.GONE);
+ mBackupContent.setVisibility(View.VISIBLE);
+ if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) {
+ mPasswordDescriptionTextView.setVisibility(View.GONE);
+ mNewFingerprintEnrolledTextView.setVisibility(View.VISIBLE);
+ mUseFingerprintFutureCheckBox.setVisibility(View.VISIBLE);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_GO) {
+ verifyPassword();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onAuthenticated() {
+ // Callback from FingerprintUiHelper. Let the activity know that authentication was
+ // successful.
+ mPassword.setText("");
+ Signature signature = mCryptoObject.getSignature();
+ // Include a client nonce in the transaction so that the nonce is also signed by the private
+ // key and the backend can verify that the same nonce can't be used to prevent replay
+ // attacks.
+ Transaction transaction = new Transaction("user", 1, new SecureRandom().nextLong());
+ try {
+ signature.update(transaction.toByteArray());
+ byte[] sigBytes = signature.sign();
+ if (mStoreBackend.verify(transaction, sigBytes)) {
+ mActivity.onPurchased(sigBytes);
+ dismiss();
+ } else {
+ mActivity.onPurchaseFailed();
+ dismiss();
+ }
+ } catch (SignatureException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void onError() {
+ goToBackup();
+ }
+
+ /**
+ * Enumeration to indicate which authentication method the user is trying to authenticate with.
+ */
+ public enum Stage {
+ FINGERPRINT,
+ NEW_FINGERPRINT_ENROLLED,
+ PASSWORD
+ }
+}
diff --git a/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/FingerprintModule.java b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/FingerprintModule.java
new file mode 100644
index 000000000..ae3acf801
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/FingerprintModule.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.asymmetricfingerprintdialog;
+
+import com.example.android.asymmetricfingerprintdialog.server.StoreBackend;
+import com.example.android.asymmetricfingerprintdialog.server.StoreBackendImpl;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.hardware.fingerprint.FingerprintManager;
+import android.preference.PreferenceManager;
+import android.security.keystore.KeyProperties;
+import android.view.inputmethod.InputMethodManager;
+
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Signature;
+
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Dagger module for Fingerprint APIs.
+ */
+@Module(
+ library = true,
+ injects = {MainActivity.class}
+)
+public class FingerprintModule {
+
+ private final Context mContext;
+
+ public FingerprintModule(Context context) {
+ mContext = context;
+ }
+
+ @Provides
+ public Context providesContext() {
+ return mContext;
+ }
+
+ @Provides
+ public FingerprintManager providesFingerprintManager(Context context) {
+ return context.getSystemService(FingerprintManager.class);
+ }
+
+ @Provides
+ public KeyguardManager providesKeyguardManager(Context context) {
+ return context.getSystemService(KeyguardManager.class);
+ }
+
+ @Provides
+ public KeyStore providesKeystore() {
+ try {
+ return KeyStore.getInstance("AndroidKeyStore");
+ } catch (KeyStoreException e) {
+ throw new RuntimeException("Failed to get an instance of KeyStore", e);
+ }
+ }
+
+ @Provides
+ public KeyPairGenerator providesKeyPairGenerator() {
+ try {
+ return KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+ throw new RuntimeException("Failed to get an instance of KeyPairGenerator", e);
+ }
+ }
+
+ @Provides
+ public Signature providesSignature(KeyStore keyStore) {
+ try {
+ return Signature.getInstance("SHA256withECDSA");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("Failed to get an instance of Signature", e);
+ }
+ }
+
+ @Provides
+ public InputMethodManager providesInputMethodManager(Context context) {
+ return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+
+ @Provides
+ public SharedPreferences providesSharedPreferences(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ @Provides
+ public StoreBackend providesStoreBackend() {
+ return new StoreBackendImpl();
+ }
+}
diff --git a/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/FingerprintUiHelper.java b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/FingerprintUiHelper.java
new file mode 100644
index 000000000..f65481161
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/FingerprintUiHelper.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.asymmetricfingerprintdialog;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.CancellationSignal;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import javax.inject.Inject;
+
+/**
+ * Small helper class to manage text/icon around fingerprint authentication UI.
+ */
+public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
+
+ @VisibleForTesting static final long ERROR_TIMEOUT_MILLIS = 1600;
+ @VisibleForTesting static final long SUCCESS_DELAY_MILLIS = 1300;
+
+ private final FingerprintManager mFingerprintManager;
+ private final ImageView mIcon;
+ private final TextView mErrorTextView;
+ private final Callback mCallback;
+ private CancellationSignal mCancellationSignal;
+
+ @VisibleForTesting boolean mSelfCancelled;
+
+ /**
+ * Builder class for {@link FingerprintUiHelper} in which injected fields from Dagger
+ * holds its fields and takes other arguments in the {@link #build} method.
+ */
+ public static class FingerprintUiHelperBuilder {
+ private final FingerprintManager mFingerPrintManager;
+
+ @Inject
+ public FingerprintUiHelperBuilder(FingerprintManager fingerprintManager) {
+ mFingerPrintManager = fingerprintManager;
+ }
+
+ public FingerprintUiHelper build(ImageView icon, TextView errorTextView, Callback callback) {
+ return new FingerprintUiHelper(mFingerPrintManager, icon, errorTextView,
+ callback);
+ }
+ }
+
+ /**
+ * Constructor for {@link FingerprintUiHelper}. This method is expected to be called from
+ * only the {@link FingerprintUiHelperBuilder} class.
+ */
+ private FingerprintUiHelper(FingerprintManager fingerprintManager,
+ ImageView icon, TextView errorTextView, Callback callback) {
+ mFingerprintManager = fingerprintManager;
+ mIcon = icon;
+ mErrorTextView = errorTextView;
+ mCallback = callback;
+ }
+
+ public boolean isFingerprintAuthAvailable() {
+ return mFingerprintManager.isHardwareDetected()
+ && mFingerprintManager.hasEnrolledFingerprints();
+ }
+
+ public void startListening(FingerprintManager.CryptoObject cryptoObject) {
+ if (!isFingerprintAuthAvailable()) {
+ return;
+ }
+ mCancellationSignal = new CancellationSignal();
+ mSelfCancelled = false;
+ mFingerprintManager
+ .authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
+ mIcon.setImageResource(R.drawable.ic_fp_40px);
+ }
+
+ public void stopListening() {
+ if (mCancellationSignal != null) {
+ mSelfCancelled = true;
+ mCancellationSignal.cancel();
+ mCancellationSignal = null;
+ }
+ }
+
+ @Override
+ public void onAuthenticationError(int errMsgId, CharSequence errString) {
+ if (!mSelfCancelled) {
+ showError(errString);
+ mIcon.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onError();
+ }
+ }, ERROR_TIMEOUT_MILLIS);
+ }
+ }
+
+ @Override
+ public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
+ showError(helpString);
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ showError(mIcon.getResources().getString(
+ R.string.fingerprint_not_recognized));
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
+ mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
+ mIcon.setImageResource(R.drawable.ic_fingerprint_success);
+ mErrorTextView.setTextColor(
+ mErrorTextView.getResources().getColor(R.color.success_color, null));
+ mErrorTextView.setText(
+ mErrorTextView.getResources().getString(R.string.fingerprint_success));
+ mIcon.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onAuthenticated();
+ }
+ }, SUCCESS_DELAY_MILLIS);
+ }
+
+ private void showError(CharSequence error) {
+ mIcon.setImageResource(R.drawable.ic_fingerprint_error);
+ mErrorTextView.setText(error);
+ mErrorTextView.setTextColor(
+ mErrorTextView.getResources().getColor(R.color.warning_color, null));
+ mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
+ mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
+ }
+
+ @VisibleForTesting
+ Runnable mResetErrorTextRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mErrorTextView.setTextColor(
+ mErrorTextView.getResources().getColor(R.color.hint_color, null));
+ mErrorTextView.setText(
+ mErrorTextView.getResources().getString(R.string.fingerprint_hint));
+ mIcon.setImageResource(R.drawable.ic_fp_40px);
+ }
+ };
+
+ public interface Callback {
+
+ void onAuthenticated();
+
+ void onError();
+ }
+}
diff --git a/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/InjectedApplication.java b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/InjectedApplication.java
new file mode 100644
index 000000000..1c3ed7e20
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/InjectedApplication.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.asymmetricfingerprintdialog;
+
+import android.app.Application;
+import android.util.Log;
+
+import dagger.ObjectGraph;
+
+/**
+ * The Application class of the sample which holds the ObjectGraph in Dagger and enables
+ * dependency injection.
+ */
+public class InjectedApplication extends Application {
+
+ private static final String TAG = InjectedApplication.class.getSimpleName();
+
+ private ObjectGraph mObjectGraph;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ initObjectGraph(new FingerprintModule(this));
+ }
+
+ /**
+ * Initialize the Dagger module. Passing null or mock modules can be used for testing.
+ *
+ * @param module for Dagger
+ */
+ public void initObjectGraph(Object module) {
+ mObjectGraph = module != null ? ObjectGraph.create(module) : null;
+ }
+
+ public void inject(Object object) {
+ if (mObjectGraph == null) {
+ // This usually happens during tests.
+ Log.i(TAG, "Object graph is not initialized.");
+ return;
+ }
+ mObjectGraph.inject(object);
+ }
+
+}
diff --git a/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/MainActivity.java b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/MainActivity.java
new file mode 100644
index 000000000..26832f2c5
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/MainActivity.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.asymmetricfingerprintdialog;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Bundle;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.security.keystore.KeyProperties;
+import android.util.Base64;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.spec.ECGenParameterSpec;
+
+import javax.inject.Inject;
+
+/**
+ * Main entry point for the sample, showing a backpack and "Purchase" button.
+ */
+public class MainActivity extends Activity {
+
+ private static final String DIALOG_FRAGMENT_TAG = "myFragment";
+ /** Alias for our key in the Android Key Store */
+ public static final String KEY_NAME = "my_key";
+
+ @Inject KeyguardManager mKeyguardManager;
+ @Inject FingerprintManager mFingerprintManager;
+ @Inject FingerprintAuthenticationDialogFragment mFragment;
+ @Inject KeyStore mKeyStore;
+ @Inject KeyPairGenerator mKeyPairGenerator;
+ @Inject Signature mSignature;
+ @Inject SharedPreferences mSharedPreferences;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ((InjectedApplication) getApplication()).inject(this);
+
+ setContentView(R.layout.activity_main);
+ Button purchaseButton = (Button) findViewById(R.id.purchase_button);
+ if (!mKeyguardManager.isKeyguardSecure()) {
+ // Show a message that the user hasn't set up a fingerprint or lock screen.
+ Toast.makeText(this,
+ "Secure lock screen hasn't set up.\n"
+ + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
+ Toast.LENGTH_LONG).show();
+ purchaseButton.setEnabled(false);
+ return;
+ }
+ //noinspection ResourceType
+ if (!mFingerprintManager.hasEnrolledFingerprints()) {
+ purchaseButton.setEnabled(false);
+ // This happens when no fingerprints are registered.
+ Toast.makeText(this,
+ "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ createKeyPair();
+ purchaseButton.setEnabled(true);
+ purchaseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ findViewById(R.id.confirmation_message).setVisibility(View.GONE);
+ findViewById(R.id.encrypted_message).setVisibility(View.GONE);
+
+ // Set up the crypto object for later. The object will be authenticated by use
+ // of the fingerprint.
+ if (initSignature()) {
+
+ // Show the fingerprint dialog. The user has the option to use the fingerprint with
+ // crypto, or you can fall back to using a server-side verified password.
+ mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature));
+ boolean useFingerprintPreference = mSharedPreferences
+ .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
+ true);
+ if (useFingerprintPreference) {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
+ } else {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
+ }
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ } else {
+ // This happens if the lock screen has been disabled or or a fingerprint got
+ // enrolled. Thus show the dialog to authenticate with their password first
+ // and ask the user if they want to authenticate with fingerprints in the
+ // future
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ }
+ }
+ });
+ }
+
+ /**
+ * Initialize the {@link Signature} instance with the created key in the
+ * {@link #createKeyPair()} method.
+ *
+ * @return {@code true} if initialization is successful, {@code false} if the lock screen has
+ * been disabled or reset after the key was generated, or if a fingerprint got enrolled after
+ * the key was generated.
+ */
+ private boolean initSignature() {
+ try {
+ mKeyStore.load(null);
+ PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null);
+ mSignature.initSign(key);
+ return true;
+ } catch (KeyPermanentlyInvalidatedException e) {
+ return false;
+ } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException
+ | NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new RuntimeException("Failed to init Cipher", e);
+ }
+ }
+
+ public void onPurchased(byte[] signature) {
+ showConfirmation(signature);
+ }
+
+ public void onPurchaseFailed() {
+ Toast.makeText(this, R.string.purchase_fail, Toast.LENGTH_SHORT).show();
+ }
+
+ // Show confirmation, if fingerprint was used show crypto information.
+ private void showConfirmation(byte[] encrypted) {
+ findViewById(R.id.confirmation_message).setVisibility(View.VISIBLE);
+ if (encrypted != null) {
+ TextView v = (TextView) findViewById(R.id.encrypted_message);
+ v.setVisibility(View.VISIBLE);
+ v.setText(Base64.encodeToString(encrypted, 0 /* flags */));
+ }
+ }
+
+ /**
+ * Generates an asymmetric key pair in the Android Keystore. Every use of the private key must
+ * be authorized by the user authenticating with fingerprint. Public key use is unrestricted.
+ */
+ public void createKeyPair() {
+ // The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint
+ // for your flow. Use of keys is necessary if you need to know if the set of
+ // enrolled fingerprints has changed.
+ try {
+ // Set the alias of the entry in Android KeyStore where the key will appear
+ // and the constrains (purposes) in the constructor of the Builder
+ mKeyPairGenerator.initialize(
+ new KeyGenParameterSpec.Builder(KEY_NAME,
+ KeyProperties.PURPOSE_SIGN)
+ .setDigests(KeyProperties.DIGEST_SHA256)
+ .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
+ // Require the user to authenticate with a fingerprint to authorize
+ // every use of the private key
+ .setUserAuthenticationRequired(true)
+ .build());
+ mKeyPairGenerator.generateKeyPair();
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+
+ if (id == R.id.action_settings) {
+ Intent intent = new Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/SettingsActivity.java b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/SettingsActivity.java
new file mode 100644
index 000000000..acc5e378c
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/SettingsActivity.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.asymmetricfingerprintdialog;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+
+public class SettingsActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Display the fragment as the main content.
+ getFragmentManager().beginTransaction().replace(android.R.id.content,
+ new SettingsFragment()).commit();
+ }
+
+ /**
+ * Fragment for settings.
+ */
+ public static class SettingsFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.preferences);
+ }
+ }
+}
+
+
diff --git a/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/server/StoreBackend.java b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/server/StoreBackend.java
new file mode 100644
index 000000000..87921ae5f
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/server/StoreBackend.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.asymmetricfingerprintdialog.server;
+
+import java.security.PublicKey;
+
+/**
+ * An interface that defines the methods required for the store backend.
+ */
+public interface StoreBackend {
+
+ /**
+ * Verifies the authenticity of the provided transaction by confirming that it was signed with
+ * the private key enrolled for the userId.
+ *
+ * @param transaction the contents of the purchase transaction, its contents are
+ * signed
+ * by the
+ * private key in the client side.
+ * @param transactionSignature the signature of the transaction's contents.
+ * @return true if the signedSignature was verified, false otherwise. If this method returns
+ * true, the server can consider the transaction is successful.
+ */
+ boolean verify(Transaction transaction, byte[] transactionSignature);
+
+ /**
+ * Verifies the authenticity of the provided transaction by password.
+ *
+ * @param transaction the contents of the purchase transaction, its contents are signed by the
+ * private key in the client side.
+ * @param password the password for the user associated with the {@code transaction}.
+ * @return true if the password is verified.
+ */
+ boolean verify(Transaction transaction, String password);
+
+ /**
+ * Enrolls a public key associated with the userId
+ *
+ * @param userId the unique ID of the user within the app including server side
+ * implementation
+ * @param password the password for the user for the server side
+ * @param publicKey the public key object to verify the signature from the user
+ * @return true if the enrollment was successful, false otherwise
+ */
+ boolean enroll(String userId, String password, PublicKey publicKey);
+}
diff --git a/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/server/StoreBackendImpl.java b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/server/StoreBackendImpl.java
new file mode 100644
index 000000000..b28dce493
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/server/StoreBackendImpl.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.asymmetricfingerprintdialog.server;
+
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A fake backend implementation of {@link StoreBackend}.
+ */
+public class StoreBackendImpl implements StoreBackend {
+
+ private final Map mPublicKeys = new HashMap<>();
+ private final Set mReceivedTransactions = new HashSet<>();
+
+ @Override
+ public boolean verify(Transaction transaction, byte[] transactionSignature) {
+ try {
+ if (mReceivedTransactions.contains(transaction)) {
+ // It verifies the equality of the transaction including the client nonce
+ // So attackers can't do replay attacks.
+ return false;
+ }
+ mReceivedTransactions.add(transaction);
+ PublicKey publicKey = mPublicKeys.get(transaction.getUserId());
+ Signature verificationFunction = Signature.getInstance("SHA256withECDSA");
+ verificationFunction.initVerify(publicKey);
+ verificationFunction.update(transaction.toByteArray());
+ if (verificationFunction.verify(transactionSignature)) {
+ // Transaction is verified with the public key associated with the user
+ // Do some post purchase processing in the server
+ return true;
+ }
+ } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
+ // In a real world, better to send some error message to the user
+ }
+ return false;
+ }
+
+ @Override
+ public boolean verify(Transaction transaction, String password) {
+ // As this is just a sample, we always assume that the password is right.
+ return true;
+ }
+
+ @Override
+ public boolean enroll(String userId, String password, PublicKey publicKey) {
+ if (publicKey != null) {
+ mPublicKeys.put(userId, publicKey);
+ }
+ // We just ignore the provided password here, but in real life, it is registered to the
+ // backend.
+ return true;
+ }
+}
diff --git a/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/server/Transaction.java b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/server/Transaction.java
new file mode 100644
index 000000000..789cc0e7c
--- /dev/null
+++ b/samples/browseable/AsymmetricFingerprintDialog/src/com.example.android.asymmetricfingerprintdialog/server/Transaction.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.asymmetricfingerprintdialog.server;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * An entity that represents a single transaction (purchase) of an item.
+ */
+public class Transaction {
+
+ /** The unique ID of the item of the purchase */
+ private final Long mItemId;
+
+ /** The unique user ID who made the transaction */
+ private final String mUserId;
+
+ /**
+ * The random long value that will be also signed by the private key and verified in the server
+ * that the same nonce can't be reused to prevent replay attacks.
+ */
+ private final Long mClientNonce;
+
+ public Transaction(String userId, long itemId, long clientNonce) {
+ mItemId = itemId;
+ mUserId = userId;
+ mClientNonce = clientNonce;
+ }
+
+ public String getUserId() {
+ return mUserId;
+ }
+
+ public byte[] toByteArray() {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ DataOutputStream dataOutputStream = null;
+ try {
+ dataOutputStream = new DataOutputStream(byteArrayOutputStream);
+ dataOutputStream.writeLong(mItemId);
+ dataOutputStream.writeUTF(mUserId);
+ dataOutputStream.writeLong(mClientNonce);
+ return byteArrayOutputStream.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ try {
+ if (dataOutputStream != null) {
+ dataOutputStream.close();
+ }
+ } catch (IOException ignore) {
+ }
+ try {
+ byteArrayOutputStream.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Transaction that = (Transaction) o;
+ return Objects.equals(mItemId, that.mItemId) && Objects.equals(mUserId, that.mUserId) &&
+ Objects.equals(mClientNonce, that.mClientNonce);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mItemId, mUserId, mClientNonce);
+ }
+}
diff --git a/samples/browseable/AutoBackupForApps/res/values-v11/template-styles.xml b/samples/browseable/AutoBackupForApps/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/AutoBackupForApps/res/values-v11/template-styles.xml
+++ b/samples/browseable/AutoBackupForApps/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/AutoBackupForApps/res/values-v21/base-template-styles.xml b/samples/browseable/AutoBackupForApps/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/AutoBackupForApps/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/AutoBackupForApps/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/AutoBackupForApps/res/values/template-styles.xml b/samples/browseable/AutoBackupForApps/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/AutoBackupForApps/res/values/template-styles.xml
+++ b/samples/browseable/AutoBackupForApps/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicAccessibility/res/values-v11/template-styles.xml b/samples/browseable/BasicAccessibility/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicAccessibility/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicAccessibility/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicAccessibility/res/values-v21/base-template-styles.xml b/samples/browseable/BasicAccessibility/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicAccessibility/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicAccessibility/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicAccessibility/res/values/template-styles.xml b/samples/browseable/BasicAccessibility/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicAccessibility/res/values/template-styles.xml
+++ b/samples/browseable/BasicAccessibility/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicAndroidKeyStore/res/values-v11/template-styles.xml b/samples/browseable/BasicAndroidKeyStore/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicAndroidKeyStore/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicAndroidKeyStore/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicAndroidKeyStore/res/values-v21/base-template-styles.xml b/samples/browseable/BasicAndroidKeyStore/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicAndroidKeyStore/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicAndroidKeyStore/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicAndroidKeyStore/src/com.example.android.basicandroidkeystore/BasicAndroidKeyStoreFragment.java b/samples/browseable/BasicAndroidKeyStore/src/com.example.android.basicandroidkeystore/BasicAndroidKeyStoreFragment.java
index 12873e847..e6244bfb6 100644
--- a/samples/browseable/BasicAndroidKeyStore/src/com.example.android.basicandroidkeystore/BasicAndroidKeyStoreFragment.java
+++ b/samples/browseable/BasicAndroidKeyStore/src/com.example.android.basicandroidkeystore/BasicAndroidKeyStoreFragment.java
@@ -156,7 +156,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment {
// generated.
Calendar start = new GregorianCalendar();
Calendar end = new GregorianCalendar();
- end.add(1, Calendar.YEAR);
+ end.add(Calendar.YEAR, 1);
//END_INCLUDE(create_valid_dates)
@@ -316,8 +316,7 @@ public class BasicAndroidKeyStoreFragment extends Fragment {
// Verify the data.
s.initVerify(((KeyStore.PrivateKeyEntry) entry).getCertificate());
s.update(data);
- boolean valid = s.verify(signature);
- return valid;
+ return s.verify(signature);
// END_INCLUDE(verify_data)
}
diff --git a/samples/browseable/BasicContactables/res/values-v11/template-styles.xml b/samples/browseable/BasicContactables/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicContactables/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicContactables/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicContactables/res/values-v21/base-template-styles.xml b/samples/browseable/BasicContactables/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicContactables/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicContactables/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicContactables/res/values/template-styles.xml b/samples/browseable/BasicContactables/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicContactables/res/values/template-styles.xml
+++ b/samples/browseable/BasicContactables/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicGestureDetect/res/values-v11/template-styles.xml b/samples/browseable/BasicGestureDetect/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicGestureDetect/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicGestureDetect/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicGestureDetect/res/values-v21/base-template-styles.xml b/samples/browseable/BasicGestureDetect/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicGestureDetect/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicGestureDetect/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicImmersiveMode/res/values-v11/template-styles.xml b/samples/browseable/BasicImmersiveMode/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicImmersiveMode/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicImmersiveMode/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicImmersiveMode/res/values-v21/base-template-styles.xml b/samples/browseable/BasicImmersiveMode/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicImmersiveMode/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicImmersiveMode/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicManagedProfile/res/values-v11/template-styles.xml b/samples/browseable/BasicManagedProfile/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicManagedProfile/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicManagedProfile/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicManagedProfile/res/values-v21/base-template-styles.xml b/samples/browseable/BasicManagedProfile/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicManagedProfile/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicManagedProfile/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicManagedProfile/res/values/template-styles.xml b/samples/browseable/BasicManagedProfile/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicManagedProfile/res/values/template-styles.xml
+++ b/samples/browseable/BasicManagedProfile/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicMediaDecoder/res/values-v11/template-styles.xml b/samples/browseable/BasicMediaDecoder/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicMediaDecoder/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicMediaDecoder/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicMediaDecoder/res/values-v21/base-template-styles.xml b/samples/browseable/BasicMediaDecoder/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicMediaDecoder/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicMediaDecoder/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicMediaDecoder/res/values/template-styles.xml b/samples/browseable/BasicMediaDecoder/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicMediaDecoder/res/values/template-styles.xml
+++ b/samples/browseable/BasicMediaDecoder/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicMediaRouter/res/values-v11/template-styles.xml b/samples/browseable/BasicMediaRouter/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicMediaRouter/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicMediaRouter/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicMediaRouter/res/values-v21/base-template-styles.xml b/samples/browseable/BasicMediaRouter/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicMediaRouter/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicMediaRouter/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicMediaRouter/res/values/template-styles.xml b/samples/browseable/BasicMediaRouter/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicMediaRouter/res/values/template-styles.xml
+++ b/samples/browseable/BasicMediaRouter/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicMultitouch/res/values-v11/template-styles.xml b/samples/browseable/BasicMultitouch/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicMultitouch/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicMultitouch/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicMultitouch/res/values-v21/base-template-styles.xml b/samples/browseable/BasicMultitouch/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicMultitouch/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicMultitouch/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicMultitouch/res/values/template-styles.xml b/samples/browseable/BasicMultitouch/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicMultitouch/res/values/template-styles.xml
+++ b/samples/browseable/BasicMultitouch/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicNetworking/res/values-v11/template-styles.xml b/samples/browseable/BasicNetworking/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicNetworking/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicNetworking/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicNetworking/res/values-v21/base-template-styles.xml b/samples/browseable/BasicNetworking/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicNetworking/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicNetworking/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicNetworking/res/values/template-styles.xml b/samples/browseable/BasicNetworking/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicNetworking/res/values/template-styles.xml
+++ b/samples/browseable/BasicNetworking/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicNotifications/res/values-v11/template-styles.xml b/samples/browseable/BasicNotifications/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicNotifications/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicNotifications/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicNotifications/res/values-v21/base-template-styles.xml b/samples/browseable/BasicNotifications/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicNotifications/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicNotifications/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicNotifications/res/values/template-styles.xml b/samples/browseable/BasicNotifications/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicNotifications/res/values/template-styles.xml
+++ b/samples/browseable/BasicNotifications/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicRenderScript/res/values-v11/template-styles.xml b/samples/browseable/BasicRenderScript/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicRenderScript/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicRenderScript/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicRenderScript/res/values-v21/base-template-styles.xml b/samples/browseable/BasicRenderScript/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicRenderScript/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicRenderScript/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicRenderScript/res/values/template-styles.xml b/samples/browseable/BasicRenderScript/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicRenderScript/res/values/template-styles.xml
+++ b/samples/browseable/BasicRenderScript/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicSyncAdapter/res/values-v11/template-styles.xml b/samples/browseable/BasicSyncAdapter/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicSyncAdapter/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicSyncAdapter/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicSyncAdapter/res/values-v21/base-template-styles.xml b/samples/browseable/BasicSyncAdapter/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicSyncAdapter/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicSyncAdapter/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicSyncAdapter/res/values/template-styles.xml b/samples/browseable/BasicSyncAdapter/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicSyncAdapter/res/values/template-styles.xml
+++ b/samples/browseable/BasicSyncAdapter/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BasicTransition/res/values-v11/template-styles.xml b/samples/browseable/BasicTransition/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BasicTransition/res/values-v11/template-styles.xml
+++ b/samples/browseable/BasicTransition/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BasicTransition/res/values-v21/base-template-styles.xml b/samples/browseable/BasicTransition/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BasicTransition/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BasicTransition/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BasicTransition/res/values/template-styles.xml b/samples/browseable/BasicTransition/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BasicTransition/res/values/template-styles.xml
+++ b/samples/browseable/BasicTransition/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BatchStepSensor/res/values-v11/template-styles.xml b/samples/browseable/BatchStepSensor/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BatchStepSensor/res/values-v11/template-styles.xml
+++ b/samples/browseable/BatchStepSensor/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BatchStepSensor/res/values-v21/base-template-styles.xml b/samples/browseable/BatchStepSensor/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BatchStepSensor/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BatchStepSensor/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BatchStepSensor/res/values/template-styles.xml b/samples/browseable/BatchStepSensor/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BatchStepSensor/res/values/template-styles.xml
+++ b/samples/browseable/BatchStepSensor/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BeamLargeFiles/res/values-v11/template-styles.xml b/samples/browseable/BeamLargeFiles/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BeamLargeFiles/res/values-v11/template-styles.xml
+++ b/samples/browseable/BeamLargeFiles/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BeamLargeFiles/res/values-v21/base-template-styles.xml b/samples/browseable/BeamLargeFiles/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BeamLargeFiles/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BeamLargeFiles/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BluetoothAdvertisements/res/values-v11/template-styles.xml b/samples/browseable/BluetoothAdvertisements/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BluetoothAdvertisements/res/values-v11/template-styles.xml
+++ b/samples/browseable/BluetoothAdvertisements/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BluetoothAdvertisements/res/values-v21/base-template-styles.xml b/samples/browseable/BluetoothAdvertisements/res/values-v21/base-template-styles.xml
index a0d1bf11d..0bb9fbf71 100644
--- a/samples/browseable/BluetoothAdvertisements/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BluetoothAdvertisements/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BluetoothChat/res/values/template-styles.xml b/samples/browseable/BluetoothChat/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BluetoothChat/res/values/template-styles.xml
+++ b/samples/browseable/BluetoothChat/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BluetoothLeGatt/res/values-v11/template-styles.xml b/samples/browseable/BluetoothLeGatt/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BluetoothLeGatt/res/values-v11/template-styles.xml
+++ b/samples/browseable/BluetoothLeGatt/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BluetoothLeGatt/res/values-v21/base-template-styles.xml b/samples/browseable/BluetoothLeGatt/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BluetoothLeGatt/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BluetoothLeGatt/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BluetoothLeGatt/res/values/template-styles.xml b/samples/browseable/BluetoothLeGatt/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BluetoothLeGatt/res/values/template-styles.xml
+++ b/samples/browseable/BluetoothLeGatt/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/BorderlessButtons/res/values-v11/template-styles.xml b/samples/browseable/BorderlessButtons/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/BorderlessButtons/res/values-v11/template-styles.xml
+++ b/samples/browseable/BorderlessButtons/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/BorderlessButtons/res/values-v21/base-template-styles.xml b/samples/browseable/BorderlessButtons/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/BorderlessButtons/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/BorderlessButtons/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/BorderlessButtons/res/values/template-styles.xml b/samples/browseable/BorderlessButtons/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/BorderlessButtons/res/values/template-styles.xml
+++ b/samples/browseable/BorderlessButtons/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/Camera2Basic/res/values-v11/template-styles.xml b/samples/browseable/Camera2Basic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/Camera2Basic/res/values-v11/template-styles.xml
+++ b/samples/browseable/Camera2Basic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/Camera2Basic/res/values-v21/base-template-styles.xml b/samples/browseable/Camera2Basic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/Camera2Basic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/Camera2Basic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/Camera2Basic/res/values/strings.xml b/samples/browseable/Camera2Basic/res/values/strings.xml
index 66f10008a..7fdf6e607 100644
--- a/samples/browseable/Camera2Basic/res/values/strings.xml
+++ b/samples/browseable/Camera2Basic/res/values/strings.xml
@@ -16,4 +16,6 @@
PictureInfo
+ This sample needs camera permission.
+ This device doesn\'t support Camera2 API.
diff --git a/samples/browseable/Camera2Basic/res/values/template-styles.xml b/samples/browseable/Camera2Basic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/Camera2Basic/res/values/template-styles.xml
+++ b/samples/browseable/Camera2Basic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/Camera2Basic/src/com.example.android.camera2basic/Camera2BasicFragment.java b/samples/browseable/Camera2Basic/src/com.example.android.camera2basic/Camera2BasicFragment.java
index 020ca14d3..747d8d83b 100644
--- a/samples/browseable/Camera2Basic/src/com.example.android.camera2basic/Camera2BasicFragment.java
+++ b/samples/browseable/Camera2Basic/src/com.example.android.camera2basic/Camera2BasicFragment.java
@@ -16,6 +16,7 @@
package com.example.android.camera2basic;
+import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
@@ -23,9 +24,11 @@ import android.app.DialogFragment;
import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
+import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
@@ -43,7 +46,9 @@ import android.media.ImageReader;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
-import android.os.Message;
+import android.support.annotation.NonNull;
+import android.support.v13.app.FragmentCompat;
+import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
@@ -66,12 +71,15 @@ import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
-public class Camera2BasicFragment extends Fragment implements View.OnClickListener {
+public class Camera2BasicFragment extends Fragment
+ implements View.OnClickListener, FragmentCompat.OnRequestPermissionsResultCallback {
/**
* Conversion from screen rotation to JPEG orientation.
*/
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
+ private static final int REQUEST_CAMERA_PERMISSION = 1;
+ private static final String FRAGMENT_DIALOG = "dialog";
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
@@ -94,19 +102,32 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
* Camera state: Waiting for the focus to be locked.
*/
private static final int STATE_WAITING_LOCK = 1;
+
/**
* Camera state: Waiting for the exposure to be precapture state.
*/
private static final int STATE_WAITING_PRECAPTURE = 2;
+
/**
* Camera state: Waiting for the exposure state to be something other than precapture.
*/
private static final int STATE_WAITING_NON_PRECAPTURE = 3;
+
/**
* Camera state: Picture was taken.
*/
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}.
@@ -148,17 +169,16 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
/**
* A {@link CameraCaptureSession } for camera preview.
*/
-
private CameraCaptureSession mCaptureSession;
+
/**
* A reference to the opened {@link CameraDevice}.
*/
-
private CameraDevice mCameraDevice;
+
/**
* The {@link android.util.Size} of camera preview.
*/
-
private Size mPreviewSize;
/**
@@ -167,7 +187,7 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
- public void onOpened(CameraDevice cameraDevice) {
+ public void onOpened(@NonNull CameraDevice cameraDevice) {
// This method is called when the camera is opened. We start camera preview here.
mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
@@ -175,14 +195,14 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
}
@Override
- public void onDisconnected(CameraDevice cameraDevice) {
+ public void onDisconnected(@NonNull CameraDevice cameraDevice) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
}
@Override
- public void onError(CameraDevice cameraDevice, int error) {
+ public void onError(@NonNull CameraDevice cameraDevice, int error) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
@@ -250,6 +270,11 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
*/
private Semaphore mCameraOpenCloseLock = new Semaphore(1);
+ /**
+ * Whether the current camera device supports Flash or not.
+ */
+ private boolean mFlashSupported;
+
/**
* A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture.
*/
@@ -303,71 +328,81 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
}
@Override
- public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request,
- CaptureResult partialResult) {
+ public void onCaptureProgressed(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull CaptureResult partialResult) {
process(partialResult);
}
@Override
- public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request,
- TotalCaptureResult result) {
+ public void onCaptureCompleted(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
process(result);
}
};
- /**
- * A {@link Handler} for showing {@link Toast}s.
- */
- private Handler mMessageHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- Activity activity = getActivity();
- if (activity != null) {
- Toast.makeText(activity, (String) msg.obj, Toast.LENGTH_SHORT).show();
- }
- }
- };
-
/**
* Shows a {@link Toast} on the UI thread.
*
* @param text The message to show
*/
- private void showToast(String text) {
- // We show a Toast by sending request message to mMessageHandler. This makes sure that the
- // Toast is shown on the UI thread.
- Message message = Message.obtain();
- message.obj = text;
- mMessageHandler.sendMessage(message);
+ private void showToast(final String text) {
+ final Activity activity = getActivity();
+ if (activity != null) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(activity, text, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
}
/**
- * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
- * width and height are at least as large as the respective requested values, and whose aspect
- * ratio matches with the specified value.
+ * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
+ * is at least as large as the respective texture view size, and that is at most as large as the
+ * respective max size, and whose aspect ratio matches with the specified value. If such size
+ * doesn't exist, choose the largest one that is at most as large as the respective max size,
+ * and whose aspect ratio matches with the specified value.
*
- * @param choices The list of sizes that the camera supports for the intended output class
- * @param width The minimum desired width
- * @param height The minimum desired height
- * @param aspectRatio The aspect ratio
+ * @param choices The list of sizes that the camera supports for the intended output
+ * class
+ * @param textureViewWidth The width of the texture view relative to sensor coordinate
+ * @param textureViewHeight The height of the texture view relative to sensor coordinate
+ * @param maxWidth The maximum width that can be chosen
+ * @param maxHeight The maximum height that can be chosen
+ * @param aspectRatio The aspect ratio
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
*/
- private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
+ private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
+ int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
+
// Collect the supported resolutions that are at least as big as the preview Surface
- List bigEnough = new ArrayList();
+ List bigEnough = new ArrayList<>();
+ // Collect the supported resolutions that are smaller than the preview Surface
+ List notBigEnough = new ArrayList<>();
int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight();
for (Size option : choices) {
- if (option.getHeight() == option.getWidth() * h / w &&
- option.getWidth() >= width && option.getHeight() >= height) {
- bigEnough.add(option);
+ if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
+ option.getHeight() == option.getWidth() * h / w) {
+ if (option.getWidth() >= textureViewWidth &&
+ option.getHeight() >= textureViewHeight) {
+ bigEnough.add(option);
+ } else {
+ notBigEnough.add(option);
+ }
}
}
- // Pick the smallest of those, assuming we found any
+ // Pick the smallest of those big enough. If there is no one big enough, pick the
+ // largest of those not big enough.
if (bigEnough.size() > 0) {
return Collections.min(bigEnough, new CompareSizesByArea());
+ } else if (notBigEnough.size() > 0) {
+ return Collections.max(notBigEnough, new CompareSizesByArea());
} else {
Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0];
@@ -375,9 +410,7 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
}
public static Camera2BasicFragment newInstance() {
- Camera2BasicFragment fragment = new Camera2BasicFragment();
- fragment.setRetainInstance(true);
- return fragment;
+ return new Camera2BasicFragment();
}
@Override
@@ -422,6 +455,28 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
super.onPause();
}
+ private void requestCameraPermission() {
+ if (FragmentCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
+ new ConfirmationDialog().show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ } else {
+ FragmentCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
+ REQUEST_CAMERA_PERMISSION);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (requestCode == REQUEST_CAMERA_PERMISSION) {
+ if (grantResults.length != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
+ ErrorDialog.newInstance(getString(R.string.request_permission))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ }
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
/**
* Sets up member variables related to camera.
*
@@ -437,13 +492,16 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
= manager.getCameraCharacteristics(cameraId);
// We don't use a front facing camera in this sample.
- if (characteristics.get(CameraCharacteristics.LENS_FACING)
- == CameraCharacteristics.LENS_FACING_FRONT) {
+ Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
+ if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
continue;
}
StreamConfigurationMap map = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+ if (map == null) {
+ continue;
+ }
// For still image captures, we use the largest available size.
Size largest = Collections.max(
@@ -454,11 +512,57 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
mImageReader.setOnImageAvailableListener(
mOnImageAvailableListener, mBackgroundHandler);
+ // Find out if we need to swap dimension to get the preview size relative to sensor
+ // coordinate.
+ int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+ int sensorOrientation =
+ characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+ boolean swappedDimensions = false;
+ switch (displayRotation) {
+ case Surface.ROTATION_0:
+ case Surface.ROTATION_180:
+ if (sensorOrientation == 90 || sensorOrientation == 270) {
+ swappedDimensions = true;
+ }
+ break;
+ case Surface.ROTATION_90:
+ case Surface.ROTATION_270:
+ if (sensorOrientation == 0 || sensorOrientation == 180) {
+ swappedDimensions = true;
+ }
+ break;
+ default:
+ Log.e(TAG, "Display rotation is invalid: " + displayRotation);
+ }
+
+ Point displaySize = new Point();
+ activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
+ int rotatedPreviewWidth = width;
+ int rotatedPreviewHeight = height;
+ int maxPreviewWidth = displaySize.x;
+ int maxPreviewHeight = displaySize.y;
+
+ if (swappedDimensions) {
+ rotatedPreviewWidth = height;
+ rotatedPreviewHeight = width;
+ maxPreviewWidth = displaySize.y;
+ maxPreviewHeight = displaySize.x;
+ }
+
+ if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
+ maxPreviewWidth = MAX_PREVIEW_WIDTH;
+ }
+
+ if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
+ maxPreviewHeight = MAX_PREVIEW_HEIGHT;
+ }
+
// Danger, W.R.! Attempting to use too large a preview size could exceed the camera
// bus' bandwidth limitation, resulting in gorgeous previews but the storage of
// garbage capture data.
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
- width, height, largest);
+ rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
+ maxPreviewHeight, largest);
// We fit the aspect ratio of TextureView to the size of preview we picked.
int orientation = getResources().getConfiguration().orientation;
@@ -470,6 +574,10 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
mPreviewSize.getHeight(), mPreviewSize.getWidth());
}
+ // Check if the flash is supported.
+ Boolean available = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+ mFlashSupported = available == null ? false : available;
+
mCameraId = cameraId;
return;
}
@@ -478,7 +586,8 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
} catch (NullPointerException e) {
// Currently an NPE is thrown when the Camera2API is used but not supported on the
// device this code runs.
- new ErrorDialog().show(getFragmentManager(), "dialog");
+ ErrorDialog.newInstance(getString(R.string.camera_error))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
}
}
@@ -486,6 +595,11 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
* Opens the camera specified by {@link Camera2BasicFragment#mCameraId}.
*/
private void openCamera(int width, int height) {
+ if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestCameraPermission();
+ return;
+ }
setUpCameraOutputs(width, height);
configureTransform(width, height);
Activity activity = getActivity();
@@ -574,7 +688,7 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
new CameraCaptureSession.StateCallback() {
@Override
- public void onConfigured(CameraCaptureSession cameraCaptureSession) {
+ public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
// The camera is already closed
if (null == mCameraDevice) {
return;
@@ -587,8 +701,7 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// Flash is automatically enabled when necessary.
- mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
- CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ setAutoFlash(mPreviewRequestBuilder);
// Finally, we start displaying the camera preview.
mPreviewRequest = mPreviewRequestBuilder.build();
@@ -600,7 +713,8 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
}
@Override
- public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
+ public void onConfigureFailed(
+ @NonNull CameraCaptureSession cameraCaptureSession) {
showToast("Failed");
}
}, null
@@ -668,8 +782,8 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
}
/**
- * Run the precapture sequence for capturing a still image. This method should be called when we
- * get a response in {@link #mCaptureCallback} from {@link #lockFocus()}.
+ * Run the precapture sequence for capturing a still image. This method should be called when
+ * we get a response in {@link #mCaptureCallback} from {@link #lockFocus()}.
*/
private void runPrecaptureSequence() {
try {
@@ -703,8 +817,7 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
// Use the same AE and AF modes as the preview.
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
- captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,
- CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ setAutoFlash(captureBuilder);
// Orientation
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
@@ -714,9 +827,11 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
= new CameraCaptureSession.CaptureCallback() {
@Override
- public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request,
- TotalCaptureResult result) {
+ public void onCaptureCompleted(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
showToast("Saved: " + mFile);
+ Log.d(TAG, mFile.toString());
unlockFocus();
}
};
@@ -729,15 +844,15 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
}
/**
- * Unlock the focus. This method should be called when still image capture sequence is finished.
+ * Unlock the focus. This method should be called when still image capture sequence is
+ * finished.
*/
private void unlockFocus() {
try {
- // Reset the autofucos trigger
+ // Reset the auto-focus trigger
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
- mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
- CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ setAutoFlash(mPreviewRequestBuilder);
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
mBackgroundHandler);
// After this, the camera will go back to the normal state of preview.
@@ -769,6 +884,13 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
}
}
+ private void setAutoFlash(CaptureRequest.Builder requestBuilder) {
+ if (mFlashSupported) {
+ requestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
+ CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
+ }
+ }
+
/**
* Saves a JPEG {@link Image} into the specified {@link File}.
*/
@@ -827,13 +949,26 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
}
+ /**
+ * Shows an error message dialog.
+ */
public static class ErrorDialog extends DialogFragment {
+ private static final String ARG_MESSAGE = "message";
+
+ public static ErrorDialog newInstance(String message) {
+ ErrorDialog dialog = new ErrorDialog();
+ Bundle args = new Bundle();
+ args.putString(ARG_MESSAGE, message);
+ dialog.setArguments(args);
+ return dialog;
+ }
+
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = getActivity();
return new AlertDialog.Builder(activity)
- .setMessage("This device doesn't support Camera2 API.")
+ .setMessage(getArguments().getString(ARG_MESSAGE))
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
@@ -845,4 +980,36 @@ public class Camera2BasicFragment extends Fragment implements View.OnClickListen
}
+ /**
+ * Shows OK/Cancel confirmation dialog about camera permission.
+ */
+ public static class ConfirmationDialog extends DialogFragment {
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Fragment parent = getParentFragment();
+ return new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.request_permission)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ FragmentCompat.requestPermissions(parent,
+ new String[]{Manifest.permission.CAMERA},
+ REQUEST_CAMERA_PERMISSION);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Activity activity = parent.getActivity();
+ if (activity != null) {
+ activity.finish();
+ }
+ }
+ })
+ .create();
+ }
+ }
+
}
diff --git a/samples/browseable/Camera2Raw/res/values-v11/template-styles.xml b/samples/browseable/Camera2Raw/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/Camera2Raw/res/values-v11/template-styles.xml
+++ b/samples/browseable/Camera2Raw/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/Camera2Raw/res/values-v21/base-template-styles.xml b/samples/browseable/Camera2Raw/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/Camera2Raw/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/Camera2Raw/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/Camera2Raw/res/values/strings.xml b/samples/browseable/Camera2Raw/res/values/strings.xml
index 0f56ce9bb..84672a082 100644
--- a/samples/browseable/Camera2Raw/res/values/strings.xml
+++ b/samples/browseable/Camera2Raw/res/values/strings.xml
@@ -16,4 +16,5 @@
PictureInfo
+ This app needs camera permission.
diff --git a/samples/browseable/Camera2Raw/res/values/template-styles.xml b/samples/browseable/Camera2Raw/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/Camera2Raw/res/values/template-styles.xml
+++ b/samples/browseable/Camera2Raw/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/Camera2Raw/src/com.example.android.camera2raw/Camera2RawFragment.java b/samples/browseable/Camera2Raw/src/com.example.android.camera2raw/Camera2RawFragment.java
index 6460bf364..bf5efe588 100644
--- a/samples/browseable/Camera2Raw/src/com.example.android.camera2raw/Camera2RawFragment.java
+++ b/samples/browseable/Camera2Raw/src/com.example.android.camera2raw/Camera2RawFragment.java
@@ -16,6 +16,7 @@
package com.example.android.camera2raw;
+import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
@@ -23,8 +24,10 @@ import android.app.DialogFragment;
import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
+import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
import android.hardware.SensorManager;
@@ -52,6 +55,8 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
+import android.support.v13.app.FragmentCompat;
+import android.support.v4.app.ActivityCompat;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
@@ -84,7 +89,7 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* A fragment that demonstrates use of the Camera2 API to capture RAW and JPEG photos.
- *
+ *
* In this example, the lifecycle of a single request to take a photo is:
*
*/
-public class Camera2RawFragment extends Fragment implements View.OnClickListener {
+public class Camera2RawFragment extends Fragment
+ implements View.OnClickListener, FragmentCompat.OnRequestPermissionsResultCallback {
+
/**
* Conversion from screen rotation to JPEG orientation.
*/
@@ -126,6 +133,20 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
ORIENTATIONS.append(Surface.ROTATION_270, 270);
}
+ /**
+ * Request code for camera permissions.
+ */
+ private static final int REQUEST_CAMERA_PERMISSIONS = 1;
+
+ /**
+ * Permissions required to take a picture.
+ */
+ private static final String[] CAMERA_PERMISSIONS = {
+ Manifest.permission.CAMERA,
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ };
+
/**
* Timeout for the pre-capture sequence.
*/
@@ -136,6 +157,16 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
*/
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}.
*/
@@ -264,9 +295,9 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
private Handler mBackgroundHandler;
/**
- * A reference counted holder wrapping the {@link ImageReader} that handles JPEG image captures.
- * This is used to allow us to clean up the {@link ImageReader} when all background tasks using
- * its {@link Image}s have completed.
+ * A reference counted holder wrapping the {@link ImageReader} that handles JPEG image
+ * captures. This is used to allow us to clean up the {@link ImageReader} when all background
+ * tasks using its {@link Image}s have completed.
*/
private RefCountedAutoCloseable mJpegImageReader;
@@ -310,8 +341,8 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
private int mState = STATE_CLOSED;
/**
- * Timer to use with pre-capture sequence to ensure a timely capture if 3A convergence is taking
- * too long.
+ * Timer to use with pre-capture sequence to ensure a timely capture if 3A convergence is
+ * taking too long.
*/
private long mCaptureTimer;
@@ -352,7 +383,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
@Override
public void onError(CameraDevice cameraDevice, int error) {
Log.e(TAG, "Received camera device error: " + error);
- synchronized(mCameraStateLock) {
+ synchronized (mCameraStateLock) {
mState = STATE_CLOSED;
mCameraOpenCloseLock.release();
cameraDevice.close();
@@ -402,7 +433,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
= new CameraCaptureSession.CaptureCallback() {
private void process(CaptureResult result) {
- synchronized(mCameraStateLock) {
+ synchronized (mCameraStateLock) {
switch (mState) {
case STATE_PREVIEW: {
// We have nothing to do when the camera preview is running normally.
@@ -416,7 +447,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
// If auto-focus has reached locked state, we are ready to capture
readyToCapture =
(afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
- afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
+ afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
}
// If we are running on an non-legacy device, we should also wait until
@@ -559,9 +590,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
};
public static Camera2RawFragment newInstance() {
- Camera2RawFragment fragment = new Camera2RawFragment();
- fragment.setRetainInstance(true);
- return fragment;
+ return new Camera2RawFragment();
}
@Override
@@ -620,6 +649,20 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
super.onPause();
}
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == REQUEST_CAMERA_PERMISSIONS) {
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ showMissingPermissionError();
+ return;
+ }
+ }
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
@Override
public void onClick(View view) {
switch (view.getId()) {
@@ -659,7 +702,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
// We only use a camera that supports RAW in this sample.
if (!contains(characteristics.get(
- CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES),
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES),
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW)) {
continue;
}
@@ -676,14 +719,14 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
Arrays.asList(map.getOutputSizes(ImageFormat.RAW_SENSOR)),
new CompareSizesByArea());
- synchronized(mCameraStateLock) {
+ synchronized (mCameraStateLock) {
// Set up ImageReaders for JPEG and RAW outputs. Place these in a reference
// counted wrapper to ensure they are only closed when all background tasks
// using them are finished.
if (mJpegImageReader == null || mJpegImageReader.getAndRetain() == null) {
mJpegImageReader = new RefCountedAutoCloseable<>(
ImageReader.newInstance(largestJpeg.getWidth(),
- largestJpeg.getHeight(), ImageFormat.JPEG, /*maxImages*/5));
+ largestJpeg.getHeight(), ImageFormat.JPEG, /*maxImages*/5));
}
mJpegImageReader.get().setOnImageAvailableListener(
mOnJpegImageAvailableListener, mBackgroundHandler);
@@ -691,7 +734,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
if (mRawImageReader == null || mRawImageReader.getAndRetain() == null) {
mRawImageReader = new RefCountedAutoCloseable<>(
ImageReader.newInstance(largestRaw.getWidth(),
- largestRaw.getHeight(), ImageFormat.RAW_SENSOR, /*maxImages*/ 5));
+ largestRaw.getHeight(), ImageFormat.RAW_SENSOR, /*maxImages*/ 5));
}
mRawImageReader.get().setOnImageAvailableListener(
mOnRawImageAvailableListener, mBackgroundHandler);
@@ -718,6 +761,10 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
if (!setUpCameraOutputs()) {
return;
}
+ if (!hasAllPermissionsGranted()) {
+ requestCameraPermissions();
+ return;
+ }
Activity activity = getActivity();
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
@@ -744,13 +791,64 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
}
}
+ /**
+ * Requests permissions necessary to use camera and save pictures.
+ */
+ private void requestCameraPermissions() {
+ if (shouldShowRationale()) {
+ PermissionConfirmationDialog.newInstance().show(getChildFragmentManager(), "dialog");
+ } else {
+ FragmentCompat.requestPermissions(this, CAMERA_PERMISSIONS, REQUEST_CAMERA_PERMISSIONS);
+ }
+ }
+
+ /**
+ * Tells whether all the necessary permissions are granted to this app.
+ *
+ * @return True if all the required permissions are granted.
+ */
+ private boolean hasAllPermissionsGranted() {
+ for (String permission : CAMERA_PERMISSIONS) {
+ if (ActivityCompat.checkSelfPermission(getActivity(), permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Gets whether you should show UI with rationale for requesting the permissions.
+ *
+ * @return True if the UI should be shown.
+ */
+ private boolean shouldShowRationale() {
+ for (String permission : CAMERA_PERMISSIONS) {
+ if (FragmentCompat.shouldShowRequestPermissionRationale(this, permission)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Shows that this app really needs the permission and finishes the app.
+ */
+ private void showMissingPermissionError() {
+ Activity activity = getActivity();
+ if (activity != null) {
+ Toast.makeText(activity, R.string.request_permission, Toast.LENGTH_SHORT).show();
+ activity.finish();
+ }
+ }
+
/**
* Closes the current {@link CameraDevice}.
*/
private void closeCamera() {
try {
mCameraOpenCloseLock.acquire();
- synchronized(mCameraStateLock) {
+ synchronized (mCameraStateLock) {
// Reset state and clean up resources used by the camera.
// Note: After calling this, the ImageReaders will be closed after any background
@@ -787,7 +885,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
private void startBackgroundThread() {
mBackgroundThread = new HandlerThread("CameraBackground");
mBackgroundThread.start();
- synchronized(mCameraStateLock) {
+ synchronized (mCameraStateLock) {
mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
}
}
@@ -810,7 +908,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Creates a new {@link CameraCaptureSession} for camera preview.
- *
+ *
* Call this only with {@link #mCameraStateLock} held.
*/
private void createCameraPreviewSessionLocked() {
@@ -829,8 +927,8 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
// Here, we create a CameraCaptureSession for camera preview.
mCameraDevice.createCaptureSession(Arrays.asList(surface,
- mJpegImageReader.get().getSurface(),
- mRawImageReader.get().getSurface()), new CameraCaptureSession.StateCallback() {
+ mJpegImageReader.get().getSurface(),
+ mRawImageReader.get().getSurface()), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
synchronized (mCameraStateLock) {
@@ -846,7 +944,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
mPreviewRequestBuilder.build(),
mPreCaptureCallback, mBackgroundHandler);
mState = STATE_PREVIEW;
- } catch (CameraAccessException|IllegalStateException e) {
+ } catch (CameraAccessException | IllegalStateException e) {
e.printStackTrace();
return;
}
@@ -869,7 +967,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Configure the given {@link CaptureRequest.Builder} to use auto-focus, auto-exposure, and
* auto-white-balance controls if available.
- *
+ *
* Call this only with {@link #mCameraStateLock} held.
*
* @param builder the builder to configure.
@@ -923,7 +1021,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Configure the necessary {@link android.graphics.Matrix} transformation to `mTextureView`,
* and start/restart the preview capture session if necessary.
- *
+ *
* This method should be called after the camera state has been initialized in
* setUpCameraOutputs.
*
@@ -932,7 +1030,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
*/
private void configureTransform(int viewWidth, int viewHeight) {
Activity activity = getActivity();
- synchronized(mCameraStateLock) {
+ synchronized (mCameraStateLock) {
if (null == mTextureView || null == activity) {
return;
}
@@ -946,6 +1044,8 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
// Find the rotation of the device relative to the native device orientation.
int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
+ Point displaySize = new Point();
+ activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
// Find the rotation of the device relative to the camera sensor's orientation.
int totalRotation = sensorToDeviceRotation(mCharacteristics, deviceRotation);
@@ -955,14 +1055,29 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
boolean swappedDimensions = totalRotation == 90 || totalRotation == 270;
int rotatedViewWidth = viewWidth;
int rotatedViewHeight = viewHeight;
+ int maxPreviewWidth = displaySize.x;
+ int maxPreviewHeight = displaySize.y;
+
if (swappedDimensions) {
rotatedViewWidth = viewHeight;
rotatedViewHeight = viewWidth;
+ maxPreviewWidth = displaySize.y;
+ maxPreviewHeight = displaySize.x;
+ }
+
+ // Preview should not be larger than display size and 1080p.
+ if (maxPreviewWidth > MAX_PREVIEW_WIDTH) {
+ maxPreviewWidth = MAX_PREVIEW_WIDTH;
+ }
+
+ if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) {
+ maxPreviewHeight = MAX_PREVIEW_HEIGHT;
}
// Find the best preview size for these view dimensions and configured JPEG size.
Size previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
- rotatedViewWidth, rotatedViewHeight, largestJpeg);
+ rotatedViewWidth, rotatedViewHeight, maxPreviewWidth, maxPreviewHeight,
+ largestJpeg);
if (swappedDimensions) {
mTextureView.setAspectRatio(
@@ -1027,14 +1142,14 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Initiate a still image capture.
- *
+ *
* This function sends a capture request that initiates a pre-capture sequence in our state
* machine that waits for auto-focus to finish, ending in a "locked" state where the lens is no
* longer moving, waits for auto-exposure to choose a good exposure value, and waits for
* auto-white-balance to converge.
*/
private void takePicture() {
- synchronized(mCameraStateLock) {
+ synchronized (mCameraStateLock) {
mPendingUserCaptures++;
// If we already triggered a pre-capture sequence, or are in a state where we cannot
@@ -1078,7 +1193,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Send a capture request to the camera device that initiates a capture targeting the JPEG and
* RAW outputs.
- *
+ *
* Call this only with {@link #mCameraStateLock} held.
*/
private void captureStillPictureLocked() {
@@ -1127,7 +1242,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Called after a RAW/JPEG capture has completed; resets the AF trigger state for the
* pre-capture sequence.
- *
+ *
* Call this only with {@link #mCameraStateLock} held.
*/
private void finishedCaptureLocked() {
@@ -1156,8 +1271,8 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
* thread.
*
* @param pendingQueue the currently active requests.
- * @param reader a reference counted wrapper containing an {@link ImageReader} from which to
- * acquire an image.
+ * @param reader a reference counted wrapper containing an {@link ImageReader} from which
+ * to acquire an image.
*/
private void dequeueAndSaveImage(TreeMap pendingQueue,
RefCountedAutoCloseable reader) {
@@ -1195,7 +1310,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Runnable that saves an {@link Image} into the specified {@link File}, and updates
* {@link android.provider.MediaStore} to include the resulting file.
- *
+ *
* This can be constructed through an {@link ImageSaverBuilder} as the necessary image and
* result information becomes available.
*/
@@ -1231,8 +1346,8 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
private final RefCountedAutoCloseable mReader;
private ImageSaver(Image image, File file, CaptureResult result,
- CameraCharacteristics characteristics, Context context,
- RefCountedAutoCloseable reader) {
+ CameraCharacteristics characteristics, Context context,
+ RefCountedAutoCloseable reader) {
mImage = image;
mFile = file;
mCaptureResult = result;
@@ -1245,7 +1360,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
public void run() {
boolean success = false;
int format = mImage.getFormat();
- switch(format) {
+ switch (format) {
case ImageFormat.JPEG: {
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
@@ -1289,7 +1404,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
// If saving the file succeeded, update MediaStore.
if (success) {
- MediaScannerConnection.scanFile(mContext, new String[] { mFile.getPath()},
+ MediaScannerConnection.scanFile(mContext, new String[]{mFile.getPath()},
/*mimeTypes*/null, new MediaScannerConnection.MediaScannerConnectionClient() {
@Override
public void onMediaScannerConnected() {
@@ -1307,7 +1422,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Builder class for constructing {@link ImageSaver}s.
- *
+ *
* This class is thread safe.
*/
public static class ImageSaverBuilder {
@@ -1320,8 +1435,9 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Construct a new ImageSaverBuilder using the given {@link Context}.
+ *
* @param context a {@link Context} to for accessing the
- * {@link android.provider.MediaStore}.
+ * {@link android.provider.MediaStore}.
*/
public ImageSaverBuilder(final Context context) {
mContext = context;
@@ -1329,7 +1445,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
public synchronized ImageSaverBuilder setRefCountedReader(
RefCountedAutoCloseable reader) {
- if (reader == null ) throw new NullPointerException();
+ if (reader == null) throw new NullPointerException();
mReader = reader;
return this;
@@ -1440,6 +1556,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Wrap the given object.
+ *
* @param object an object to wrap.
*/
public RefCountedAutoCloseable(T object) {
@@ -1491,31 +1608,47 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
}
/**
- * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
- * width and height are at least as large as the respective requested values, and whose aspect
- * ratio matches with the specified value.
+ * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that
+ * is at least as large as the respective texture view size, and that is at most as large as the
+ * respective max size, and whose aspect ratio matches with the specified value. If such size
+ * doesn't exist, choose the largest one that is at most as large as the respective max size,
+ * and whose aspect ratio matches with the specified value.
*
- * @param choices The list of sizes that the camera supports for the intended output class
- * @param width The minimum desired width
- * @param height The minimum desired height
- * @param aspectRatio The aspect ratio
+ * @param choices The list of sizes that the camera supports for the intended output
+ * class
+ * @param textureViewWidth The width of the texture view relative to sensor coordinate
+ * @param textureViewHeight The height of the texture view relative to sensor coordinate
+ * @param maxWidth The maximum width that can be chosen
+ * @param maxHeight The maximum height that can be chosen
+ * @param aspectRatio The aspect ratio
* @return The optimal {@code Size}, or an arbitrary one if none were big enough
*/
- private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
+ private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
+ int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
// Collect the supported resolutions that are at least as big as the preview Surface
List bigEnough = new ArrayList<>();
+ // Collect the supported resolutions that are smaller than the preview Surface
+ List notBigEnough = new ArrayList<>();
int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight();
for (Size option : choices) {
- if (option.getHeight() == option.getWidth() * h / w &&
- option.getWidth() >= width && option.getHeight() >= height) {
- bigEnough.add(option);
+ if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
+ option.getHeight() == option.getWidth() * h / w) {
+ if (option.getWidth() >= textureViewWidth &&
+ option.getHeight() >= textureViewHeight) {
+ bigEnough.add(option);
+ } else {
+ notBigEnough.add(option);
+ }
}
}
- // Pick the smallest of those, assuming we found any
+ // Pick the smallest of those big enough. If there is no one big enough, pick the
+ // largest of those not big enough.
if (bigEnough.size() > 0) {
return Collections.min(bigEnough, new CompareSizesByArea());
+ } else if (notBigEnough.size() > 0) {
+ return Collections.max(notBigEnough, new CompareSizesByArea());
} else {
Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0];
@@ -1551,7 +1684,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
* Return true if the given array contains the given integer.
*
* @param modes array to check.
- * @param mode integer to get for.
+ * @param mode integer to get for.
* @return true if the array contains the given integer, otherwise false.
*/
private static boolean contains(int[] modes, int mode) {
@@ -1582,7 +1715,9 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Rotation need to transform from the camera sensor orientation to the device's current
* orientation.
- * @param c the {@link CameraCharacteristics} to query for the camera sensor orientation.
+ *
+ * @param c the {@link CameraCharacteristics} to query for the camera sensor
+ * orientation.
* @param deviceOrientation the current device orientation relative to the native device
* orientation.
* @return the total rotation from the sensor orientation to the current device orientation.
@@ -1620,12 +1755,12 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
* If the given request has been completed, remove it from the queue of active requests and
* send an {@link ImageSaver} with the results from this request to a background thread to
* save a file.
- *
+ *
* Call this only with {@link #mCameraStateLock} held.
*
* @param requestId the ID of the {@link CaptureRequest} to handle.
- * @param builder the {@link ImageSaver.ImageSaverBuilder} for this request.
- * @param queue the queue to remove this request from, if completed.
+ * @param builder the {@link ImageSaver.ImageSaverBuilder} for this request.
+ * @param queue the queue to remove this request from, if completed.
*/
private void handleCompletionLocked(int requestId, ImageSaver.ImageSaverBuilder builder,
TreeMap queue) {
@@ -1639,7 +1774,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Check if we are using a device that only supports the LEGACY hardware level.
- *
+ *
* Call this only with {@link #mCameraStateLock} held.
*
* @return true if this is a legacy device.
@@ -1651,7 +1786,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Start the timer for the pre-capture sequence.
- *
+ *
* Call this only with {@link #mCameraStateLock} held.
*/
private void startTimerLocked() {
@@ -1660,7 +1795,7 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
/**
* Check if the timer for the pre-capture sequence has been hit.
- *
+ *
* Call this only with {@link #mCameraStateLock} held.
*
* @return true if the timeout occurred.
@@ -1669,6 +1804,37 @@ public class Camera2RawFragment extends Fragment implements View.OnClickListener
return (SystemClock.elapsedRealtime() - mCaptureTimer) > PRECAPTURE_TIMEOUT_MS;
}
- // *********************************************************************************************
+ /**
+ * A dialog that explains about the necessary permissions.
+ */
+ public static class PermissionConfirmationDialog extends DialogFragment {
+
+ public static PermissionConfirmationDialog newInstance() {
+ return new PermissionConfirmationDialog();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Fragment parent = getParentFragment();
+ return new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.request_permission)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ FragmentCompat.requestPermissions(parent, CAMERA_PERMISSIONS,
+ REQUEST_CAMERA_PERMISSIONS);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ getActivity().finish();
+ }
+ })
+ .create();
+ }
+
+ }
}
diff --git a/samples/browseable/Camera2Video/res/values-v11/template-styles.xml b/samples/browseable/Camera2Video/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/Camera2Video/res/values-v11/template-styles.xml
+++ b/samples/browseable/Camera2Video/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/Camera2Video/res/values-v21/base-template-styles.xml b/samples/browseable/Camera2Video/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/Camera2Video/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/Camera2Video/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/Camera2Video/res/values/strings.xml b/samples/browseable/Camera2Video/res/values/strings.xml
index bf5e439da..bce323f1a 100644
--- a/samples/browseable/Camera2Video/res/values/strings.xml
+++ b/samples/browseable/Camera2Video/res/values/strings.xml
@@ -3,4 +3,6 @@
RecordStopInfo
+ This sample needs permission for camera and audio recording.
+ This device doesn\'t support Camera2 API.
\ No newline at end of file
diff --git a/samples/browseable/Camera2Video/res/values/template-styles.xml b/samples/browseable/Camera2Video/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/Camera2Video/res/values/template-styles.xml
+++ b/samples/browseable/Camera2Video/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/Camera2Video/src/com.example.android.camera2video/Camera2VideoFragment.java b/samples/browseable/Camera2Video/src/com.example.android.camera2video/Camera2VideoFragment.java
index 78e276a97..1ea53187f 100644
--- a/samples/browseable/Camera2Video/src/com.example.android.camera2video/Camera2VideoFragment.java
+++ b/samples/browseable/Camera2Video/src/com.example.android.camera2video/Camera2VideoFragment.java
@@ -16,6 +16,7 @@
package com.example.android.camera2video;
+import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
@@ -23,6 +24,7 @@ import android.app.DialogFragment;
import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Matrix;
import android.graphics.RectF;
@@ -39,6 +41,9 @@ import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
+import android.support.annotation.NonNull;
+import android.support.v13.app.FragmentCompat;
+import android.support.v4.app.ActivityCompat;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
@@ -59,11 +64,19 @@ import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
-public class Camera2VideoFragment extends Fragment implements View.OnClickListener {
+public class Camera2VideoFragment extends Fragment
+ implements View.OnClickListener, FragmentCompat.OnRequestPermissionsResultCallback {
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
private static final String TAG = "Camera2VideoFragment";
+ private static final int REQUEST_VIDEO_PERMISSIONS = 1;
+ private static final String FRAGMENT_DIALOG = "dialog";
+
+ private static final String[] VIDEO_PERMISSIONS = {
+ Manifest.permission.CAMERA,
+ Manifest.permission.RECORD_AUDIO,
+ };
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
@@ -88,7 +101,8 @@ public class Camera2VideoFragment extends Fragment implements View.OnClickListen
private CameraDevice mCameraDevice;
/**
- * A reference to the current {@link android.hardware.camera2.CameraCaptureSession} for preview.
+ * A reference to the current {@link android.hardware.camera2.CameraCaptureSession} for
+ * preview.
*/
private CameraCaptureSession mPreviewSession;
@@ -198,14 +212,12 @@ public class Camera2VideoFragment extends Fragment implements View.OnClickListen
};
public static Camera2VideoFragment newInstance() {
- Camera2VideoFragment fragment = new Camera2VideoFragment();
- fragment.setRetainInstance(true);
- return fragment;
+ return new Camera2VideoFragment();
}
/**
- * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes larger
- * than 1080p, since MediaRecorder cannot handle such a high-resolution video.
+ * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes
+ * larger than 1080p, since MediaRecorder cannot handle such a high-resolution video.
*
* @param choices The list of available sizes
* @return The video size
@@ -331,16 +343,79 @@ public class Camera2VideoFragment extends Fragment implements View.OnClickListen
}
}
+ /**
+ * Gets whether you should show UI with rationale for requesting permissions.
+ *
+ * @param permissions The permissions your app wants to request.
+ * @return Whether you can show permission rationale UI.
+ */
+ private boolean shouldShowRequestPermissionRationale(String[] permissions) {
+ for (String permission : permissions) {
+ if (FragmentCompat.shouldShowRequestPermissionRationale(this, permission)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Requests permissions needed for recording video.
+ */
+ private void requestVideoPermissions() {
+ if (shouldShowRequestPermissionRationale(VIDEO_PERMISSIONS)) {
+ new ConfirmationDialog().show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ } else {
+ FragmentCompat.requestPermissions(this, VIDEO_PERMISSIONS, REQUEST_VIDEO_PERMISSIONS);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ Log.d(TAG, "onRequestPermissionsResult");
+ if (requestCode == REQUEST_VIDEO_PERMISSIONS) {
+ if (grantResults.length == VIDEO_PERMISSIONS.length) {
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ ErrorDialog.newInstance(getString(R.string.permission_request))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ break;
+ }
+ }
+ } else {
+ ErrorDialog.newInstance(getString(R.string.permission_request))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
+ }
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ private boolean hasPermissionsGranted(String[] permissions) {
+ for (String permission : permissions) {
+ if (ActivityCompat.checkSelfPermission(getActivity(), permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Tries to open a {@link CameraDevice}. The result is listened by `mStateCallback`.
*/
private void openCamera(int width, int height) {
+ if (!hasPermissionsGranted(VIDEO_PERMISSIONS)) {
+ requestVideoPermissions();
+ return;
+ }
final Activity activity = getActivity();
if (null == activity || activity.isFinishing()) {
return;
}
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
+ Log.d(TAG, "tryAcquire");
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
@@ -369,7 +444,8 @@ public class Camera2VideoFragment extends Fragment implements View.OnClickListen
} catch (NullPointerException e) {
// Currently an NPE is thrown when the Camera2API is used but not supported on the
// device this code runs.
- new ErrorDialog().show(getFragmentManager(), "dialog");
+ ErrorDialog.newInstance(getString(R.string.camera_error))
+ .show(getChildFragmentManager(), FRAGMENT_DIALOG);
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera opening.");
}
@@ -389,7 +465,7 @@ public class Camera2VideoFragment extends Fragment implements View.OnClickListen
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera closing.");
} finally {
- mCameraOpenCloseLock.release();
+ mCameraOpenCloseLock.release();
}
}
@@ -559,11 +635,21 @@ public class Camera2VideoFragment extends Fragment implements View.OnClickListen
public static class ErrorDialog extends DialogFragment {
+ private static final String ARG_MESSAGE = "message";
+
+ public static ErrorDialog newInstance(String message) {
+ ErrorDialog dialog = new ErrorDialog();
+ Bundle args = new Bundle();
+ args.putString(ARG_MESSAGE, message);
+ dialog.setArguments(args);
+ return dialog;
+ }
+
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = getActivity();
return new AlertDialog.Builder(activity)
- .setMessage("This device doesn't support Camera2 API.")
+ .setMessage(getArguments().getString(ARG_MESSAGE))
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
@@ -575,4 +661,30 @@ public class Camera2VideoFragment extends Fragment implements View.OnClickListen
}
+ public static class ConfirmationDialog extends DialogFragment {
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Fragment parent = getParentFragment();
+ return new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.permission_request)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ FragmentCompat.requestPermissions(parent, VIDEO_PERMISSIONS,
+ REQUEST_VIDEO_PERMISSIONS);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ parent.getActivity().finish();
+ }
+ })
+ .create();
+ }
+
+ }
+
}
diff --git a/samples/browseable/CardEmulation/res/values-v11/template-styles.xml b/samples/browseable/CardEmulation/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/CardEmulation/res/values-v11/template-styles.xml
+++ b/samples/browseable/CardEmulation/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/CardEmulation/res/values-v21/base-template-styles.xml b/samples/browseable/CardEmulation/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/CardEmulation/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/CardEmulation/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/CardEmulation/res/values/template-styles.xml b/samples/browseable/CardEmulation/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/CardEmulation/res/values/template-styles.xml
+++ b/samples/browseable/CardEmulation/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/CardReader/res/values-v11/template-styles.xml b/samples/browseable/CardReader/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/CardReader/res/values-v11/template-styles.xml
+++ b/samples/browseable/CardReader/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/CardReader/res/values-v21/base-template-styles.xml b/samples/browseable/CardReader/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/CardReader/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/CardReader/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/CardReader/res/values/template-styles.xml b/samples/browseable/CardReader/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/CardReader/res/values/template-styles.xml
+++ b/samples/browseable/CardReader/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/CardView/res/values-v11/template-styles.xml b/samples/browseable/CardView/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/CardView/res/values-v11/template-styles.xml
+++ b/samples/browseable/CardView/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/CardView/res/values-v21/base-template-styles.xml b/samples/browseable/CardView/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/CardView/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/CardView/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/CardView/res/values/template-styles.xml b/samples/browseable/CardView/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/CardView/res/values/template-styles.xml
+++ b/samples/browseable/CardView/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ClippingBasic/res/values-v11/template-styles.xml b/samples/browseable/ClippingBasic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ClippingBasic/res/values-v11/template-styles.xml
+++ b/samples/browseable/ClippingBasic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ClippingBasic/res/values-v21/base-template-styles.xml b/samples/browseable/ClippingBasic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ClippingBasic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ClippingBasic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ClippingBasic/res/values/template-styles.xml b/samples/browseable/ClippingBasic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ClippingBasic/res/values/template-styles.xml
+++ b/samples/browseable/ClippingBasic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ConfirmCredential/_index.jd b/samples/browseable/ConfirmCredential/_index.jd
index 206db4457..8fb4ecfd1 100644
--- a/samples/browseable/ConfirmCredential/_index.jd
+++ b/samples/browseable/ConfirmCredential/_index.jd
@@ -1,6 +1,6 @@
page.tags="Confirm Credential"
-sample.group=security
+sample.group=Security
@jd:body
+
+This sample demonstrates how to provide the Direct Share feature. The app shows some options
+directly in the list of share intent candidates.
+
+
diff --git a/samples/browseable/DirectShare/res/drawable-hdpi/tile.9.png b/samples/browseable/DirectShare/res/drawable-hdpi/tile.9.png
new file mode 100644
index 000000000..135862883
Binary files /dev/null and b/samples/browseable/DirectShare/res/drawable-hdpi/tile.9.png differ
diff --git a/samples/browseable/DirectShare/res/layout/contact.xml b/samples/browseable/DirectShare/res/layout/contact.xml
new file mode 100644
index 000000000..81122e170
--- /dev/null
+++ b/samples/browseable/DirectShare/res/layout/contact.xml
@@ -0,0 +1,30 @@
+
+
+
diff --git a/samples/browseable/DirectShare/res/layout/main.xml b/samples/browseable/DirectShare/res/layout/main.xml
new file mode 100644
index 000000000..5b24b4a89
--- /dev/null
+++ b/samples/browseable/DirectShare/res/layout/main.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/DirectShare/res/layout/select_contact.xml b/samples/browseable/DirectShare/res/layout/select_contact.xml
new file mode 100644
index 000000000..f219805f4
--- /dev/null
+++ b/samples/browseable/DirectShare/res/layout/select_contact.xml
@@ -0,0 +1,25 @@
+
+
+
diff --git a/samples/browseable/DirectShare/res/layout/send_message.xml b/samples/browseable/DirectShare/res/layout/send_message.xml
new file mode 100644
index 000000000..86671fcc2
--- /dev/null
+++ b/samples/browseable/DirectShare/res/layout/send_message.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/DirectShare/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/DirectShare/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..988f2ec8a
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/browseable/DirectShare/res/mipmap-hdpi/logo_avatar.png b/samples/browseable/DirectShare/res/mipmap-hdpi/logo_avatar.png
new file mode 100644
index 000000000..8892c08dc
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-hdpi/logo_avatar.png differ
diff --git a/samples/browseable/DirectShare/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/DirectShare/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..0baa1cc7d
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/browseable/DirectShare/res/mipmap-mdpi/logo_avatar.png b/samples/browseable/DirectShare/res/mipmap-mdpi/logo_avatar.png
new file mode 100644
index 000000000..c2de7747c
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-mdpi/logo_avatar.png differ
diff --git a/samples/browseable/DirectShare/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/DirectShare/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..da0aa2f2b
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/browseable/DirectShare/res/mipmap-xhdpi/logo_avatar.png b/samples/browseable/DirectShare/res/mipmap-xhdpi/logo_avatar.png
new file mode 100644
index 000000000..10c2dc9af
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-xhdpi/logo_avatar.png differ
diff --git a/samples/browseable/DirectShare/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/DirectShare/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..e1cc1ff1a
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/DirectShare/res/mipmap-xxhdpi/logo_avatar.png b/samples/browseable/DirectShare/res/mipmap-xxhdpi/logo_avatar.png
new file mode 100644
index 000000000..df02f04f7
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-xxhdpi/logo_avatar.png differ
diff --git a/samples/browseable/DirectShare/res/mipmap-xxxhdpi/ic_launcher.png b/samples/browseable/DirectShare/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8b0f60c49
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/DirectShare/res/mipmap-xxxhdpi/logo_avatar.png b/samples/browseable/DirectShare/res/mipmap-xxxhdpi/logo_avatar.png
new file mode 100644
index 000000000..dc8d37694
Binary files /dev/null and b/samples/browseable/DirectShare/res/mipmap-xxxhdpi/logo_avatar.png differ
diff --git a/samples/browseable/DirectShare/res/values-sw600dp/template-dimens.xml b/samples/browseable/DirectShare/res/values-sw600dp/template-dimens.xml
new file mode 100644
index 000000000..22074a2bd
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values-sw600dp/template-dimens.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ @dimen/margin_huge
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/DirectShare/res/values-sw600dp/template-styles.xml b/samples/browseable/DirectShare/res/values-sw600dp/template-styles.xml
new file mode 100644
index 000000000..03d197418
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values-sw600dp/template-styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/DirectShare/res/values-v11/template-styles.xml b/samples/browseable/DirectShare/res/values-v11/template-styles.xml
new file mode 100644
index 000000000..8c1ea66f2
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values-v11/template-styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/DirectShare/res/values-v21/base-colors.xml b/samples/browseable/DirectShare/res/values-v21/base-colors.xml
new file mode 100644
index 000000000..8b6ec3f85
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values-v21/base-colors.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/DirectShare/res/values-v21/base-template-styles.xml b/samples/browseable/DirectShare/res/values-v21/base-template-styles.xml
new file mode 100644
index 000000000..c778e4f98
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values-v21/base-template-styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/DirectShare/res/values/base-strings.xml b/samples/browseable/DirectShare/res/values/base-strings.xml
new file mode 100644
index 000000000..b3f15fbb1
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values/base-strings.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ DirectShare
+
+
+
+
diff --git a/samples/browseable/DirectShare/res/values/colors.xml b/samples/browseable/DirectShare/res/values/colors.xml
new file mode 100644
index 000000000..c5a6a3db7
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values/colors.xml
@@ -0,0 +1,26 @@
+
+
+
+ #3F51B5
+ #303F9F
+ #C5CAE9
+ #00BCD4
+ #212121
+ #727272
+ #FFFFFF
+ #B6B6B6
+
diff --git a/samples/browseable/DirectShare/res/values/dimens.xml b/samples/browseable/DirectShare/res/values/dimens.xml
new file mode 100644
index 000000000..2d05f5d3b
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values/dimens.xml
@@ -0,0 +1,20 @@
+
+
+
+ 24dp
+ 8dp
+
diff --git a/samples/browseable/DirectShare/res/values/strings.xml b/samples/browseable/DirectShare/res/values/strings.xml
new file mode 100644
index 000000000..ddc858a96
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values/strings.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+ This app demonstrates how to implement Direct Share. Use some other app and share a text.
+ For your convenience, you can also use the input below to share the text.
+
+ Hello!
+ Share
+ Send a message via:
+
+
+
+ Sending a message
+ To:
+ Send
+ Body:
+ Edit your message.
+ Sent a message \"%1$s\" to %2$s.
+ Text to share
+
+
diff --git a/samples/browseable/DirectShare/res/values/styles.xml b/samples/browseable/DirectShare/res/values/styles.xml
new file mode 100644
index 000000000..ae312cc42
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values/styles.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/DirectShare/res/values/template-dimens.xml b/samples/browseable/DirectShare/res/values/template-dimens.xml
new file mode 100644
index 000000000..39e710b5c
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values/template-dimens.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ 4dp
+ 8dp
+ 16dp
+ 32dp
+ 64dp
+
+
+
+ @dimen/margin_medium
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/DirectShare/res/values/template-styles.xml b/samples/browseable/DirectShare/res/values/template-styles.xml
new file mode 100644
index 000000000..6e7d593dd
--- /dev/null
+++ b/samples/browseable/DirectShare/res/values/template-styles.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/DirectShare/src/com.example.android.directshare/Contact.java b/samples/browseable/DirectShare/src/com.example.android.directshare/Contact.java
new file mode 100644
index 000000000..4a1665e36
--- /dev/null
+++ b/samples/browseable/DirectShare/src/com.example.android.directshare/Contact.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.directshare;
+
+/**
+ * Provides the list of dummy contacts. This sample implements this as constants, but real-life apps
+ * should use a database and such.
+ */
+public class Contact {
+
+ /**
+ * The list of dummy contacts.
+ */
+ public static final Contact[] CONTACTS = {
+ new Contact("Tereasa"),
+ new Contact("Chang"),
+ new Contact("Kory"),
+ new Contact("Clare"),
+ new Contact("Landon"),
+ new Contact("Kyle"),
+ new Contact("Deana"),
+ new Contact("Daria"),
+ new Contact("Melisa"),
+ new Contact("Sammie"),
+ };
+
+ /**
+ * The contact ID.
+ */
+ public static final String ID = "contact_id";
+
+ /**
+ * Representative invalid contact ID.
+ */
+ public static final int INVALID_ID = -1;
+
+ /**
+ * The name of this contact.
+ */
+ private final String mName;
+
+ /**
+ * Instantiates a new {@link Contact}.
+ *
+ * @param name The name of the contact.
+ */
+ public Contact(String name) {
+ mName = name;
+ }
+
+ /**
+ * Finds a {@link Contact} specified by a contact ID.
+ *
+ * @param id The contact ID. This needs to be a valid ID.
+ * @return A {@link Contact}
+ */
+ public static Contact byId(int id) {
+ return CONTACTS[id];
+ }
+
+ /**
+ * Gets the name of this contact.
+ *
+ * @return The name of this contact.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Gets the icon of this contact.
+ *
+ * @return The icon.
+ */
+ public int getIcon() {
+ return R.mipmap.logo_avatar;
+ }
+
+}
diff --git a/samples/browseable/DirectShare/src/com.example.android.directshare/ContactViewBinder.java b/samples/browseable/DirectShare/src/com.example.android.directshare/ContactViewBinder.java
new file mode 100644
index 000000000..5287b1c79
--- /dev/null
+++ b/samples/browseable/DirectShare/src/com.example.android.directshare/ContactViewBinder.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.directshare;
+
+import android.widget.TextView;
+
+/**
+ * A simple utility to bind a {@link TextView} with a {@link Contact}.
+ */
+public class ContactViewBinder {
+
+ /**
+ * Binds the {@code textView} with the specified {@code contact}.
+ *
+ * @param contact The contact.
+ * @param textView The TextView.
+ */
+ public static void bind(Contact contact, TextView textView) {
+ textView.setText(contact.getName());
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(contact.getIcon(), 0, 0, 0);
+ }
+
+}
diff --git a/samples/browseable/DirectShare/src/com.example.android.directshare/MainActivity.java b/samples/browseable/DirectShare/src/com.example.android.directshare/MainActivity.java
new file mode 100644
index 000000000..d68018621
--- /dev/null
+++ b/samples/browseable/DirectShare/src/com.example.android.directshare/MainActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.directshare;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Toolbar;
+
+/**
+ * Provides the landing screen of this sample. There is nothing particularly interesting here. All
+ * the codes related to the Direct Share feature are in {@link SampleChooserTargetService}.
+ */
+public class MainActivity extends Activity {
+
+ private EditText mEditBody;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+ setActionBar((Toolbar) findViewById(R.id.toolbar));
+ mEditBody = (EditText) findViewById(R.id.body);
+ findViewById(R.id.share).setOnClickListener(mOnClickListener);
+ }
+
+ private View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.share:
+ share();
+ break;
+ }
+ }
+ };
+
+ /**
+ * Emits a sample share {@link Intent}.
+ */
+ private void share() {
+ Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
+ sharingIntent.setType("text/plain");
+ sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, mEditBody.getText().toString());
+ startActivity(Intent.createChooser(sharingIntent, getString(R.string.send_intent_title)));
+ }
+
+}
diff --git a/samples/browseable/DirectShare/src/com.example.android.directshare/SampleChooserTargetService.java b/samples/browseable/DirectShare/src/com.example.android.directshare/SampleChooserTargetService.java
new file mode 100644
index 000000000..1e3259918
--- /dev/null
+++ b/samples/browseable/DirectShare/src/com.example.android.directshare/SampleChooserTargetService.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.directshare;
+
+import android.content.ComponentName;
+import android.content.IntentFilter;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.service.chooser.ChooserTarget;
+import android.service.chooser.ChooserTargetService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides the Direct Share items to the system.
+ */
+public class SampleChooserTargetService extends ChooserTargetService {
+
+ @Override
+ public List onGetChooserTargets(ComponentName targetActivityName,
+ IntentFilter matchedFilter) {
+ ComponentName componentName = new ComponentName(getPackageName(),
+ SendMessageActivity.class.getCanonicalName());
+ // The list of Direct Share items. The system will show the items the way they are sorted
+ // in this list.
+ ArrayList targets = new ArrayList<>();
+ for (int i = 0; i < Contact.CONTACTS.length; ++i) {
+ Contact contact = Contact.byId(i);
+ Bundle extras = new Bundle();
+ extras.putInt(Contact.ID, i);
+ targets.add(new ChooserTarget(
+ // The name of this target.
+ contact.getName(),
+ // The icon to represent this target.
+ Icon.createWithResource(this, contact.getIcon()),
+ // The ranking score for this target (0.0-1.0); the system will omit items with
+ // low scores when there are too many Direct Share items.
+ 0.5f,
+ // The name of the component to be launched if this target is chosen.
+ componentName,
+ // The extra values here will be merged into the Intent when this target is
+ // chosen.
+ extras));
+ }
+ return targets;
+ }
+
+}
diff --git a/samples/browseable/DirectShare/src/com.example.android.directshare/SelectContactActivity.java b/samples/browseable/DirectShare/src/com.example.android.directshare/SelectContactActivity.java
new file mode 100644
index 000000000..440facb16
--- /dev/null
+++ b/samples/browseable/DirectShare/src/com.example.android.directshare/SelectContactActivity.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.directshare;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * The dialog for selecting a contact to share the text with. This dialog is shown when the user
+ * taps on this sample's icon rather than any of the Direct Share contacts.
+ */
+public class SelectContactActivity extends Activity {
+
+ /**
+ * The action string for Intents.
+ */
+ public static final String ACTION_SELECT_CONTACT
+ = "com.example.android.directshare.intent.action.SELECT_CONTACT";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.select_contact);
+ Intent intent = getIntent();
+ if (!ACTION_SELECT_CONTACT.equals(intent.getAction())) {
+ finish();
+ return;
+ }
+ // Set up the list of contacts
+ ListView list = (ListView) findViewById(R.id.list);
+ list.setAdapter(mAdapter);
+ list.setOnItemClickListener(mOnItemClickListener);
+ }
+
+ private final ListAdapter mAdapter = new BaseAdapter() {
+ @Override
+ public int getCount() {
+ return Contact.CONTACTS.length;
+ }
+
+ @Override
+ public Object getItem(int i) {
+ return Contact.byId(i);
+ }
+
+ @Override
+ public long getItemId(int i) {
+ return i;
+ }
+
+ @Override
+ public View getView(int i, View view, ViewGroup parent) {
+ if (view == null) {
+ view = LayoutInflater.from(parent.getContext()).inflate(R.layout.contact, parent,
+ false);
+ }
+ TextView textView = (TextView) view;
+ Contact contact = (Contact) getItem(i);
+ ContactViewBinder.bind(contact, textView);
+ return textView;
+ }
+ };
+
+ private final AdapterView.OnItemClickListener mOnItemClickListener
+ = new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
+ Intent data = new Intent();
+ data.putExtra(Contact.ID, i);
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ };
+
+}
diff --git a/samples/browseable/DirectShare/src/com.example.android.directshare/SendMessageActivity.java b/samples/browseable/DirectShare/src/com.example.android.directshare/SendMessageActivity.java
new file mode 100644
index 000000000..d291172e6
--- /dev/null
+++ b/samples/browseable/DirectShare/src/com.example.android.directshare/SendMessageActivity.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.directshare;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * Provides the UI for sharing a text with a {@link Contact}.
+ */
+public class SendMessageActivity extends Activity {
+
+ /**
+ * The request code for {@link SelectContactActivity}. This is used when the user doesn't select
+ * any of Direct Share icons.
+ */
+ private static final int REQUEST_SELECT_CONTACT = 1;
+
+ /**
+ * The text to share.
+ */
+ private String mBody;
+
+ /**
+ * The ID of the contact to share the text with.
+ */
+ private int mContactId;
+
+ // View references.
+ private TextView mTextContactName;
+ private TextView mTextMessageBody;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.send_message);
+ setTitle(R.string.sending_message);
+ // View references.
+ mTextContactName = (TextView) findViewById(R.id.contact_name);
+ mTextMessageBody = (TextView) findViewById(R.id.message_body);
+ // Resolve the share Intent.
+ boolean resolved = resolveIntent(getIntent());
+ if (!resolved) {
+ finish();
+ return;
+ }
+ // Bind event handlers.
+ findViewById(R.id.send).setOnClickListener(mOnClickListener);
+ // Set up the UI.
+ prepareUi();
+ // The contact ID will not be passed on when the user clicks on the app icon rather than any
+ // of the Direct Share icons. In this case, we show another dialog for selecting a contact.
+ if (mContactId == Contact.INVALID_ID) {
+ selectContact();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_SELECT_CONTACT:
+ if (resultCode == RESULT_OK) {
+ mContactId = data.getIntExtra(Contact.ID, Contact.INVALID_ID);
+ }
+ // Give up sharing the send_message if the user didn't choose a contact.
+ if (mContactId == Contact.INVALID_ID) {
+ finish();
+ return;
+ }
+ prepareUi();
+ break;
+ default:
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ /**
+ * Resolves the passed {@link Intent}. This method can only resolve intents for sharing a plain
+ * text. {@link #mBody} and {@link #mContactId} are modified accordingly.
+ *
+ * @param intent The {@link Intent}.
+ * @return True if the {@code intent} is resolved properly.
+ */
+ private boolean resolveIntent(Intent intent) {
+ if (Intent.ACTION_SEND.equals(intent.getAction()) &&
+ "text/plain".equals(intent.getType())) {
+ mBody = intent.getStringExtra(Intent.EXTRA_TEXT);
+ mContactId = intent.getIntExtra(Contact.ID, Contact.INVALID_ID);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Sets up the UI.
+ */
+ private void prepareUi() {
+ if (mContactId != Contact.INVALID_ID) {
+ Contact contact = Contact.byId(mContactId);
+ ContactViewBinder.bind(contact, mTextContactName);
+ }
+ mTextMessageBody.setText(mBody);
+ }
+
+ /**
+ * Delegates selection of a {@Contact} to {@link SelectContactActivity}.
+ */
+ private void selectContact() {
+ Intent intent = new Intent(this, SelectContactActivity.class);
+ intent.setAction(SelectContactActivity.ACTION_SELECT_CONTACT);
+ startActivityForResult(intent, REQUEST_SELECT_CONTACT);
+ }
+
+ private View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.send:
+ send();
+ break;
+ }
+ }
+ };
+
+ /**
+ * Pretends to send the text to the contact. This only shows a dummy message.
+ */
+ private void send() {
+ Toast.makeText(this,
+ getString(R.string.message_sent, mBody, Contact.byId(mContactId).getName()),
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+}
diff --git a/samples/browseable/DirectorySelection/res/layout/fragment_directory_selection.xml b/samples/browseable/DirectorySelection/res/layout/fragment_directory_selection.xml
index d63219c5d..631a8fc40 100644
--- a/samples/browseable/DirectorySelection/res/layout/fragment_directory_selection.xml
+++ b/samples/browseable/DirectorySelection/res/layout/fragment_directory_selection.xml
@@ -16,6 +16,7 @@
-->
+ android:layout_height="match_parent"
+ app:layoutManager="LinearLayoutManager"
+ />
diff --git a/samples/browseable/DirectorySelection/res/values-v11/template-styles.xml b/samples/browseable/DirectorySelection/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/DirectorySelection/res/values-v11/template-styles.xml
+++ b/samples/browseable/DirectorySelection/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/DirectorySelection/res/values-v21/base-template-styles.xml b/samples/browseable/DirectorySelection/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/DirectorySelection/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/DirectorySelection/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/DirectorySelection/res/values/template-styles.xml b/samples/browseable/DirectorySelection/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/DirectorySelection/res/values/template-styles.xml
+++ b/samples/browseable/DirectorySelection/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/DirectorySelection/src/com.example.android.directoryselection/DirectorySelectionFragment.java b/samples/browseable/DirectorySelection/src/com.example.android.directoryselection/DirectorySelectionFragment.java
index 4af55dbe1..075f39b77 100644
--- a/samples/browseable/DirectorySelection/src/com.example.android.directoryselection/DirectorySelectionFragment.java
+++ b/samples/browseable/DirectorySelection/src/com.example.android.directoryselection/DirectorySelectionFragment.java
@@ -27,7 +27,6 @@ import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.support.v4.app.Fragment;
-import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
@@ -124,8 +123,7 @@ public class DirectorySelectionFragment extends Fragment {
}
});
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_directory_entries);
- mLayoutManager = new LinearLayoutManager(getActivity());
- mRecyclerView.setLayoutManager(mLayoutManager);
+ mLayoutManager = mRecyclerView.getLayoutManager();
mRecyclerView.scrollToPosition(0);
mAdapter = new DirectoryEntryAdapter(new ArrayList());
mRecyclerView.setAdapter(mAdapter);
diff --git a/samples/browseable/DisplayingBitmaps/res/layout/image_detail_fragment.xml b/samples/browseable/DisplayingBitmaps/res/layout/image_detail_fragment.xml
index 97ac520cc..d8bd2f705 100644
--- a/samples/browseable/DisplayingBitmaps/res/layout/image_detail_fragment.xml
+++ b/samples/browseable/DisplayingBitmaps/res/layout/image_detail_fragment.xml
@@ -20,6 +20,7 @@
android:layout_height="fill_parent" >
-
+
diff --git a/samples/browseable/DisplayingBitmaps/res/values-v21/base-template-styles.xml b/samples/browseable/DisplayingBitmaps/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/DisplayingBitmaps/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/DisplayingBitmaps/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/DisplayingBitmaps/res/values/template-styles.xml b/samples/browseable/DisplayingBitmaps/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/DisplayingBitmaps/res/values/template-styles.xml
+++ b/samples/browseable/DisplayingBitmaps/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailFragment.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailFragment.java
index 506729a7e..75d16c1d9 100644
--- a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailFragment.java
+++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailFragment.java
@@ -23,6 +23,7 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
+import android.widget.ProgressBar;
import com.example.android.displayingbitmaps.R;
import com.example.android.displayingbitmaps.util.ImageFetcher;
@@ -32,10 +33,11 @@ import com.example.android.displayingbitmaps.util.Utils;
/**
* This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}.
*/
-public class ImageDetailFragment extends Fragment {
+public class ImageDetailFragment extends Fragment implements ImageWorker.OnImageLoadedListener {
private static final String IMAGE_DATA_EXTRA = "extra_image_data";
private String mImageUrl;
private ImageView mImageView;
+ private ProgressBar mProgressBar;
private ImageFetcher mImageFetcher;
/**
@@ -75,6 +77,7 @@ public class ImageDetailFragment extends Fragment {
// Inflate and locate the main ImageView
final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
mImageView = (ImageView) v.findViewById(R.id.imageView);
+ mProgressBar = (ProgressBar) v.findViewById(R.id.progressbar);
return v;
}
@@ -86,7 +89,7 @@ public class ImageDetailFragment extends Fragment {
// cache can be used over all pages in the ViewPager
if (ImageDetailActivity.class.isInstance(getActivity())) {
mImageFetcher = ((ImageDetailActivity) getActivity()).getImageFetcher();
- mImageFetcher.loadImage(mImageUrl, mImageView);
+ mImageFetcher.loadImage(mImageUrl, mImageView, this);
}
// Pass clicks on the ImageView to the parent activity to handle
@@ -104,4 +107,11 @@ public class ImageDetailFragment extends Fragment {
mImageView.setImageDrawable(null);
}
}
+
+ @Override
+ public void onImageLoaded(boolean success) {
+ // Set loading spinner to gone once image has loaded. Cloud also show
+ // an error view here if needed.
+ mProgressBar.setVisibility(View.GONE);
+ }
}
diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageWorker.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageWorker.java
index f44d00d5b..d42d3c505 100644
--- a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageWorker.java
+++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageWorker.java
@@ -71,8 +71,9 @@ public abstract class ImageWorker {
*
* @param data The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
+ * @param listener A listener that will be called back once the image has been loaded.
*/
- public void loadImage(Object data, ImageView imageView) {
+ public void loadImage(Object data, ImageView imageView, OnImageLoadedListener listener) {
if (data == null) {
return;
}
@@ -86,9 +87,12 @@ public abstract class ImageWorker {
if (value != null) {
// Bitmap found in memory cache
imageView.setImageDrawable(value);
+ if (listener != null) {
+ listener.onImageLoaded(true);
+ }
} else if (cancelPotentialWork(data, imageView)) {
//BEGIN_INCLUDE(execute_background_task)
- final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
+ final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView, listener);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(mResources, mLoadingBitmap, task);
imageView.setImageDrawable(asyncDrawable);
@@ -101,6 +105,21 @@ public abstract class ImageWorker {
}
}
+ /**
+ * Load an image specified by the data parameter into an ImageView (override
+ * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and
+ * disk cache will be used if an {@link ImageCache} has been added using
+ * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the
+ * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask}
+ * will be created to asynchronously load the bitmap.
+ *
+ * @param data The URL of the image to download.
+ * @param imageView The ImageView to bind the downloaded image to.
+ */
+ public void loadImage(Object data, ImageView imageView) {
+ loadImage(data, imageView, null);
+ }
+
/**
* Set placeholder bitmap that shows when the the background thread is running.
*
@@ -238,10 +257,18 @@ public abstract class ImageWorker {
private class BitmapWorkerTask extends AsyncTask {
private Object mData;
private final WeakReference imageViewReference;
+ private final OnImageLoadedListener mOnImageLoadedListener;
public BitmapWorkerTask(Object data, ImageView imageView) {
mData = data;
imageViewReference = new WeakReference(imageView);
+ mOnImageLoadedListener = null;
+ }
+
+ public BitmapWorkerTask(Object data, ImageView imageView, OnImageLoadedListener listener) {
+ mData = data;
+ imageViewReference = new WeakReference(imageView);
+ mOnImageLoadedListener = listener;
}
/**
@@ -318,6 +345,7 @@ public abstract class ImageWorker {
@Override
protected void onPostExecute(BitmapDrawable value) {
//BEGIN_INCLUDE(complete_background_work)
+ boolean success = false;
// if cancel was called on this task or the "exit early" flag is set then we're done
if (isCancelled() || mExitTasksEarly) {
value = null;
@@ -328,8 +356,12 @@ public abstract class ImageWorker {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onPostExecute - setting bitmap");
}
+ success = true;
setImageDrawable(imageView, value);
}
+ if (mOnImageLoadedListener != null) {
+ mOnImageLoadedListener.onImageLoaded(success);
+ }
//END_INCLUDE(complete_background_work)
}
@@ -357,6 +389,19 @@ public abstract class ImageWorker {
}
}
+ /**
+ * Interface definition for callback on image loaded successfully.
+ */
+ public interface OnImageLoadedListener {
+
+ /**
+ * Called once the image has been loaded.
+ * @param success True if the image was loaded successfully, false if
+ * there was an error.
+ */
+ void onImageLoaded(boolean success);
+ }
+
/**
* A custom Drawable that will be attached to the imageView while the work is in progress.
* Contains a reference to the actual worker task, so that it can be stopped if a new binding is
diff --git a/samples/browseable/DocumentCentricApps/res/values-v11/template-styles.xml b/samples/browseable/DocumentCentricApps/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/DocumentCentricApps/res/values-v11/template-styles.xml
+++ b/samples/browseable/DocumentCentricApps/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/DocumentCentricApps/res/values-v21/base-template-styles.xml b/samples/browseable/DocumentCentricApps/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/DocumentCentricApps/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/DocumentCentricApps/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/DocumentCentricApps/res/values/template-styles.xml b/samples/browseable/DocumentCentricApps/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/DocumentCentricApps/res/values/template-styles.xml
+++ b/samples/browseable/DocumentCentricApps/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/DocumentCentricRelinquishIdentity/res/values-v11/template-styles.xml b/samples/browseable/DocumentCentricRelinquishIdentity/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/DocumentCentricRelinquishIdentity/res/values-v11/template-styles.xml
+++ b/samples/browseable/DocumentCentricRelinquishIdentity/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/DocumentCentricRelinquishIdentity/res/values-v21/base-template-styles.xml b/samples/browseable/DocumentCentricRelinquishIdentity/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/DocumentCentricRelinquishIdentity/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/DocumentCentricRelinquishIdentity/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/DocumentCentricRelinquishIdentity/res/values/template-styles.xml b/samples/browseable/DocumentCentricRelinquishIdentity/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/DocumentCentricRelinquishIdentity/res/values/template-styles.xml
+++ b/samples/browseable/DocumentCentricRelinquishIdentity/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/DoneBar/res/values-v11/template-styles.xml b/samples/browseable/DoneBar/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/DoneBar/res/values-v11/template-styles.xml
+++ b/samples/browseable/DoneBar/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/DoneBar/res/values-v21/base-template-styles.xml b/samples/browseable/DoneBar/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/DoneBar/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/DoneBar/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/DrawableTinting/res/values-v11/template-styles.xml b/samples/browseable/DrawableTinting/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/DrawableTinting/res/values-v11/template-styles.xml
+++ b/samples/browseable/DrawableTinting/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/DrawableTinting/res/values-v21/base-template-styles.xml b/samples/browseable/DrawableTinting/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/DrawableTinting/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/DrawableTinting/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/DrawableTinting/res/values/template-styles.xml b/samples/browseable/DrawableTinting/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/DrawableTinting/res/values/template-styles.xml
+++ b/samples/browseable/DrawableTinting/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ElevationBasic/res/values-v11/template-styles.xml b/samples/browseable/ElevationBasic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ElevationBasic/res/values-v11/template-styles.xml
+++ b/samples/browseable/ElevationBasic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ElevationBasic/res/values-v21/base-template-styles.xml b/samples/browseable/ElevationBasic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ElevationBasic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ElevationBasic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ElevationBasic/res/values/template-styles.xml b/samples/browseable/ElevationBasic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ElevationBasic/res/values/template-styles.xml
+++ b/samples/browseable/ElevationBasic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ElevationDrag/res/values-v11/template-styles.xml b/samples/browseable/ElevationDrag/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ElevationDrag/res/values-v11/template-styles.xml
+++ b/samples/browseable/ElevationDrag/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ElevationDrag/res/values-v21/base-template-styles.xml b/samples/browseable/ElevationDrag/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ElevationDrag/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ElevationDrag/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ElevationDrag/res/values/template-styles.xml b/samples/browseable/ElevationDrag/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ElevationDrag/res/values/template-styles.xml
+++ b/samples/browseable/ElevationDrag/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ElizaChat/AndroidManifest.xml b/samples/browseable/ElizaChat/AndroidManifest.xml
index 8f35c5651..b544ed05b 100644
--- a/samples/browseable/ElizaChat/AndroidManifest.xml
+++ b/samples/browseable/ElizaChat/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.elizachat" >
+ android:targetSdkVersion="23" />
-
+
diff --git a/samples/browseable/ElizaChat/res/values-v21/base-template-styles.xml b/samples/browseable/ElizaChat/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ElizaChat/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ElizaChat/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ElizaChat/res/values/template-styles.xml b/samples/browseable/ElizaChat/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ElizaChat/res/values/template-styles.xml
+++ b/samples/browseable/ElizaChat/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/ElizaChat/src/com.example.android.wearable.elizachat/ResponderService.java b/samples/browseable/ElizaChat/src/com.example.android.wearable.elizachat/ResponderService.java
index 3bef19c6b..2406668cf 100644
--- a/samples/browseable/ElizaChat/src/com.example.android.wearable.elizachat/ResponderService.java
+++ b/samples/browseable/ElizaChat/src/com.example.android.wearable.elizachat/ResponderService.java
@@ -96,7 +96,7 @@ public class ResponderService extends Service {
.setContentText(mLastResponse)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.bg_eliza))
.setSmallIcon(R.drawable.bg_eliza)
- .setPriority(NotificationCompat.PRIORITY_MIN);
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT);
Intent intent = new Intent(ACTION_RESPONSE);
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent,
diff --git a/samples/browseable/FindMyPhone/Application/AndroidManifest.xml b/samples/browseable/FindMyPhone/Application/AndroidManifest.xml
index af108af42..a59cd7d9c 100644
--- a/samples/browseable/FindMyPhone/Application/AndroidManifest.xml
+++ b/samples/browseable/FindMyPhone/Application/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.findphone">
+ android:targetSdkVersion="23" />
-
+
diff --git a/samples/browseable/FindMyPhone/Application/res/values-v21/base-template-styles.xml b/samples/browseable/FindMyPhone/Application/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/FindMyPhone/Application/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/FindMyPhone/Application/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/FindMyPhone/Application/res/values/template-styles.xml b/samples/browseable/FindMyPhone/Application/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/FindMyPhone/Application/res/values/template-styles.xml
+++ b/samples/browseable/FindMyPhone/Application/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/FindMyPhone/Wearable/src/com.example.android.wearable.findphone/FindPhoneService.java b/samples/browseable/FindMyPhone/Wearable/src/com.example.android.wearable.findphone/FindPhoneService.java
index c6c6d67b5..a51a9b20e 100644
--- a/samples/browseable/FindMyPhone/Wearable/src/com.example.android.wearable.findphone/FindPhoneService.java
+++ b/samples/browseable/FindMyPhone/Wearable/src/com.example.android.wearable.findphone/FindPhoneService.java
@@ -100,6 +100,7 @@ public class FindPhoneService extends IntentService implements GoogleApiClient.C
// when it receives the change.
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(PATH_SOUND_ALARM);
putDataMapRequest.getDataMap().putBoolean(FIELD_ALARM_ON, alarmOn);
+ putDataMapRequest.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, putDataMapRequest.asPutDataRequest())
.await();
} else {
diff --git a/samples/browseable/FingerprintDialog/_index.jd b/samples/browseable/FingerprintDialog/_index.jd
index 204440068..ddbfe7be0 100644
--- a/samples/browseable/FingerprintDialog/_index.jd
+++ b/samples/browseable/FingerprintDialog/_index.jd
@@ -1,6 +1,6 @@
-page.tags="Fingerprint Dialog Sample"
-sample.group=security
+page.tags="FingerprintDialog"
+sample.group=Security
@jd:body
diff --git a/samples/browseable/FingerprintDialog/res/values-v11/template-styles.xml b/samples/browseable/FingerprintDialog/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/FingerprintDialog/res/values-v11/template-styles.xml
+++ b/samples/browseable/FingerprintDialog/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/FingerprintDialog/res/values-v21/base-template-styles.xml b/samples/browseable/FingerprintDialog/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/FingerprintDialog/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/FingerprintDialog/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/FingerprintDialog/res/values/base-strings.xml b/samples/browseable/FingerprintDialog/res/values/base-strings.xml
index f3545f570..a4a93f7a4 100644
--- a/samples/browseable/FingerprintDialog/res/values/base-strings.xml
+++ b/samples/browseable/FingerprintDialog/res/values/base-strings.xml
@@ -16,7 +16,7 @@
-->
- Fingerprint Dialog Sample
+ FingerprintDialog
-
+
diff --git a/samples/browseable/FingerprintDialog/src/com.example.android.fingerprintdialog/MainActivity.java b/samples/browseable/FingerprintDialog/src/com.example.android.fingerprintdialog/MainActivity.java
index c954bfa7b..7caf9e69a 100644
--- a/samples/browseable/FingerprintDialog/src/com.example.android.fingerprintdialog/MainActivity.java
+++ b/samples/browseable/FingerprintDialog/src/com.example.android.fingerprintdialog/MainActivity.java
@@ -16,12 +16,10 @@
package com.example.android.fingerprintdialog;
-import android.Manifest;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Intent;
import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import android.security.keystore.KeyGenParameterSpec;
@@ -64,8 +62,6 @@ public class MainActivity extends Activity {
/** Alias for our key in the Android Key Store */
private static final String KEY_NAME = "my_key";
- private static final int FINGERPRINT_PERMISSION_REQUEST_CODE = 0;
-
@Inject KeyguardManager mKeyguardManager;
@Inject FingerprintManager mFingerprintManager;
@Inject FingerprintAuthenticationDialogFragment mFragment;
@@ -79,72 +75,65 @@ public class MainActivity extends Activity {
super.onCreate(savedInstanceState);
((InjectedApplication) getApplication()).inject(this);
- requestPermissions(new String[]{Manifest.permission.USE_FINGERPRINT},
- FINGERPRINT_PERMISSION_REQUEST_CODE);
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) {
- if (requestCode == FINGERPRINT_PERMISSION_REQUEST_CODE
- && state[0] == PackageManager.PERMISSION_GRANTED) {
- setContentView(R.layout.activity_main);
- Button purchaseButton = (Button) findViewById(R.id.purchase_button);
- if (!mKeyguardManager.isKeyguardSecure()) {
- // Show a message that the user hasn't set up a fingerprint or lock screen.
- Toast.makeText(this,
- "Secure lock screen hasn't set up.\n"
- + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
- Toast.LENGTH_LONG).show();
- purchaseButton.setEnabled(false);
- return;
- }
- if (!mFingerprintManager.hasEnrolledFingerprints()) {
- purchaseButton.setEnabled(false);
- // This happens when no fingerprints are registered.
- Toast.makeText(this,
- "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
- Toast.LENGTH_LONG).show();
- return;
- }
- createKey();
- purchaseButton.setEnabled(true);
- purchaseButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- findViewById(R.id.confirmation_message).setVisibility(View.GONE);
- findViewById(R.id.encrypted_message).setVisibility(View.GONE);
-
- // Set up the crypto object for later. The object will be authenticated by use
- // of the fingerprint.
- if (initCipher()) {
-
- // Show the fingerprint dialog. The user has the option to use the fingerprint with
- // crypto, or you can fall back to using a server-side verified password.
- mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
- boolean useFingerprintPreference = mSharedPreferences
- .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
- true);
- if (useFingerprintPreference) {
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
- } else {
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
- }
- mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
- } else {
- // This happens if the lock screen has been disabled or or a fingerprint got
- // enrolled. Thus show the dialog to authenticate with their password first
- // and ask the user if they want to authenticate with fingerprints in the
- // future
- mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
- mFragment.setStage(
- FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
- mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
- }
- }
- });
+ setContentView(R.layout.activity_main);
+ Button purchaseButton = (Button) findViewById(R.id.purchase_button);
+ if (!mKeyguardManager.isKeyguardSecure()) {
+ // Show a message that the user hasn't set up a fingerprint or lock screen.
+ Toast.makeText(this,
+ "Secure lock screen hasn't set up.\n"
+ + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
+ Toast.LENGTH_LONG).show();
+ purchaseButton.setEnabled(false);
+ return;
}
+
+ //noinspection ResourceType
+ if (!mFingerprintManager.hasEnrolledFingerprints()) {
+ purchaseButton.setEnabled(false);
+ // This happens when no fingerprints are registered.
+ Toast.makeText(this,
+ "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ createKey();
+ purchaseButton.setEnabled(true);
+ purchaseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ findViewById(R.id.confirmation_message).setVisibility(View.GONE);
+ findViewById(R.id.encrypted_message).setVisibility(View.GONE);
+
+ // Set up the crypto object for later. The object will be authenticated by use
+ // of the fingerprint.
+ if (initCipher()) {
+
+ // Show the fingerprint dialog. The user has the option to use the fingerprint with
+ // crypto, or you can fall back to using a server-side verified password.
+ mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
+ boolean useFingerprintPreference = mSharedPreferences
+ .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
+ true);
+ if (useFingerprintPreference) {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
+ } else {
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
+ }
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ } else {
+ // This happens if the lock screen has been disabled or or a fingerprint got
+ // enrolled. Thus show the dialog to authenticate with their password first
+ // and ask the user if they want to authenticate with fingerprints in the
+ // future
+ mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mCipher));
+ mFragment.setStage(
+ FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
+ mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+ }
+ }
+ });
}
/**
diff --git a/samples/browseable/Flashlight/AndroidManifest.xml b/samples/browseable/Flashlight/AndroidManifest.xml
index 738ba9d37..1eb15d072 100644
--- a/samples/browseable/Flashlight/AndroidManifest.xml
+++ b/samples/browseable/Flashlight/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.flashlight" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/Flashlight/_index.jd b/samples/browseable/Flashlight/_index.jd
index 1858b3ec4..15a65fc78 100644
--- a/samples/browseable/Flashlight/_index.jd
+++ b/samples/browseable/Flashlight/_index.jd
@@ -6,6 +6,6 @@ sample.group=Wearable
Wearable activity that uses your wearable screen as a flashlight. There is also
- a party-mode option, if you want to make things interesting.
+ a party-mode option (swipe left), if you want to make things interesting.
+
+This sample demonstrates how to use the MIDI API to receive and process MIDI signals coming from an
+attached input device.
+
+
diff --git a/samples/browseable/MidiScope/res/drawable-hdpi/ic_clear_all.png b/samples/browseable/MidiScope/res/drawable-hdpi/ic_clear_all.png
new file mode 100755
index 000000000..e23d886cd
Binary files /dev/null and b/samples/browseable/MidiScope/res/drawable-hdpi/ic_clear_all.png differ
diff --git a/samples/browseable/MidiScope/res/drawable-hdpi/tile.9.png b/samples/browseable/MidiScope/res/drawable-hdpi/tile.9.png
new file mode 100644
index 000000000..135862883
Binary files /dev/null and b/samples/browseable/MidiScope/res/drawable-hdpi/tile.9.png differ
diff --git a/samples/browseable/MidiScope/res/drawable-mdpi/ic_clear_all.png b/samples/browseable/MidiScope/res/drawable-mdpi/ic_clear_all.png
new file mode 100755
index 000000000..dca304878
Binary files /dev/null and b/samples/browseable/MidiScope/res/drawable-mdpi/ic_clear_all.png differ
diff --git a/samples/browseable/MidiScope/res/drawable-xhdpi/ic_clear_all.png b/samples/browseable/MidiScope/res/drawable-xhdpi/ic_clear_all.png
new file mode 100755
index 000000000..fef5dcd28
Binary files /dev/null and b/samples/browseable/MidiScope/res/drawable-xhdpi/ic_clear_all.png differ
diff --git a/samples/browseable/MidiScope/res/drawable-xxhdpi/ic_clear_all.png b/samples/browseable/MidiScope/res/drawable-xxhdpi/ic_clear_all.png
new file mode 100755
index 000000000..51d2d3dad
Binary files /dev/null and b/samples/browseable/MidiScope/res/drawable-xxhdpi/ic_clear_all.png differ
diff --git a/samples/browseable/MidiScope/res/drawable-xxxhdpi/ic_clear_all.png b/samples/browseable/MidiScope/res/drawable-xxxhdpi/ic_clear_all.png
new file mode 100755
index 000000000..9dbccf364
Binary files /dev/null and b/samples/browseable/MidiScope/res/drawable-xxxhdpi/ic_clear_all.png differ
diff --git a/samples/browseable/MidiScope/res/layout/main.xml b/samples/browseable/MidiScope/res/layout/main.xml
new file mode 100644
index 000000000..71c52aa6e
--- /dev/null
+++ b/samples/browseable/MidiScope/res/layout/main.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiScope/res/menu/main.xml b/samples/browseable/MidiScope/res/menu/main.xml
new file mode 100644
index 000000000..abb1842ab
--- /dev/null
+++ b/samples/browseable/MidiScope/res/menu/main.xml
@@ -0,0 +1,32 @@
+
+
+
diff --git a/samples/browseable/MidiScope/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/MidiScope/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 000000000..4a755241d
Binary files /dev/null and b/samples/browseable/MidiScope/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiScope/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/MidiScope/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 000000000..09a427109
Binary files /dev/null and b/samples/browseable/MidiScope/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiScope/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/MidiScope/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 000000000..e9c9a3612
Binary files /dev/null and b/samples/browseable/MidiScope/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiScope/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/MidiScope/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 000000000..6e79c3bc3
Binary files /dev/null and b/samples/browseable/MidiScope/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiScope/res/mipmap-xxxhdpi/ic_launcher.png b/samples/browseable/MidiScope/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 000000000..638d4e13f
Binary files /dev/null and b/samples/browseable/MidiScope/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiScope/res/values-sw600dp/template-dimens.xml b/samples/browseable/MidiScope/res/values-sw600dp/template-dimens.xml
new file mode 100644
index 000000000..22074a2bd
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values-sw600dp/template-dimens.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ @dimen/margin_huge
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/MidiScope/res/values-sw600dp/template-styles.xml b/samples/browseable/MidiScope/res/values-sw600dp/template-styles.xml
new file mode 100644
index 000000000..03d197418
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values-sw600dp/template-styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiScope/res/values-v11/template-styles.xml b/samples/browseable/MidiScope/res/values-v11/template-styles.xml
new file mode 100644
index 000000000..8c1ea66f2
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values-v11/template-styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiScope/res/values-v21/base-colors.xml b/samples/browseable/MidiScope/res/values-v21/base-colors.xml
new file mode 100644
index 000000000..8b6ec3f85
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values-v21/base-colors.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiScope/res/values-v21/base-template-styles.xml b/samples/browseable/MidiScope/res/values-v21/base-template-styles.xml
new file mode 100644
index 000000000..c778e4f98
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values-v21/base-template-styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiScope/res/values/base-strings.xml b/samples/browseable/MidiScope/res/values/base-strings.xml
new file mode 100644
index 000000000..fb7d652fc
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values/base-strings.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ MidiScope
+
+
+
+
diff --git a/samples/browseable/MidiScope/res/values/colors.xml b/samples/browseable/MidiScope/res/values/colors.xml
new file mode 100644
index 000000000..eef48d880
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values/colors.xml
@@ -0,0 +1,26 @@
+
+
+
+ #009688
+ #00796B
+ #B2DFDB
+ #FFC107
+ #212121
+ #727272
+ #FFFFFF
+ #B6B6B6
+
diff --git a/samples/browseable/MidiScope/res/values/strings.xml b/samples/browseable/MidiScope/res/values/strings.xml
new file mode 100644
index 000000000..52beb4e1a
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values/strings.xml
@@ -0,0 +1,24 @@
+
+
+
+ Select a MIDI source from the Spinner above or send messages to MidiScope.
+ Clear Log
+ Keep Screen On
+
+ "none"
+
+
diff --git a/samples/browseable/MidiScope/res/values/styles.xml b/samples/browseable/MidiScope/res/values/styles.xml
new file mode 100644
index 000000000..003600969
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values/styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiScope/res/values/template-dimens.xml b/samples/browseable/MidiScope/res/values/template-dimens.xml
new file mode 100644
index 000000000..39e710b5c
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values/template-dimens.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ 4dp
+ 8dp
+ 16dp
+ 32dp
+ 64dp
+
+
+
+ @dimen/margin_medium
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/MidiScope/res/values/template-styles.xml b/samples/browseable/MidiScope/res/values/template-styles.xml
new file mode 100644
index 000000000..6e7d593dd
--- /dev/null
+++ b/samples/browseable/MidiScope/res/values/template-styles.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiScope/res/xml/scope_device_info.xml b/samples/browseable/MidiScope/res/xml/scope_device_info.xml
new file mode 100644
index 000000000..f89f11099
--- /dev/null
+++ b/samples/browseable/MidiScope/res/xml/scope_device_info.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/EventScheduler.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/EventScheduler.java
new file mode 100644
index 000000000..37c0140dc
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/EventScheduler.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Store SchedulableEvents in a timestamped buffer.
+ * Events may be written in any order.
+ * Events will be read in sorted order.
+ * Events with the same timestamp will be read in the order they were added.
+ *
+ * Only one Thread can write into the buffer.
+ * And only one Thread can read from the buffer.
+ */
+public class EventScheduler {
+ private static final long NANOS_PER_MILLI = 1000000;
+
+ private final Object lock = new Object();
+ private SortedMap mEventBuffer;
+ // This does not have to be guarded. It is only set by the writing thread.
+ // If the reader sees a null right before being set then that is OK.
+ private FastEventQueue mEventPool = null;
+ private static final int MAX_POOL_SIZE = 200;
+
+ public EventScheduler() {
+ mEventBuffer = new TreeMap();
+ }
+
+ // If we keep at least one node in the list then it can be atomic
+ // and non-blocking.
+ private class FastEventQueue {
+ // One thread takes from the beginning of the list.
+ volatile SchedulableEvent mFirst;
+ // A second thread returns events to the end of the list.
+ volatile SchedulableEvent mLast;
+ volatile long mEventsAdded;
+ volatile long mEventsRemoved;
+
+ FastEventQueue(SchedulableEvent event) {
+ mFirst = event;
+ mLast = mFirst;
+ mEventsAdded = 1; // Always created with one event added. Never empty.
+ mEventsRemoved = 0; // None removed yet.
+ }
+
+ int size() {
+ return (int)(mEventsAdded - mEventsRemoved);
+ }
+
+ /**
+ * Do not call this unless there is more than one event
+ * in the list.
+ * @return first event in the list
+ */
+ public SchedulableEvent remove() {
+ // Take first event.
+ mEventsRemoved++;
+ SchedulableEvent event = mFirst;
+ mFirst = event.mNext;
+ return event;
+ }
+
+ /**
+ * @param event
+ */
+ public void add(SchedulableEvent event) {
+ event.mNext = null;
+ mLast.mNext = event;
+ mLast = event;
+ mEventsAdded++;
+ }
+ }
+
+ /**
+ * Base class for events that can be stored in the EventScheduler.
+ */
+ public static class SchedulableEvent {
+ private long mTimestamp;
+ private SchedulableEvent mNext = null;
+
+ /**
+ * @param timestamp
+ */
+ public SchedulableEvent(long timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ /**
+ * @return timestamp
+ */
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ /**
+ * The timestamp should not be modified when the event is in the
+ * scheduling buffer.
+ */
+ public void setTimestamp(long timestamp) {
+ mTimestamp = timestamp;
+ }
+ }
+
+ /**
+ * Get an event from the pool.
+ * Always leave at least one event in the pool.
+ * @return event or null
+ */
+ public SchedulableEvent removeEventfromPool() {
+ SchedulableEvent event = null;
+ if (mEventPool != null && (mEventPool.size() > 1)) {
+ event = mEventPool.remove();
+ }
+ return event;
+ }
+
+ /**
+ * Return events to a pool so they can be reused.
+ *
+ * @param event
+ */
+ public void addEventToPool(SchedulableEvent event) {
+ if (mEventPool == null) {
+ mEventPool = new FastEventQueue(event);
+ // If we already have enough items in the pool then just
+ // drop the event. This prevents unbounded memory leaks.
+ } else if (mEventPool.size() < MAX_POOL_SIZE) {
+ mEventPool.add(event);
+ }
+ }
+
+ /**
+ * Add an event to the scheduler. Events with the same time will be
+ * processed in order.
+ *
+ * @param event
+ */
+ public void add(SchedulableEvent event) {
+ synchronized (lock) {
+ FastEventQueue list = mEventBuffer.get(event.getTimestamp());
+ if (list == null) {
+ long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE
+ : mEventBuffer.firstKey();
+ list = new FastEventQueue(event);
+ mEventBuffer.put(event.getTimestamp(), list);
+ // If the event we added is earlier than the previous earliest
+ // event then notify any threads waiting for the next event.
+ if (event.getTimestamp() < lowestTime) {
+ lock.notify();
+ }
+ } else {
+ list.add(event);
+ }
+ }
+ }
+
+ // Caller must synchronize on lock before calling.
+ private SchedulableEvent removeNextEventLocked(long lowestTime) {
+ SchedulableEvent event;
+ FastEventQueue list = mEventBuffer.get(lowestTime);
+ // Remove list from tree if this is the last node.
+ if ((list.size() == 1)) {
+ mEventBuffer.remove(lowestTime);
+ }
+ event = list.remove();
+ return event;
+ }
+
+ /**
+ * Check to see if any scheduled events are ready to be processed.
+ *
+ * @param timestamp
+ * @return next event or null if none ready
+ */
+ public SchedulableEvent getNextEvent(long time) {
+ SchedulableEvent event = null;
+ synchronized (lock) {
+ if (!mEventBuffer.isEmpty()) {
+ long lowestTime = mEventBuffer.firstKey();
+ // Is it time for this list to be processed?
+ if (lowestTime <= time) {
+ event = removeNextEventLocked(lowestTime);
+ }
+ }
+ }
+ // Log.i(TAG, "getNextEvent: event = " + event);
+ return event;
+ }
+
+ /**
+ * Return the next available event or wait until there is an event ready to
+ * be processed. This method assumes that the timestamps are in nanoseconds
+ * and that the current time is System.nanoTime().
+ *
+ * @return event
+ * @throws InterruptedException
+ */
+ public SchedulableEvent waitNextEvent() throws InterruptedException {
+ SchedulableEvent event = null;
+ while (true) {
+ long millisToWait = Integer.MAX_VALUE;
+ synchronized (lock) {
+ if (!mEventBuffer.isEmpty()) {
+ long now = System.nanoTime();
+ long lowestTime = mEventBuffer.firstKey();
+ // Is it time for the earliest list to be processed?
+ if (lowestTime <= now) {
+ event = removeNextEventLocked(lowestTime);
+ break;
+ } else {
+ // Figure out how long to sleep until next event.
+ long nanosToWait = lowestTime - now;
+ // Add 1 millisecond so we don't wake up before it is
+ // ready.
+ millisToWait = 1 + (nanosToWait / NANOS_PER_MILLI);
+ // Clip 64-bit value to 32-bit max.
+ if (millisToWait > Integer.MAX_VALUE) {
+ millisToWait = Integer.MAX_VALUE;
+ }
+ }
+ }
+ lock.wait((int) millisToWait);
+ }
+ }
+ return event;
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiConstants.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiConstants.java
new file mode 100644
index 000000000..38c25d505
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiConstants.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+/**
+ * MIDI related constants and static methods.
+ * These values are defined in the MIDI Standard 1.0
+ * available from the MIDI Manufacturers Association.
+ */
+public class MidiConstants {
+ protected final static String TAG = "MidiTools";
+ public static final byte STATUS_COMMAND_MASK = (byte) 0xF0;
+ public static final byte STATUS_CHANNEL_MASK = (byte) 0x0F;
+
+ // Channel voice messages.
+ public static final byte STATUS_NOTE_OFF = (byte) 0x80;
+ public static final byte STATUS_NOTE_ON = (byte) 0x90;
+ public static final byte STATUS_POLYPHONIC_AFTERTOUCH = (byte) 0xA0;
+ public static final byte STATUS_CONTROL_CHANGE = (byte) 0xB0;
+ public static final byte STATUS_PROGRAM_CHANGE = (byte) 0xC0;
+ public static final byte STATUS_CHANNEL_PRESSURE = (byte) 0xD0;
+ public static final byte STATUS_PITCH_BEND = (byte) 0xE0;
+
+ // System Common Messages.
+ public static final byte STATUS_SYSTEM_EXCLUSIVE = (byte) 0xF0;
+ public static final byte STATUS_MIDI_TIME_CODE = (byte) 0xF1;
+ public static final byte STATUS_SONG_POSITION = (byte) 0xF2;
+ public static final byte STATUS_SONG_SELECT = (byte) 0xF3;
+ public static final byte STATUS_TUNE_REQUEST = (byte) 0xF6;
+ public static final byte STATUS_END_SYSEX = (byte) 0xF7;
+
+ // System Real-Time Messages
+ public static final byte STATUS_TIMING_CLOCK = (byte) 0xF8;
+ public static final byte STATUS_START = (byte) 0xFA;
+ public static final byte STATUS_CONTINUE = (byte) 0xFB;
+ public static final byte STATUS_STOP = (byte) 0xFC;
+ public static final byte STATUS_ACTIVE_SENSING = (byte) 0xFE;
+ public static final byte STATUS_RESET = (byte) 0xFF;
+
+ /** Number of bytes in a message nc from 8c to Ec */
+ public final static int CHANNEL_BYTE_LENGTHS[] = { 3, 3, 3, 3, 2, 2, 3 };
+
+ /** Number of bytes in a message Fn from F0 to FF */
+ public final static int SYSTEM_BYTE_LENGTHS[] = { 1, 2, 3, 2, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1 };
+
+ /**
+ * MIDI messages, except for SysEx, are 1,2 or 3 bytes long.
+ * You can tell how long a MIDI message is from the first status byte.
+ * Do not call this for SysEx, which has variable length.
+ * @param statusByte
+ * @return number of bytes in a complete message, zero if data byte passed
+ */
+ public static int getBytesPerMessage(byte statusByte) {
+ // Java bytes are signed so we need to mask off the high bits
+ // to get a value between 0 and 255.
+ int statusInt = statusByte & 0xFF;
+ if (statusInt >= 0xF0) {
+ // System messages use low nibble for size.
+ return SYSTEM_BYTE_LENGTHS[statusInt & 0x0F];
+ } else if(statusInt >= 0x80) {
+ // Channel voice messages use high nibble for size.
+ return CHANNEL_BYTE_LENGTHS[(statusInt >> 4) - 8];
+ } else {
+ return 0; // data byte
+ }
+ }
+
+ /**
+ * @param msg
+ * @param offset
+ * @param count
+ * @return true if the entire message is ActiveSensing commands
+ */
+ public static boolean isAllActiveSensing(byte[] msg, int offset,
+ int count) {
+ // Count bytes that are not active sensing.
+ int goodBytes = 0;
+ for (int i = 0; i < count; i++) {
+ byte b = msg[offset + i];
+ if (b != MidiConstants.STATUS_ACTIVE_SENSING) {
+ goodBytes++;
+ }
+ }
+ return (goodBytes == 0);
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiDispatcher.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiDispatcher.java
new file mode 100644
index 000000000..b7f1fe1e8
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiDispatcher.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiReceiver;
+import android.media.midi.MidiSender;
+
+import java.io.IOException;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s.
+ * This class subclasses {@link MidiReceiver} and dispatches any data it receives
+ * to its receiver list. Any receivers that throw an exception upon receiving data will
+ * be automatically removed from the receiver list, but no IOException will be returned
+ * from the dispatcher's {@link MidiReceiver#onReceive} in that case.
+ */
+public final class MidiDispatcher extends MidiReceiver {
+
+ private final CopyOnWriteArrayList mReceivers
+ = new CopyOnWriteArrayList();
+
+ private final MidiSender mSender = new MidiSender() {
+ /**
+ * Called to connect a {@link MidiReceiver} to the sender
+ *
+ * @param receiver the receiver to connect
+ */
+ @Override
+ public void onConnect(MidiReceiver receiver) {
+ mReceivers.add(receiver);
+ }
+
+ /**
+ * Called to disconnect a {@link MidiReceiver} from the sender
+ *
+ * @param receiver the receiver to disconnect
+ */
+ @Override
+ public void onDisconnect(MidiReceiver receiver) {
+ mReceivers.remove(receiver);
+ }
+ };
+
+ /**
+ * Returns the number of {@link MidiReceiver}s this dispatcher contains.
+ * @return the number of receivers
+ */
+ public int getReceiverCount() {
+ return mReceivers.size();
+ }
+
+ /**
+ * Returns a {@link MidiSender} which is used to add and remove
+ * {@link MidiReceiver}s
+ * to the dispatcher's receiver list.
+ * @return the dispatcher's MidiSender
+ */
+ public MidiSender getSender() {
+ return mSender;
+ }
+
+ @Override
+ public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
+ for (MidiReceiver receiver : mReceivers) {
+ try {
+ receiver.send(msg, offset, count, timestamp);
+ } catch (IOException e) {
+ // if the receiver fails we remove the receiver but do not propagate the exception
+ mReceivers.remove(receiver);
+ }
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ for (MidiReceiver receiver : mReceivers) {
+ receiver.flush();
+ }
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiEventScheduler.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiEventScheduler.java
new file mode 100644
index 000000000..513d3939b
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiEventScheduler.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiReceiver;
+
+import java.io.IOException;
+
+/**
+ * Add MIDI Events to an EventScheduler
+ */
+public class MidiEventScheduler extends EventScheduler {
+ private static final String TAG = "MidiEventScheduler";
+ // Maintain a pool of scheduled events to reduce memory allocation.
+ // This pool increases performance by about 14%.
+ private final static int POOL_EVENT_SIZE = 16;
+ private MidiReceiver mReceiver = new SchedulingReceiver();
+
+ private class SchedulingReceiver extends MidiReceiver
+ {
+ /**
+ * Store these bytes in the EventScheduler to be delivered at the specified
+ * time.
+ */
+ @Override
+ public void onSend(byte[] msg, int offset, int count, long timestamp)
+ throws IOException {
+ MidiEvent event = createScheduledEvent(msg, offset, count, timestamp);
+ if (event != null) {
+ add(event);
+ }
+ }
+ }
+
+ public static class MidiEvent extends SchedulableEvent {
+ public int count = 0;
+ public byte[] data;
+
+ private MidiEvent(int count) {
+ super(0);
+ data = new byte[count];
+ }
+
+ private MidiEvent(byte[] msg, int offset, int count, long timestamp) {
+ super(timestamp);
+ data = new byte[count];
+ System.arraycopy(msg, offset, data, 0, count);
+ this.count = count;
+ }
+
+ @Override
+ public String toString() {
+ String text = "Event: ";
+ for (int i = 0; i < count; i++) {
+ text += data[i] + ", ";
+ }
+ return text;
+ }
+ }
+
+ /**
+ * Create an event that contains the message.
+ */
+ private MidiEvent createScheduledEvent(byte[] msg, int offset, int count,
+ long timestamp) {
+ MidiEvent event;
+ if (count > POOL_EVENT_SIZE) {
+ event = new MidiEvent(msg, offset, count, timestamp);
+ } else {
+ event = (MidiEvent) removeEventfromPool();
+ if (event == null) {
+ event = new MidiEvent(POOL_EVENT_SIZE);
+ }
+ System.arraycopy(msg, offset, event.data, 0, count);
+ event.count = count;
+ event.setTimestamp(timestamp);
+ }
+ return event;
+ }
+
+ /**
+ * Return events to a pool so they can be reused.
+ *
+ * @param event
+ */
+ @Override
+ public void addEventToPool(SchedulableEvent event) {
+ // Make sure the event is suitable for the pool.
+ if (event instanceof MidiEvent) {
+ MidiEvent midiEvent = (MidiEvent) event;
+ if (midiEvent.data.length == POOL_EVENT_SIZE) {
+ super.addEventToPool(event);
+ }
+ }
+ }
+
+ /**
+ * This MidiReceiver will write date to the scheduling buffer.
+ * @return the MidiReceiver
+ */
+ public MidiReceiver getReceiver() {
+ return mReceiver;
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiEventThread.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiEventThread.java
new file mode 100644
index 000000000..626e83cf0
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiEventThread.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiSender;
+import android.util.Log;
+
+import java.io.IOException;
+
+public class MidiEventThread extends MidiEventScheduler {
+ protected static final String TAG = "MidiEventThread";
+
+ private EventThread mEventThread;
+ MidiDispatcher mDispatcher = new MidiDispatcher();
+
+ class EventThread extends Thread {
+ private boolean go = true;
+
+ @Override
+ public void run() {
+ while (go) {
+ try {
+ MidiEvent event = (MidiEvent) waitNextEvent();
+ try {
+ Log.i(TAG, "Fire event " + event.data[0] + " at "
+ + event.getTimestamp());
+ mDispatcher.send(event.data, 0,
+ event.count, event.getTimestamp());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // Put event back in the pool for future use.
+ addEventToPool(event);
+ } catch (InterruptedException e) {
+ // OK, this is how we stop the thread.
+ }
+ }
+ }
+
+ /**
+ * Asynchronously tell the thread to stop.
+ */
+ public void requestStop() {
+ go = false;
+ interrupt();
+ }
+ }
+
+ public void start() {
+ stop();
+ mEventThread = new EventThread();
+ mEventThread.start();
+ }
+
+ /**
+ * Asks the thread to stop then waits for it to stop.
+ */
+ public void stop() {
+ if (mEventThread != null) {
+ mEventThread.requestStop();
+ try {
+ mEventThread.join(500);
+ } catch (InterruptedException e) {
+ Log.e(TAG,
+ "Interrupted while waiting for MIDI EventScheduler thread to stop.");
+ } finally {
+ mEventThread = null;
+ }
+ }
+ }
+
+ public MidiSender getSender() {
+ return mDispatcher.getSender();
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiFramer.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiFramer.java
new file mode 100644
index 000000000..c274925ac
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiFramer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiReceiver;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Convert stream of arbitrary MIDI bytes into discrete messages.
+ *
+ * Parses the incoming bytes and then posts individual messages to the receiver
+ * specified in the constructor. Short messages of 1-3 bytes will be complete.
+ * System Exclusive messages may be posted in pieces.
+ *
+ * Resolves Running Status and interleaved System Real-Time messages.
+ */
+public class MidiFramer extends MidiReceiver {
+ private MidiReceiver mReceiver;
+ private byte[] mBuffer = new byte[3];
+ private int mCount;
+ private byte mRunningStatus;
+ private int mNeeded;
+ private boolean mInSysEx;
+
+ public MidiFramer(MidiReceiver receiver) {
+ mReceiver = receiver;
+ }
+
+ /*
+ * @see android.midi.MidiReceiver#onSend(byte[], int, int, long)
+ */
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ int sysExStartOffset = (mInSysEx ? offset : -1);
+
+ for (int i = 0; i < count; i++) {
+ final byte currentByte = data[offset];
+ final int currentInt = currentByte & 0xFF;
+ if (currentInt >= 0x80) { // status byte?
+ if (currentInt < 0xF0) { // channel message?
+ mRunningStatus = currentByte;
+ mCount = 1;
+ mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1;
+ } else if (currentInt < 0xF8) { // system common?
+ if (currentInt == 0xF0 /* SysEx Start */) {
+ // Log.i(TAG, "SysEx Start");
+ mInSysEx = true;
+ sysExStartOffset = offset;
+ } else if (currentInt == 0xF7 /* SysEx End */) {
+ // Log.i(TAG, "SysEx End");
+ if (mInSysEx) {
+ mReceiver.send(data, sysExStartOffset,
+ offset - sysExStartOffset + 1, timestamp);
+ mInSysEx = false;
+ sysExStartOffset = -1;
+ }
+ } else {
+ mBuffer[0] = currentByte;
+ mRunningStatus = 0;
+ mCount = 1;
+ mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1;
+ }
+ } else { // real-time?
+ // Single byte message interleaved with other data.
+ if (mInSysEx) {
+ mReceiver.send(data, sysExStartOffset,
+ offset - sysExStartOffset, timestamp);
+ sysExStartOffset = offset + 1;
+ }
+ mReceiver.send(data, offset, 1, timestamp);
+ }
+ } else { // data byte
+ if (!mInSysEx) {
+ mBuffer[mCount++] = currentByte;
+ if (--mNeeded == 0) {
+ if (mRunningStatus != 0) {
+ mBuffer[0] = mRunningStatus;
+ }
+ mReceiver.send(mBuffer, 0, mCount, timestamp);
+ mNeeded = MidiConstants.getBytesPerMessage(mBuffer[0]) - 1;
+ mCount = 1;
+ }
+ }
+ }
+ ++offset;
+ }
+
+ // send any accumulatedSysEx data
+ if (sysExStartOffset >= 0 && sysExStartOffset < offset) {
+ mReceiver.send(data, sysExStartOffset,
+ offset - sysExStartOffset, timestamp);
+ }
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiInputPortSelector.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiInputPortSelector.java
new file mode 100644
index 000000000..7c665bace
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiInputPortSelector.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiInputPort;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiReceiver;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Manages a Spinner for selecting a MidiInputPort.
+ */
+public class MidiInputPortSelector extends MidiPortSelector {
+
+ private MidiInputPort mInputPort;
+ private MidiDevice mOpenDevice;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId ID from the layout resource
+ */
+ public MidiInputPortSelector(MidiManager midiManager, Activity activity,
+ int spinnerId) {
+ super(midiManager, activity, spinnerId, MidiDeviceInfo.PortInfo.TYPE_INPUT);
+ }
+
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ close();
+ final MidiDeviceInfo info = wrapper.getDeviceInfo();
+ if (info != null) {
+ mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ Log.e(MidiConstants.TAG, "could not open " + info);
+ } else {
+ mOpenDevice = device;
+ mInputPort = mOpenDevice.openInputPort(
+ wrapper.getPortIndex());
+ if (mInputPort == null) {
+ Log.e(MidiConstants.TAG, "could not open input port on " + info);
+ }
+ }
+ }
+ }, null);
+ // Don't run the callback on the UI thread because openInputPort might take a while.
+ }
+ }
+
+ public MidiReceiver getReceiver() {
+ return mInputPort;
+ }
+
+ @Override
+ public void onClose() {
+ try {
+ if (mInputPort != null) {
+ Log.i(MidiConstants.TAG, "MidiInputPortSelector.onClose() - close port");
+ mInputPort.close();
+ }
+ mInputPort = null;
+ if (mOpenDevice != null) {
+ mOpenDevice.close();
+ }
+ mOpenDevice = null;
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "cleanup failed", e);
+ }
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiOutputPortConnectionSelector.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiOutputPortConnectionSelector.java
new file mode 100644
index 000000000..ca1ade48c
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiOutputPortConnectionSelector.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Select an output port and connect it to a destination input port.
+ */
+public class MidiOutputPortConnectionSelector extends MidiPortSelector {
+
+ private MidiPortConnector mSynthConnector;
+ private MidiDeviceInfo mDestinationDeviceInfo;
+ private int mDestinationPortIndex;
+ private MidiPortConnector.OnPortsConnectedListener mConnectedListener;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId
+ * @param type
+ */
+ public MidiOutputPortConnectionSelector(MidiManager midiManager,
+ Activity activity, int spinnerId,
+ MidiDeviceInfo destinationDeviceInfo, int destinationPortIndex) {
+ super(midiManager, activity, spinnerId,
+ MidiDeviceInfo.PortInfo.TYPE_OUTPUT);
+ mDestinationDeviceInfo = destinationDeviceInfo;
+ mDestinationPortIndex = destinationPortIndex;
+ }
+
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ Log.i(MidiConstants.TAG, "connectPortToSynth: " + wrapper);
+ onClose();
+ if (wrapper.getDeviceInfo() != null) {
+ mSynthConnector = new MidiPortConnector(mMidiManager);
+ mSynthConnector.connectToDevicePort(wrapper.getDeviceInfo(),
+ wrapper.getPortIndex(), mDestinationDeviceInfo,
+ mDestinationPortIndex,
+ // not safe on UI thread
+ mConnectedListener, null);
+ }
+ }
+
+ @Override
+ public void onClose() {
+ try {
+ if (mSynthConnector != null) {
+ mSynthConnector.close();
+ mSynthConnector = null;
+ }
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "Exception in closeSynthResources()", e);
+ }
+ }
+
+ /**
+ * @param myPortsConnectedListener
+ */
+ public void setConnectedListener(
+ MidiPortConnector.OnPortsConnectedListener connectedListener) {
+ mConnectedListener = connectedListener;
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiOutputPortSelector.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiOutputPortSelector.java
new file mode 100644
index 000000000..5aebf727e
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiOutputPortSelector.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiOutputPort;
+import android.media.midi.MidiSender;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Manages a Spinner for selecting a MidiOutputPort.
+ */
+public class MidiOutputPortSelector extends MidiPortSelector {
+ private MidiOutputPort mOutputPort;
+ private MidiDispatcher mDispatcher = new MidiDispatcher();
+ private MidiDevice mOpenDevice;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId ID from the layout resource
+ */
+ public MidiOutputPortSelector(MidiManager midiManager, Activity activity,
+ int spinnerId) {
+ super(midiManager, activity, spinnerId, MidiDeviceInfo.PortInfo.TYPE_OUTPUT);
+ }
+
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ Log.i(MidiConstants.TAG, "onPortSelected: " + wrapper);
+ close();
+
+ final MidiDeviceInfo info = wrapper.getDeviceInfo();
+ if (info != null) {
+ mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
+
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ Log.e(MidiConstants.TAG, "could not open " + info);
+ } else {
+ mOpenDevice = device;
+ mOutputPort = device.openOutputPort(wrapper.getPortIndex());
+ if (mOutputPort == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open output port for " + info);
+ return;
+ }
+ mOutputPort.connect(mDispatcher);
+ }
+ }
+ }, null);
+ // Don't run the callback on the UI thread because openOutputPort might take a while.
+ }
+ }
+
+ @Override
+ public void onClose() {
+ try {
+ if (mOutputPort != null) {
+ mOutputPort.disconnect(mDispatcher);
+ }
+ mOutputPort = null;
+ if (mOpenDevice != null) {
+ mOpenDevice.close();
+ }
+ mOpenDevice = null;
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "cleanup failed", e);
+ }
+ }
+
+ /**
+ * You can connect your MidiReceivers to this sender. The user will then select which output
+ * port will send messages through this MidiSender.
+ * @return a MidiSender that will send the messages from the selected port.
+ */
+ public MidiSender getSender() {
+ return mDispatcher.getSender();
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiPortConnector.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiPortConnector.java
new file mode 100644
index 000000000..457494d1b
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiPortConnector.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDevice.MidiConnection;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiInputPort;
+import android.media.midi.MidiManager;
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Tool for connecting MIDI ports on two remote devices.
+ */
+public class MidiPortConnector {
+ private final MidiManager mMidiManager;
+ private MidiDevice mSourceDevice;
+ private MidiDevice mDestinationDevice;
+ private MidiConnection mConnection;
+
+ /**
+ * @param mMidiManager
+ */
+ public MidiPortConnector(MidiManager midiManager) {
+ mMidiManager = midiManager;
+ }
+
+ public void close() throws IOException {
+ if (mConnection != null) {
+ Log.i(MidiConstants.TAG,
+ "MidiPortConnector closing connection " + mConnection);
+ mConnection.close();
+ mConnection = null;
+ }
+ if (mSourceDevice != null) {
+ mSourceDevice.close();
+ mSourceDevice = null;
+ }
+ if (mDestinationDevice != null) {
+ mDestinationDevice.close();
+ mDestinationDevice = null;
+ }
+ }
+
+ private void safeClose() {
+ try {
+ close();
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "could not close resources", e);
+ }
+ }
+
+ /**
+ * Listener class used for receiving the results of
+ * {@link #connectToDevicePort}
+ */
+ public interface OnPortsConnectedListener {
+ /**
+ * Called to respond to a {@link #connectToDevicePort} request
+ *
+ * @param connection
+ * a {@link MidiConnection} that represents the connected
+ * ports, or null if connection failed
+ */
+ abstract public void onPortsConnected(MidiConnection connection);
+ }
+
+ /**
+ * Open two devices and connect their ports.
+ *
+ * @param sourceDeviceInfo
+ * @param sourcePortIndex
+ * @param destinationDeviceInfo
+ * @param destinationPortIndex
+ */
+ public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo,
+ final int sourcePortIndex,
+ final MidiDeviceInfo destinationDeviceInfo,
+ final int destinationPortIndex) {
+ connectToDevicePort(sourceDeviceInfo, sourcePortIndex,
+ destinationDeviceInfo, destinationPortIndex, null, null);
+ }
+
+ /**
+ * Open two devices and connect their ports.
+ *
+ * @param sourceDeviceInfo
+ * @param sourcePortIndex
+ * @param destinationDeviceInfo
+ * @param destinationPortIndex
+ */
+ public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo,
+ final int sourcePortIndex,
+ final MidiDeviceInfo destinationDeviceInfo,
+ final int destinationPortIndex,
+ final OnPortsConnectedListener listener, final Handler handler) {
+ safeClose();
+ mMidiManager.openDevice(destinationDeviceInfo,
+ new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice destinationDevice) {
+ if (destinationDevice == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open " + destinationDeviceInfo);
+ if (listener != null) {
+ listener.onPortsConnected(null);
+ }
+ } else {
+ mDestinationDevice = destinationDevice;
+ Log.i(MidiConstants.TAG,
+ "connectToDevicePort opened "
+ + destinationDeviceInfo);
+ // Destination device was opened so go to next step.
+ MidiInputPort destinationInputPort = destinationDevice
+ .openInputPort(destinationPortIndex);
+ if (destinationInputPort != null) {
+ Log.i(MidiConstants.TAG,
+ "connectToDevicePort opened port on "
+ + destinationDeviceInfo);
+ connectToDevicePort(sourceDeviceInfo,
+ sourcePortIndex,
+ destinationInputPort,
+ listener, handler);
+ } else {
+ Log.e(MidiConstants.TAG,
+ "could not open port on "
+ + destinationDeviceInfo);
+ safeClose();
+ if (listener != null) {
+ listener.onPortsConnected(null);
+ }
+ }
+ }
+ }
+ }, handler);
+ }
+
+
+ /**
+ * Open a source device and connect its output port to the
+ * destinationInputPort.
+ *
+ * @param sourceDeviceInfo
+ * @param sourcePortIndex
+ * @param destinationInputPort
+ */
+ private void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo,
+ final int sourcePortIndex,
+ final MidiInputPort destinationInputPort,
+ final OnPortsConnectedListener listener, final Handler handler) {
+ mMidiManager.openDevice(sourceDeviceInfo,
+ new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open " + sourceDeviceInfo);
+ safeClose();
+ if (listener != null) {
+ listener.onPortsConnected(null);
+ }
+ } else {
+ Log.i(MidiConstants.TAG,
+ "connectToDevicePort opened "
+ + sourceDeviceInfo);
+ // Device was opened so connect the ports.
+ mSourceDevice = device;
+ mConnection = device.connectPorts(
+ destinationInputPort, sourcePortIndex);
+ if (mConnection == null) {
+ Log.e(MidiConstants.TAG, "could not connect to "
+ + sourceDeviceInfo);
+ safeClose();
+ }
+ if (listener != null) {
+ listener.onPortsConnected(mConnection);
+ }
+ }
+ }
+ }, handler);
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiPortSelector.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiPortSelector.java
new file mode 100644
index 000000000..39f983e38
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiPortSelector.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiManager.DeviceCallback;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+
+import java.util.HashSet;
+
+/**
+ * Base class that uses a Spinner to select available MIDI ports.
+ */
+public abstract class MidiPortSelector extends DeviceCallback {
+ private int mType = MidiDeviceInfo.PortInfo.TYPE_INPUT;
+ protected ArrayAdapter mAdapter;
+ protected HashSet mBusyPorts = new HashSet();
+ private Spinner mSpinner;
+ protected MidiManager mMidiManager;
+ protected Activity mActivity;
+ private MidiPortWrapper mCurrentWrapper;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId
+ * ID from the layout resource
+ * @param type
+ * TYPE_INPUT or TYPE_OUTPUT
+ */
+ public MidiPortSelector(MidiManager midiManager, Activity activity,
+ int spinnerId, int type) {
+ mMidiManager = midiManager;
+ mActivity = activity;
+ mType = type;
+ mAdapter = new ArrayAdapter(activity,
+ android.R.layout.simple_spinner_item);
+ mAdapter.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ mAdapter.add(new MidiPortWrapper(null, 0, 0));
+
+ mSpinner = (Spinner) activity.findViewById(spinnerId);
+ mSpinner.setOnItemSelectedListener(
+ new AdapterView.OnItemSelectedListener() {
+
+ public void onItemSelected(AdapterView> parent, View view,
+ int pos, long id) {
+ mCurrentWrapper = mAdapter.getItem(pos);
+ onPortSelected(mCurrentWrapper);
+ }
+
+ public void onNothingSelected(AdapterView> parent) {
+ onPortSelected(null);
+ mCurrentWrapper = null;
+ }
+ });
+ mSpinner.setAdapter(mAdapter);
+
+ mMidiManager.registerDeviceCallback(this,
+ new Handler(Looper.getMainLooper()));
+
+ MidiDeviceInfo[] infos = mMidiManager.getDevices();
+ for (MidiDeviceInfo info : infos) {
+ onDeviceAdded(info);
+ }
+ }
+
+ /**
+ * Set to no port selected.
+ */
+ public void clearSelection() {
+ mSpinner.setSelection(0);
+ }
+
+ private int getInfoPortCount(final MidiDeviceInfo info) {
+ int portCount = (mType == MidiDeviceInfo.PortInfo.TYPE_INPUT)
+ ? info.getInputPortCount() : info.getOutputPortCount();
+ return portCount;
+ }
+
+ @Override
+ public void onDeviceAdded(final MidiDeviceInfo info) {
+ int portCount = getInfoPortCount(info);
+ for (int i = 0; i < portCount; ++i) {
+ MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
+ mAdapter.add(wrapper);
+ Log.i(MidiConstants.TAG, wrapper + " was added");
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onDeviceRemoved(final MidiDeviceInfo info) {
+ int portCount = getInfoPortCount(info);
+ for (int i = 0; i < portCount; ++i) {
+ MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
+ MidiPortWrapper currentWrapper = mCurrentWrapper;
+ mAdapter.remove(wrapper);
+ // If the currently selected port was removed then select no port.
+ if (wrapper.equals(currentWrapper)) {
+ clearSelection();
+ }
+ mAdapter.notifyDataSetChanged();
+ Log.i(MidiConstants.TAG, wrapper + " was removed");
+ }
+ }
+
+ @Override
+ public void onDeviceStatusChanged(final MidiDeviceStatus status) {
+ // If an input port becomes busy then remove it from the menu.
+ // If it becomes free then add it back to the menu.
+ if (mType == MidiDeviceInfo.PortInfo.TYPE_INPUT) {
+ MidiDeviceInfo info = status.getDeviceInfo();
+ Log.i(MidiConstants.TAG, "MidiPortSelector.onDeviceStatusChanged status = " + status
+ + ", mType = " + mType
+ + ", activity = " + mActivity.getPackageName()
+ + ", info = " + info);
+ // Look for transitions from free to busy.
+ int portCount = info.getInputPortCount();
+ for (int i = 0; i < portCount; ++i) {
+ MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
+ if (!wrapper.equals(mCurrentWrapper)) {
+ if (status.isInputPortOpen(i)) { // busy?
+ if (!mBusyPorts.contains(wrapper)) {
+ // was free, now busy
+ mBusyPorts.add(wrapper);
+ mAdapter.remove(wrapper);
+ mAdapter.notifyDataSetChanged();
+ }
+ } else {
+ if (mBusyPorts.remove(wrapper)) {
+ // was busy, now free
+ mAdapter.add(wrapper);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Implement this method to handle the user selecting a port on a device.
+ *
+ * @param wrapper
+ */
+ public abstract void onPortSelected(MidiPortWrapper wrapper);
+
+ /**
+ * Implement this method to clean up any open resources.
+ */
+ public abstract void onClose();
+
+ /**
+ *
+ */
+ public void close() {
+ onClose();
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiPortWrapper.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiPortWrapper.java
new file mode 100644
index 000000000..77aa73458
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiPortWrapper.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceInfo.PortInfo;
+import android.util.Log;
+
+// Wrapper for a MIDI device and port description.
+public class MidiPortWrapper {
+ private MidiDeviceInfo mInfo;
+ private int mPortIndex;
+ private int mType;
+ private String mString;
+
+ /**
+ * Wrapper for a MIDI device and port description.
+ * @param info
+ * @param portType
+ * @param portIndex
+ */
+ public MidiPortWrapper(MidiDeviceInfo info, int portType, int portIndex) {
+ mInfo = info;
+ mType = portType;
+ mPortIndex = portIndex;
+ }
+
+ private void updateString() {
+ if (mInfo == null) {
+ mString = "- - - - - -";
+ } else {
+ StringBuilder sb = new StringBuilder();
+ String name = mInfo.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_NAME);
+ if (name == null) {
+ name = mInfo.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER) + ", "
+ + mInfo.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_PRODUCT);
+ }
+ sb.append("#" + mInfo.getId());
+ sb.append(", ").append(name);
+ PortInfo portInfo = findPortInfo();
+ sb.append("[" + mPortIndex + "]");
+ if (portInfo != null) {
+ sb.append(", ").append(portInfo.getName());
+ } else {
+ sb.append(", null");
+ }
+ mString = sb.toString();
+ }
+ }
+
+ /**
+ * @param info
+ * @param portIndex
+ * @return
+ */
+ private PortInfo findPortInfo() {
+ PortInfo[] ports = mInfo.getPorts();
+ for (PortInfo portInfo : ports) {
+ if (portInfo.getPortNumber() == mPortIndex
+ && portInfo.getType() == mType) {
+ return portInfo;
+ }
+ }
+ return null;
+ }
+
+ public int getPortIndex() {
+ return mPortIndex;
+ }
+
+ public MidiDeviceInfo getDeviceInfo() {
+ return mInfo;
+ }
+
+ @Override
+ public String toString() {
+ if (mString == null) {
+ updateString();
+ }
+ return mString;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null)
+ return false;
+ if (!(other instanceof MidiPortWrapper))
+ return false;
+ MidiPortWrapper otherWrapper = (MidiPortWrapper) other;
+ if (mPortIndex != otherWrapper.mPortIndex)
+ return false;
+ if (mType != otherWrapper.mType)
+ return false;
+ if (mInfo == null)
+ return (otherWrapper.mInfo == null);
+ return mInfo.equals(otherWrapper.mInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 1;
+ hashCode = 31 * hashCode + mPortIndex;
+ hashCode = 31 * hashCode + mType;
+ hashCode = 31 * hashCode + mInfo.hashCode();
+ return hashCode;
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiTools.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiTools.java
new file mode 100644
index 000000000..82e3de4ba
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/MidiTools.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+
+/**
+ * Miscellaneous tools for Android MIDI.
+ */
+public class MidiTools {
+
+ /**
+ * @return a device that matches the manufacturer and product or null
+ */
+ public static MidiDeviceInfo findDevice(MidiManager midiManager,
+ String manufacturer, String product) {
+ for (MidiDeviceInfo info : midiManager.getDevices()) {
+ String deviceManufacturer = info.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER);
+ if ((manufacturer != null)
+ && manufacturer.equals(deviceManufacturer)) {
+ String deviceProduct = info.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_PRODUCT);
+ if ((product != null) && product.equals(deviceProduct)) {
+ return info;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/EnvelopeADSR.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/EnvelopeADSR.java
new file mode 100644
index 000000000..a29a1933e
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/EnvelopeADSR.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Very simple Attack, Decay, Sustain, Release envelope with linear ramps.
+ *
+ * Times are in seconds.
+ */
+public class EnvelopeADSR extends SynthUnit {
+ private static final int IDLE = 0;
+ private static final int ATTACK = 1;
+ private static final int DECAY = 2;
+ private static final int SUSTAIN = 3;
+ private static final int RELEASE = 4;
+ private static final int FINISHED = 5;
+ private static final float MIN_TIME = 0.001f;
+
+ private float mAttackRate;
+ private float mRreleaseRate;
+ private float mSustainLevel;
+ private float mDecayRate;
+ private float mCurrent;
+ private int mSstate = IDLE;
+
+ public EnvelopeADSR() {
+ setAttackTime(0.003f);
+ setDecayTime(0.08f);
+ setSustainLevel(0.3f);
+ setReleaseTime(1.0f);
+ }
+
+ public void setAttackTime(float time) {
+ if (time < MIN_TIME)
+ time = MIN_TIME;
+ mAttackRate = 1.0f / (SynthEngine.FRAME_RATE * time);
+ }
+
+ public void setDecayTime(float time) {
+ if (time < MIN_TIME)
+ time = MIN_TIME;
+ mDecayRate = 1.0f / (SynthEngine.FRAME_RATE * time);
+ }
+
+ public void setSustainLevel(float level) {
+ if (level < 0.0f)
+ level = 0.0f;
+ mSustainLevel = level;
+ }
+
+ public void setReleaseTime(float time) {
+ if (time < MIN_TIME)
+ time = MIN_TIME;
+ mRreleaseRate = 1.0f / (SynthEngine.FRAME_RATE * time);
+ }
+
+ public void on() {
+ mSstate = ATTACK;
+ }
+
+ public void off() {
+ mSstate = RELEASE;
+ }
+
+ @Override
+ public float render() {
+ switch (mSstate) {
+ case ATTACK:
+ mCurrent += mAttackRate;
+ if (mCurrent > 1.0f) {
+ mCurrent = 1.0f;
+ mSstate = DECAY;
+ }
+ break;
+ case DECAY:
+ mCurrent -= mDecayRate;
+ if (mCurrent < mSustainLevel) {
+ mCurrent = mSustainLevel;
+ mSstate = SUSTAIN;
+ }
+ break;
+ case RELEASE:
+ mCurrent -= mRreleaseRate;
+ if (mCurrent < 0.0f) {
+ mCurrent = 0.0f;
+ mSstate = FINISHED;
+ }
+ break;
+ }
+ return mCurrent;
+ }
+
+ public boolean isDone() {
+ return mSstate == FINISHED;
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SawOscillator.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SawOscillator.java
new file mode 100644
index 000000000..c02a6a1a5
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SawOscillator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+public class SawOscillator extends SynthUnit {
+ private float mPhase = 0.0f;
+ private float mPhaseIncrement = 0.01f;
+ private float mFrequency = 0.0f;
+ private float mFrequencyScaler = 1.0f;
+ private float mAmplitude = 1.0f;
+
+ public void setPitch(float pitch) {
+ float freq = (float) pitchToFrequency(pitch);
+ setFrequency(freq);
+ }
+
+ public void setFrequency(float frequency) {
+ mFrequency = frequency;
+ updatePhaseIncrement();
+ }
+
+ private void updatePhaseIncrement() {
+ mPhaseIncrement = 2.0f * mFrequency * mFrequencyScaler / 48000.0f;
+ }
+
+ public void setAmplitude(float amplitude) {
+ mAmplitude = amplitude;
+ }
+
+ public float getAmplitude() {
+ return mAmplitude;
+ }
+
+ public float getFrequencyScaler() {
+ return mFrequencyScaler;
+ }
+
+ public void setFrequencyScaler(float frequencyScaler) {
+ mFrequencyScaler = frequencyScaler;
+ updatePhaseIncrement();
+ }
+
+ float incrementWrapPhase() {
+ mPhase += mPhaseIncrement;
+ while (mPhase > 1.0) {
+ mPhase -= 2.0;
+ }
+ while (mPhase < -1.0) {
+ mPhase += 2.0;
+ }
+ return mPhase;
+ }
+
+ @Override
+ public float render() {
+ return incrementWrapPhase() * mAmplitude;
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SawOscillatorDPW.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SawOscillatorDPW.java
new file mode 100644
index 000000000..e5d661d56
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SawOscillatorDPW.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Band limited sawtooth oscillator.
+ * This will have very little aliasing at high frequencies.
+ */
+public class SawOscillatorDPW extends SawOscillator {
+ private float mZ1 = 0.0f; // delayed values
+ private float mZ2 = 0.0f;
+ private float mScaler; // frequency dependent scaler
+ private final static float VERY_LOW_FREQ = 0.0000001f;
+
+ @Override
+ public void setFrequency(float freq) {
+ /* Calculate scaling based on frequency. */
+ freq = Math.abs(freq);
+ super.setFrequency(freq);
+ if (freq < VERY_LOW_FREQ) {
+ mScaler = (float) (0.125 * 44100 / VERY_LOW_FREQ);
+ } else {
+ mScaler = (float) (0.125 * 44100 / freq);
+ }
+ }
+
+ @Override
+ public float render() {
+ float phase = incrementWrapPhase();
+ /* Square the raw sawtooth. */
+ float squared = phase * phase;
+ float diffed = squared - mZ2;
+ mZ2 = mZ1;
+ mZ1 = squared;
+ return diffed * mScaler * getAmplitude();
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SawVoice.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SawVoice.java
new file mode 100644
index 000000000..3b3e543e8
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SawVoice.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Sawtooth oscillator with an ADSR.
+ */
+public class SawVoice extends SynthVoice {
+ private SawOscillator mOscillator;
+ private EnvelopeADSR mEnvelope;
+
+ public SawVoice() {
+ mOscillator = createOscillator();
+ mEnvelope = new EnvelopeADSR();
+ }
+
+ protected SawOscillator createOscillator() {
+ return new SawOscillator();
+ }
+
+ @Override
+ public void noteOn(int noteIndex, int velocity) {
+ super.noteOn(noteIndex, velocity);
+ mOscillator.setPitch(noteIndex);
+ mOscillator.setAmplitude(getAmplitude());
+ mEnvelope.on();
+ }
+
+ @Override
+ public void noteOff() {
+ super.noteOff();
+ mEnvelope.off();
+ }
+
+ @Override
+ public void setFrequencyScaler(float scaler) {
+ mOscillator.setFrequencyScaler(scaler);
+ }
+
+ @Override
+ public float render() {
+ float output = mOscillator.render() * mEnvelope.render();
+ return output;
+ }
+
+ @Override
+ public boolean isDone() {
+ return mEnvelope.isDone();
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SimpleAudioOutput.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SimpleAudioOutput.java
new file mode 100644
index 000000000..04aa19c0b
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SimpleAudioOutput.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.util.Log;
+
+/**
+ * Simple base class for implementing audio output for examples.
+ * This can be sub-classed for experimentation or to redirect audio output.
+ */
+public class SimpleAudioOutput {
+
+ private static final String TAG = "AudioOutputTrack";
+ public static final int SAMPLES_PER_FRAME = 2;
+ public static final int BYTES_PER_SAMPLE = 4; // float
+ public static final int BYTES_PER_FRAME = SAMPLES_PER_FRAME * BYTES_PER_SAMPLE;
+ private AudioTrack mAudioTrack;
+ private int mFrameRate;
+
+ /**
+ *
+ */
+ public SimpleAudioOutput() {
+ super();
+ }
+
+ /**
+ * Create an audio track then call play().
+ *
+ * @param frameRate
+ */
+ public void start(int frameRate) {
+ stop();
+ mFrameRate = frameRate;
+ mAudioTrack = createAudioTrack(frameRate);
+ // AudioTrack will wait until it has enough data before starting.
+ mAudioTrack.play();
+ }
+
+ public AudioTrack createAudioTrack(int frameRate) {
+ int minBufferSizeBytes = AudioTrack.getMinBufferSize(frameRate,
+ AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_FLOAT);
+ Log.i(TAG, "AudioTrack.minBufferSize = " + minBufferSizeBytes
+ + " bytes = " + (minBufferSizeBytes / BYTES_PER_FRAME)
+ + " frames");
+ int bufferSize = 8 * minBufferSizeBytes / 8;
+ int outputBufferSizeFrames = bufferSize / BYTES_PER_FRAME;
+ Log.i(TAG, "actual bufferSize = " + bufferSize + " bytes = "
+ + outputBufferSizeFrames + " frames");
+
+ AudioTrack player = new AudioTrack(AudioManager.STREAM_MUSIC,
+ mFrameRate, AudioFormat.CHANNEL_OUT_STEREO,
+ AudioFormat.ENCODING_PCM_FLOAT, bufferSize,
+ AudioTrack.MODE_STREAM);
+ Log.i(TAG, "created AudioTrack");
+ return player;
+ }
+
+ public int write(float[] buffer, int offset, int length) {
+ return mAudioTrack.write(buffer, offset, length,
+ AudioTrack.WRITE_BLOCKING);
+ }
+
+ public void stop() {
+ if (mAudioTrack != null) {
+ mAudioTrack.stop();
+ mAudioTrack = null;
+ }
+ }
+
+ public int getFrameRate() {
+ return mFrameRate;
+ }
+
+ public AudioTrack getAudioTrack() {
+ return mAudioTrack;
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SineOscillator.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SineOscillator.java
new file mode 100644
index 000000000..c638c344c
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SineOscillator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Sinewave oscillator.
+ */
+public class SineOscillator extends SawOscillator {
+ // Factorial constants.
+ private static final float IF3 = 1.0f / (2 * 3);
+ private static final float IF5 = IF3 / (4 * 5);
+ private static final float IF7 = IF5 / (6 * 7);
+ private static final float IF9 = IF7 / (8 * 9);
+ private static final float IF11 = IF9 / (10 * 11);
+
+ /**
+ * Calculate sine using Taylor expansion. Do not use values outside the range.
+ *
+ * @param currentPhase in the range of -1.0 to +1.0 for one cycle
+ */
+ public static float fastSin(float currentPhase) {
+
+ /* Wrap phase back into region where results are more accurate. */
+ float yp = (currentPhase > 0.5f) ? 1.0f - currentPhase
+ : ((currentPhase < (-0.5f)) ? (-1.0f) - currentPhase : currentPhase);
+
+ float x = (float) (yp * Math.PI);
+ float x2 = (x * x);
+ /* Taylor expansion out to x**11/11! factored into multiply-adds */
+ return x * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1);
+ }
+
+ @Override
+ public float render() {
+ // Convert raw sawtooth to sine.
+ float phase = incrementWrapPhase();
+ return fastSin(phase) * getAmplitude();
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SineVoice.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SineVoice.java
new file mode 100644
index 000000000..e80d2c7eb
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SineVoice.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Replace sawtooth with a sine wave.
+ */
+public class SineVoice extends SawVoice {
+ @Override
+ protected SawOscillator createOscillator() {
+ return new SineOscillator();
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SynthEngine.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SynthEngine.java
new file mode 100644
index 000000000..6cd02a609
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SynthEngine.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+import android.media.midi.MidiReceiver;
+import android.util.Log;
+
+import com.example.android.common.midi.MidiConstants;
+import com.example.android.common.midi.MidiEventScheduler;
+import com.example.android.common.midi.MidiEventScheduler.MidiEvent;
+import com.example.android.common.midi.MidiFramer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.Iterator;
+
+/**
+ * Very simple polyphonic, single channel synthesizer. It runs a background
+ * thread that processes MIDI events and synthesizes audio.
+ */
+public class SynthEngine extends MidiReceiver {
+
+ private static final String TAG = "SynthEngine";
+
+ public static final int FRAME_RATE = 48000;
+ private static final int FRAMES_PER_BUFFER = 240;
+ private static final int SAMPLES_PER_FRAME = 2;
+
+ private boolean go;
+ private Thread mThread;
+ private float[] mBuffer = new float[FRAMES_PER_BUFFER * SAMPLES_PER_FRAME];
+ private float mFrequencyScaler = 1.0f;
+ private float mBendRange = 2.0f; // semitones
+ private int mProgram;
+
+ private ArrayList mFreeVoices = new ArrayList();
+ private Hashtable
+ mVoices = new Hashtable();
+ private MidiEventScheduler mEventScheduler;
+ private MidiFramer mFramer;
+ private MidiReceiver mReceiver = new MyReceiver();
+ private SimpleAudioOutput mAudioOutput;
+
+ public SynthEngine() {
+ this(new SimpleAudioOutput());
+ }
+
+ public SynthEngine(SimpleAudioOutput audioOutput) {
+ mReceiver = new MyReceiver();
+ mFramer = new MidiFramer(mReceiver);
+ mAudioOutput = audioOutput;
+ }
+
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ if (mEventScheduler != null) {
+ if (!MidiConstants.isAllActiveSensing(data, offset, count)) {
+ mEventScheduler.getReceiver().send(data, offset, count,
+ timestamp);
+ }
+ }
+ }
+
+ private class MyReceiver extends MidiReceiver {
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ byte command = (byte) (data[0] & MidiConstants.STATUS_COMMAND_MASK);
+ int channel = (byte) (data[0] & MidiConstants.STATUS_CHANNEL_MASK);
+ switch (command) {
+ case MidiConstants.STATUS_NOTE_OFF:
+ noteOff(channel, data[1], data[2]);
+ break;
+ case MidiConstants.STATUS_NOTE_ON:
+ noteOn(channel, data[1], data[2]);
+ break;
+ case MidiConstants.STATUS_PITCH_BEND:
+ int bend = (data[2] << 7) + data[1];
+ pitchBend(channel, bend);
+ break;
+ case MidiConstants.STATUS_PROGRAM_CHANGE:
+ mProgram = data[1];
+ mFreeVoices.clear();
+ break;
+ default:
+ logMidiMessage(data, offset, count);
+ break;
+ }
+ }
+ }
+
+ class MyRunnable implements Runnable {
+ @Override
+ public void run() {
+ try {
+ mAudioOutput.start(FRAME_RATE);
+ onLoopStarted();
+ while (go) {
+ processMidiEvents();
+ generateBuffer();
+ mAudioOutput.write(mBuffer, 0, mBuffer.length);
+ onBufferCompleted(FRAMES_PER_BUFFER);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "SynthEngine background thread exception.", e);
+ } finally {
+ onLoopEnded();
+ mAudioOutput.stop();
+ }
+ }
+ }
+
+ /**
+ * This is called form the synthesis thread before it starts looping.
+ */
+ public void onLoopStarted() {
+ }
+
+ /**
+ * This is called once at the end of each synthesis loop.
+ *
+ * @param framesPerBuffer
+ */
+ public void onBufferCompleted(int framesPerBuffer) {
+ }
+
+ /**
+ * This is called form the synthesis thread when it stop looping.
+ */
+ public void onLoopEnded() {
+ }
+
+ /**
+ * Assume message has been aligned to the start of a MIDI message.
+ *
+ * @param data
+ * @param offset
+ * @param count
+ */
+ public void logMidiMessage(byte[] data, int offset, int count) {
+ String text = "Received: ";
+ for (int i = 0; i < count; i++) {
+ text += String.format("0x%02X, ", data[offset + i]);
+ }
+ Log.i(TAG, text);
+ }
+
+ /**
+ * @throws IOException
+ *
+ */
+ private void processMidiEvents() throws IOException {
+ long now = System.nanoTime(); // TODO use audio presentation time
+ MidiEvent event = (MidiEvent) mEventScheduler.getNextEvent(now);
+ while (event != null) {
+ mFramer.send(event.data, 0, event.count, event.getTimestamp());
+ mEventScheduler.addEventToPool(event);
+ event = (MidiEvent) mEventScheduler.getNextEvent(now);
+ }
+ }
+
+ /**
+ *
+ */
+ private void generateBuffer() {
+ for (int i = 0; i < mBuffer.length; i++) {
+ mBuffer[i] = 0.0f;
+ }
+ Iterator iterator = mVoices.values().iterator();
+ while (iterator.hasNext()) {
+ SynthVoice voice = iterator.next();
+ if (voice.isDone()) {
+ iterator.remove();
+ // mFreeVoices.add(voice);
+ } else {
+ voice.mix(mBuffer, SAMPLES_PER_FRAME, 0.25f);
+ }
+ }
+ }
+
+ public void noteOff(int channel, int noteIndex, int velocity) {
+ SynthVoice voice = mVoices.get(noteIndex);
+ if (voice != null) {
+ voice.noteOff();
+ }
+ }
+
+ public void allNotesOff() {
+ Iterator iterator = mVoices.values().iterator();
+ while (iterator.hasNext()) {
+ SynthVoice voice = iterator.next();
+ voice.noteOff();
+ }
+ }
+
+ /**
+ * Create a SynthVoice.
+ */
+ public SynthVoice createVoice(int program) {
+ // For every odd program number use a sine wave.
+ if ((program & 1) == 1) {
+ return new SineVoice();
+ } else {
+ return new SawVoice();
+ }
+ }
+
+ /**
+ *
+ * @param channel
+ * @param noteIndex
+ * @param velocity
+ */
+ public void noteOn(int channel, int noteIndex, int velocity) {
+ if (velocity == 0) {
+ noteOff(channel, noteIndex, velocity);
+ } else {
+ mVoices.remove(noteIndex);
+ SynthVoice voice;
+ if (mFreeVoices.size() > 0) {
+ voice = mFreeVoices.remove(mFreeVoices.size() - 1);
+ } else {
+ voice = createVoice(mProgram);
+ }
+ voice.setFrequencyScaler(mFrequencyScaler);
+ voice.noteOn(noteIndex, velocity);
+ mVoices.put(noteIndex, voice);
+ }
+ }
+
+ public void pitchBend(int channel, int bend) {
+ double semitones = (mBendRange * (bend - 0x2000)) / 0x2000;
+ mFrequencyScaler = (float) Math.pow(2.0, semitones / 12.0);
+ Iterator iterator = mVoices.values().iterator();
+ while (iterator.hasNext()) {
+ SynthVoice voice = iterator.next();
+ voice.setFrequencyScaler(mFrequencyScaler);
+ }
+ }
+
+ /**
+ * Start the synthesizer.
+ */
+ public void start() {
+ stop();
+ go = true;
+ mThread = new Thread(new MyRunnable());
+ mEventScheduler = new MidiEventScheduler();
+ mThread.start();
+ }
+
+ /**
+ * Stop the synthesizer.
+ */
+ public void stop() {
+ go = false;
+ if (mThread != null) {
+ try {
+ mThread.interrupt();
+ mThread.join(500);
+ } catch (InterruptedException e) {
+ // OK, just stopping safely.
+ }
+ mThread = null;
+ mEventScheduler = null;
+ }
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SynthUnit.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SynthUnit.java
new file mode 100644
index 000000000..90599e284
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SynthUnit.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+public abstract class SynthUnit {
+
+ private static final double CONCERT_A_PITCH = 69.0;
+ private static final double CONCERT_A_FREQUENCY = 440.0;
+
+ /**
+ * @param pitch
+ * MIDI pitch in semitones
+ * @return frequency
+ */
+ public static double pitchToFrequency(double pitch) {
+ double semitones = pitch - CONCERT_A_PITCH;
+ return CONCERT_A_FREQUENCY * Math.pow(2.0, semitones / 12.0);
+ }
+
+ public abstract float render();
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SynthVoice.java b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SynthVoice.java
new file mode 100644
index 000000000..78ba09ac4
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.common.midi/synth/SynthVoice.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Base class for a polyphonic synthesizer voice.
+ */
+public abstract class SynthVoice {
+ private int mNoteIndex;
+ private float mAmplitude;
+ public static final int STATE_OFF = 0;
+ public static final int STATE_ON = 1;
+ private int mState = STATE_OFF;
+
+ public SynthVoice() {
+ mNoteIndex = -1;
+ }
+
+ public void noteOn(int noteIndex, int velocity) {
+ mState = STATE_ON;
+ this.mNoteIndex = noteIndex;
+ setAmplitude(velocity / 128.0f);
+ }
+
+ public void noteOff() {
+ mState = STATE_OFF;
+ }
+
+ /**
+ * Add the output of this voice to an output buffer.
+ *
+ * @param outputBuffer
+ * @param samplesPerFrame
+ * @param level
+ */
+ public void mix(float[] outputBuffer, int samplesPerFrame, float level) {
+ int numFrames = outputBuffer.length / samplesPerFrame;
+ for (int i = 0; i < numFrames; i++) {
+ float output = render();
+ int offset = i * samplesPerFrame;
+ for (int jf = 0; jf < samplesPerFrame; jf++) {
+ outputBuffer[offset + jf] += output * level;
+ }
+ }
+ }
+
+ public abstract float render();
+
+ public boolean isDone() {
+ return mState == STATE_OFF;
+ }
+
+ public int getNoteIndex() {
+ return mNoteIndex;
+ }
+
+ public float getAmplitude() {
+ return mAmplitude;
+ }
+
+ public void setAmplitude(float amplitude) {
+ this.mAmplitude = amplitude;
+ }
+
+ /**
+ * @param scaler
+ */
+ public void setFrequencyScaler(float scaler) {
+ }
+
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.midiscope/LoggingReceiver.java b/samples/browseable/MidiScope/src/com.example.android.midiscope/LoggingReceiver.java
new file mode 100644
index 000000000..23ce8f7c0
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.midiscope/LoggingReceiver.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.midiscope;
+
+import android.media.midi.MidiReceiver;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Convert incoming MIDI messages to a string and write them to a ScopeLogger.
+ * Assume that messages have been aligned using a MidiFramer.
+ */
+public class LoggingReceiver extends MidiReceiver {
+ public static final String TAG = "MidiScope";
+ private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1);
+ private long mStartTime;
+ private ScopeLogger mLogger;
+
+ public LoggingReceiver(ScopeLogger logger) {
+ mStartTime = System.nanoTime();
+ mLogger = logger;
+ }
+
+ /*
+ * @see android.media.midi.MidiReceiver#onReceive(byte[], int, int, long)
+ */
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ StringBuilder sb = new StringBuilder();
+ if (timestamp == 0) {
+ sb.append(String.format("-----0----: "));
+ } else {
+ long monoTime = timestamp - mStartTime;
+ double seconds = (double) monoTime / NANOS_PER_SECOND;
+ sb.append(String.format("%10.3f: ", seconds));
+ }
+ sb.append(MidiPrinter.formatBytes(data, offset, count));
+ sb.append(": ");
+ sb.append(MidiPrinter.formatMessage(data, offset, count));
+ String text = sb.toString();
+ mLogger.log(text);
+ Log.i(TAG, text);
+ }
+
+}
\ No newline at end of file
diff --git a/samples/browseable/MidiScope/src/com.example.android.midiscope/MainActivity.java b/samples/browseable/MidiScope/src/com.example.android.midiscope/MainActivity.java
new file mode 100644
index 000000000..41d74f035
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.midiscope/MainActivity.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.midiscope;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiReceiver;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toolbar;
+
+import com.example.android.common.midi.MidiFramer;
+import com.example.android.common.midi.MidiOutputPortSelector;
+import com.example.android.common.midi.MidiPortWrapper;
+
+import java.util.LinkedList;
+
+/**
+ * App that provides a MIDI echo service.
+ */
+public class MainActivity extends Activity implements ScopeLogger {
+
+ private static final int MAX_LINES = 100;
+
+ private final LinkedList mLogLines = new LinkedList<>();
+ private TextView mLog;
+ private ScrollView mScroller;
+ private MidiOutputPortSelector mLogSenderSelector;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ setActionBar((Toolbar) findViewById(R.id.toolbar));
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(false);
+ }
+
+ mLog = (TextView) findViewById(R.id.log);
+ mScroller = (ScrollView) findViewById(R.id.scroll);
+
+ // Setup MIDI
+ MidiManager midiManager = (MidiManager) getSystemService(MIDI_SERVICE);
+
+ // Receiver that prints the messages.
+ MidiReceiver loggingReceiver = new LoggingReceiver(this);
+
+ // Receiver that parses raw data into complete messages.
+ MidiFramer connectFramer = new MidiFramer(loggingReceiver);
+
+ // Setup a menu to select an input source.
+ mLogSenderSelector = new MidiOutputPortSelector(midiManager, this, R.id.spinner_senders) {
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ super.onPortSelected(wrapper);
+ if (wrapper != null) {
+ mLogLines.clear();
+ MidiDeviceInfo deviceInfo = wrapper.getDeviceInfo();
+ if (deviceInfo == null) {
+ log(getString(R.string.header_text));
+ } else {
+ log(MidiPrinter.formatDeviceInfo(deviceInfo));
+ }
+ }
+ }
+ };
+ mLogSenderSelector.getSender().connect(connectFramer);
+
+ // Tell the virtual device to log its messages here..
+ MidiScope.setScopeLogger(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ mLogSenderSelector.onClose();
+ // The scope will live on as a service so we need to tell it to stop
+ // writing log messages to this Activity.
+ MidiScope.setScopeLogger(null);
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main, menu);
+ setKeepScreenOn(menu.findItem(R.id.action_keep_screen_on).isChecked());
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_clear_all:
+ mLogLines.clear();
+ logOnUiThread("");
+ break;
+ case R.id.action_keep_screen_on:
+ boolean checked = !item.isChecked();
+ setKeepScreenOn(checked);
+ item.setChecked(checked);
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void setKeepScreenOn(boolean keepScreenOn) {
+ if (keepScreenOn) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+
+ @Override
+ public void log(final String string) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ logOnUiThread(string);
+ }
+ });
+ }
+
+ /**
+ * Logs a message to our TextView. This needs to be called from the UI thread.
+ */
+ private void logOnUiThread(String s) {
+ mLogLines.add(s);
+ if (mLogLines.size() > MAX_LINES) {
+ mLogLines.removeFirst();
+ }
+ // Render line buffer to one String.
+ StringBuilder sb = new StringBuilder();
+ for (String line : mLogLines) {
+ sb.append(line).append('\n');
+ }
+ mLog.setText(sb.toString());
+ mScroller.fullScroll(View.FOCUS_DOWN);
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.midiscope/MidiPrinter.java b/samples/browseable/MidiScope/src/com.example.android.midiscope/MidiPrinter.java
new file mode 100644
index 000000000..9e97c04cb
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.midiscope/MidiPrinter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.midiscope;
+
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceInfo.PortInfo;
+import android.os.Bundle;
+
+import com.example.android.common.midi.MidiConstants;
+
+/**
+ * Format a MIDI message for printing.
+ */
+public class MidiPrinter {
+
+ public static final String[] CHANNEL_COMMAND_NAMES = { "NoteOff", "NoteOn",
+ "PolyTouch", "Control", "Program", "Pressure", "Bend" };
+ public static final String[] SYSTEM_COMMAND_NAMES = { "SysEx", // F0
+ "TimeCode", // F1
+ "SongPos", // F2
+ "SongSel", // F3
+ "F4", // F4
+ "F5", // F5
+ "TuneReq", // F6
+ "EndSysex", // F7
+ "TimingClock", // F8
+ "F9", // F9
+ "Start", // FA
+ "Continue", // FB
+ "Stop", // FC
+ "FD", // FD
+ "ActiveSensing", // FE
+ "Reset" // FF
+ };
+
+ public static String getName(int status) {
+ if (status >= 0xF0) {
+ int index = status & 0x0F;
+ return SYSTEM_COMMAND_NAMES[index];
+ } else if (status >= 0x80) {
+ int index = (status >> 4) & 0x07;
+ return CHANNEL_COMMAND_NAMES[index];
+ } else {
+ return "data";
+ }
+ }
+
+ public static String formatBytes(byte[] data, int offset, int count) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < count; i++) {
+ sb.append(String.format(" %02X", data[offset + i]));
+ }
+ return sb.toString();
+ }
+
+ public static String formatMessage(byte[] data, int offset, int count) {
+ StringBuilder sb = new StringBuilder();
+ byte statusByte = data[offset++];
+ int status = statusByte & 0xFF;
+ sb.append(getName(status)).append("(");
+ int numData = MidiConstants.getBytesPerMessage(statusByte) - 1;
+ if ((status >= 0x80) && (status < 0xF0)) { // channel message
+ int channel = status & 0x0F;
+ // Add 1 for humans who think channels are numbered 1-16.
+ sb.append((channel + 1)).append(", ");
+ }
+ for (int i = 0; i < numData; i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ sb.append(data[offset++]);
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ public static String formatDeviceInfo(MidiDeviceInfo info) {
+ StringBuilder sb = new StringBuilder();
+ if (info != null) {
+ Bundle properties = info.getProperties();
+ for (String key : properties.keySet()) {
+ Object value = properties.get(key);
+ sb.append(key).append(" = ").append(value).append('\n');
+ }
+ for (PortInfo port : info.getPorts()) {
+ sb.append((port.getType() == PortInfo.TYPE_INPUT) ? "input"
+ : "output");
+ sb.append("[").append(port.getPortNumber()).append("] = \"").append(port.getName()
+ + "\"\n");
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.midiscope/MidiScope.java b/samples/browseable/MidiScope/src/com.example.android.midiscope/MidiScope.java
new file mode 100644
index 000000000..3965d83cf
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.midiscope/MidiScope.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.midiscope;
+
+import android.media.midi.MidiDeviceService;
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiReceiver;
+
+import com.example.android.common.midi.MidiFramer;
+
+import java.io.IOException;
+
+/**
+ * Virtual MIDI Device that logs messages to a ScopeLogger.
+ */
+
+public class MidiScope extends MidiDeviceService {
+
+ private static ScopeLogger mScopeLogger;
+ private MidiReceiver mInputReceiver = new MyReceiver();
+ private static MidiFramer mDeviceFramer;
+
+ @Override
+ public MidiReceiver[] onGetInputPortReceivers() {
+ return new MidiReceiver[] { mInputReceiver };
+ }
+
+ public static ScopeLogger getScopeLogger() {
+ return mScopeLogger;
+ }
+
+ public static void setScopeLogger(ScopeLogger logger) {
+ if (logger != null) {
+ // Receiver that prints the messages.
+ LoggingReceiver loggingReceiver = new LoggingReceiver(logger);
+ mDeviceFramer = new MidiFramer(loggingReceiver);
+ }
+ mScopeLogger = logger;
+ }
+
+ private static class MyReceiver extends MidiReceiver {
+ @Override
+ public void onSend(byte[] data, int offset, int count,
+ long timestamp) throws IOException {
+ if (mScopeLogger != null) {
+ // Send raw data to be parsed into discrete messages.
+ mDeviceFramer.send(data, offset, count, timestamp);
+ }
+ }
+ }
+
+ /**
+ * This will get called when clients connect or disconnect.
+ * Log device information.
+ */
+ @Override
+ public void onDeviceStatusChanged(MidiDeviceStatus status) {
+ if (mScopeLogger != null) {
+ if (status.isInputPortOpen(0)) {
+ mScopeLogger.log("=== connected ===");
+ String text = MidiPrinter.formatDeviceInfo(
+ status.getDeviceInfo());
+ mScopeLogger.log(text);
+ } else {
+ mScopeLogger.log("--- disconnected ---");
+ }
+ }
+ }
+}
diff --git a/samples/browseable/MidiScope/src/com.example.android.midiscope/ScopeLogger.java b/samples/browseable/MidiScope/src/com.example.android.midiscope/ScopeLogger.java
new file mode 100644
index 000000000..dc52efd65
--- /dev/null
+++ b/samples/browseable/MidiScope/src/com.example.android.midiscope/ScopeLogger.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.midiscope;
+
+public interface ScopeLogger {
+ /**
+ * Write the text string somewhere that the user can see it.
+ * @param text
+ */
+ void log(String text);
+}
diff --git a/samples/browseable/MidiSynth/AndroidManifest.xml b/samples/browseable/MidiSynth/AndroidManifest.xml
new file mode 100644
index 000000000..100241920
--- /dev/null
+++ b/samples/browseable/MidiSynth/AndroidManifest.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiSynth/_index.jd b/samples/browseable/MidiSynth/_index.jd
new file mode 100644
index 000000000..33842a0dd
--- /dev/null
+++ b/samples/browseable/MidiSynth/_index.jd
@@ -0,0 +1,11 @@
+
+page.tags="MidiSynth"
+sample.group=Media
+@jd:body
+
+
+
+This sample demonstrates how to use the MIDI API to receive and play MIDI messages coming from an
+attached input device.
+
+
diff --git a/samples/browseable/MidiSynth/res/drawable-hdpi/tile.9.png b/samples/browseable/MidiSynth/res/drawable-hdpi/tile.9.png
new file mode 100644
index 000000000..135862883
Binary files /dev/null and b/samples/browseable/MidiSynth/res/drawable-hdpi/tile.9.png differ
diff --git a/samples/browseable/MidiSynth/res/layout/main.xml b/samples/browseable/MidiSynth/res/layout/main.xml
new file mode 100644
index 000000000..6b9e2c7b5
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/layout/main.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiSynth/res/menu/main.xml b/samples/browseable/MidiSynth/res/menu/main.xml
new file mode 100644
index 000000000..33093f0a5
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/menu/main.xml
@@ -0,0 +1,25 @@
+
+
diff --git a/samples/browseable/MidiSynth/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/MidiSynth/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..38250e7f2
Binary files /dev/null and b/samples/browseable/MidiSynth/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiSynth/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/MidiSynth/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..58c0025b0
Binary files /dev/null and b/samples/browseable/MidiSynth/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiSynth/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/MidiSynth/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..369485533
Binary files /dev/null and b/samples/browseable/MidiSynth/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiSynth/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/MidiSynth/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..fb50ad157
Binary files /dev/null and b/samples/browseable/MidiSynth/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiSynth/res/mipmap-xxxhdpi/ic_launcher.png b/samples/browseable/MidiSynth/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..801bf9c10
Binary files /dev/null and b/samples/browseable/MidiSynth/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/MidiSynth/res/values-sw600dp/template-dimens.xml b/samples/browseable/MidiSynth/res/values-sw600dp/template-dimens.xml
new file mode 100644
index 000000000..22074a2bd
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values-sw600dp/template-dimens.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ @dimen/margin_huge
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/MidiSynth/res/values-sw600dp/template-styles.xml b/samples/browseable/MidiSynth/res/values-sw600dp/template-styles.xml
new file mode 100644
index 000000000..03d197418
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values-sw600dp/template-styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiSynth/res/values-v11/template-styles.xml b/samples/browseable/MidiSynth/res/values-v11/template-styles.xml
new file mode 100644
index 000000000..8c1ea66f2
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values-v11/template-styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiSynth/res/values-v21/base-colors.xml b/samples/browseable/MidiSynth/res/values-v21/base-colors.xml
new file mode 100644
index 000000000..8b6ec3f85
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values-v21/base-colors.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiSynth/res/values-v21/base-template-styles.xml b/samples/browseable/MidiSynth/res/values-v21/base-template-styles.xml
new file mode 100644
index 000000000..c778e4f98
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values-v21/base-template-styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiSynth/res/values/base-strings.xml b/samples/browseable/MidiSynth/res/values/base-strings.xml
new file mode 100644
index 000000000..d38815ff6
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values/base-strings.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ MidiSynth
+
+
+
+
diff --git a/samples/browseable/MidiSynth/res/values/colors.xml b/samples/browseable/MidiSynth/res/values/colors.xml
new file mode 100644
index 000000000..4d2204f9d
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values/colors.xml
@@ -0,0 +1,26 @@
+
+
+
+ #4CAF50
+ #388E3C
+ #C8E6C9
+ #FFEB3B
+ #212121
+ #727272
+ #FFFFFF
+ #B6B6B6
+
diff --git a/samples/browseable/MidiSynth/res/values/strings.xml b/samples/browseable/MidiSynth/res/values/strings.xml
new file mode 100644
index 000000000..76a8fa266
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values/strings.xml
@@ -0,0 +1,25 @@
+
+
+
+ Select Sender for Synth
+ Selected port is in use or unavailable.
+ Port opened OK.
+
+ "none"
+
+ Keep screen on
+
diff --git a/samples/browseable/MidiSynth/res/values/styles.xml b/samples/browseable/MidiSynth/res/values/styles.xml
new file mode 100644
index 000000000..30d645565
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values/styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiSynth/res/values/template-dimens.xml b/samples/browseable/MidiSynth/res/values/template-dimens.xml
new file mode 100644
index 000000000..39e710b5c
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values/template-dimens.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ 4dp
+ 8dp
+ 16dp
+ 32dp
+ 64dp
+
+
+
+ @dimen/margin_medium
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/MidiSynth/res/values/template-styles.xml b/samples/browseable/MidiSynth/res/values/template-styles.xml
new file mode 100644
index 000000000..6e7d593dd
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/values/template-styles.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiSynth/res/xml/synth_device_info.xml b/samples/browseable/MidiSynth/res/xml/synth_device_info.xml
new file mode 100644
index 000000000..405e87e11
--- /dev/null
+++ b/samples/browseable/MidiSynth/res/xml/synth_device_info.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/EventScheduler.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/EventScheduler.java
new file mode 100644
index 000000000..37c0140dc
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/EventScheduler.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Store SchedulableEvents in a timestamped buffer.
+ * Events may be written in any order.
+ * Events will be read in sorted order.
+ * Events with the same timestamp will be read in the order they were added.
+ *
+ * Only one Thread can write into the buffer.
+ * And only one Thread can read from the buffer.
+ */
+public class EventScheduler {
+ private static final long NANOS_PER_MILLI = 1000000;
+
+ private final Object lock = new Object();
+ private SortedMap mEventBuffer;
+ // This does not have to be guarded. It is only set by the writing thread.
+ // If the reader sees a null right before being set then that is OK.
+ private FastEventQueue mEventPool = null;
+ private static final int MAX_POOL_SIZE = 200;
+
+ public EventScheduler() {
+ mEventBuffer = new TreeMap();
+ }
+
+ // If we keep at least one node in the list then it can be atomic
+ // and non-blocking.
+ private class FastEventQueue {
+ // One thread takes from the beginning of the list.
+ volatile SchedulableEvent mFirst;
+ // A second thread returns events to the end of the list.
+ volatile SchedulableEvent mLast;
+ volatile long mEventsAdded;
+ volatile long mEventsRemoved;
+
+ FastEventQueue(SchedulableEvent event) {
+ mFirst = event;
+ mLast = mFirst;
+ mEventsAdded = 1; // Always created with one event added. Never empty.
+ mEventsRemoved = 0; // None removed yet.
+ }
+
+ int size() {
+ return (int)(mEventsAdded - mEventsRemoved);
+ }
+
+ /**
+ * Do not call this unless there is more than one event
+ * in the list.
+ * @return first event in the list
+ */
+ public SchedulableEvent remove() {
+ // Take first event.
+ mEventsRemoved++;
+ SchedulableEvent event = mFirst;
+ mFirst = event.mNext;
+ return event;
+ }
+
+ /**
+ * @param event
+ */
+ public void add(SchedulableEvent event) {
+ event.mNext = null;
+ mLast.mNext = event;
+ mLast = event;
+ mEventsAdded++;
+ }
+ }
+
+ /**
+ * Base class for events that can be stored in the EventScheduler.
+ */
+ public static class SchedulableEvent {
+ private long mTimestamp;
+ private SchedulableEvent mNext = null;
+
+ /**
+ * @param timestamp
+ */
+ public SchedulableEvent(long timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ /**
+ * @return timestamp
+ */
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ /**
+ * The timestamp should not be modified when the event is in the
+ * scheduling buffer.
+ */
+ public void setTimestamp(long timestamp) {
+ mTimestamp = timestamp;
+ }
+ }
+
+ /**
+ * Get an event from the pool.
+ * Always leave at least one event in the pool.
+ * @return event or null
+ */
+ public SchedulableEvent removeEventfromPool() {
+ SchedulableEvent event = null;
+ if (mEventPool != null && (mEventPool.size() > 1)) {
+ event = mEventPool.remove();
+ }
+ return event;
+ }
+
+ /**
+ * Return events to a pool so they can be reused.
+ *
+ * @param event
+ */
+ public void addEventToPool(SchedulableEvent event) {
+ if (mEventPool == null) {
+ mEventPool = new FastEventQueue(event);
+ // If we already have enough items in the pool then just
+ // drop the event. This prevents unbounded memory leaks.
+ } else if (mEventPool.size() < MAX_POOL_SIZE) {
+ mEventPool.add(event);
+ }
+ }
+
+ /**
+ * Add an event to the scheduler. Events with the same time will be
+ * processed in order.
+ *
+ * @param event
+ */
+ public void add(SchedulableEvent event) {
+ synchronized (lock) {
+ FastEventQueue list = mEventBuffer.get(event.getTimestamp());
+ if (list == null) {
+ long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE
+ : mEventBuffer.firstKey();
+ list = new FastEventQueue(event);
+ mEventBuffer.put(event.getTimestamp(), list);
+ // If the event we added is earlier than the previous earliest
+ // event then notify any threads waiting for the next event.
+ if (event.getTimestamp() < lowestTime) {
+ lock.notify();
+ }
+ } else {
+ list.add(event);
+ }
+ }
+ }
+
+ // Caller must synchronize on lock before calling.
+ private SchedulableEvent removeNextEventLocked(long lowestTime) {
+ SchedulableEvent event;
+ FastEventQueue list = mEventBuffer.get(lowestTime);
+ // Remove list from tree if this is the last node.
+ if ((list.size() == 1)) {
+ mEventBuffer.remove(lowestTime);
+ }
+ event = list.remove();
+ return event;
+ }
+
+ /**
+ * Check to see if any scheduled events are ready to be processed.
+ *
+ * @param timestamp
+ * @return next event or null if none ready
+ */
+ public SchedulableEvent getNextEvent(long time) {
+ SchedulableEvent event = null;
+ synchronized (lock) {
+ if (!mEventBuffer.isEmpty()) {
+ long lowestTime = mEventBuffer.firstKey();
+ // Is it time for this list to be processed?
+ if (lowestTime <= time) {
+ event = removeNextEventLocked(lowestTime);
+ }
+ }
+ }
+ // Log.i(TAG, "getNextEvent: event = " + event);
+ return event;
+ }
+
+ /**
+ * Return the next available event or wait until there is an event ready to
+ * be processed. This method assumes that the timestamps are in nanoseconds
+ * and that the current time is System.nanoTime().
+ *
+ * @return event
+ * @throws InterruptedException
+ */
+ public SchedulableEvent waitNextEvent() throws InterruptedException {
+ SchedulableEvent event = null;
+ while (true) {
+ long millisToWait = Integer.MAX_VALUE;
+ synchronized (lock) {
+ if (!mEventBuffer.isEmpty()) {
+ long now = System.nanoTime();
+ long lowestTime = mEventBuffer.firstKey();
+ // Is it time for the earliest list to be processed?
+ if (lowestTime <= now) {
+ event = removeNextEventLocked(lowestTime);
+ break;
+ } else {
+ // Figure out how long to sleep until next event.
+ long nanosToWait = lowestTime - now;
+ // Add 1 millisecond so we don't wake up before it is
+ // ready.
+ millisToWait = 1 + (nanosToWait / NANOS_PER_MILLI);
+ // Clip 64-bit value to 32-bit max.
+ if (millisToWait > Integer.MAX_VALUE) {
+ millisToWait = Integer.MAX_VALUE;
+ }
+ }
+ }
+ lock.wait((int) millisToWait);
+ }
+ }
+ return event;
+ }
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiConstants.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiConstants.java
new file mode 100644
index 000000000..38c25d505
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiConstants.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+/**
+ * MIDI related constants and static methods.
+ * These values are defined in the MIDI Standard 1.0
+ * available from the MIDI Manufacturers Association.
+ */
+public class MidiConstants {
+ protected final static String TAG = "MidiTools";
+ public static final byte STATUS_COMMAND_MASK = (byte) 0xF0;
+ public static final byte STATUS_CHANNEL_MASK = (byte) 0x0F;
+
+ // Channel voice messages.
+ public static final byte STATUS_NOTE_OFF = (byte) 0x80;
+ public static final byte STATUS_NOTE_ON = (byte) 0x90;
+ public static final byte STATUS_POLYPHONIC_AFTERTOUCH = (byte) 0xA0;
+ public static final byte STATUS_CONTROL_CHANGE = (byte) 0xB0;
+ public static final byte STATUS_PROGRAM_CHANGE = (byte) 0xC0;
+ public static final byte STATUS_CHANNEL_PRESSURE = (byte) 0xD0;
+ public static final byte STATUS_PITCH_BEND = (byte) 0xE0;
+
+ // System Common Messages.
+ public static final byte STATUS_SYSTEM_EXCLUSIVE = (byte) 0xF0;
+ public static final byte STATUS_MIDI_TIME_CODE = (byte) 0xF1;
+ public static final byte STATUS_SONG_POSITION = (byte) 0xF2;
+ public static final byte STATUS_SONG_SELECT = (byte) 0xF3;
+ public static final byte STATUS_TUNE_REQUEST = (byte) 0xF6;
+ public static final byte STATUS_END_SYSEX = (byte) 0xF7;
+
+ // System Real-Time Messages
+ public static final byte STATUS_TIMING_CLOCK = (byte) 0xF8;
+ public static final byte STATUS_START = (byte) 0xFA;
+ public static final byte STATUS_CONTINUE = (byte) 0xFB;
+ public static final byte STATUS_STOP = (byte) 0xFC;
+ public static final byte STATUS_ACTIVE_SENSING = (byte) 0xFE;
+ public static final byte STATUS_RESET = (byte) 0xFF;
+
+ /** Number of bytes in a message nc from 8c to Ec */
+ public final static int CHANNEL_BYTE_LENGTHS[] = { 3, 3, 3, 3, 2, 2, 3 };
+
+ /** Number of bytes in a message Fn from F0 to FF */
+ public final static int SYSTEM_BYTE_LENGTHS[] = { 1, 2, 3, 2, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1 };
+
+ /**
+ * MIDI messages, except for SysEx, are 1,2 or 3 bytes long.
+ * You can tell how long a MIDI message is from the first status byte.
+ * Do not call this for SysEx, which has variable length.
+ * @param statusByte
+ * @return number of bytes in a complete message, zero if data byte passed
+ */
+ public static int getBytesPerMessage(byte statusByte) {
+ // Java bytes are signed so we need to mask off the high bits
+ // to get a value between 0 and 255.
+ int statusInt = statusByte & 0xFF;
+ if (statusInt >= 0xF0) {
+ // System messages use low nibble for size.
+ return SYSTEM_BYTE_LENGTHS[statusInt & 0x0F];
+ } else if(statusInt >= 0x80) {
+ // Channel voice messages use high nibble for size.
+ return CHANNEL_BYTE_LENGTHS[(statusInt >> 4) - 8];
+ } else {
+ return 0; // data byte
+ }
+ }
+
+ /**
+ * @param msg
+ * @param offset
+ * @param count
+ * @return true if the entire message is ActiveSensing commands
+ */
+ public static boolean isAllActiveSensing(byte[] msg, int offset,
+ int count) {
+ // Count bytes that are not active sensing.
+ int goodBytes = 0;
+ for (int i = 0; i < count; i++) {
+ byte b = msg[offset + i];
+ if (b != MidiConstants.STATUS_ACTIVE_SENSING) {
+ goodBytes++;
+ }
+ }
+ return (goodBytes == 0);
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiDispatcher.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiDispatcher.java
new file mode 100644
index 000000000..b7f1fe1e8
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiDispatcher.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiReceiver;
+import android.media.midi.MidiSender;
+
+import java.io.IOException;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Utility class for dispatching MIDI data to a list of {@link MidiReceiver}s.
+ * This class subclasses {@link MidiReceiver} and dispatches any data it receives
+ * to its receiver list. Any receivers that throw an exception upon receiving data will
+ * be automatically removed from the receiver list, but no IOException will be returned
+ * from the dispatcher's {@link MidiReceiver#onReceive} in that case.
+ */
+public final class MidiDispatcher extends MidiReceiver {
+
+ private final CopyOnWriteArrayList mReceivers
+ = new CopyOnWriteArrayList();
+
+ private final MidiSender mSender = new MidiSender() {
+ /**
+ * Called to connect a {@link MidiReceiver} to the sender
+ *
+ * @param receiver the receiver to connect
+ */
+ @Override
+ public void onConnect(MidiReceiver receiver) {
+ mReceivers.add(receiver);
+ }
+
+ /**
+ * Called to disconnect a {@link MidiReceiver} from the sender
+ *
+ * @param receiver the receiver to disconnect
+ */
+ @Override
+ public void onDisconnect(MidiReceiver receiver) {
+ mReceivers.remove(receiver);
+ }
+ };
+
+ /**
+ * Returns the number of {@link MidiReceiver}s this dispatcher contains.
+ * @return the number of receivers
+ */
+ public int getReceiverCount() {
+ return mReceivers.size();
+ }
+
+ /**
+ * Returns a {@link MidiSender} which is used to add and remove
+ * {@link MidiReceiver}s
+ * to the dispatcher's receiver list.
+ * @return the dispatcher's MidiSender
+ */
+ public MidiSender getSender() {
+ return mSender;
+ }
+
+ @Override
+ public void onSend(byte[] msg, int offset, int count, long timestamp) throws IOException {
+ for (MidiReceiver receiver : mReceivers) {
+ try {
+ receiver.send(msg, offset, count, timestamp);
+ } catch (IOException e) {
+ // if the receiver fails we remove the receiver but do not propagate the exception
+ mReceivers.remove(receiver);
+ }
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ for (MidiReceiver receiver : mReceivers) {
+ receiver.flush();
+ }
+ }
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiEventScheduler.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiEventScheduler.java
new file mode 100644
index 000000000..513d3939b
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiEventScheduler.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiReceiver;
+
+import java.io.IOException;
+
+/**
+ * Add MIDI Events to an EventScheduler
+ */
+public class MidiEventScheduler extends EventScheduler {
+ private static final String TAG = "MidiEventScheduler";
+ // Maintain a pool of scheduled events to reduce memory allocation.
+ // This pool increases performance by about 14%.
+ private final static int POOL_EVENT_SIZE = 16;
+ private MidiReceiver mReceiver = new SchedulingReceiver();
+
+ private class SchedulingReceiver extends MidiReceiver
+ {
+ /**
+ * Store these bytes in the EventScheduler to be delivered at the specified
+ * time.
+ */
+ @Override
+ public void onSend(byte[] msg, int offset, int count, long timestamp)
+ throws IOException {
+ MidiEvent event = createScheduledEvent(msg, offset, count, timestamp);
+ if (event != null) {
+ add(event);
+ }
+ }
+ }
+
+ public static class MidiEvent extends SchedulableEvent {
+ public int count = 0;
+ public byte[] data;
+
+ private MidiEvent(int count) {
+ super(0);
+ data = new byte[count];
+ }
+
+ private MidiEvent(byte[] msg, int offset, int count, long timestamp) {
+ super(timestamp);
+ data = new byte[count];
+ System.arraycopy(msg, offset, data, 0, count);
+ this.count = count;
+ }
+
+ @Override
+ public String toString() {
+ String text = "Event: ";
+ for (int i = 0; i < count; i++) {
+ text += data[i] + ", ";
+ }
+ return text;
+ }
+ }
+
+ /**
+ * Create an event that contains the message.
+ */
+ private MidiEvent createScheduledEvent(byte[] msg, int offset, int count,
+ long timestamp) {
+ MidiEvent event;
+ if (count > POOL_EVENT_SIZE) {
+ event = new MidiEvent(msg, offset, count, timestamp);
+ } else {
+ event = (MidiEvent) removeEventfromPool();
+ if (event == null) {
+ event = new MidiEvent(POOL_EVENT_SIZE);
+ }
+ System.arraycopy(msg, offset, event.data, 0, count);
+ event.count = count;
+ event.setTimestamp(timestamp);
+ }
+ return event;
+ }
+
+ /**
+ * Return events to a pool so they can be reused.
+ *
+ * @param event
+ */
+ @Override
+ public void addEventToPool(SchedulableEvent event) {
+ // Make sure the event is suitable for the pool.
+ if (event instanceof MidiEvent) {
+ MidiEvent midiEvent = (MidiEvent) event;
+ if (midiEvent.data.length == POOL_EVENT_SIZE) {
+ super.addEventToPool(event);
+ }
+ }
+ }
+
+ /**
+ * This MidiReceiver will write date to the scheduling buffer.
+ * @return the MidiReceiver
+ */
+ public MidiReceiver getReceiver() {
+ return mReceiver;
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiEventThread.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiEventThread.java
new file mode 100644
index 000000000..626e83cf0
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiEventThread.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiSender;
+import android.util.Log;
+
+import java.io.IOException;
+
+public class MidiEventThread extends MidiEventScheduler {
+ protected static final String TAG = "MidiEventThread";
+
+ private EventThread mEventThread;
+ MidiDispatcher mDispatcher = new MidiDispatcher();
+
+ class EventThread extends Thread {
+ private boolean go = true;
+
+ @Override
+ public void run() {
+ while (go) {
+ try {
+ MidiEvent event = (MidiEvent) waitNextEvent();
+ try {
+ Log.i(TAG, "Fire event " + event.data[0] + " at "
+ + event.getTimestamp());
+ mDispatcher.send(event.data, 0,
+ event.count, event.getTimestamp());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // Put event back in the pool for future use.
+ addEventToPool(event);
+ } catch (InterruptedException e) {
+ // OK, this is how we stop the thread.
+ }
+ }
+ }
+
+ /**
+ * Asynchronously tell the thread to stop.
+ */
+ public void requestStop() {
+ go = false;
+ interrupt();
+ }
+ }
+
+ public void start() {
+ stop();
+ mEventThread = new EventThread();
+ mEventThread.start();
+ }
+
+ /**
+ * Asks the thread to stop then waits for it to stop.
+ */
+ public void stop() {
+ if (mEventThread != null) {
+ mEventThread.requestStop();
+ try {
+ mEventThread.join(500);
+ } catch (InterruptedException e) {
+ Log.e(TAG,
+ "Interrupted while waiting for MIDI EventScheduler thread to stop.");
+ } finally {
+ mEventThread = null;
+ }
+ }
+ }
+
+ public MidiSender getSender() {
+ return mDispatcher.getSender();
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiFramer.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiFramer.java
new file mode 100644
index 000000000..c274925ac
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiFramer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiReceiver;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Convert stream of arbitrary MIDI bytes into discrete messages.
+ *
+ * Parses the incoming bytes and then posts individual messages to the receiver
+ * specified in the constructor. Short messages of 1-3 bytes will be complete.
+ * System Exclusive messages may be posted in pieces.
+ *
+ * Resolves Running Status and interleaved System Real-Time messages.
+ */
+public class MidiFramer extends MidiReceiver {
+ private MidiReceiver mReceiver;
+ private byte[] mBuffer = new byte[3];
+ private int mCount;
+ private byte mRunningStatus;
+ private int mNeeded;
+ private boolean mInSysEx;
+
+ public MidiFramer(MidiReceiver receiver) {
+ mReceiver = receiver;
+ }
+
+ /*
+ * @see android.midi.MidiReceiver#onSend(byte[], int, int, long)
+ */
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ int sysExStartOffset = (mInSysEx ? offset : -1);
+
+ for (int i = 0; i < count; i++) {
+ final byte currentByte = data[offset];
+ final int currentInt = currentByte & 0xFF;
+ if (currentInt >= 0x80) { // status byte?
+ if (currentInt < 0xF0) { // channel message?
+ mRunningStatus = currentByte;
+ mCount = 1;
+ mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1;
+ } else if (currentInt < 0xF8) { // system common?
+ if (currentInt == 0xF0 /* SysEx Start */) {
+ // Log.i(TAG, "SysEx Start");
+ mInSysEx = true;
+ sysExStartOffset = offset;
+ } else if (currentInt == 0xF7 /* SysEx End */) {
+ // Log.i(TAG, "SysEx End");
+ if (mInSysEx) {
+ mReceiver.send(data, sysExStartOffset,
+ offset - sysExStartOffset + 1, timestamp);
+ mInSysEx = false;
+ sysExStartOffset = -1;
+ }
+ } else {
+ mBuffer[0] = currentByte;
+ mRunningStatus = 0;
+ mCount = 1;
+ mNeeded = MidiConstants.getBytesPerMessage(currentByte) - 1;
+ }
+ } else { // real-time?
+ // Single byte message interleaved with other data.
+ if (mInSysEx) {
+ mReceiver.send(data, sysExStartOffset,
+ offset - sysExStartOffset, timestamp);
+ sysExStartOffset = offset + 1;
+ }
+ mReceiver.send(data, offset, 1, timestamp);
+ }
+ } else { // data byte
+ if (!mInSysEx) {
+ mBuffer[mCount++] = currentByte;
+ if (--mNeeded == 0) {
+ if (mRunningStatus != 0) {
+ mBuffer[0] = mRunningStatus;
+ }
+ mReceiver.send(mBuffer, 0, mCount, timestamp);
+ mNeeded = MidiConstants.getBytesPerMessage(mBuffer[0]) - 1;
+ mCount = 1;
+ }
+ }
+ }
+ ++offset;
+ }
+
+ // send any accumulatedSysEx data
+ if (sysExStartOffset >= 0 && sysExStartOffset < offset) {
+ mReceiver.send(data, sysExStartOffset,
+ offset - sysExStartOffset, timestamp);
+ }
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiInputPortSelector.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiInputPortSelector.java
new file mode 100644
index 000000000..7c665bace
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiInputPortSelector.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiInputPort;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiReceiver;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Manages a Spinner for selecting a MidiInputPort.
+ */
+public class MidiInputPortSelector extends MidiPortSelector {
+
+ private MidiInputPort mInputPort;
+ private MidiDevice mOpenDevice;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId ID from the layout resource
+ */
+ public MidiInputPortSelector(MidiManager midiManager, Activity activity,
+ int spinnerId) {
+ super(midiManager, activity, spinnerId, MidiDeviceInfo.PortInfo.TYPE_INPUT);
+ }
+
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ close();
+ final MidiDeviceInfo info = wrapper.getDeviceInfo();
+ if (info != null) {
+ mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ Log.e(MidiConstants.TAG, "could not open " + info);
+ } else {
+ mOpenDevice = device;
+ mInputPort = mOpenDevice.openInputPort(
+ wrapper.getPortIndex());
+ if (mInputPort == null) {
+ Log.e(MidiConstants.TAG, "could not open input port on " + info);
+ }
+ }
+ }
+ }, null);
+ // Don't run the callback on the UI thread because openInputPort might take a while.
+ }
+ }
+
+ public MidiReceiver getReceiver() {
+ return mInputPort;
+ }
+
+ @Override
+ public void onClose() {
+ try {
+ if (mInputPort != null) {
+ Log.i(MidiConstants.TAG, "MidiInputPortSelector.onClose() - close port");
+ mInputPort.close();
+ }
+ mInputPort = null;
+ if (mOpenDevice != null) {
+ mOpenDevice.close();
+ }
+ mOpenDevice = null;
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "cleanup failed", e);
+ }
+ }
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiOutputPortConnectionSelector.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiOutputPortConnectionSelector.java
new file mode 100644
index 000000000..ca1ade48c
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiOutputPortConnectionSelector.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Select an output port and connect it to a destination input port.
+ */
+public class MidiOutputPortConnectionSelector extends MidiPortSelector {
+
+ private MidiPortConnector mSynthConnector;
+ private MidiDeviceInfo mDestinationDeviceInfo;
+ private int mDestinationPortIndex;
+ private MidiPortConnector.OnPortsConnectedListener mConnectedListener;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId
+ * @param type
+ */
+ public MidiOutputPortConnectionSelector(MidiManager midiManager,
+ Activity activity, int spinnerId,
+ MidiDeviceInfo destinationDeviceInfo, int destinationPortIndex) {
+ super(midiManager, activity, spinnerId,
+ MidiDeviceInfo.PortInfo.TYPE_OUTPUT);
+ mDestinationDeviceInfo = destinationDeviceInfo;
+ mDestinationPortIndex = destinationPortIndex;
+ }
+
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ Log.i(MidiConstants.TAG, "connectPortToSynth: " + wrapper);
+ onClose();
+ if (wrapper.getDeviceInfo() != null) {
+ mSynthConnector = new MidiPortConnector(mMidiManager);
+ mSynthConnector.connectToDevicePort(wrapper.getDeviceInfo(),
+ wrapper.getPortIndex(), mDestinationDeviceInfo,
+ mDestinationPortIndex,
+ // not safe on UI thread
+ mConnectedListener, null);
+ }
+ }
+
+ @Override
+ public void onClose() {
+ try {
+ if (mSynthConnector != null) {
+ mSynthConnector.close();
+ mSynthConnector = null;
+ }
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "Exception in closeSynthResources()", e);
+ }
+ }
+
+ /**
+ * @param myPortsConnectedListener
+ */
+ public void setConnectedListener(
+ MidiPortConnector.OnPortsConnectedListener connectedListener) {
+ mConnectedListener = connectedListener;
+ }
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiOutputPortSelector.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiOutputPortSelector.java
new file mode 100644
index 000000000..5aebf727e
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiOutputPortSelector.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiOutputPort;
+import android.media.midi.MidiSender;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Manages a Spinner for selecting a MidiOutputPort.
+ */
+public class MidiOutputPortSelector extends MidiPortSelector {
+ private MidiOutputPort mOutputPort;
+ private MidiDispatcher mDispatcher = new MidiDispatcher();
+ private MidiDevice mOpenDevice;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId ID from the layout resource
+ */
+ public MidiOutputPortSelector(MidiManager midiManager, Activity activity,
+ int spinnerId) {
+ super(midiManager, activity, spinnerId, MidiDeviceInfo.PortInfo.TYPE_OUTPUT);
+ }
+
+ @Override
+ public void onPortSelected(final MidiPortWrapper wrapper) {
+ Log.i(MidiConstants.TAG, "onPortSelected: " + wrapper);
+ close();
+
+ final MidiDeviceInfo info = wrapper.getDeviceInfo();
+ if (info != null) {
+ mMidiManager.openDevice(info, new MidiManager.OnDeviceOpenedListener() {
+
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ Log.e(MidiConstants.TAG, "could not open " + info);
+ } else {
+ mOpenDevice = device;
+ mOutputPort = device.openOutputPort(wrapper.getPortIndex());
+ if (mOutputPort == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open output port for " + info);
+ return;
+ }
+ mOutputPort.connect(mDispatcher);
+ }
+ }
+ }, null);
+ // Don't run the callback on the UI thread because openOutputPort might take a while.
+ }
+ }
+
+ @Override
+ public void onClose() {
+ try {
+ if (mOutputPort != null) {
+ mOutputPort.disconnect(mDispatcher);
+ }
+ mOutputPort = null;
+ if (mOpenDevice != null) {
+ mOpenDevice.close();
+ }
+ mOpenDevice = null;
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "cleanup failed", e);
+ }
+ }
+
+ /**
+ * You can connect your MidiReceivers to this sender. The user will then select which output
+ * port will send messages through this MidiSender.
+ * @return a MidiSender that will send the messages from the selected port.
+ */
+ public MidiSender getSender() {
+ return mDispatcher.getSender();
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiPortConnector.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiPortConnector.java
new file mode 100644
index 000000000..457494d1b
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiPortConnector.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiDevice;
+import android.media.midi.MidiDevice.MidiConnection;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiInputPort;
+import android.media.midi.MidiManager;
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Tool for connecting MIDI ports on two remote devices.
+ */
+public class MidiPortConnector {
+ private final MidiManager mMidiManager;
+ private MidiDevice mSourceDevice;
+ private MidiDevice mDestinationDevice;
+ private MidiConnection mConnection;
+
+ /**
+ * @param mMidiManager
+ */
+ public MidiPortConnector(MidiManager midiManager) {
+ mMidiManager = midiManager;
+ }
+
+ public void close() throws IOException {
+ if (mConnection != null) {
+ Log.i(MidiConstants.TAG,
+ "MidiPortConnector closing connection " + mConnection);
+ mConnection.close();
+ mConnection = null;
+ }
+ if (mSourceDevice != null) {
+ mSourceDevice.close();
+ mSourceDevice = null;
+ }
+ if (mDestinationDevice != null) {
+ mDestinationDevice.close();
+ mDestinationDevice = null;
+ }
+ }
+
+ private void safeClose() {
+ try {
+ close();
+ } catch (IOException e) {
+ Log.e(MidiConstants.TAG, "could not close resources", e);
+ }
+ }
+
+ /**
+ * Listener class used for receiving the results of
+ * {@link #connectToDevicePort}
+ */
+ public interface OnPortsConnectedListener {
+ /**
+ * Called to respond to a {@link #connectToDevicePort} request
+ *
+ * @param connection
+ * a {@link MidiConnection} that represents the connected
+ * ports, or null if connection failed
+ */
+ abstract public void onPortsConnected(MidiConnection connection);
+ }
+
+ /**
+ * Open two devices and connect their ports.
+ *
+ * @param sourceDeviceInfo
+ * @param sourcePortIndex
+ * @param destinationDeviceInfo
+ * @param destinationPortIndex
+ */
+ public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo,
+ final int sourcePortIndex,
+ final MidiDeviceInfo destinationDeviceInfo,
+ final int destinationPortIndex) {
+ connectToDevicePort(sourceDeviceInfo, sourcePortIndex,
+ destinationDeviceInfo, destinationPortIndex, null, null);
+ }
+
+ /**
+ * Open two devices and connect their ports.
+ *
+ * @param sourceDeviceInfo
+ * @param sourcePortIndex
+ * @param destinationDeviceInfo
+ * @param destinationPortIndex
+ */
+ public void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo,
+ final int sourcePortIndex,
+ final MidiDeviceInfo destinationDeviceInfo,
+ final int destinationPortIndex,
+ final OnPortsConnectedListener listener, final Handler handler) {
+ safeClose();
+ mMidiManager.openDevice(destinationDeviceInfo,
+ new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice destinationDevice) {
+ if (destinationDevice == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open " + destinationDeviceInfo);
+ if (listener != null) {
+ listener.onPortsConnected(null);
+ }
+ } else {
+ mDestinationDevice = destinationDevice;
+ Log.i(MidiConstants.TAG,
+ "connectToDevicePort opened "
+ + destinationDeviceInfo);
+ // Destination device was opened so go to next step.
+ MidiInputPort destinationInputPort = destinationDevice
+ .openInputPort(destinationPortIndex);
+ if (destinationInputPort != null) {
+ Log.i(MidiConstants.TAG,
+ "connectToDevicePort opened port on "
+ + destinationDeviceInfo);
+ connectToDevicePort(sourceDeviceInfo,
+ sourcePortIndex,
+ destinationInputPort,
+ listener, handler);
+ } else {
+ Log.e(MidiConstants.TAG,
+ "could not open port on "
+ + destinationDeviceInfo);
+ safeClose();
+ if (listener != null) {
+ listener.onPortsConnected(null);
+ }
+ }
+ }
+ }
+ }, handler);
+ }
+
+
+ /**
+ * Open a source device and connect its output port to the
+ * destinationInputPort.
+ *
+ * @param sourceDeviceInfo
+ * @param sourcePortIndex
+ * @param destinationInputPort
+ */
+ private void connectToDevicePort(final MidiDeviceInfo sourceDeviceInfo,
+ final int sourcePortIndex,
+ final MidiInputPort destinationInputPort,
+ final OnPortsConnectedListener listener, final Handler handler) {
+ mMidiManager.openDevice(sourceDeviceInfo,
+ new MidiManager.OnDeviceOpenedListener() {
+ @Override
+ public void onDeviceOpened(MidiDevice device) {
+ if (device == null) {
+ Log.e(MidiConstants.TAG,
+ "could not open " + sourceDeviceInfo);
+ safeClose();
+ if (listener != null) {
+ listener.onPortsConnected(null);
+ }
+ } else {
+ Log.i(MidiConstants.TAG,
+ "connectToDevicePort opened "
+ + sourceDeviceInfo);
+ // Device was opened so connect the ports.
+ mSourceDevice = device;
+ mConnection = device.connectPorts(
+ destinationInputPort, sourcePortIndex);
+ if (mConnection == null) {
+ Log.e(MidiConstants.TAG, "could not connect to "
+ + sourceDeviceInfo);
+ safeClose();
+ }
+ if (listener != null) {
+ listener.onPortsConnected(mConnection);
+ }
+ }
+ }
+ }, handler);
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiPortSelector.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiPortSelector.java
new file mode 100644
index 000000000..39f983e38
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiPortSelector.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.app.Activity;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiManager;
+import android.media.midi.MidiManager.DeviceCallback;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+
+import java.util.HashSet;
+
+/**
+ * Base class that uses a Spinner to select available MIDI ports.
+ */
+public abstract class MidiPortSelector extends DeviceCallback {
+ private int mType = MidiDeviceInfo.PortInfo.TYPE_INPUT;
+ protected ArrayAdapter mAdapter;
+ protected HashSet mBusyPorts = new HashSet();
+ private Spinner mSpinner;
+ protected MidiManager mMidiManager;
+ protected Activity mActivity;
+ private MidiPortWrapper mCurrentWrapper;
+
+ /**
+ * @param midiManager
+ * @param activity
+ * @param spinnerId
+ * ID from the layout resource
+ * @param type
+ * TYPE_INPUT or TYPE_OUTPUT
+ */
+ public MidiPortSelector(MidiManager midiManager, Activity activity,
+ int spinnerId, int type) {
+ mMidiManager = midiManager;
+ mActivity = activity;
+ mType = type;
+ mAdapter = new ArrayAdapter(activity,
+ android.R.layout.simple_spinner_item);
+ mAdapter.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ mAdapter.add(new MidiPortWrapper(null, 0, 0));
+
+ mSpinner = (Spinner) activity.findViewById(spinnerId);
+ mSpinner.setOnItemSelectedListener(
+ new AdapterView.OnItemSelectedListener() {
+
+ public void onItemSelected(AdapterView> parent, View view,
+ int pos, long id) {
+ mCurrentWrapper = mAdapter.getItem(pos);
+ onPortSelected(mCurrentWrapper);
+ }
+
+ public void onNothingSelected(AdapterView> parent) {
+ onPortSelected(null);
+ mCurrentWrapper = null;
+ }
+ });
+ mSpinner.setAdapter(mAdapter);
+
+ mMidiManager.registerDeviceCallback(this,
+ new Handler(Looper.getMainLooper()));
+
+ MidiDeviceInfo[] infos = mMidiManager.getDevices();
+ for (MidiDeviceInfo info : infos) {
+ onDeviceAdded(info);
+ }
+ }
+
+ /**
+ * Set to no port selected.
+ */
+ public void clearSelection() {
+ mSpinner.setSelection(0);
+ }
+
+ private int getInfoPortCount(final MidiDeviceInfo info) {
+ int portCount = (mType == MidiDeviceInfo.PortInfo.TYPE_INPUT)
+ ? info.getInputPortCount() : info.getOutputPortCount();
+ return portCount;
+ }
+
+ @Override
+ public void onDeviceAdded(final MidiDeviceInfo info) {
+ int portCount = getInfoPortCount(info);
+ for (int i = 0; i < portCount; ++i) {
+ MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
+ mAdapter.add(wrapper);
+ Log.i(MidiConstants.TAG, wrapper + " was added");
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onDeviceRemoved(final MidiDeviceInfo info) {
+ int portCount = getInfoPortCount(info);
+ for (int i = 0; i < portCount; ++i) {
+ MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
+ MidiPortWrapper currentWrapper = mCurrentWrapper;
+ mAdapter.remove(wrapper);
+ // If the currently selected port was removed then select no port.
+ if (wrapper.equals(currentWrapper)) {
+ clearSelection();
+ }
+ mAdapter.notifyDataSetChanged();
+ Log.i(MidiConstants.TAG, wrapper + " was removed");
+ }
+ }
+
+ @Override
+ public void onDeviceStatusChanged(final MidiDeviceStatus status) {
+ // If an input port becomes busy then remove it from the menu.
+ // If it becomes free then add it back to the menu.
+ if (mType == MidiDeviceInfo.PortInfo.TYPE_INPUT) {
+ MidiDeviceInfo info = status.getDeviceInfo();
+ Log.i(MidiConstants.TAG, "MidiPortSelector.onDeviceStatusChanged status = " + status
+ + ", mType = " + mType
+ + ", activity = " + mActivity.getPackageName()
+ + ", info = " + info);
+ // Look for transitions from free to busy.
+ int portCount = info.getInputPortCount();
+ for (int i = 0; i < portCount; ++i) {
+ MidiPortWrapper wrapper = new MidiPortWrapper(info, mType, i);
+ if (!wrapper.equals(mCurrentWrapper)) {
+ if (status.isInputPortOpen(i)) { // busy?
+ if (!mBusyPorts.contains(wrapper)) {
+ // was free, now busy
+ mBusyPorts.add(wrapper);
+ mAdapter.remove(wrapper);
+ mAdapter.notifyDataSetChanged();
+ }
+ } else {
+ if (mBusyPorts.remove(wrapper)) {
+ // was busy, now free
+ mAdapter.add(wrapper);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Implement this method to handle the user selecting a port on a device.
+ *
+ * @param wrapper
+ */
+ public abstract void onPortSelected(MidiPortWrapper wrapper);
+
+ /**
+ * Implement this method to clean up any open resources.
+ */
+ public abstract void onClose();
+
+ /**
+ *
+ */
+ public void close() {
+ onClose();
+ }
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiPortWrapper.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiPortWrapper.java
new file mode 100644
index 000000000..77aa73458
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiPortWrapper.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiDeviceInfo.PortInfo;
+import android.util.Log;
+
+// Wrapper for a MIDI device and port description.
+public class MidiPortWrapper {
+ private MidiDeviceInfo mInfo;
+ private int mPortIndex;
+ private int mType;
+ private String mString;
+
+ /**
+ * Wrapper for a MIDI device and port description.
+ * @param info
+ * @param portType
+ * @param portIndex
+ */
+ public MidiPortWrapper(MidiDeviceInfo info, int portType, int portIndex) {
+ mInfo = info;
+ mType = portType;
+ mPortIndex = portIndex;
+ }
+
+ private void updateString() {
+ if (mInfo == null) {
+ mString = "- - - - - -";
+ } else {
+ StringBuilder sb = new StringBuilder();
+ String name = mInfo.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_NAME);
+ if (name == null) {
+ name = mInfo.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER) + ", "
+ + mInfo.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_PRODUCT);
+ }
+ sb.append("#" + mInfo.getId());
+ sb.append(", ").append(name);
+ PortInfo portInfo = findPortInfo();
+ sb.append("[" + mPortIndex + "]");
+ if (portInfo != null) {
+ sb.append(", ").append(portInfo.getName());
+ } else {
+ sb.append(", null");
+ }
+ mString = sb.toString();
+ }
+ }
+
+ /**
+ * @param info
+ * @param portIndex
+ * @return
+ */
+ private PortInfo findPortInfo() {
+ PortInfo[] ports = mInfo.getPorts();
+ for (PortInfo portInfo : ports) {
+ if (portInfo.getPortNumber() == mPortIndex
+ && portInfo.getType() == mType) {
+ return portInfo;
+ }
+ }
+ return null;
+ }
+
+ public int getPortIndex() {
+ return mPortIndex;
+ }
+
+ public MidiDeviceInfo getDeviceInfo() {
+ return mInfo;
+ }
+
+ @Override
+ public String toString() {
+ if (mString == null) {
+ updateString();
+ }
+ return mString;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null)
+ return false;
+ if (!(other instanceof MidiPortWrapper))
+ return false;
+ MidiPortWrapper otherWrapper = (MidiPortWrapper) other;
+ if (mPortIndex != otherWrapper.mPortIndex)
+ return false;
+ if (mType != otherWrapper.mType)
+ return false;
+ if (mInfo == null)
+ return (otherWrapper.mInfo == null);
+ return mInfo.equals(otherWrapper.mInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 1;
+ hashCode = 31 * hashCode + mPortIndex;
+ hashCode = 31 * hashCode + mType;
+ hashCode = 31 * hashCode + mInfo.hashCode();
+ return hashCode;
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiTools.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiTools.java
new file mode 100644
index 000000000..82e3de4ba
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/MidiTools.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi;
+
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+
+/**
+ * Miscellaneous tools for Android MIDI.
+ */
+public class MidiTools {
+
+ /**
+ * @return a device that matches the manufacturer and product or null
+ */
+ public static MidiDeviceInfo findDevice(MidiManager midiManager,
+ String manufacturer, String product) {
+ for (MidiDeviceInfo info : midiManager.getDevices()) {
+ String deviceManufacturer = info.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_MANUFACTURER);
+ if ((manufacturer != null)
+ && manufacturer.equals(deviceManufacturer)) {
+ String deviceProduct = info.getProperties()
+ .getString(MidiDeviceInfo.PROPERTY_PRODUCT);
+ if ((product != null) && product.equals(deviceProduct)) {
+ return info;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/EnvelopeADSR.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/EnvelopeADSR.java
new file mode 100644
index 000000000..a29a1933e
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/EnvelopeADSR.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Very simple Attack, Decay, Sustain, Release envelope with linear ramps.
+ *
+ * Times are in seconds.
+ */
+public class EnvelopeADSR extends SynthUnit {
+ private static final int IDLE = 0;
+ private static final int ATTACK = 1;
+ private static final int DECAY = 2;
+ private static final int SUSTAIN = 3;
+ private static final int RELEASE = 4;
+ private static final int FINISHED = 5;
+ private static final float MIN_TIME = 0.001f;
+
+ private float mAttackRate;
+ private float mRreleaseRate;
+ private float mSustainLevel;
+ private float mDecayRate;
+ private float mCurrent;
+ private int mSstate = IDLE;
+
+ public EnvelopeADSR() {
+ setAttackTime(0.003f);
+ setDecayTime(0.08f);
+ setSustainLevel(0.3f);
+ setReleaseTime(1.0f);
+ }
+
+ public void setAttackTime(float time) {
+ if (time < MIN_TIME)
+ time = MIN_TIME;
+ mAttackRate = 1.0f / (SynthEngine.FRAME_RATE * time);
+ }
+
+ public void setDecayTime(float time) {
+ if (time < MIN_TIME)
+ time = MIN_TIME;
+ mDecayRate = 1.0f / (SynthEngine.FRAME_RATE * time);
+ }
+
+ public void setSustainLevel(float level) {
+ if (level < 0.0f)
+ level = 0.0f;
+ mSustainLevel = level;
+ }
+
+ public void setReleaseTime(float time) {
+ if (time < MIN_TIME)
+ time = MIN_TIME;
+ mRreleaseRate = 1.0f / (SynthEngine.FRAME_RATE * time);
+ }
+
+ public void on() {
+ mSstate = ATTACK;
+ }
+
+ public void off() {
+ mSstate = RELEASE;
+ }
+
+ @Override
+ public float render() {
+ switch (mSstate) {
+ case ATTACK:
+ mCurrent += mAttackRate;
+ if (mCurrent > 1.0f) {
+ mCurrent = 1.0f;
+ mSstate = DECAY;
+ }
+ break;
+ case DECAY:
+ mCurrent -= mDecayRate;
+ if (mCurrent < mSustainLevel) {
+ mCurrent = mSustainLevel;
+ mSstate = SUSTAIN;
+ }
+ break;
+ case RELEASE:
+ mCurrent -= mRreleaseRate;
+ if (mCurrent < 0.0f) {
+ mCurrent = 0.0f;
+ mSstate = FINISHED;
+ }
+ break;
+ }
+ return mCurrent;
+ }
+
+ public boolean isDone() {
+ return mSstate == FINISHED;
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SawOscillator.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SawOscillator.java
new file mode 100644
index 000000000..c02a6a1a5
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SawOscillator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+public class SawOscillator extends SynthUnit {
+ private float mPhase = 0.0f;
+ private float mPhaseIncrement = 0.01f;
+ private float mFrequency = 0.0f;
+ private float mFrequencyScaler = 1.0f;
+ private float mAmplitude = 1.0f;
+
+ public void setPitch(float pitch) {
+ float freq = (float) pitchToFrequency(pitch);
+ setFrequency(freq);
+ }
+
+ public void setFrequency(float frequency) {
+ mFrequency = frequency;
+ updatePhaseIncrement();
+ }
+
+ private void updatePhaseIncrement() {
+ mPhaseIncrement = 2.0f * mFrequency * mFrequencyScaler / 48000.0f;
+ }
+
+ public void setAmplitude(float amplitude) {
+ mAmplitude = amplitude;
+ }
+
+ public float getAmplitude() {
+ return mAmplitude;
+ }
+
+ public float getFrequencyScaler() {
+ return mFrequencyScaler;
+ }
+
+ public void setFrequencyScaler(float frequencyScaler) {
+ mFrequencyScaler = frequencyScaler;
+ updatePhaseIncrement();
+ }
+
+ float incrementWrapPhase() {
+ mPhase += mPhaseIncrement;
+ while (mPhase > 1.0) {
+ mPhase -= 2.0;
+ }
+ while (mPhase < -1.0) {
+ mPhase += 2.0;
+ }
+ return mPhase;
+ }
+
+ @Override
+ public float render() {
+ return incrementWrapPhase() * mAmplitude;
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SawOscillatorDPW.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SawOscillatorDPW.java
new file mode 100644
index 000000000..e5d661d56
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SawOscillatorDPW.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Band limited sawtooth oscillator.
+ * This will have very little aliasing at high frequencies.
+ */
+public class SawOscillatorDPW extends SawOscillator {
+ private float mZ1 = 0.0f; // delayed values
+ private float mZ2 = 0.0f;
+ private float mScaler; // frequency dependent scaler
+ private final static float VERY_LOW_FREQ = 0.0000001f;
+
+ @Override
+ public void setFrequency(float freq) {
+ /* Calculate scaling based on frequency. */
+ freq = Math.abs(freq);
+ super.setFrequency(freq);
+ if (freq < VERY_LOW_FREQ) {
+ mScaler = (float) (0.125 * 44100 / VERY_LOW_FREQ);
+ } else {
+ mScaler = (float) (0.125 * 44100 / freq);
+ }
+ }
+
+ @Override
+ public float render() {
+ float phase = incrementWrapPhase();
+ /* Square the raw sawtooth. */
+ float squared = phase * phase;
+ float diffed = squared - mZ2;
+ mZ2 = mZ1;
+ mZ1 = squared;
+ return diffed * mScaler * getAmplitude();
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SawVoice.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SawVoice.java
new file mode 100644
index 000000000..3b3e543e8
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SawVoice.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Sawtooth oscillator with an ADSR.
+ */
+public class SawVoice extends SynthVoice {
+ private SawOscillator mOscillator;
+ private EnvelopeADSR mEnvelope;
+
+ public SawVoice() {
+ mOscillator = createOscillator();
+ mEnvelope = new EnvelopeADSR();
+ }
+
+ protected SawOscillator createOscillator() {
+ return new SawOscillator();
+ }
+
+ @Override
+ public void noteOn(int noteIndex, int velocity) {
+ super.noteOn(noteIndex, velocity);
+ mOscillator.setPitch(noteIndex);
+ mOscillator.setAmplitude(getAmplitude());
+ mEnvelope.on();
+ }
+
+ @Override
+ public void noteOff() {
+ super.noteOff();
+ mEnvelope.off();
+ }
+
+ @Override
+ public void setFrequencyScaler(float scaler) {
+ mOscillator.setFrequencyScaler(scaler);
+ }
+
+ @Override
+ public float render() {
+ float output = mOscillator.render() * mEnvelope.render();
+ return output;
+ }
+
+ @Override
+ public boolean isDone() {
+ return mEnvelope.isDone();
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SimpleAudioOutput.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SimpleAudioOutput.java
new file mode 100644
index 000000000..04aa19c0b
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SimpleAudioOutput.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTrack;
+import android.util.Log;
+
+/**
+ * Simple base class for implementing audio output for examples.
+ * This can be sub-classed for experimentation or to redirect audio output.
+ */
+public class SimpleAudioOutput {
+
+ private static final String TAG = "AudioOutputTrack";
+ public static final int SAMPLES_PER_FRAME = 2;
+ public static final int BYTES_PER_SAMPLE = 4; // float
+ public static final int BYTES_PER_FRAME = SAMPLES_PER_FRAME * BYTES_PER_SAMPLE;
+ private AudioTrack mAudioTrack;
+ private int mFrameRate;
+
+ /**
+ *
+ */
+ public SimpleAudioOutput() {
+ super();
+ }
+
+ /**
+ * Create an audio track then call play().
+ *
+ * @param frameRate
+ */
+ public void start(int frameRate) {
+ stop();
+ mFrameRate = frameRate;
+ mAudioTrack = createAudioTrack(frameRate);
+ // AudioTrack will wait until it has enough data before starting.
+ mAudioTrack.play();
+ }
+
+ public AudioTrack createAudioTrack(int frameRate) {
+ int minBufferSizeBytes = AudioTrack.getMinBufferSize(frameRate,
+ AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_FLOAT);
+ Log.i(TAG, "AudioTrack.minBufferSize = " + minBufferSizeBytes
+ + " bytes = " + (minBufferSizeBytes / BYTES_PER_FRAME)
+ + " frames");
+ int bufferSize = 8 * minBufferSizeBytes / 8;
+ int outputBufferSizeFrames = bufferSize / BYTES_PER_FRAME;
+ Log.i(TAG, "actual bufferSize = " + bufferSize + " bytes = "
+ + outputBufferSizeFrames + " frames");
+
+ AudioTrack player = new AudioTrack(AudioManager.STREAM_MUSIC,
+ mFrameRate, AudioFormat.CHANNEL_OUT_STEREO,
+ AudioFormat.ENCODING_PCM_FLOAT, bufferSize,
+ AudioTrack.MODE_STREAM);
+ Log.i(TAG, "created AudioTrack");
+ return player;
+ }
+
+ public int write(float[] buffer, int offset, int length) {
+ return mAudioTrack.write(buffer, offset, length,
+ AudioTrack.WRITE_BLOCKING);
+ }
+
+ public void stop() {
+ if (mAudioTrack != null) {
+ mAudioTrack.stop();
+ mAudioTrack = null;
+ }
+ }
+
+ public int getFrameRate() {
+ return mFrameRate;
+ }
+
+ public AudioTrack getAudioTrack() {
+ return mAudioTrack;
+ }
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SineOscillator.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SineOscillator.java
new file mode 100644
index 000000000..c638c344c
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SineOscillator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Sinewave oscillator.
+ */
+public class SineOscillator extends SawOscillator {
+ // Factorial constants.
+ private static final float IF3 = 1.0f / (2 * 3);
+ private static final float IF5 = IF3 / (4 * 5);
+ private static final float IF7 = IF5 / (6 * 7);
+ private static final float IF9 = IF7 / (8 * 9);
+ private static final float IF11 = IF9 / (10 * 11);
+
+ /**
+ * Calculate sine using Taylor expansion. Do not use values outside the range.
+ *
+ * @param currentPhase in the range of -1.0 to +1.0 for one cycle
+ */
+ public static float fastSin(float currentPhase) {
+
+ /* Wrap phase back into region where results are more accurate. */
+ float yp = (currentPhase > 0.5f) ? 1.0f - currentPhase
+ : ((currentPhase < (-0.5f)) ? (-1.0f) - currentPhase : currentPhase);
+
+ float x = (float) (yp * Math.PI);
+ float x2 = (x * x);
+ /* Taylor expansion out to x**11/11! factored into multiply-adds */
+ return x * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1);
+ }
+
+ @Override
+ public float render() {
+ // Convert raw sawtooth to sine.
+ float phase = incrementWrapPhase();
+ return fastSin(phase) * getAmplitude();
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SineVoice.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SineVoice.java
new file mode 100644
index 000000000..e80d2c7eb
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SineVoice.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Replace sawtooth with a sine wave.
+ */
+public class SineVoice extends SawVoice {
+ @Override
+ protected SawOscillator createOscillator() {
+ return new SineOscillator();
+ }
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SynthEngine.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SynthEngine.java
new file mode 100644
index 000000000..6cd02a609
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SynthEngine.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+import android.media.midi.MidiReceiver;
+import android.util.Log;
+
+import com.example.android.common.midi.MidiConstants;
+import com.example.android.common.midi.MidiEventScheduler;
+import com.example.android.common.midi.MidiEventScheduler.MidiEvent;
+import com.example.android.common.midi.MidiFramer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.Iterator;
+
+/**
+ * Very simple polyphonic, single channel synthesizer. It runs a background
+ * thread that processes MIDI events and synthesizes audio.
+ */
+public class SynthEngine extends MidiReceiver {
+
+ private static final String TAG = "SynthEngine";
+
+ public static final int FRAME_RATE = 48000;
+ private static final int FRAMES_PER_BUFFER = 240;
+ private static final int SAMPLES_PER_FRAME = 2;
+
+ private boolean go;
+ private Thread mThread;
+ private float[] mBuffer = new float[FRAMES_PER_BUFFER * SAMPLES_PER_FRAME];
+ private float mFrequencyScaler = 1.0f;
+ private float mBendRange = 2.0f; // semitones
+ private int mProgram;
+
+ private ArrayList mFreeVoices = new ArrayList();
+ private Hashtable
+ mVoices = new Hashtable();
+ private MidiEventScheduler mEventScheduler;
+ private MidiFramer mFramer;
+ private MidiReceiver mReceiver = new MyReceiver();
+ private SimpleAudioOutput mAudioOutput;
+
+ public SynthEngine() {
+ this(new SimpleAudioOutput());
+ }
+
+ public SynthEngine(SimpleAudioOutput audioOutput) {
+ mReceiver = new MyReceiver();
+ mFramer = new MidiFramer(mReceiver);
+ mAudioOutput = audioOutput;
+ }
+
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ if (mEventScheduler != null) {
+ if (!MidiConstants.isAllActiveSensing(data, offset, count)) {
+ mEventScheduler.getReceiver().send(data, offset, count,
+ timestamp);
+ }
+ }
+ }
+
+ private class MyReceiver extends MidiReceiver {
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ byte command = (byte) (data[0] & MidiConstants.STATUS_COMMAND_MASK);
+ int channel = (byte) (data[0] & MidiConstants.STATUS_CHANNEL_MASK);
+ switch (command) {
+ case MidiConstants.STATUS_NOTE_OFF:
+ noteOff(channel, data[1], data[2]);
+ break;
+ case MidiConstants.STATUS_NOTE_ON:
+ noteOn(channel, data[1], data[2]);
+ break;
+ case MidiConstants.STATUS_PITCH_BEND:
+ int bend = (data[2] << 7) + data[1];
+ pitchBend(channel, bend);
+ break;
+ case MidiConstants.STATUS_PROGRAM_CHANGE:
+ mProgram = data[1];
+ mFreeVoices.clear();
+ break;
+ default:
+ logMidiMessage(data, offset, count);
+ break;
+ }
+ }
+ }
+
+ class MyRunnable implements Runnable {
+ @Override
+ public void run() {
+ try {
+ mAudioOutput.start(FRAME_RATE);
+ onLoopStarted();
+ while (go) {
+ processMidiEvents();
+ generateBuffer();
+ mAudioOutput.write(mBuffer, 0, mBuffer.length);
+ onBufferCompleted(FRAMES_PER_BUFFER);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "SynthEngine background thread exception.", e);
+ } finally {
+ onLoopEnded();
+ mAudioOutput.stop();
+ }
+ }
+ }
+
+ /**
+ * This is called form the synthesis thread before it starts looping.
+ */
+ public void onLoopStarted() {
+ }
+
+ /**
+ * This is called once at the end of each synthesis loop.
+ *
+ * @param framesPerBuffer
+ */
+ public void onBufferCompleted(int framesPerBuffer) {
+ }
+
+ /**
+ * This is called form the synthesis thread when it stop looping.
+ */
+ public void onLoopEnded() {
+ }
+
+ /**
+ * Assume message has been aligned to the start of a MIDI message.
+ *
+ * @param data
+ * @param offset
+ * @param count
+ */
+ public void logMidiMessage(byte[] data, int offset, int count) {
+ String text = "Received: ";
+ for (int i = 0; i < count; i++) {
+ text += String.format("0x%02X, ", data[offset + i]);
+ }
+ Log.i(TAG, text);
+ }
+
+ /**
+ * @throws IOException
+ *
+ */
+ private void processMidiEvents() throws IOException {
+ long now = System.nanoTime(); // TODO use audio presentation time
+ MidiEvent event = (MidiEvent) mEventScheduler.getNextEvent(now);
+ while (event != null) {
+ mFramer.send(event.data, 0, event.count, event.getTimestamp());
+ mEventScheduler.addEventToPool(event);
+ event = (MidiEvent) mEventScheduler.getNextEvent(now);
+ }
+ }
+
+ /**
+ *
+ */
+ private void generateBuffer() {
+ for (int i = 0; i < mBuffer.length; i++) {
+ mBuffer[i] = 0.0f;
+ }
+ Iterator iterator = mVoices.values().iterator();
+ while (iterator.hasNext()) {
+ SynthVoice voice = iterator.next();
+ if (voice.isDone()) {
+ iterator.remove();
+ // mFreeVoices.add(voice);
+ } else {
+ voice.mix(mBuffer, SAMPLES_PER_FRAME, 0.25f);
+ }
+ }
+ }
+
+ public void noteOff(int channel, int noteIndex, int velocity) {
+ SynthVoice voice = mVoices.get(noteIndex);
+ if (voice != null) {
+ voice.noteOff();
+ }
+ }
+
+ public void allNotesOff() {
+ Iterator iterator = mVoices.values().iterator();
+ while (iterator.hasNext()) {
+ SynthVoice voice = iterator.next();
+ voice.noteOff();
+ }
+ }
+
+ /**
+ * Create a SynthVoice.
+ */
+ public SynthVoice createVoice(int program) {
+ // For every odd program number use a sine wave.
+ if ((program & 1) == 1) {
+ return new SineVoice();
+ } else {
+ return new SawVoice();
+ }
+ }
+
+ /**
+ *
+ * @param channel
+ * @param noteIndex
+ * @param velocity
+ */
+ public void noteOn(int channel, int noteIndex, int velocity) {
+ if (velocity == 0) {
+ noteOff(channel, noteIndex, velocity);
+ } else {
+ mVoices.remove(noteIndex);
+ SynthVoice voice;
+ if (mFreeVoices.size() > 0) {
+ voice = mFreeVoices.remove(mFreeVoices.size() - 1);
+ } else {
+ voice = createVoice(mProgram);
+ }
+ voice.setFrequencyScaler(mFrequencyScaler);
+ voice.noteOn(noteIndex, velocity);
+ mVoices.put(noteIndex, voice);
+ }
+ }
+
+ public void pitchBend(int channel, int bend) {
+ double semitones = (mBendRange * (bend - 0x2000)) / 0x2000;
+ mFrequencyScaler = (float) Math.pow(2.0, semitones / 12.0);
+ Iterator iterator = mVoices.values().iterator();
+ while (iterator.hasNext()) {
+ SynthVoice voice = iterator.next();
+ voice.setFrequencyScaler(mFrequencyScaler);
+ }
+ }
+
+ /**
+ * Start the synthesizer.
+ */
+ public void start() {
+ stop();
+ go = true;
+ mThread = new Thread(new MyRunnable());
+ mEventScheduler = new MidiEventScheduler();
+ mThread.start();
+ }
+
+ /**
+ * Stop the synthesizer.
+ */
+ public void stop() {
+ go = false;
+ if (mThread != null) {
+ try {
+ mThread.interrupt();
+ mThread.join(500);
+ } catch (InterruptedException e) {
+ // OK, just stopping safely.
+ }
+ mThread = null;
+ mEventScheduler = null;
+ }
+ }
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SynthUnit.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SynthUnit.java
new file mode 100644
index 000000000..90599e284
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SynthUnit.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+public abstract class SynthUnit {
+
+ private static final double CONCERT_A_PITCH = 69.0;
+ private static final double CONCERT_A_FREQUENCY = 440.0;
+
+ /**
+ * @param pitch
+ * MIDI pitch in semitones
+ * @return frequency
+ */
+ public static double pitchToFrequency(double pitch) {
+ double semitones = pitch - CONCERT_A_PITCH;
+ return CONCERT_A_FREQUENCY * Math.pow(2.0, semitones / 12.0);
+ }
+
+ public abstract float render();
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SynthVoice.java b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SynthVoice.java
new file mode 100644
index 000000000..78ba09ac4
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.common.midi/synth/SynthVoice.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.common.midi.synth;
+
+/**
+ * Base class for a polyphonic synthesizer voice.
+ */
+public abstract class SynthVoice {
+ private int mNoteIndex;
+ private float mAmplitude;
+ public static final int STATE_OFF = 0;
+ public static final int STATE_ON = 1;
+ private int mState = STATE_OFF;
+
+ public SynthVoice() {
+ mNoteIndex = -1;
+ }
+
+ public void noteOn(int noteIndex, int velocity) {
+ mState = STATE_ON;
+ this.mNoteIndex = noteIndex;
+ setAmplitude(velocity / 128.0f);
+ }
+
+ public void noteOff() {
+ mState = STATE_OFF;
+ }
+
+ /**
+ * Add the output of this voice to an output buffer.
+ *
+ * @param outputBuffer
+ * @param samplesPerFrame
+ * @param level
+ */
+ public void mix(float[] outputBuffer, int samplesPerFrame, float level) {
+ int numFrames = outputBuffer.length / samplesPerFrame;
+ for (int i = 0; i < numFrames; i++) {
+ float output = render();
+ int offset = i * samplesPerFrame;
+ for (int jf = 0; jf < samplesPerFrame; jf++) {
+ outputBuffer[offset + jf] += output * level;
+ }
+ }
+ }
+
+ public abstract float render();
+
+ public boolean isDone() {
+ return mState == STATE_OFF;
+ }
+
+ public int getNoteIndex() {
+ return mNoteIndex;
+ }
+
+ public float getAmplitude() {
+ return mAmplitude;
+ }
+
+ public void setAmplitude(float amplitude) {
+ this.mAmplitude = amplitude;
+ }
+
+ /**
+ * @param scaler
+ */
+ public void setFrequencyScaler(float scaler) {
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.midisynth/MainActivity.java b/samples/browseable/MidiSynth/src/com.example.android.midisynth/MainActivity.java
new file mode 100644
index 000000000..92964b483
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.midisynth/MainActivity.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.midisynth;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.media.midi.MidiDevice.MidiConnection;
+import android.media.midi.MidiDeviceInfo;
+import android.media.midi.MidiManager;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.WindowManager;
+import android.widget.Toast;
+import android.widget.Toolbar;
+
+import com.example.android.common.midi.MidiOutputPortConnectionSelector;
+import com.example.android.common.midi.MidiPortConnector;
+import com.example.android.common.midi.MidiTools;
+
+/**
+ * Simple synthesizer as a MIDI Device.
+ */
+public class MainActivity extends Activity {
+ static final String TAG = "MidiSynthExample";
+
+ private MidiManager mMidiManager;
+ private MidiOutputPortConnectionSelector mPortSelector;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+ setActionBar((Toolbar) findViewById(R.id.toolbar));
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(false);
+ }
+
+ if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI)) {
+ setupMidi();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main, menu);
+ setKeepScreenOn(menu.findItem(R.id.action_keep_screen_on).isChecked());
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_keep_screen_on:
+ boolean checked = !item.isChecked();
+ setKeepScreenOn(checked);
+ item.setChecked(checked);
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void setKeepScreenOn(boolean keepScreenOn) {
+ if (keepScreenOn) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+
+ private void setupMidi() {
+ // Setup MIDI
+ mMidiManager = (MidiManager) getSystemService(MIDI_SERVICE);
+
+ MidiDeviceInfo synthInfo = MidiTools.findDevice(mMidiManager, "AndroidTest",
+ "SynthExample");
+ int portIndex = 0;
+ mPortSelector = new MidiOutputPortConnectionSelector(mMidiManager, this,
+ R.id.spinner_synth_sender, synthInfo, portIndex);
+ mPortSelector.setConnectedListener(new MyPortsConnectedListener());
+ }
+
+ private void closeSynthResources() {
+ if (mPortSelector != null) {
+ mPortSelector.close();
+ }
+ }
+
+ // TODO A better way would be to listen to the synth server
+ // for open/close events and then disable/enable the spinner.
+ private class MyPortsConnectedListener
+ implements MidiPortConnector.OnPortsConnectedListener {
+ @Override
+ public void onPortsConnected(final MidiConnection connection) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (connection == null) {
+ Toast.makeText(MainActivity.this,
+ R.string.error_port_busy, Toast.LENGTH_SHORT)
+ .show();
+ mPortSelector.clearSelection();
+ } else {
+ Toast.makeText(MainActivity.this,
+ R.string.port_open_ok, Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ closeSynthResources();
+ super.onDestroy();
+ }
+
+}
diff --git a/samples/browseable/MidiSynth/src/com.example.android.midisynth/MidiSynthDeviceService.java b/samples/browseable/MidiSynth/src/com.example.android.midisynth/MidiSynthDeviceService.java
new file mode 100644
index 000000000..b9f25eeb7
--- /dev/null
+++ b/samples/browseable/MidiSynth/src/com.example.android.midisynth/MidiSynthDeviceService.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.midisynth;
+
+import android.media.midi.MidiDeviceService;
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiReceiver;
+
+import com.example.android.common.midi.synth.SynthEngine;
+
+public class MidiSynthDeviceService extends MidiDeviceService {
+
+ private static final String TAG = MainActivity.TAG;
+ private SynthEngine mSynthEngine = new SynthEngine();
+ private boolean mSynthStarted = false;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+
+ @Override
+ public void onDestroy() {
+ mSynthEngine.stop();
+ super.onDestroy();
+ }
+
+ @Override
+ public MidiReceiver[] onGetInputPortReceivers() {
+ return new MidiReceiver[]{mSynthEngine};
+ }
+
+ /**
+ * This will get called when clients connect or disconnect.
+ */
+ @Override
+ public void onDeviceStatusChanged(MidiDeviceStatus status) {
+ if (status.isInputPortOpen(0) && !mSynthStarted) {
+ mSynthEngine.start();
+ mSynthStarted = true;
+ } else if (!status.isInputPortOpen(0) && mSynthStarted) {
+ mSynthEngine.stop();
+ mSynthStarted = false;
+ }
+ }
+
+}
diff --git a/samples/browseable/NavigationDrawer/res/layout/activity_navigation_drawer.xml b/samples/browseable/NavigationDrawer/res/layout/activity_navigation_drawer.xml
index 4e61639a8..673ef6309 100644
--- a/samples/browseable/NavigationDrawer/res/layout/activity_navigation_drawer.xml
+++ b/samples/browseable/NavigationDrawer/res/layout/activity_navigation_drawer.xml
@@ -19,9 +19,12 @@
+ android:layout_height="match_parent"
+ tools:openDrawer="start" >
@@ -44,5 +47,6 @@
android:layout_gravity="left|start"
android:choiceMode="singleChoice"
android:divider="@null"
+ app:layoutManager="LinearLayoutManager"
/>
-
\ No newline at end of file
+
diff --git a/samples/browseable/NavigationDrawer/res/values-v11/template-styles.xml b/samples/browseable/NavigationDrawer/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/NavigationDrawer/res/values-v11/template-styles.xml
+++ b/samples/browseable/NavigationDrawer/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/NavigationDrawer/res/values-v21/base-template-styles.xml b/samples/browseable/NavigationDrawer/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/NavigationDrawer/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/NavigationDrawer/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/NavigationDrawer/src/com.example.android.navigationdrawer/NavigationDrawerActivity.java b/samples/browseable/NavigationDrawer/src/com.example.android.navigationdrawer/NavigationDrawerActivity.java
index 117675701..26d677b79 100644
--- a/samples/browseable/NavigationDrawer/src/com.example.android.navigationdrawer/NavigationDrawerActivity.java
+++ b/samples/browseable/NavigationDrawer/src/com.example.android.navigationdrawer/NavigationDrawerActivity.java
@@ -27,7 +27,6 @@ import android.os.Bundle;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
-import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -89,7 +88,6 @@ public class NavigationDrawerActivity extends Activity implements PlanetAdapter.
mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
// improve performance by indicating the list if fixed size.
mDrawerList.setHasFixedSize(true);
- mDrawerList.setLayoutManager(new LinearLayoutManager(this));
// set up the drawer's list view with items and click listener
mDrawerList.setAdapter(new PlanetAdapter(mPlanetTitles, this));
diff --git a/samples/browseable/NetworkConnect/res/values-v11/template-styles.xml b/samples/browseable/NetworkConnect/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/NetworkConnect/res/values-v11/template-styles.xml
+++ b/samples/browseable/NetworkConnect/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/NetworkConnect/res/values-v21/base-template-styles.xml b/samples/browseable/NetworkConnect/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/NetworkConnect/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/NetworkConnect/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/NetworkConnect/res/values/template-styles.xml b/samples/browseable/NetworkConnect/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/NetworkConnect/res/values/template-styles.xml
+++ b/samples/browseable/NetworkConnect/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/NfcProvisioning/res/values-v11/template-styles.xml b/samples/browseable/NfcProvisioning/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/NfcProvisioning/res/values-v11/template-styles.xml
+++ b/samples/browseable/NfcProvisioning/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/NfcProvisioning/res/values-v21/base-template-styles.xml b/samples/browseable/NfcProvisioning/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/NfcProvisioning/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/NfcProvisioning/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/NfcProvisioning/res/values/template-styles.xml b/samples/browseable/NfcProvisioning/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/NfcProvisioning/res/values/template-styles.xml
+++ b/samples/browseable/NfcProvisioning/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/Notifications/Application/AndroidManifest.xml b/samples/browseable/Notifications/Application/AndroidManifest.xml
index 3f1274d87..6a17ad8c3 100644
--- a/samples/browseable/Notifications/Application/AndroidManifest.xml
+++ b/samples/browseable/Notifications/Application/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.support.wearable.notifications" >
+ android:targetSdkVersion="23" />
diff --git a/samples/browseable/Notifications/Application/res/values-v11/template-styles.xml b/samples/browseable/Notifications/Application/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/Notifications/Application/res/values-v11/template-styles.xml
+++ b/samples/browseable/Notifications/Application/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/Notifications/Application/res/values-v21/base-template-styles.xml b/samples/browseable/Notifications/Application/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/Notifications/Application/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/Notifications/Application/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/Notifications/Application/res/values/template-styles.xml b/samples/browseable/Notifications/Application/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/Notifications/Application/res/values/template-styles.xml
+++ b/samples/browseable/Notifications/Application/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/Notifications/Wearable/AndroidManifest.xml b/samples/browseable/Notifications/Wearable/AndroidManifest.xml
index 34a29ff66..a446fd9bb 100644
--- a/samples/browseable/Notifications/Wearable/AndroidManifest.xml
+++ b/samples/browseable/Notifications/Wearable/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.support.wearable.notifications" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml b/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml
+++ b/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/PdfRendererBasic/res/values-v21/base-template-styles.xml b/samples/browseable/PdfRendererBasic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/PdfRendererBasic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/PdfRendererBasic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/PdfRendererBasic/res/values/template-styles.xml b/samples/browseable/PdfRendererBasic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/PdfRendererBasic/res/values/template-styles.xml
+++ b/samples/browseable/PdfRendererBasic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/PermissionRequest/res/values-v11/template-styles.xml b/samples/browseable/PermissionRequest/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/PermissionRequest/res/values-v11/template-styles.xml
+++ b/samples/browseable/PermissionRequest/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/PermissionRequest/res/values-v21/base-template-styles.xml b/samples/browseable/PermissionRequest/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/PermissionRequest/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/PermissionRequest/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/PermissionRequest/res/values/template-styles.xml b/samples/browseable/PermissionRequest/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/PermissionRequest/res/values/template-styles.xml
+++ b/samples/browseable/PermissionRequest/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/Quiz/Application/AndroidManifest.xml b/samples/browseable/Quiz/Application/AndroidManifest.xml
index 801a47323..8fabd42de 100644
--- a/samples/browseable/Quiz/Application/AndroidManifest.xml
+++ b/samples/browseable/Quiz/Application/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.quiz" >
+ android:targetSdkVersion="23" />
-
+
diff --git a/samples/browseable/Quiz/Application/res/values-v21/base-template-styles.xml b/samples/browseable/Quiz/Application/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/Quiz/Application/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/Quiz/Application/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/Quiz/Application/res/values/template-styles.xml b/samples/browseable/Quiz/Application/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/Quiz/Application/res/values/template-styles.xml
+++ b/samples/browseable/Quiz/Application/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/Quiz/Application/src/com.example.android.wearable.quiz/MainActivity.java b/samples/browseable/Quiz/Application/src/com.example.android.wearable.quiz/MainActivity.java
index de8eb7450..d1e2d739b 100644
--- a/samples/browseable/Quiz/Application/src/com.example.android.wearable.quiz/MainActivity.java
+++ b/samples/browseable/Quiz/Application/src/com.example.android.wearable.quiz/MainActivity.java
@@ -248,7 +248,9 @@ public class MainActivity extends Activity implements DataApi.DataListener,
dataMap.putInt(QUESTION_INDEX, questionIndex);
dataMap.putStringArray(ANSWERS, answers);
dataMap.putInt(CORRECT_ANSWER_INDEX, correctAnswerIndex);
- return request.asPutDataRequest();
+ PutDataRequest putDataRequest = request.asPutDataRequest();
+ putDataRequest.setUrgent();
+ return putDataRequest;
}
}
@@ -496,7 +498,10 @@ public class MainActivity extends Activity implements DataApi.DataListener,
dataMap.putBoolean(QUESTION_WAS_DELETED, false);
if (!mHasQuestionBeenAsked && dataMap.getInt(QUESTION_INDEX) == 0) {
// Ask the first question now.
- Wearable.DataApi.putDataItem(mGoogleApiClient, request.asPutDataRequest());
+ PutDataRequest putDataRequest = request.asPutDataRequest();
+ // Set to high priority in case it isn't already.
+ putDataRequest.setUrgent();
+ Wearable.DataApi.putDataItem(mGoogleApiClient, putDataRequest);
setHasQuestionBeenAsked(true);
} else {
// Enqueue future questions.
diff --git a/samples/browseable/Quiz/Wearable/src/com.example.android.wearable.quiz/DeleteQuestionService.java b/samples/browseable/Quiz/Wearable/src/com.example.android.wearable.quiz/DeleteQuestionService.java
index 353903cda..d71541100 100644
--- a/samples/browseable/Quiz/Wearable/src/com.example.android.wearable.quiz/DeleteQuestionService.java
+++ b/samples/browseable/Quiz/Wearable/src/com.example.android.wearable.quiz/DeleteQuestionService.java
@@ -76,6 +76,7 @@ public class DeleteQuestionService extends IntentService
DataMap dataMap = putDataMapRequest.getDataMap();
dataMap.putBoolean(QUESTION_WAS_DELETED, true);
PutDataRequest request = putDataMapRequest.asPutDataRequest();
+ request.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, request).await();
mGoogleApiClient.disconnect();
}
diff --git a/samples/browseable/Quiz/Wearable/src/com.example.android.wearable.quiz/UpdateQuestionService.java b/samples/browseable/Quiz/Wearable/src/com.example.android.wearable.quiz/UpdateQuestionService.java
index 7b8f730c9..50425b03b 100644
--- a/samples/browseable/Quiz/Wearable/src/com.example.android.wearable.quiz/UpdateQuestionService.java
+++ b/samples/browseable/Quiz/Wearable/src/com.example.android.wearable.quiz/UpdateQuestionService.java
@@ -88,6 +88,7 @@ public class UpdateQuestionService extends IntentService
dataMap.putBoolean(CHOSEN_ANSWER_CORRECT, chosenAnswerCorrect);
dataMap.putBoolean(QUESTION_WAS_ANSWERED, true);
PutDataRequest request = putDataMapRequest.asPutDataRequest();
+ request.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, request).await();
// Remove this question notification.
diff --git a/samples/browseable/RecipeAssistant/Application/AndroidManifest.xml b/samples/browseable/RecipeAssistant/Application/AndroidManifest.xml
index 1786d278b..141da9a08 100644
--- a/samples/browseable/RecipeAssistant/Application/AndroidManifest.xml
+++ b/samples/browseable/RecipeAssistant/Application/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.recipeassistant" >
+ android:targetSdkVersion="23" />
-
+
diff --git a/samples/browseable/RecipeAssistant/Application/res/values-v21/base-template-styles.xml b/samples/browseable/RecipeAssistant/Application/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/RecipeAssistant/Application/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/RecipeAssistant/Application/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/RecipeAssistant/Application/res/values/template-styles.xml b/samples/browseable/RecipeAssistant/Application/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/RecipeAssistant/Application/res/values/template-styles.xml
+++ b/samples/browseable/RecipeAssistant/Application/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/RecyclerView/res/values-v11/template-styles.xml b/samples/browseable/RecyclerView/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/RecyclerView/res/values-v11/template-styles.xml
+++ b/samples/browseable/RecyclerView/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/RecyclerView/res/values-v21/base-template-styles.xml b/samples/browseable/RecyclerView/res/values-v21/base-template-styles.xml
index 8be56b864..0995e4d29 100644
--- a/samples/browseable/RecyclerView/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/RecyclerView/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/RecyclerView/res/values/template-styles.xml b/samples/browseable/RecyclerView/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/RecyclerView/res/values/template-styles.xml
+++ b/samples/browseable/RecyclerView/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/RenderScriptIntrinsic/res/values-v11/template-styles.xml b/samples/browseable/RenderScriptIntrinsic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/RenderScriptIntrinsic/res/values-v11/template-styles.xml
+++ b/samples/browseable/RenderScriptIntrinsic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/RenderScriptIntrinsic/res/values-v21/base-template-styles.xml b/samples/browseable/RenderScriptIntrinsic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/RenderScriptIntrinsic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/RenderScriptIntrinsic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/RenderScriptIntrinsic/res/values/template-styles.xml b/samples/browseable/RenderScriptIntrinsic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/RenderScriptIntrinsic/res/values/template-styles.xml
+++ b/samples/browseable/RenderScriptIntrinsic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/RepeatingAlarm/res/values-v11/template-styles.xml b/samples/browseable/RepeatingAlarm/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/RepeatingAlarm/res/values-v11/template-styles.xml
+++ b/samples/browseable/RepeatingAlarm/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/RepeatingAlarm/res/values-v21/base-template-styles.xml b/samples/browseable/RepeatingAlarm/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/RepeatingAlarm/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/RepeatingAlarm/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/RevealEffectBasic/res/values-v11/template-styles.xml b/samples/browseable/RevealEffectBasic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/RevealEffectBasic/res/values-v11/template-styles.xml
+++ b/samples/browseable/RevealEffectBasic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/RevealEffectBasic/res/values-v21/base-template-styles.xml b/samples/browseable/RevealEffectBasic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/RevealEffectBasic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/RevealEffectBasic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/RevealEffectBasic/res/values/template-styles.xml b/samples/browseable/RevealEffectBasic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/RevealEffectBasic/res/values/template-styles.xml
+++ b/samples/browseable/RevealEffectBasic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/RuntimePermissions/AndroidManifest.xml b/samples/browseable/RuntimePermissions/AndroidManifest.xml
index 050b0515a..0acbb93b6 100644
--- a/samples/browseable/RuntimePermissions/AndroidManifest.xml
+++ b/samples/browseable/RuntimePermissions/AndroidManifest.xml
@@ -34,7 +34,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:theme="@style/AppTheme" >
+ android:theme="@style/Theme.AppCompat.Light" >
diff --git a/samples/browseable/RuntimePermissions/res/values-v11/template-styles.xml b/samples/browseable/RuntimePermissions/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/RuntimePermissions/res/values-v11/template-styles.xml
+++ b/samples/browseable/RuntimePermissions/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/RuntimePermissions/res/values-v21/base-template-styles.xml b/samples/browseable/RuntimePermissions/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/RuntimePermissions/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/RuntimePermissions/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/RuntimePermissions/res/values/template-styles.xml b/samples/browseable/RuntimePermissions/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/RuntimePermissions/res/values/template-styles.xml
+++ b/samples/browseable/RuntimePermissions/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/RuntimePermissionsBasic/AndroidManifest.xml b/samples/browseable/RuntimePermissionsBasic/AndroidManifest.xml
index 1f7ea6a1d..bfae9b92b 100644
--- a/samples/browseable/RuntimePermissionsBasic/AndroidManifest.xml
+++ b/samples/browseable/RuntimePermissionsBasic/AndroidManifest.xml
@@ -28,7 +28,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:theme="@style/AppTheme">
+ android:theme="@style/Theme.AppCompat.Light">
diff --git a/samples/browseable/RuntimePermissionsBasic/res/values-v11/template-styles.xml b/samples/browseable/RuntimePermissionsBasic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/RuntimePermissionsBasic/res/values-v11/template-styles.xml
+++ b/samples/browseable/RuntimePermissionsBasic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/RuntimePermissionsBasic/res/values-v21/base-template-styles.xml b/samples/browseable/RuntimePermissionsBasic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/RuntimePermissionsBasic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/RuntimePermissionsBasic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/RuntimePermissionsBasic/res/values/template-styles.xml b/samples/browseable/RuntimePermissionsBasic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/RuntimePermissionsBasic/res/values/template-styles.xml
+++ b/samples/browseable/RuntimePermissionsBasic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/AndroidManifest.xml b/samples/browseable/RuntimePermissionsWear/Application/AndroidManifest.xml
new file mode 100644
index 000000000..861cad3b4
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/AndroidManifest.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_file_folder.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_file_folder.png
new file mode 100644
index 000000000..8fb69a5a2
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_file_folder.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_hardware_watch.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_hardware_watch.png
new file mode 100644
index 000000000..e05cb6ac1
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_hardware_watch.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_permission_approved.png
new file mode 100644
index 000000000..79893302c
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_permission_denied.png
new file mode 100644
index 000000000..814bb6359
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/tile.9.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/tile.9.png
new file mode 100644
index 000000000..135862883
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-hdpi/tile.9.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_file_folder.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_file_folder.png
new file mode 100644
index 000000000..ef11a064a
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_file_folder.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_hardware_watch.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_hardware_watch.png
new file mode 100644
index 000000000..5f5900ee3
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_hardware_watch.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_permission_approved.png
new file mode 100644
index 000000000..1e63d37c0
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_permission_denied.png
new file mode 100644
index 000000000..45a0d8750
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-mdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_file_folder.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_file_folder.png
new file mode 100644
index 000000000..687710301
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_file_folder.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_hardware_watch.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_hardware_watch.png
new file mode 100644
index 000000000..7c6773c04
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_hardware_watch.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_permission_approved.png
new file mode 100644
index 000000000..24d1efbcc
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_permission_denied.png
new file mode 100644
index 000000000..17f093d88
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xhdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_file_folder.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_file_folder.png
new file mode 100644
index 000000000..3f2db91bc
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_file_folder.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_hardware_watch.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_hardware_watch.png
new file mode 100644
index 000000000..e8a5f7403
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_hardware_watch.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_permission_approved.png
new file mode 100644
index 000000000..f29c5a341
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_permission_denied.png
new file mode 100644
index 000000000..52b0671f6
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxhdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_file_folder.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_file_folder.png
new file mode 100644
index 000000000..de3c50f1c
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_file_folder.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_hardware_watch.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_hardware_watch.png
new file mode 100644
index 000000000..8daad4fd9
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_hardware_watch.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_permission_approved.png
new file mode 100644
index 000000000..ec642b50d
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_permission_denied.png
new file mode 100644
index 000000000..35d6c4fab
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/drawable-xxxhdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/layout/activity_main.xml b/samples/browseable/RuntimePermissionsWear/Application/res/layout/activity_main.xml
new file mode 100644
index 000000000..d35cb0c4f
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/layout/activity_main.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/layout/activity_phone_permission_request.xml b/samples/browseable/RuntimePermissionsWear/Application/res/layout/activity_phone_permission_request.xml
new file mode 100644
index 000000000..f6c5720ad
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/layout/activity_phone_permission_request.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/layout/activity_wear_permission_request.xml b/samples/browseable/RuntimePermissionsWear/Application/res/layout/activity_wear_permission_request.xml
new file mode 100644
index 000000000..b656cf561
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/layout/activity_wear_permission_request.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..cde69bccc
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c133a0cbd
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bfa42f0e7
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..324e72cdd
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Application/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values-sw600dp/template-dimens.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values-sw600dp/template-dimens.xml
new file mode 100644
index 000000000..22074a2bd
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values-sw600dp/template-dimens.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ @dimen/margin_huge
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values-sw600dp/template-styles.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values-sw600dp/template-styles.xml
new file mode 100644
index 000000000..03d197418
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values-sw600dp/template-styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values-v11/template-styles.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values-v11/template-styles.xml
new file mode 100644
index 000000000..8c1ea66f2
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values-v11/template-styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values-v21/base-colors.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values-v21/base-colors.xml
new file mode 100644
index 000000000..8b6ec3f85
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values-v21/base-colors.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values-v21/base-template-styles.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values-v21/base-template-styles.xml
new file mode 100644
index 000000000..c778e4f98
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values-v21/base-template-styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values-w820dp/dimens.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values-w820dp/dimens.xml
new file mode 100644
index 000000000..74184fc8a
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values-w820dp/dimens.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ 64dp
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values/base-strings.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values/base-strings.xml
new file mode 100644
index 000000000..bf92a0b2f
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values/base-strings.xml
@@ -0,0 +1,30 @@
+
+
+
+
+ RuntimePermissionsWear
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values/dimens.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values/dimens.xml
new file mode 100644
index 000000000..e9366a9a3
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values/dimens.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ 16dp
+ 16dp
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values/strings.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values/strings.xml
new file mode 100644
index 000000000..95a2c8325
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values/strings.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ Happy equals approved, sad equals denied.\n\nTo see results or request permissions, click on the buttons above.
+ You do not have the correct permissions. Tap sad face to bring up permission dialog again.
+ Wear Sensors
+ Phone Storage
+
+ PhonePermissionRequestActivity
+ See your directory structure by letting us read your phone\'s storage.
+ Your phone and watch experience need access to your phone\'s storage to show your top level directories.
+ No Thanks
+ Continue
+
+ WearPermissionRequestActivity
+ See your total sensor count by letting us read your wear\'s sensors.
+ Your phone and watch experience need access to your wear\'s sensors to show sensor count.
+ No Thanks
+ Open on Watch
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values/template-dimens.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values/template-dimens.xml
new file mode 100644
index 000000000..39e710b5c
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values/template-dimens.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ 4dp
+ 8dp
+ 16dp
+ 32dp
+ 64dp
+
+
+
+ @dimen/margin_medium
+ @dimen/margin_medium
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values/template-styles.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values/template-styles.xml
new file mode 100644
index 000000000..6e7d593dd
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values/template-styles.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Application/res/values/wear.xml b/samples/browseable/RuntimePermissionsWear/Application/res/values/wear.xml
new file mode 100644
index 000000000..278797263
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/res/values/wear.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ phone_app_runtime_permissions
+
+
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/IncomingRequestPhoneService.java b/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/IncomingRequestPhoneService.java
new file mode 100644
index 000000000..4cad2fa1c
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/IncomingRequestPhoneService.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.runtimepermissions;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.support.v4.app.ActivityCompat;
+import android.util.Log;
+
+import com.example.android.wearable.runtimepermissions.common.Constants;
+
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.MessageApi;
+import com.google.android.gms.wearable.MessageEvent;
+import com.google.android.gms.wearable.Wearable;
+import com.google.android.gms.wearable.WearableListenerService;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handles all incoming requests for phone data (and permissions) from wear devices.
+ */
+public class IncomingRequestPhoneService extends WearableListenerService {
+
+ private static final String TAG = "IncomingRequestService";
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "onCreate()");
+ }
+
+ @Override
+ public void onMessageReceived(MessageEvent messageEvent) {
+ super.onMessageReceived(messageEvent);
+ Log.d(TAG, "onMessageReceived(): " + messageEvent);
+
+ String messagePath = messageEvent.getPath();
+
+ if (messagePath.equals(Constants.MESSAGE_PATH_PHONE)) {
+
+ DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
+ int requestType = dataMap.getInt(Constants.KEY_COMM_TYPE, 0);
+
+ if (requestType == Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION) {
+ promptUserForStoragePermission(messageEvent.getSourceNodeId());
+
+ } else if (requestType == Constants.COMM_TYPE_REQUEST_DATA) {
+ respondWithStorageInformation(messageEvent.getSourceNodeId());
+ }
+ }
+ }
+
+ private void promptUserForStoragePermission(String nodeId) {
+ boolean storagePermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (storagePermissionApproved) {
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
+ sendMessage(nodeId, dataMap);
+ } else {
+ // Launch Phone Activity to grant storage permissions.
+ Intent startIntent = new Intent(this, PhonePermissionRequestActivity.class);
+ startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ /* This extra is included to alert MainPhoneActivity to send back the permission
+ * results after the user has made their decision in PhonePermissionRequestActivity
+ * and it finishes.
+ */
+ startIntent.putExtra(MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR, true);
+ startActivity(startIntent);
+ }
+ }
+
+ private void respondWithStorageInformation(String nodeId) {
+
+ boolean storagePermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (!storagePermissionApproved) {
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED);
+ sendMessage(nodeId, dataMap);
+ } else {
+ /* To keep the sample simple, we are only displaying the top level list of directories.
+ * Otherwise, it will return a message that the media wasn't available.
+ */
+ StringBuilder stringBuilder = new StringBuilder();
+
+ if (isExternalStorageReadable()) {
+ File externalStorageDirectory = Environment.getExternalStorageDirectory();
+ String[] fileList = externalStorageDirectory.list();
+
+ if (fileList.length > 0) {
+ stringBuilder.append("List of directories on phone:\n");
+ for (String file : fileList) {
+ stringBuilder.append(" - " + file + "\n");
+ }
+ } else {
+ stringBuilder.append("No files in external storage.");
+ }
+ } else {
+ stringBuilder.append("No external media is available.");
+ }
+
+ // Send valid results
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_DATA);
+ dataMap.putString(Constants.KEY_PAYLOAD, stringBuilder.toString());
+ sendMessage(nodeId, dataMap);
+
+ }
+ }
+
+ private void sendMessage(String nodeId, DataMap dataMap) {
+ Log.d(TAG, "sendMessage() Node: " + nodeId);
+
+ GoogleApiClient client = new GoogleApiClient.Builder(this)
+ .addApi(Wearable.API)
+ .build();
+ client.blockingConnect(Constants.CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS);
+
+
+ PendingResult pendingMessageResult =
+ Wearable.MessageApi.sendMessage(
+ client,
+ nodeId,
+ Constants.MESSAGE_PATH_WEAR,
+ dataMap.toByteArray());
+
+ MessageApi.SendMessageResult sendMessageResult =
+ pendingMessageResult.await(
+ Constants.CONNECTION_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS);
+
+ if (!sendMessageResult.getStatus().isSuccess()) {
+ Log.d(TAG, "Sending message failed, status: "
+ + sendMessageResult.getStatus());
+ } else {
+ Log.d(TAG, "Message sent successfully");
+ }
+ client.disconnect();
+ }
+
+ private boolean isExternalStorageReadable() {
+ String state = Environment.getExternalStorageState();
+
+ return Environment.MEDIA_MOUNTED.equals(state)
+ || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/MainPhoneActivity.java b/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/MainPhoneActivity.java
new file mode 100644
index 000000000..196b03bd4
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/MainPhoneActivity.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.runtimepermissions;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.os.Looper;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.example.android.wearable.runtimepermissions.common.Constants;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.wearable.CapabilityApi;
+import com.google.android.gms.wearable.CapabilityInfo;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.MessageApi;
+import com.google.android.gms.wearable.MessageEvent;
+import com.google.android.gms.wearable.Node;
+import com.google.android.gms.wearable.Wearable;
+
+import java.io.File;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Displays data that requires runtime permissions both locally (READ_EXTERNAL_STORAGE) and
+ * remotely on wear (BODY_SENSORS).
+ *
+ * The class also handles sending back the results of a permission request from a remote wear device
+ * when the permission has not been approved yet on the phone (uses EXTRA as trigger). In that case,
+ * the IncomingRequestPhoneService launches the splash Activity (PhonePermissionRequestActivity) to
+ * inform user of permission request. After the user decides what to do, it falls back to this
+ * Activity (which has all the GoogleApiClient code) to handle sending data across and keeps user
+ * in app experience.
+ */
+public class MainPhoneActivity extends AppCompatActivity implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ CapabilityApi.CapabilityListener,
+ MessageApi.MessageListener,
+ ResultCallback {
+
+ private static final String TAG = "MainPhoneActivity";
+
+ /*
+ * Alerts Activity that the initial request for permissions came from wear, and the Activity
+ * needs to send back the results (data or permission rejection).
+ */
+ public static final String EXTRA_PROMPT_PERMISSION_FROM_WEAR =
+ "com.example.android.wearable.runtimepermissions.extra.PROMPT_PERMISSION_FROM_WEAR";
+
+ private static final int REQUEST_WEAR_PERMISSION_RATIONALE = 1;
+
+ private boolean mWearBodySensorsPermissionApproved;
+ private boolean mPhoneStoragePermissionApproved;
+
+ private boolean mWearRequestingPhoneStoragePermission;
+
+ private Button mWearBodySensorsPermissionButton;
+ private Button mPhoneStoragePermissionButton;
+ private TextView mOutputTextView;
+
+ private Set mWearNodeIds;
+
+ private GoogleApiClient mGoogleApiClient;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.d(TAG, "onCreate()");
+ super.onCreate(savedInstanceState);
+
+ /*
+ * Since this is a remote permission, we initialize it to false and then check the remote
+ * permission once the GoogleApiClient is connected.
+ */
+ mWearBodySensorsPermissionApproved = false;
+
+ setContentView(R.layout.activity_main);
+
+ // Checks if wear app requested phone permission (permission request opens later if true).
+ mWearRequestingPhoneStoragePermission =
+ getIntent().getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_WEAR, false);
+
+ mPhoneStoragePermissionButton =
+ (Button) findViewById(R.id.phoneStoragePermissionButton);
+
+ mWearBodySensorsPermissionButton =
+ (Button) findViewById(R.id.wearBodySensorsPermissionButton);
+
+ mOutputTextView = (TextView) findViewById(R.id.output);
+
+ mGoogleApiClient = new GoogleApiClient.Builder(this)
+ .addApi(Wearable.API)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+ }
+
+ public void onClickWearBodySensors(View view) {
+
+ logToUi("Requested info from wear device(s). New approval may be required.");
+
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE, Constants.COMM_TYPE_REQUEST_DATA);
+ sendMessage(dataMap);
+ }
+
+ public void onClickPhoneStorage(View view) {
+
+ if (mPhoneStoragePermissionApproved) {
+ logToUi(getPhoneStorageInformation());
+
+ } else {
+ // On 23+ (M+) devices, Storage permission not granted. Request permission.
+ Intent startIntent = new Intent(this, PhonePermissionRequestActivity.class);
+ startActivity(startIntent);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ Log.d(TAG, "onPause()");
+ super.onPause();
+ if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) {
+ Wearable.CapabilityApi.removeCapabilityListener(
+ mGoogleApiClient,
+ this,
+ Constants.CAPABILITY_WEAR_APP);
+ Wearable.MessageApi.removeListener(mGoogleApiClient, this);
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ Log.d(TAG, "onResume()");
+ super.onResume();
+
+ /* Enables app to handle 23+ (M+) style permissions. It also covers user changing
+ * permission in settings and coming back to the app.
+ */
+ mPhoneStoragePermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (mPhoneStoragePermissionApproved) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ }
+
+ if (mGoogleApiClient != null) {
+ mGoogleApiClient.connect();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.d(TAG, "onActivityResult()");
+ if (requestCode == REQUEST_WEAR_PERMISSION_RATIONALE) {
+
+ if (resultCode == Activity.RESULT_OK) {
+ logToUi("Requested permission on wear device(s).");
+
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION);
+ sendMessage(dataMap);
+ }
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle bundle) {
+ Log.d(TAG, "onConnected()");
+
+ // Set up listeners for capability and message changes.
+ Wearable.CapabilityApi.addCapabilityListener(
+ mGoogleApiClient,
+ this,
+ Constants.CAPABILITY_WEAR_APP);
+ Wearable.MessageApi.addListener(mGoogleApiClient, this);
+
+ // Initial check of capabilities to find the wear nodes.
+ PendingResult pendingResult =
+ Wearable.CapabilityApi.getCapability(
+ mGoogleApiClient,
+ Constants.CAPABILITY_WEAR_APP,
+ CapabilityApi.FILTER_REACHABLE);
+
+ pendingResult.setResultCallback(new ResultCallback() {
+ @Override
+ public void onResult(CapabilityApi.GetCapabilityResult getCapabilityResult) {
+
+ CapabilityInfo capabilityInfo = getCapabilityResult.getCapability();
+ String capabilityName = capabilityInfo.getName();
+
+ boolean wearSupportsSampleApp =
+ capabilityName.equals(Constants.CAPABILITY_WEAR_APP);
+
+ if (wearSupportsSampleApp) {
+ mWearNodeIds = capabilityInfo.getNodes();
+
+ /*
+ * Upon getting all wear nodes, we now need to check if the original request to
+ * launch this activity (and PhonePermissionRequestActivity) was initiated by
+ * a wear device. If it was, we need to send back the permission results (data
+ * or rejection of permission) to the wear device.
+ *
+ * Also, note we set variable to false, this enables the user to continue
+ * changing permissions without sending updates to the wear every time.
+ */
+ if (mWearRequestingPhoneStoragePermission) {
+ mWearRequestingPhoneStoragePermission = false;
+ sendWearPermissionResults();
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult connectionResult) {
+ Log.e(TAG, "onConnectionFailed(): connection to location client failed");
+ }
+
+
+ public void onCapabilityChanged(CapabilityInfo capabilityInfo) {
+ Log.d(TAG, "onCapabilityChanged(): " + capabilityInfo);
+
+ mWearNodeIds = capabilityInfo.getNodes();
+ }
+
+ public void onMessageReceived(MessageEvent messageEvent) {
+ Log.d(TAG, "onMessageReceived(): " + messageEvent);
+
+ String messagePath = messageEvent.getPath();
+
+ if (messagePath.equals(Constants.MESSAGE_PATH_PHONE)) {
+ DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
+
+ int commType = dataMap.getInt(Constants.KEY_COMM_TYPE, 0);
+
+ if (commType == Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED) {
+ mWearBodySensorsPermissionApproved = false;
+ updateWearButtonOnUiThread();
+
+ /* Because our request for remote data requires a remote permission, we now launch
+ * a splash activity informing the user we need those permissions (along with
+ * other helpful information to approve).
+ */
+ Intent wearPermissionRationale =
+ new Intent(this, WearPermissionRequestActivity.class);
+ startActivityForResult(wearPermissionRationale, REQUEST_WEAR_PERMISSION_RATIONALE);
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION) {
+ mWearBodySensorsPermissionApproved = true;
+ updateWearButtonOnUiThread();
+ logToUi("User approved permission on remote device, requesting data again.");
+ DataMap outgoingDataRequestDataMap = new DataMap();
+ outgoingDataRequestDataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_DATA);
+ sendMessage(outgoingDataRequestDataMap);
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION) {
+ mWearBodySensorsPermissionApproved = false;
+ updateWearButtonOnUiThread();
+ logToUi("User denied permission on remote device.");
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_DATA) {
+ mWearBodySensorsPermissionApproved = true;
+ String storageDetails = dataMap.getString(Constants.KEY_PAYLOAD);
+ updateWearButtonOnUiThread();
+ logToUi(storageDetails);
+
+ } else {
+ Log.d(TAG, "Unrecognized communication type received.");
+ }
+ }
+ }
+
+ @Override
+ public void onResult(MessageApi.SendMessageResult sendMessageResult) {
+ if (!sendMessageResult.getStatus().isSuccess()) {
+ Log.d(TAG, "Sending message failed, onResult: " + sendMessageResult);
+ updateWearButtonOnUiThread();
+ logToUi("Sending message failed.");
+
+ } else {
+ Log.d(TAG, "Message sent.");
+ }
+ }
+
+ private void sendMessage(DataMap dataMap) {
+ Log.d(TAG, "sendMessage(): " + mWearNodeIds);
+
+ if ((mWearNodeIds != null) && (!mWearNodeIds.isEmpty())) {
+
+ PendingResult pendingResult;
+
+ for (Node node : mWearNodeIds) {
+
+ pendingResult = Wearable.MessageApi.sendMessage(
+ mGoogleApiClient,
+ node.getId(),
+ Constants.MESSAGE_PATH_WEAR,
+ dataMap.toByteArray());
+
+ pendingResult.setResultCallback(this, Constants.CONNECTION_TIME_OUT_MS,
+ TimeUnit.SECONDS);
+ }
+ } else {
+ // Unable to retrieve node with proper capability
+ mWearBodySensorsPermissionApproved = false;
+ updateWearButtonOnUiThread();
+ logToUi("Wear devices not available to send message.");
+ }
+ }
+
+ private void updateWearButtonOnUiThread() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mWearBodySensorsPermissionApproved) {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ } else {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+ }
+ }
+ });
+ }
+
+ /*
+ * Handles all messages for the UI coming on and off the main thread. Not all callbacks happen
+ * on the main thread.
+ */
+ private void logToUi(final String message) {
+
+ boolean mainUiThread = (Looper.myLooper() == Looper.getMainLooper());
+
+ if (mainUiThread) {
+
+ if (!message.isEmpty()) {
+ Log.d(TAG, message);
+ mOutputTextView.setText(message);
+ }
+
+ } else {
+ if (!message.isEmpty()) {
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ Log.d(TAG, message);
+ mOutputTextView.setText(message);
+ }
+ });
+ }
+ }
+ }
+
+ private String getPhoneStorageInformation() {
+
+ StringBuilder stringBuilder = new StringBuilder();
+
+ String state = Environment.getExternalStorageState();
+ boolean isExternalStorageReadable = Environment.MEDIA_MOUNTED.equals(state)
+ || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
+
+ if (isExternalStorageReadable) {
+ File externalStorageDirectory = Environment.getExternalStorageDirectory();
+ String[] fileList = externalStorageDirectory.list();
+
+ if (fileList.length > 0) {
+
+ stringBuilder.append("List of files\n");
+ for (String file : fileList) {
+ stringBuilder.append(" - " + file + "\n");
+ }
+
+ } else {
+ stringBuilder.append("No files in external storage.");
+ }
+
+ } else {
+ stringBuilder.append("No external media is available.");
+ }
+
+ return stringBuilder.toString();
+ }
+
+ private void sendWearPermissionResults() {
+
+ Log.d(TAG, "sendWearPermissionResults()");
+
+ DataMap dataMap = new DataMap();
+
+ if (mPhoneStoragePermissionApproved) {
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
+ } else {
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION);
+ }
+ sendMessage(dataMap);
+ }
+}
diff --git a/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/PhonePermissionRequestActivity.java b/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/PhonePermissionRequestActivity.java
new file mode 100644
index 000000000..0b10e35d1
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/PhonePermissionRequestActivity.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.runtimepermissions;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+
+/**
+ * This is a simple splash screen (activity) for giving more details on why the user should approve
+ * phone permissions for storage. If they choose to move forward, the permission screen
+ * is brought up. Either way (approve or disapprove), this will exit to the MainPhoneActivity after
+ * they are finished with their final decision.
+ *
+ * If this activity is started by our service (IncomingRequestPhoneService) it is marked via an
+ * extra (MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR). That service only starts
+ * this activity if the phone permission hasn't been approved for the data wear is trying to access.
+ * When the user decides within this Activity what to do with the permission request, it closes and
+ * opens the MainPhoneActivity (to maintain the app experience). It also again passes along the same
+ * extra (MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR) to alert MainPhoneActivity to
+ * send the results of the user's decision to the wear device.
+ */
+public class PhonePermissionRequestActivity extends AppCompatActivity implements
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final String TAG = "PhoneRationale";
+
+ /* Id to identify Location permission request. */
+ private static final int PERMISSION_REQUEST_READ_STORAGE = 1;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // If permissions granted, we start the main activity (shut this activity down).
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED) {
+ startMainActivity();
+ }
+
+ setContentView(R.layout.activity_phone_permission_request);
+ }
+
+ public void onClickApprovePermissionRequest(View view) {
+ Log.d(TAG, "onClickApprovePermissionRequest()");
+
+ // On 23+ (M+) devices, External storage permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
+ PERMISSION_REQUEST_READ_STORAGE);
+ }
+
+ public void onClickDenyPermissionRequest(View view) {
+ Log.d(TAG, "onClickDenyPermissionRequest()");
+ startMainActivity();
+ }
+
+ /*
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+
+ String permissionResult = "Request code: " + requestCode + ", Permissions: " + permissions
+ + ", Results: " + grantResults;
+ Log.d(TAG, "onRequestPermissionsResult(): " + permissionResult);
+
+ if (requestCode == PERMISSION_REQUEST_READ_STORAGE) {
+ // Close activity regardless of user's decision (decision picked up in main activity).
+ startMainActivity();
+ }
+ }
+
+ private void startMainActivity() {
+
+ Intent mainActivityIntent = new Intent(this, MainPhoneActivity.class);
+
+ /*
+ * If service started this Activity (b/c wear requested data where permissions were not
+ * approved), tells MainPhoneActivity to send results to wear device (via this extra).
+ */
+ boolean serviceStartedActivity = getIntent().getBooleanExtra(
+ MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR, false);
+
+ if (serviceStartedActivity) {
+ mainActivityIntent.putExtra(
+ MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR, true);
+ }
+
+ startActivity(mainActivityIntent);
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/WearPermissionRequestActivity.java b/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/WearPermissionRequestActivity.java
new file mode 100644
index 000000000..3340ef6bd
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Application/src/com.example.android.wearable.runtimepermissions/WearPermissionRequestActivity.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.runtimepermissions;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+
+/**
+ * This is a simple splash screen (activity) for giving more details on why the user should approve
+ * phone permissions for storage. If they choose to move forward, the permission screen
+ * is brought up. Either way (approve or disapprove), this will exit to the MainPhoneActivity after
+ * they are finished with their final decision.
+ *
+ * If this activity is started by our service (IncomingRequestPhoneService) it is marked via an
+ * extra (MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR). That service only starts
+ * this activity if the phone permission hasn't been approved for the data wear is trying to access.
+ * When the user decides within this Activity what to do with the permission request, it closes and
+ * opens the MainPhoneActivity (to maintain the app experience). It also again passes along the same
+ * extra (MainPhoneActivity.EXTRA_PROMPT_PERMISSION_FROM_WEAR) to alert MainPhoneActivity to
+ * send the results of the user's decision to the wear device.
+ */
+public class WearPermissionRequestActivity extends AppCompatActivity implements
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final String TAG = "WearRationale";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_wear_permission_request);
+ }
+
+ public void onClickApprovePermissionRequest(View view) {
+ Log.d(TAG, "onClickApprovePermissionRequest()");
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+
+ public void onClickDenyPermissionRequest(View view) {
+ Log.d(TAG, "onClickDenyPermissionRequest()");
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Shared/AndroidManifest.xml b/samples/browseable/RuntimePermissionsWear/Shared/AndroidManifest.xml
new file mode 100644
index 000000000..fa262fa5e
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Shared/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Shared/res/values/strings.xml b/samples/browseable/RuntimePermissionsWear/Shared/res/values/strings.xml
new file mode 100644
index 000000000..cc0aaa9af
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Shared/res/values/strings.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ Shared
+
diff --git a/samples/browseable/RuntimePermissionsWear/Shared/src/com.example.android.wearable.runtimepermissions.common/Constants.java b/samples/browseable/RuntimePermissionsWear/Shared/src/com.example.android.wearable.runtimepermissions.common/Constants.java
new file mode 100644
index 000000000..d124400a8
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Shared/src/com.example.android.wearable.runtimepermissions.common/Constants.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.runtimepermissions.common;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A collection of constants that is shared between the wearable and handset apps.
+ */
+public class Constants {
+
+ // Shared
+ public static final long CONNECTION_TIME_OUT_MS = TimeUnit.SECONDS.toMillis(5);
+
+ public static final String KEY_COMM_TYPE = "communicationType";
+ public static final String KEY_PAYLOAD = "payload";
+
+ // Requests
+ public static final int COMM_TYPE_REQUEST_PROMPT_PERMISSION = 1;
+ public static final int COMM_TYPE_REQUEST_DATA = 2;
+
+ // Responses
+ public static final int COMM_TYPE_RESPONSE_PERMISSION_REQUIRED = 1001;
+ public static final int COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION = 1002;
+ public static final int COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION = 1003;
+ public static final int COMM_TYPE_RESPONSE_DATA = 1004;
+
+ // Phone
+ public static final String CAPABILITY_PHONE_APP = "phone_app_runtime_permissions";
+ public static final String MESSAGE_PATH_PHONE = "/phone_message_path";
+
+ // Wear
+ public static final String CAPABILITY_WEAR_APP = "wear_app_runtime_permissions";
+ public static final String MESSAGE_PATH_WEAR = "/wear_message_path";
+
+ private Constants() {}
+}
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/AndroidManifest.xml b/samples/browseable/RuntimePermissionsWear/Wearable/AndroidManifest.xml
new file mode 100644
index 000000000..43218d789
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/AndroidManifest.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_cc_open_on_phone.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_cc_open_on_phone.png
new file mode 100644
index 000000000..618a44f67
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_cc_open_on_phone.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_approved.png
new file mode 100644
index 000000000..79893302c
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_approved_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_approved_bw.png
new file mode 100644
index 000000000..bbd7e8a96
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_approved_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_denied.png
new file mode 100644
index 000000000..814bb6359
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_denied_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_denied_bw.png
new file mode 100644
index 000000000..accd6c002
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-hdpi/ic_permission_denied_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_cc_open_on_phone.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_cc_open_on_phone.png
new file mode 100644
index 000000000..e66ba6b25
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_cc_open_on_phone.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_approved.png
new file mode 100644
index 000000000..1e63d37c0
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_approved_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_approved_bw.png
new file mode 100644
index 000000000..16050cb25
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_approved_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_denied.png
new file mode 100644
index 000000000..45a0d8750
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_denied_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_denied_bw.png
new file mode 100644
index 000000000..376d47193
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-mdpi/ic_permission_denied_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_cc_open_on_phone.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_cc_open_on_phone.png
new file mode 100644
index 000000000..5522d6c3f
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_cc_open_on_phone.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_approved.png
new file mode 100644
index 000000000..24d1efbcc
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_approved_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_approved_bw.png
new file mode 100644
index 000000000..2682e30ae
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_approved_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_denied.png
new file mode 100644
index 000000000..17f093d88
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_denied_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_denied_bw.png
new file mode 100644
index 000000000..cd9d000b4
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xhdpi/ic_permission_denied_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_approved.png
new file mode 100644
index 000000000..f29c5a341
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_approved_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_approved_bw.png
new file mode 100644
index 000000000..1bcf27a78
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_approved_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_denied.png
new file mode 100644
index 000000000..52b0671f6
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_denied_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_denied_bw.png
new file mode 100644
index 000000000..229203349
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxhdpi/ic_permission_denied_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_approved.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_approved.png
new file mode 100644
index 000000000..ec642b50d
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_approved.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_approved_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_approved_bw.png
new file mode 100644
index 000000000..c9eab8546
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_approved_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_denied.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_denied.png
new file mode 100644
index 000000000..35d6c4fab
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_denied.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_denied_bw.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_denied_bw.png
new file mode 100644
index 000000000..81e5355a7
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/drawable-xxxhdpi/ic_permission_denied_bw.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/activity_main.xml b/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/activity_main.xml
new file mode 100644
index 000000000..588ff9a88
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/activity_main.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/activity_request_permission_on_phone.xml b/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/activity_request_permission_on_phone.xml
new file mode 100644
index 000000000..c8a5d0558
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/activity_request_permission_on_phone.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/rect_activity_main.xml b/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/rect_activity_main.xml
new file mode 100644
index 000000000..5a44894a3
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/rect_activity_main.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/round_activity_main.xml b/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/round_activity_main.xml
new file mode 100644
index 000000000..d35012de6
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/res/layout/round_activity_main.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..cde69bccc
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c133a0cbd
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bfa42f0e7
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..324e72cdd
Binary files /dev/null and b/samples/browseable/RuntimePermissionsWear/Wearable/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/values/dimens.xml b/samples/browseable/RuntimePermissionsWear/Wearable/res/values/dimens.xml
new file mode 100644
index 000000000..2f2eb2a13
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/res/values/dimens.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ 40dp
+ 10dp
+ 5dp
+ 35dp
+ 40dp
+
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/values/strings.xml b/samples/browseable/RuntimePermissionsWear/Wearable/res/values/strings.xml
new file mode 100644
index 000000000..67f460af8
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/res/values/strings.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ Runtime Permissions
+
+ Happy equals approved, sad equals denied.\n\nTo see results or request permissions, click on the buttons above.
+ Wear Sensors
+ Phone Storage
+
+ PhonePermissionRationale
+ App requires access to your phone\'s storage.
+ Open on phone
+
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/res/values/wear.xml b/samples/browseable/RuntimePermissionsWear/Wearable/res/values/wear.xml
new file mode 100644
index 000000000..42e922ffc
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/res/values/wear.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ wear_app_runtime_permissions
+
+
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/src/com.example.android.wearable.runtimepermissions/IncomingRequestWearService.java b/samples/browseable/RuntimePermissionsWear/Wearable/src/com.example.android.wearable.runtimepermissions/IncomingRequestWearService.java
new file mode 100644
index 000000000..5dc246777
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/src/com.example.android.wearable.runtimepermissions/IncomingRequestWearService.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.runtimepermissions;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.support.v4.app.ActivityCompat;
+import android.util.Log;
+
+import com.example.android.wearable.runtimepermissions.common.Constants;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.wearable.CapabilityApi;
+import com.google.android.gms.wearable.CapabilityInfo;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.MessageApi;
+import com.google.android.gms.wearable.MessageEvent;
+import com.google.android.gms.wearable.Node;
+import com.google.android.gms.wearable.Wearable;
+import com.google.android.gms.wearable.WearableListenerService;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handles all incoming requests for wear data (and permissions) from phone devices.
+ */
+public class IncomingRequestWearService extends WearableListenerService {
+
+ private static final String TAG = "IncomingRequestService";
+
+ public IncomingRequestWearService() {
+ Log.d(TAG, "IncomingRequestWearService()");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "onCreate()");
+ }
+
+ @Override
+ public void onMessageReceived(MessageEvent messageEvent) {
+ Log.d(TAG, "onMessageReceived(): " + messageEvent);
+
+ String messagePath = messageEvent.getPath();
+
+ if (messagePath.equals(Constants.MESSAGE_PATH_WEAR)) {
+ DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
+
+ int requestType = dataMap.getInt(Constants.KEY_COMM_TYPE);
+
+ if (requestType == Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION) {
+ promptUserForSensorPermission();
+
+ } else if (requestType == Constants.COMM_TYPE_REQUEST_DATA) {
+ respondWithSensorInformation();
+ }
+ }
+ }
+
+ private void promptUserForSensorPermission() {
+ Log.d(TAG, "promptUserForSensorPermission()");
+
+ boolean sensorPermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (sensorPermissionApproved) {
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
+ sendMessage(dataMap);
+ } else {
+ // Launch Activity to grant sensor permissions.
+ Intent startIntent = new Intent(this, MainWearActivity.class);
+ startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startIntent.putExtra(MainWearActivity.EXTRA_PROMPT_PERMISSION_FROM_PHONE, true);
+ startActivity(startIntent);
+ }
+ }
+
+ private void respondWithSensorInformation() {
+ Log.d(TAG, "respondWithSensorInformation()");
+
+ boolean sensorPermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (!sensorPermissionApproved) {
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED);
+ sendMessage(dataMap);
+ } else {
+ /* To keep the sample simple, we are only displaying the number of sensors. You could do
+ * something much more complicated.
+ */
+ SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
+ List sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
+ int numberOfSensorsOnDevice = sensorList.size();
+
+ String sensorSummary = numberOfSensorsOnDevice + " sensors on wear device(s)!";
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_DATA);
+ dataMap.putString(Constants.KEY_PAYLOAD, sensorSummary);
+ sendMessage(dataMap);
+ }
+ }
+
+ private void sendMessage(DataMap dataMap) {
+
+ Log.d(TAG, "sendMessage(): " + dataMap);
+
+ GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
+ .addApi(Wearable.API)
+ .build();
+ ConnectionResult connectionResult =
+ googleApiClient.blockingConnect(
+ Constants.CONNECTION_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS);
+
+ if (!connectionResult.isSuccess()) {
+ Log.d(TAG, "Google API Client failed to connect.");
+ return;
+ }
+
+ PendingResult pendingCapabilityResult =
+ Wearable.CapabilityApi.getCapability(
+ googleApiClient,
+ Constants.CAPABILITY_PHONE_APP,
+ CapabilityApi.FILTER_REACHABLE);
+
+ CapabilityApi.GetCapabilityResult getCapabilityResult =
+ pendingCapabilityResult.await(
+ Constants.CONNECTION_TIME_OUT_MS,
+ TimeUnit.MILLISECONDS);
+
+ if (!getCapabilityResult.getStatus().isSuccess()) {
+ Log.d(TAG, "CapabilityApi failed to return any results.");
+ googleApiClient.disconnect();
+ return;
+ }
+
+ CapabilityInfo capabilityInfo = getCapabilityResult.getCapability();
+ String phoneNodeId = pickBestNodeId(capabilityInfo.getNodes());
+
+ PendingResult pendingMessageResult =
+ Wearable.MessageApi.sendMessage(
+ googleApiClient,
+ phoneNodeId,
+ Constants.MESSAGE_PATH_PHONE,
+ dataMap.toByteArray());
+
+ MessageApi.SendMessageResult sendMessageResult =
+ pendingMessageResult.await(Constants.CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS);
+
+ if (!sendMessageResult.getStatus().isSuccess()) {
+ Log.d(TAG, "Sending message failed, onResult: " + sendMessageResult.getStatus());
+ } else {
+ Log.d(TAG, "Message sent successfully");
+ }
+
+ googleApiClient.disconnect();
+ }
+
+ /*
+ * There should only ever be one phone in a node set (much less w/ the correct capability), so
+ * I am just grabbing the first one (which should be the only one).
+ */
+ private String pickBestNodeId(Set nodes) {
+
+ Log.d(TAG, "pickBestNodeId: " + nodes);
+
+
+ String bestNodeId = null;
+ /* Find a nearby node or pick one arbitrarily. There should be only one phone connected
+ * that supports this sample.
+ */
+ for (Node node : nodes) {
+ if (node.isNearby()) {
+ return node.getId();
+ }
+ bestNodeId = node.getId();
+ }
+ return bestNodeId;
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/src/com.example.android.wearable.runtimepermissions/MainWearActivity.java b/samples/browseable/RuntimePermissionsWear/Wearable/src/com.example.android.wearable.runtimepermissions/MainWearActivity.java
new file mode 100644
index 000000000..b2a259542
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/src/com.example.android.wearable.runtimepermissions/MainWearActivity.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.runtimepermissions;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.os.Bundle;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.wearable.activity.WearableActivity;
+import android.support.wearable.view.WatchViewStub;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.example.android.wearable.runtimepermissions.common.Constants;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.wearable.CapabilityApi;
+import com.google.android.gms.wearable.CapabilityInfo;
+import com.google.android.gms.wearable.DataMap;
+import com.google.android.gms.wearable.MessageApi;
+import com.google.android.gms.wearable.MessageEvent;
+import com.google.android.gms.wearable.Node;
+import com.google.android.gms.wearable.Wearable;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Displays data that requires runtime permissions both locally (BODY_SENSORS) and remotely on
+ * the phone (READ_EXTERNAL_STORAGE).
+ *
+ * The class is also launched by IncomingRequestWearService when the permission for the data the
+ * phone is trying to access hasn't been granted (wear's sensors). If granted in that scenario,
+ * this Activity also sends back the results of the permission request to the phone device (and
+ * the sensor data if approved).
+ */
+public class MainWearActivity extends WearableActivity implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ CapabilityApi.CapabilityListener,
+ MessageApi.MessageListener,
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final String TAG = "MainWearActivity";
+
+ /* Id to identify local permission request for body sensors. */
+ private static final int PERMISSION_REQUEST_READ_BODY_SENSORS = 1;
+
+ /* Id to identify starting/closing RequestPermissionOnPhoneActivity (startActivityForResult). */
+ private static final int REQUEST_PHONE_PERMISSION = 1;
+
+ public static final String EXTRA_PROMPT_PERMISSION_FROM_PHONE =
+ "com.example.android.wearable.runtimepermissions.extra.PROMPT_PERMISSION_FROM_PHONE";
+
+ private boolean mWearBodySensorsPermissionApproved;
+ private boolean mPhoneStoragePermissionApproved;
+
+ private boolean mPhoneRequestingWearSensorPermission;
+
+ private Button mWearBodySensorsPermissionButton;
+ private Button mPhoneStoragePermissionButton;
+ private TextView mOutputTextView;
+
+ private String mPhoneNodeId;
+
+ private GoogleApiClient mGoogleApiClient;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.d(TAG, "onCreate()");
+ super.onCreate(savedInstanceState);;
+
+ /*
+ * Since this is a remote permission, we initialize it to false and then check the remote
+ * permission once the GoogleApiClient is connected.
+ */
+ mPhoneStoragePermissionApproved = false;
+
+ setContentView(R.layout.activity_main);
+ setAmbientEnabled();
+
+ // Checks if phone app requested wear permission (permission request opens later if true).
+ mPhoneRequestingWearSensorPermission =
+ getIntent().getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_PHONE, false);
+
+ final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
+ stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
+ @Override
+ public void onLayoutInflated(WatchViewStub stub) {
+
+ mWearBodySensorsPermissionButton =
+ (Button) stub.findViewById(R.id.wearBodySensorsPermissionButton);
+
+ if (mWearBodySensorsPermissionApproved) {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ }
+
+ mPhoneStoragePermissionButton =
+ (Button) stub.findViewById(R.id.phoneStoragePermissionButton);
+
+ mOutputTextView = (TextView) stub.findViewById(R.id.output);
+
+ if (mPhoneRequestingWearSensorPermission) {
+ launchPermissionDialogForPhone();
+ }
+
+ }
+ });
+
+ mGoogleApiClient = new GoogleApiClient.Builder(this)
+ .addApi(Wearable.API)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+ }
+
+ public void onClickWearBodySensors(View view) {
+
+ if (mWearBodySensorsPermissionApproved) {
+
+ // To keep the sample simple, we are only displaying the number of sensors.
+ SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
+ List sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
+ int numberOfSensorsOnDevice = sensorList.size();
+
+ logToUi(numberOfSensorsOnDevice + " sensors on device(s)!");
+
+ } else {
+ logToUi("Requested local permission.");
+ // On 23+ (M+) devices, GPS permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{Manifest.permission.BODY_SENSORS},
+ PERMISSION_REQUEST_READ_BODY_SENSORS);
+ }
+ }
+
+ public void onClickPhoneStorage(View view) {
+
+ logToUi("Requested info from phone. New approval may be required.");
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_DATA);
+ sendMessage(dataMap);
+ }
+
+ @Override
+ protected void onPause() {
+ Log.d(TAG, "onPause()");
+ super.onPause();
+ if ((mGoogleApiClient != null) && mGoogleApiClient.isConnected()) {
+ Wearable.CapabilityApi.removeCapabilityListener(
+ mGoogleApiClient,
+ this,
+ Constants.CAPABILITY_PHONE_APP);
+ Wearable.MessageApi.removeListener(mGoogleApiClient, this);
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ Log.d(TAG, "onResume()");
+ super.onResume();
+ if (mGoogleApiClient != null) {
+ mGoogleApiClient.connect();
+ }
+
+ // Enables app to handle 23+ (M+) style permissions.
+ mWearBodySensorsPermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /*
+ * Because this wear activity is marked "android:launchMode='singleInstance'" in the manifest,
+ * we need to allow the permissions dialog to be opened up from the phone even if the wear app
+ * is in the foreground. By overriding onNewIntent, we can cover that use case.
+ */
+ @Override
+ protected void onNewIntent (Intent intent) {
+ Log.d(TAG, "onNewIntent()");
+ super.onNewIntent(intent);
+
+ // Checks if phone app requested wear permissions (opens up permission request if true).
+ mPhoneRequestingWearSensorPermission =
+ intent.getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_PHONE, false);
+
+ if (mPhoneRequestingWearSensorPermission) {
+ launchPermissionDialogForPhone();
+ }
+ }
+
+ @Override
+ public void onEnterAmbient(Bundle ambientDetails) {
+ Log.d(TAG, "onEnterAmbient() " + ambientDetails);
+
+ if (mWearBodySensorsPermissionApproved) {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved_bw, 0, 0, 0);
+ } else {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied_bw, 0, 0, 0);
+ }
+
+ if (mPhoneStoragePermissionApproved) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved_bw, 0, 0, 0);
+ } else {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied_bw, 0, 0, 0);
+ }
+ super.onEnterAmbient(ambientDetails);
+ }
+
+ @Override
+ public void onExitAmbient() {
+ Log.d(TAG, "onExitAmbient()");
+
+ if (mWearBodySensorsPermissionApproved) {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ } else {
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+ }
+
+ if (mPhoneStoragePermissionApproved) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ } else {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+ }
+ super.onExitAmbient();
+ }
+
+ @Override
+ public void onConnected(Bundle bundle) {
+ Log.d(TAG, "onConnected()");
+
+ // Set up listeners for capability and message changes.
+ Wearable.CapabilityApi.addCapabilityListener(
+ mGoogleApiClient,
+ this,
+ Constants.CAPABILITY_PHONE_APP);
+ Wearable.MessageApi.addListener(mGoogleApiClient, this);
+
+ // Initial check of capabilities to find the phone.
+ PendingResult pendingResult =
+ Wearable.CapabilityApi.getCapability(
+ mGoogleApiClient,
+ Constants.CAPABILITY_PHONE_APP,
+ CapabilityApi.FILTER_REACHABLE);
+
+ pendingResult.setResultCallback(new ResultCallback() {
+ @Override
+ public void onResult(CapabilityApi.GetCapabilityResult getCapabilityResult) {
+
+ if (getCapabilityResult.getStatus().isSuccess()) {
+ CapabilityInfo capabilityInfo = getCapabilityResult.getCapability();
+ mPhoneNodeId = pickBestNodeId(capabilityInfo.getNodes());
+
+ } else {
+ Log.d(TAG, "Failed CapabilityApi result: "
+ + getCapabilityResult.getStatus());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended(int i) {
+ Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult connectionResult) {
+ Log.e(TAG, "onConnectionFailed(): connection to location client failed");
+ }
+
+ public void onCapabilityChanged(CapabilityInfo capabilityInfo) {
+ Log.d(TAG, "onCapabilityChanged(): " + capabilityInfo);
+
+ mPhoneNodeId = pickBestNodeId(capabilityInfo.getNodes());
+ }
+
+ /*
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+
+ String permissionResult = "Request code: " + requestCode + ", Permissions: " + permissions
+ + ", Results: " + grantResults;
+ Log.d(TAG, "onRequestPermissionsResult(): " + permissionResult);
+
+
+ if (requestCode == PERMISSION_REQUEST_READ_BODY_SENSORS) {
+
+ if ((grantResults.length == 1)
+ && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
+
+ mWearBodySensorsPermissionApproved = true;
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+
+ // To keep the sample simple, we are only displaying the number of sensors.
+ SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
+ List sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
+ int numberOfSensorsOnDevice = sensorList.size();
+
+ String sensorSummary = numberOfSensorsOnDevice + " sensors on this device!";
+ logToUi(sensorSummary);
+
+ if (mPhoneRequestingWearSensorPermission) {
+ // Resets so this isn't triggered every time permission is changed in app.
+ mPhoneRequestingWearSensorPermission = false;
+
+ // Send 'approved' message to remote phone since it started Activity.
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
+ sendMessage(dataMap);
+ }
+
+ } else {
+
+ mWearBodySensorsPermissionApproved = false;
+ mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+
+ if (mPhoneRequestingWearSensorPermission) {
+ // Resets so this isn't triggered every time permission is changed in app.
+ mPhoneRequestingWearSensorPermission = false;
+ // Send 'denied' message to remote phone since it started Activity.
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION);
+ sendMessage(dataMap);
+ }
+ }
+ }
+ }
+
+ public void onMessageReceived(MessageEvent messageEvent) {
+ Log.d(TAG, "onMessageReceived(): " + messageEvent);
+
+ String messagePath = messageEvent.getPath();
+
+ if (messagePath.equals(Constants.MESSAGE_PATH_WEAR)) {
+
+ DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
+ int commType = dataMap.getInt(Constants.KEY_COMM_TYPE, 0);
+
+ if (commType == Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED) {
+ mPhoneStoragePermissionApproved = false;
+ updatePhoneButtonOnUiThread();
+
+ /* Because our request for remote data requires a remote permission, we now launch
+ * a splash activity informing the user we need those permissions (along with
+ * other helpful information to approve).
+ */
+ Intent phonePermissionRationaleIntent =
+ new Intent(this, RequestPermissionOnPhoneActivity.class);
+ startActivityForResult(phonePermissionRationaleIntent, REQUEST_PHONE_PERMISSION);
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION) {
+ mPhoneStoragePermissionApproved = true;
+ updatePhoneButtonOnUiThread();
+ logToUi("User approved permission on remote device, requesting data again.");
+ DataMap outgoingDataRequestDataMap = new DataMap();
+ outgoingDataRequestDataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_DATA);
+ sendMessage(outgoingDataRequestDataMap);
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION) {
+ mPhoneStoragePermissionApproved = false;
+ updatePhoneButtonOnUiThread();
+ logToUi("User denied permission on remote device.");
+
+ } else if (commType == Constants.COMM_TYPE_RESPONSE_DATA) {
+ mPhoneStoragePermissionApproved = true;
+ String storageDetails = dataMap.getString(Constants.KEY_PAYLOAD);
+ updatePhoneButtonOnUiThread();
+ logToUi(storageDetails);
+ }
+ }
+ }
+
+ private void sendMessage(DataMap dataMap) {
+ Log.d(TAG, "sendMessage(): " + mPhoneNodeId);
+
+ if (mPhoneNodeId != null) {
+
+ PendingResult pendingResult =
+ Wearable.MessageApi.sendMessage(
+ mGoogleApiClient,
+ mPhoneNodeId,
+ Constants.MESSAGE_PATH_PHONE,
+ dataMap.toByteArray());
+
+ pendingResult.setResultCallback(new ResultCallback() {
+ @Override
+ public void onResult(MessageApi.SendMessageResult sendMessageResult) {
+
+ if (!sendMessageResult.getStatus().isSuccess()) {
+ updatePhoneButtonOnUiThread();
+ logToUi("Sending message failed.");
+
+ } else {
+ Log.d(TAG, "Message sent successfully.");
+ }
+ }
+ }, Constants.CONNECTION_TIME_OUT_MS, TimeUnit.SECONDS);
+
+ } else {
+ // Unable to retrieve node with proper capability
+ mPhoneStoragePermissionApproved = false;
+ updatePhoneButtonOnUiThread();
+ logToUi("Phone not available to send message.");
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // Check which request we're responding to
+ if (requestCode == REQUEST_PHONE_PERMISSION) {
+ // Make sure the request was successful
+ if (resultCode == RESULT_OK) {
+ logToUi("Requested permission on phone.");
+ DataMap dataMap = new DataMap();
+ dataMap.putInt(Constants.KEY_COMM_TYPE,
+ Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION);
+ sendMessage(dataMap);
+ }
+ }
+ }
+
+ /*
+ * There should only ever be one phone in a node set (much less w/ the correct capability), so
+ * I am just grabbing the first one (which should be the only one).
+ */
+ private String pickBestNodeId(Set nodes) {
+
+ String bestNodeId = null;
+ // Find a nearby node or pick one arbitrarily.
+ for (Node node : nodes) {
+ if (node.isNearby()) {
+ return node.getId();
+ }
+ bestNodeId = node.getId();
+ }
+ return bestNodeId;
+ }
+
+ /*
+ * If Phone triggered the wear app for permissions, we open up the permission
+ * dialog after inflation.
+ */
+ private void launchPermissionDialogForPhone() {
+ Log.d(TAG, "launchPermissionDialogForPhone()");
+
+ if (!mWearBodySensorsPermissionApproved) {
+ // On 23+ (M+) devices, GPS permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ MainWearActivity.this,
+ new String[]{Manifest.permission.BODY_SENSORS},
+ PERMISSION_REQUEST_READ_BODY_SENSORS);
+ }
+ }
+
+ private void updatePhoneButtonOnUiThread() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ if (mPhoneStoragePermissionApproved) {
+
+ if (isAmbient()) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved_bw, 0, 0, 0);
+ } else {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_approved, 0, 0, 0);
+ }
+
+ } else {
+
+ if (isAmbient()) {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied_bw, 0, 0, 0);
+ } else {
+ mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_permission_denied, 0, 0, 0);
+ }
+ }
+ }
+ });
+ }
+
+ /*
+ * Handles all messages for the UI coming on and off the main thread. Not all callbacks happen
+ * on the main thread.
+ */
+ private void logToUi(final String message) {
+
+ boolean mainUiThread = (Looper.myLooper() == Looper.getMainLooper());
+
+ if (mainUiThread) {
+
+ if (!message.isEmpty()) {
+ Log.d(TAG, message);
+ mOutputTextView.setText(message);
+ }
+
+ } else {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (!message.isEmpty()) {
+ Log.d(TAG, message);
+ mOutputTextView.setText(message);
+ }
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/Wearable/src/com.example.android.wearable.runtimepermissions/RequestPermissionOnPhoneActivity.java b/samples/browseable/RuntimePermissionsWear/Wearable/src/com.example.android.wearable.runtimepermissions/RequestPermissionOnPhoneActivity.java
new file mode 100644
index 000000000..f4ee7fc84
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/Wearable/src/com.example.android.wearable.runtimepermissions/RequestPermissionOnPhoneActivity.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.runtimepermissions;
+
+import android.os.Bundle;
+import android.support.wearable.activity.WearableActivity;
+import android.view.View;
+
+/**
+ * Asks user if they want to open permission screen on their remote device (phone).
+ */
+public class RequestPermissionOnPhoneActivity extends WearableActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_request_permission_on_phone);
+ setAmbientEnabled();
+ }
+
+ public void onClickPermissionPhoneStorage(View view) {
+ setResult(RESULT_OK);
+ finish();
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/RuntimePermissionsWear/_index.jd b/samples/browseable/RuntimePermissionsWear/_index.jd
new file mode 100644
index 000000000..e323f5053
--- /dev/null
+++ b/samples/browseable/RuntimePermissionsWear/_index.jd
@@ -0,0 +1,11 @@
+
+page.tags="RuntimePermissionsWear"
+sample.group=Wearable
+@jd:body
+
+
+
+A sample that shows how you can handle remote data that requires permissions both on
+a wearable device and a mobile device.
+
+
diff --git a/samples/browseable/ScreenCapture/res/values-v11/template-styles.xml b/samples/browseable/ScreenCapture/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/ScreenCapture/res/values-v11/template-styles.xml
+++ b/samples/browseable/ScreenCapture/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/ScreenCapture/res/values-v21/base-template-styles.xml b/samples/browseable/ScreenCapture/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/ScreenCapture/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/ScreenCapture/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/ScreenCapture/res/values/template-styles.xml b/samples/browseable/ScreenCapture/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/ScreenCapture/res/values/template-styles.xml
+++ b/samples/browseable/ScreenCapture/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/SkeletonWearableApp/AndroidManifest.xml b/samples/browseable/SkeletonWearableApp/AndroidManifest.xml
index f99d785cc..f9e897829 100644
--- a/samples/browseable/SkeletonWearableApp/AndroidManifest.xml
+++ b/samples/browseable/SkeletonWearableApp/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.google.wearable.app" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/SlidingTabsBasic/res/values-v11/template-styles.xml b/samples/browseable/SlidingTabsBasic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/SlidingTabsBasic/res/values-v11/template-styles.xml
+++ b/samples/browseable/SlidingTabsBasic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/SlidingTabsBasic/res/values-v21/base-template-styles.xml b/samples/browseable/SlidingTabsBasic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/SlidingTabsBasic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/SlidingTabsBasic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/SlidingTabsBasic/res/values/template-styles.xml b/samples/browseable/SlidingTabsBasic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/SlidingTabsBasic/res/values/template-styles.xml
+++ b/samples/browseable/SlidingTabsBasic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/SlidingTabsColors/res/values-v11/template-styles.xml b/samples/browseable/SlidingTabsColors/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/SlidingTabsColors/res/values-v11/template-styles.xml
+++ b/samples/browseable/SlidingTabsColors/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/SlidingTabsColors/res/values-v21/base-template-styles.xml b/samples/browseable/SlidingTabsColors/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/SlidingTabsColors/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/SlidingTabsColors/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/SlidingTabsColors/res/values/template-styles.xml b/samples/browseable/SlidingTabsColors/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/SlidingTabsColors/res/values/template-styles.xml
+++ b/samples/browseable/SlidingTabsColors/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/SpeedTracker/Application/AndroidManifest.xml b/samples/browseable/SpeedTracker/Application/AndroidManifest.xml
index 44284d4fd..be88f6d68 100644
--- a/samples/browseable/SpeedTracker/Application/AndroidManifest.xml
+++ b/samples/browseable/SpeedTracker/Application/AndroidManifest.xml
@@ -2,25 +2,35 @@
+
+
+
+
+
+
-
-
+
+
+
-
+
+
-
+ android:theme="@style/Theme.AppCompat.Light" >
diff --git a/samples/browseable/SpeedTracker/Application/res/layout/main_activity.xml b/samples/browseable/SpeedTracker/Application/res/layout/main_activity.xml
index a18c64484..17a8f6a9d 100644
--- a/samples/browseable/SpeedTracker/Application/res/layout/main_activity.xml
+++ b/samples/browseable/SpeedTracker/Application/res/layout/main_activity.xml
@@ -21,7 +21,8 @@
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp">
diff --git a/samples/browseable/SpeedTracker/Application/res/values-v21/base-template-styles.xml b/samples/browseable/SpeedTracker/Application/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/SpeedTracker/Application/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/SpeedTracker/Application/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/SpeedTracker/Application/res/values/template-styles.xml b/samples/browseable/SpeedTracker/Application/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/SpeedTracker/Application/res/values/template-styles.xml
+++ b/samples/browseable/SpeedTracker/Application/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/SpeedTracker/Application/src/com.example.android.wearable.speedtracker/PhoneMainActivity.java b/samples/browseable/SpeedTracker/Application/src/com.example.android.wearable.speedtracker/PhoneMainActivity.java
index 76f609b15..c645bdd62 100644
--- a/samples/browseable/SpeedTracker/Application/src/com.example.android.wearable.speedtracker/PhoneMainActivity.java
+++ b/samples/browseable/SpeedTracker/Application/src/com.example.android.wearable.speedtracker/PhoneMainActivity.java
@@ -23,10 +23,10 @@ import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.PolylineOptions;
-import android.app.Activity;
import android.app.DatePickerDialog;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.View;
@@ -45,7 +45,8 @@ import java.util.List;
* a map. This data is then saved into an internal database and the corresponding data items are
* deleted.
*/
-public class PhoneMainActivity extends Activity implements DatePickerDialog.OnDateSetListener {
+public class PhoneMainActivity extends AppCompatActivity implements
+ DatePickerDialog.OnDateSetListener {
private static final String TAG = "PhoneMainActivity";
private static final int BOUNDING_BOX_PADDING_PX = 50;
diff --git a/samples/browseable/SpeedTracker/Wearable/AndroidManifest.xml b/samples/browseable/SpeedTracker/Wearable/AndroidManifest.xml
index ab19d5e6b..c9cbad183 100644
--- a/samples/browseable/SpeedTracker/Wearable/AndroidManifest.xml
+++ b/samples/browseable/SpeedTracker/Wearable/AndroidManifest.xml
@@ -19,18 +19,22 @@
- \
+
+ android:targetSdkVersion="23" />
+
+
+
+
-
@@ -48,12 +51,6 @@
-
-
-
-
-
diff --git a/samples/browseable/SpeedTracker/Wearable/res/layout/main_activity.xml b/samples/browseable/SpeedTracker/Wearable/res/layout/main_activity.xml
index a1b9081a0..a2b678eb8 100644
--- a/samples/browseable/SpeedTracker/Wearable/res/layout/main_activity.xml
+++ b/samples/browseable/SpeedTracker/Wearable/res/layout/main_activity.xml
@@ -29,11 +29,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
+ android:paddingLeft="16dp"
android:fontFamily="sans-serif-light"
+ android:textAlignment="center"
android:textSize="17sp"
android:textStyle="italic"
- android:id="@+id/acquiring_gps"
- android:text="@string/acquiring_gps"/>
+ android:id="@+id/gps_issue_text"
+ android:text=""/>
+ android:layout_marginLeft="50dp" />
+ android:layout_alignBottom="@+id/gps_permission"
+ android:layout_marginRight="50dp"/>
diff --git a/samples/browseable/SpeedTracker/Wearable/res/values/strings.xml b/samples/browseable/SpeedTracker/Wearable/res/values/strings.xml
index dda3ecd6b..b0c37478d 100644
--- a/samples/browseable/SpeedTracker/Wearable/res/values/strings.xml
+++ b/samples/browseable/SpeedTracker/Wearable/res/values/strings.xml
@@ -25,11 +25,16 @@
Limit: %1$d mphAcquiring GPS Fix ...%1$d mph
- Start Recording GPS?
- Stop Recording GPS?
+
+ Enable Location Permission?
+
mphSpeed Limit
- GPS not available.
+ No GPS on device. Will use phone GPS when available.OK%.0f
+
+ App requires location permission to function, tap GPS icon.
+
+
diff --git a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/SpeedPickerActivity.java b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/SpeedPickerActivity.java
index d55d7dfb6..d178891f8 100644
--- a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/SpeedPickerActivity.java
+++ b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/SpeedPickerActivity.java
@@ -17,9 +17,8 @@
package com.example.android.wearable.speedtracker;
import android.app.Activity;
-import android.content.SharedPreferences;
+import android.content.Intent;
import android.os.Bundle;
-import android.preference.PreferenceManager;
import android.support.wearable.view.WearableListView;
import android.widget.TextView;
@@ -31,6 +30,9 @@ import com.example.android.wearable.speedtracker.ui.SpeedPickerListAdapter;
*/
public class SpeedPickerActivity extends Activity implements WearableListView.ClickListener {
+ public static final String EXTRA_NEW_SPEED_LIMIT =
+ "com.example.android.wearable.speedtracker.extra.NEW_SPEED_LIMIT";
+
/* Speeds, in mph, that will be shown on the list */
private int[] speeds = {25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75};
@@ -75,9 +77,13 @@ public class SpeedPickerActivity extends Activity implements WearableListView.Cl
@Override
public void onClick(WearableListView.ViewHolder viewHolder) {
- SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
- pref.edit().putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY,
- speeds[viewHolder.getPosition()]).apply();
+
+ int newSpeedLimit = speeds[viewHolder.getPosition()];
+
+ Intent resultIntent = new Intent(Intent.ACTION_PICK);
+ resultIntent.putExtra(EXTRA_NEW_SPEED_LIMIT, newSpeedLimit);
+ setResult(RESULT_OK, resultIntent);
+
finish();
}
diff --git a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/WearableMainActivity.java b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/WearableMainActivity.java
index f3015bf87..25f424cac 100644
--- a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/WearableMainActivity.java
+++ b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/WearableMainActivity.java
@@ -28,7 +28,7 @@ import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.PutDataRequest;
import com.google.android.gms.wearable.Wearable;
-import android.app.Activity;
+import android.Manifest;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
@@ -38,18 +38,19 @@ import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.wearable.activity.WearableActivity;
import android.util.Log;
import android.view.View;
-import android.view.WindowManager;
-import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.example.android.wearable.speedtracker.common.Constants;
import com.example.android.wearable.speedtracker.common.LocationEntry;
-import com.example.android.wearable.speedtracker.ui.LocationSettingActivity;
import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
/**
* The main activity for the wearable app. User can pick a speed limit, and after this activity
@@ -58,33 +59,54 @@ import java.util.Calendar;
* and if the user exceeds the speed limit, it will turn red. In order to show the user that GPS
* location data is coming in, a small green dot keeps on blinking while GPS data is available.
*/
-public class WearableMainActivity extends Activity implements GoogleApiClient.ConnectionCallbacks,
- GoogleApiClient.OnConnectionFailedListener, LocationListener {
+public class WearableMainActivity extends WearableActivity implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ ActivityCompat.OnRequestPermissionsResultCallback,
+ LocationListener {
private static final String TAG = "WearableActivity";
- private static final long UPDATE_INTERVAL_MS = 5 * 1000;
- private static final long FASTEST_INTERVAL_MS = 5 * 1000;
+ private static final long UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
+ private static final long FASTEST_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
- public static final float MPH_IN_METERS_PER_SECOND = 2.23694f;
+ private static final float MPH_IN_METERS_PER_SECOND = 2.23694f;
+
+ private static final int SPEED_LIMIT_DEFAULT_MPH = 45;
- public static final String PREFS_SPEED_LIMIT_KEY = "speed_limit";
- public static final int SPEED_LIMIT_DEFAULT_MPH = 45;
private static final long INDICATOR_DOT_FADE_AWAY_MS = 500L;
- private GoogleApiClient mGoogleApiClient;
- private TextView mSpeedLimitText;
- private TextView mCurrentSpeedText;
- private ImageView mSaveImageView;
- private TextView mAcquiringGps;
- private TextView mCurrentSpeedMphText;
+ // Request codes for changing speed limit and location permissions.
+ private static final int REQUEST_PICK_SPEED_LIMIT = 0;
+
+ // Id to identify Location permission request.
+ private static final int REQUEST_GPS_PERMISSION = 1;
+
+ // Shared Preferences for saving speed limit and location permission between app launches.
+ private static final String PREFS_SPEED_LIMIT_KEY = "SpeedLimit";
- private int mCurrentSpeedLimit;
- private float mCurrentSpeed;
- private View mDot;
- private Handler mHandler = new Handler();
private Calendar mCalendar;
- private boolean mSaveGpsLocation;
+
+ private TextView mSpeedLimitTextView;
+ private TextView mSpeedTextView;
+ private ImageView mGpsPermissionImageView;
+ private TextView mCurrentSpeedMphTextView;
+ private TextView mGpsIssueTextView;
+ private View mBlinkingGpsStatusDotView;
+
+ private String mGpsPermissionNeededMessage;
+ private String mAcquiringGpsMessage;
+
+ private int mSpeedLimit;
+ private float mSpeed;
+
+ private boolean mGpsPermissionApproved;
+
+ private boolean mWaitingForGpsSignal;
+
+ private GoogleApiClient mGoogleApiClient;
+
+ private Handler mHandler = new Handler();
private enum SpeedState {
BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above);
@@ -104,20 +126,53 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ Log.d(TAG, "onCreate()");
+
+
setContentView(R.layout.main_activity);
+
+ /*
+ * Enables Always-on, so our app doesn't shut down when the watch goes into ambient mode.
+ * Best practice is to override onEnterAmbient(), onUpdateAmbient(), and onExitAmbient() to
+ * optimize the display for ambient mode. However, for brevity, we aren't doing that here
+ * to focus on learning location and permissions. For more information on best practices
+ * in ambient mode, check this page:
+ * https://developer.android.com/training/wearables/apps/always-on.html
+ */
+ setAmbientEnabled();
+
+ mCalendar = Calendar.getInstance();
+
+ // Enables app to handle 23+ (M+) style permissions.
+ mGpsPermissionApproved =
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED;
+
+ mGpsPermissionNeededMessage = getString(R.string.permission_rationale);
+ mAcquiringGpsMessage = getString(R.string.acquiring_gps);
+
+
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ mSpeedLimit = sharedPreferences.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH);
+
+ mSpeed = 0;
+
+ mWaitingForGpsSignal = true;
+
+
+ /*
+ * If this hardware doesn't support GPS, we warn the user. Note that when such device is
+ * connected to a phone with GPS capabilities, the framework automatically routes the
+ * location requests from the phone. However, if the phone becomes disconnected and the
+ * wearable doesn't support GPS, no location is recorded until the phone is reconnected.
+ */
if (!hasGps()) {
- // If this hardware doesn't support GPS, we prefer to exit.
- // Note that when such device is connected to a phone with GPS capabilities, the
- // framework automatically routes the location requests to the phone. For this
- // application, this would not be desirable so we exit the app but for some other
- // applications, that might be a valid scenario.
- Log.w(TAG, "This hardware doesn't have GPS, so we exit");
+ Log.w(TAG, "This hardware doesn't have GPS, so we warn user.");
new AlertDialog.Builder(this)
.setMessage(getString(R.string.gps_not_available))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
- finish();
dialog.cancel();
}
})
@@ -125,7 +180,6 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
@Override
public void onDismiss(DialogInterface dialog) {
dialog.cancel();
- finish();
}
})
.setCancelable(false)
@@ -133,164 +187,216 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
.show();
}
+
setupViews();
- updateSpeedVisibility(false);
- setSpeedLimit();
+
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
- mGoogleApiClient.connect();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected()) &&
+ (mGoogleApiClient.isConnecting())) {
+ LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
+ mGoogleApiClient.disconnect();
+ }
+
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (mGoogleApiClient != null) {
+ mGoogleApiClient.connect();
+ }
}
private void setupViews() {
- mSpeedLimitText = (TextView) findViewById(R.id.max_speed_text);
- mCurrentSpeedText = (TextView) findViewById(R.id.current_speed_text);
- mSaveImageView = (ImageView) findViewById(R.id.saving);
- ImageButton settingButton = (ImageButton) findViewById(R.id.settings);
- mAcquiringGps = (TextView) findViewById(R.id.acquiring_gps);
- mCurrentSpeedMphText = (TextView) findViewById(R.id.current_speed_mph);
- mDot = findViewById(R.id.dot);
+ mSpeedLimitTextView = (TextView) findViewById(R.id.max_speed_text);
+ mSpeedTextView = (TextView) findViewById(R.id.current_speed_text);
+ mCurrentSpeedMphTextView = (TextView) findViewById(R.id.current_speed_mph);
- settingButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent speedIntent = new Intent(WearableMainActivity.this,
- SpeedPickerActivity.class);
- startActivity(speedIntent);
- }
- });
+ mGpsPermissionImageView = (ImageView) findViewById(R.id.gps_permission);
+ mGpsIssueTextView = (TextView) findViewById(R.id.gps_issue_text);
+ mBlinkingGpsStatusDotView = findViewById(R.id.dot);
- mSaveImageView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent savingIntent = new Intent(WearableMainActivity.this,
- LocationSettingActivity.class);
- startActivity(savingIntent);
- }
- });
+ updateActivityViewsBasedOnLocationPermissions();
}
- private void setSpeedLimit(int speedLimit) {
- mSpeedLimitText.setText(getString(R.string.speed_limit, speedLimit));
+ public void onSpeedLimitClick(View view) {
+ Intent speedIntent = new Intent(WearableMainActivity.this,
+ SpeedPickerActivity.class);
+ startActivityForResult(speedIntent, REQUEST_PICK_SPEED_LIMIT);
}
- private void setSpeedLimit() {
- SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
- mCurrentSpeedLimit = pref.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH);
- setSpeedLimit(mCurrentSpeedLimit);
- }
+ public void onGpsPermissionClick(View view) {
- private void setCurrentSpeed(float speed) {
- mCurrentSpeed = speed;
- mCurrentSpeedText.setText(String.format(getString(R.string.speed_format), speed));
- adjustColor();
+ if (!mGpsPermissionApproved) {
+
+ Log.i(TAG, "Location permission has NOT been granted. Requesting permission.");
+
+ // On 23+ (M+) devices, GPS permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
+ REQUEST_GPS_PERMISSION);
+ }
}
/**
- * Adjusts the color of the speed based on its value relative to the speed limit.
+ * Adjusts the visibility of views based on location permissions.
*/
- private void adjustColor() {
- SpeedState state = SpeedState.ABOVE;
- if (mCurrentSpeed <= mCurrentSpeedLimit - 5) {
- state = SpeedState.BELOW;
- } else if (mCurrentSpeed <= mCurrentSpeedLimit) {
- state = SpeedState.CLOSE;
- }
+ private void updateActivityViewsBasedOnLocationPermissions() {
- mCurrentSpeedText.setTextColor(getResources().getColor(state.getColor()));
+ /*
+ * If the user has approved location but we don't have a signal yet, we let the user know
+ * we are waiting on the GPS signal (this sometimes takes a little while). Otherwise, the
+ * user might think something is wrong.
+ */
+ if (mGpsPermissionApproved && mWaitingForGpsSignal) {
+
+ // We are getting a GPS signal w/ user permission.
+ mGpsIssueTextView.setText(mAcquiringGpsMessage);
+ mGpsIssueTextView.setVisibility(View.VISIBLE);
+ mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
+
+ mSpeedTextView.setVisibility(View.GONE);
+ mSpeedLimitTextView.setVisibility(View.GONE);
+ mCurrentSpeedMphTextView.setVisibility(View.GONE);
+
+ } else if (mGpsPermissionApproved) {
+
+ mGpsIssueTextView.setVisibility(View.GONE);
+
+ mSpeedTextView.setVisibility(View.VISIBLE);
+ mSpeedLimitTextView.setVisibility(View.VISIBLE);
+ mCurrentSpeedMphTextView.setVisibility(View.VISIBLE);
+ mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
+
+ } else {
+
+ // User needs to enable location for the app to work.
+ mGpsIssueTextView.setVisibility(View.VISIBLE);
+ mGpsIssueTextView.setText(mGpsPermissionNeededMessage);
+ mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_not_saving_grey600_96dp);
+
+ mSpeedTextView.setVisibility(View.GONE);
+ mSpeedLimitTextView.setVisibility(View.GONE);
+ mCurrentSpeedMphTextView.setVisibility(View.GONE);
+ }
+ }
+
+ private void updateSpeedInViews() {
+
+ if (mGpsPermissionApproved) {
+
+ mSpeedLimitTextView.setText(getString(R.string.speed_limit, mSpeedLimit));
+ mSpeedTextView.setText(String.format(getString(R.string.speed_format), mSpeed));
+
+ // Adjusts the color of the speed based on its value relative to the speed limit.
+ SpeedState state = SpeedState.ABOVE;
+ if (mSpeed <= mSpeedLimit - 5) {
+ state = SpeedState.BELOW;
+ } else if (mSpeed <= mSpeedLimit) {
+ state = SpeedState.CLOSE;
+ }
+
+ mSpeedTextView.setTextColor(getResources().getColor(state.getColor()));
+
+ // Causes the (green) dot blinks when new GPS location data is acquired.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
+ }
+ });
+ mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mBlinkingGpsStatusDotView.setVisibility(View.INVISIBLE);
+ }
+ }, INDICATOR_DOT_FADE_AWAY_MS);
+ }
}
@Override
public void onConnected(Bundle bundle) {
- LocationRequest locationRequest = LocationRequest.create()
- .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
- .setInterval(UPDATE_INTERVAL_MS)
- .setFastestInterval(FASTEST_INTERVAL_MS);
- LocationServices.FusedLocationApi
- .requestLocationUpdates(mGoogleApiClient, locationRequest, this)
- .setResultCallback(new ResultCallback() {
+ Log.d(TAG, "onConnected()");
- @Override
- public void onResult(Status status) {
- if (status.getStatus().isSuccess()) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Successfully requested location updates");
+ /*
+ * mGpsPermissionApproved covers 23+ (M+) style permissions. If that is already approved or
+ * the device is pre-23, the app uses mSaveGpsLocation to save the user's location
+ * preference.
+ */
+ if (mGpsPermissionApproved) {
+
+ LocationRequest locationRequest = LocationRequest.create()
+ .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
+ .setInterval(UPDATE_INTERVAL_MS)
+ .setFastestInterval(FASTEST_INTERVAL_MS);
+
+ LocationServices.FusedLocationApi
+ .requestLocationUpdates(mGoogleApiClient, locationRequest, this)
+ .setResultCallback(new ResultCallback() {
+
+ @Override
+ public void onResult(Status status) {
+ if (status.getStatus().isSuccess()) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Successfully requested location updates");
+ }
+ } else {
+ Log.e(TAG,
+ "Failed in requesting location updates, "
+ + "status code: "
+ + status.getStatusCode() + ", message: " + status
+ .getStatusMessage());
}
- } else {
- Log.e(TAG,
- "Failed in requesting location updates, "
- + "status code: "
- + status.getStatusCode() + ", message: " + status
- .getStatusMessage());
}
- }
- });
+ });
+ }
}
@Override
public void onConnectionSuspended(int i) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
- }
+ Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
+
LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
- Log.e(TAG, "onConnectionFailed(): connection to location client failed");
+ Log.e(TAG, "onConnectionFailed(): " + connectionResult.getErrorMessage());
}
@Override
public void onLocationChanged(Location location) {
- updateSpeedVisibility(true);
- setCurrentSpeed(location.getSpeed() * MPH_IN_METERS_PER_SECOND);
- flashDot();
+ Log.d(TAG, "onLocationChanged() : " + location);
+
+
+ if (mWaitingForGpsSignal) {
+ mWaitingForGpsSignal = false;
+ updateActivityViewsBasedOnLocationPermissions();
+ }
+
+ mSpeed = location.getSpeed() * MPH_IN_METERS_PER_SECOND;
+ updateSpeedInViews();
addLocationEntry(location.getLatitude(), location.getLongitude());
}
- /**
- * Causes the (green) dot blinks when new GPS location data is acquired.
- */
- private void flashDot() {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mDot.setVisibility(View.VISIBLE);
- }
- });
- mDot.setVisibility(View.VISIBLE);
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- mDot.setVisibility(View.INVISIBLE);
- }
- }, INDICATOR_DOT_FADE_AWAY_MS);
- }
-
- /**
- * Adjusts the visibility of speed indicator based on the arrival of GPS data.
- */
- private void updateSpeedVisibility(boolean speedVisible) {
- if (speedVisible) {
- mAcquiringGps.setVisibility(View.GONE);
- mCurrentSpeedText.setVisibility(View.VISIBLE);
- mCurrentSpeedMphText.setVisibility(View.VISIBLE);
- } else {
- mAcquiringGps.setVisibility(View.VISIBLE);
- mCurrentSpeedText.setVisibility(View.GONE);
- mCurrentSpeedMphText.setVisibility(View.GONE);
- }
- }
-
- /**
- * Adds a data item to the data Layer storage
+ /*
+ * Adds a data item to the data Layer storage.
*/
private void addLocationEntry(double latitude, double longitude) {
- if (!mSaveGpsLocation || !mGoogleApiClient.isConnected()) {
+ if (!mGpsPermissionApproved || !mGoogleApiClient.isConnected()) {
return;
}
mCalendar.setTimeInMillis(System.currentTimeMillis());
@@ -302,6 +408,7 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
putDataMapRequest.getDataMap()
.putLong(Constants.KEY_TIME, entry.calendar.getTimeInMillis());
PutDataRequest request = putDataMapRequest.asPutDataRequest();
+ request.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, request)
.setResultCallback(new ResultCallback() {
@Override
@@ -315,29 +422,56 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
});
}
+ /**
+ * Handles user choices for both speed limit and location permissions (GPS tracking).
+ */
@Override
- protected void onStop() {
- super.onStop();
- if (mGoogleApiClient.isConnected()) {
- LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+
+ if (requestCode == REQUEST_PICK_SPEED_LIMIT) {
+ if (resultCode == RESULT_OK) {
+ // The user updated the speed limit.
+ int newSpeedLimit =
+ data.getIntExtra(SpeedPickerActivity.EXTRA_NEW_SPEED_LIMIT, mSpeedLimit);
+
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(this);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY, newSpeedLimit);
+ editor.apply();
+
+ mSpeedLimit = newSpeedLimit;
+
+ updateSpeedInViews();
+ }
}
- mGoogleApiClient.disconnect();
}
+ /**
+ * Callback received when a permissions request has been completed.
+ */
@Override
- protected void onResume() {
- super.onResume();
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- mCalendar = Calendar.getInstance();
- setSpeedLimit();
- adjustColor();
- updateRecordingIcon();
- }
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- private void updateRecordingIcon() {
- mSaveGpsLocation = LocationSettingActivity.getGpsRecordingStatusFromPreferences(this);
- mSaveImageView.setImageResource(mSaveGpsLocation ? R.drawable.ic_gps_saving_grey600_96dp
- : R.drawable.ic_gps_not_saving_grey600_96dp);
+ Log.d(TAG, "onRequestPermissionsResult(): " + permissions);
+
+
+ if (requestCode == REQUEST_GPS_PERMISSION) {
+ Log.i(TAG, "Received response for GPS permission request.");
+
+ if ((grantResults.length == 1)
+ && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
+ Log.i(TAG, "GPS permission granted.");
+ mGpsPermissionApproved = true;
+ } else {
+ Log.i(TAG, "GPS permission NOT granted.");
+ mGpsPermissionApproved = false;
+ }
+
+ updateActivityViewsBasedOnLocationPermissions();
+
+ }
}
/**
@@ -346,4 +480,4 @@ public class WearableMainActivity extends Activity implements GoogleApiClient.Co
private boolean hasGps() {
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS);
}
-}
+}
\ No newline at end of file
diff --git a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/SpeedPickerListAdapter.java b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/SpeedPickerListAdapter.java
index e3b284bfa..df25a6a89 100644
--- a/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/SpeedPickerListAdapter.java
+++ b/samples/browseable/SpeedTracker/Wearable/src/com.example.android.wearable.speedtracker/ui/SpeedPickerListAdapter.java
@@ -41,6 +41,9 @@ public class SpeedPickerListAdapter extends WearableListView.Adapter {
mDataSet = dataset;
}
+ /**
+ * Displays all possible speed limit choices.
+ */
public static class ItemViewHolder extends WearableListView.ViewHolder {
private TextView mTextView;
diff --git a/samples/browseable/StorageClient/res/values-v11/template-styles.xml b/samples/browseable/StorageClient/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/StorageClient/res/values-v11/template-styles.xml
+++ b/samples/browseable/StorageClient/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/StorageClient/res/values-v21/base-template-styles.xml b/samples/browseable/StorageClient/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/StorageClient/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/StorageClient/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/StorageProvider/res/values-v11/template-styles.xml b/samples/browseable/StorageProvider/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/StorageProvider/res/values-v11/template-styles.xml
+++ b/samples/browseable/StorageProvider/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/StorageProvider/res/values-v21/base-template-styles.xml b/samples/browseable/StorageProvider/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/StorageProvider/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/StorageProvider/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudProvider.java b/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudProvider.java
index d8be8138b..9f9249a33 100644
--- a/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudProvider.java
+++ b/samples/browseable/StorageProvider/src/com.example.android.storageprovider/MyCloudProvider.java
@@ -51,7 +51,7 @@ import java.util.Set;
* Manages documents and exposes them to the Android system for sharing.
*/
public class MyCloudProvider extends DocumentsProvider {
- private static final String TAG = MyCloudProvider.class.getSimpleName();
+ private static final String TAG = "MyCloudProvider";
// Use these as the default columns to return information about a root if no specific
// columns are requested in a query.
diff --git a/samples/browseable/StorageProvider/src/com.example.android.storageprovider/StorageProviderFragment.java b/samples/browseable/StorageProvider/src/com.example.android.storageprovider/StorageProviderFragment.java
new file mode 100644
index 000000000..80d0296d5
--- /dev/null
+++ b/samples/browseable/StorageProvider/src/com.example.android.storageprovider/StorageProviderFragment.java
@@ -0,0 +1,105 @@
+/*
+* Copyright 2013 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+
+package com.example.android.storageprovider;
+
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.provider.DocumentsContract;
+import android.support.v4.app.Fragment;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.example.android.common.logger.Log;
+
+/**
+ * Toggles the user's login status via a login menu option, and enables/disables the cloud storage
+ * content provider.
+ */
+public class StorageProviderFragment extends Fragment {
+
+ private static final String TAG = "StorageProviderFragment";
+ private static final String AUTHORITY = "com.example.android.storageprovider.documents";
+ private boolean mLoggedIn = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mLoggedIn = readLoginValue();
+
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ MenuItem item = menu.findItem(R.id.sample_action);
+ item.setTitle(mLoggedIn ? R.string.log_out : R.string.log_in);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.sample_action) {
+ toggleLogin();
+ item.setTitle(mLoggedIn ? R.string.log_out : R.string.log_in);
+
+ // BEGIN_INCLUDE(notify_change)
+ // Notify the system that the status of our roots has changed. This will trigger
+ // a call to MyCloudProvider.queryRoots() and force a refresh of the system
+ // picker UI. It's important to call this or stale results may persist.
+ getActivity().getContentResolver().notifyChange(DocumentsContract.buildRootsUri
+ (AUTHORITY), null, false);
+ // END_INCLUDE(notify_change)
+ }
+ return true;
+ }
+
+ /**
+ * Dummy function to change the user's authorization status.
+ */
+ private void toggleLogin() {
+ // Replace this with your standard method of authentication to determine if your app
+ // should make the user's documents available.
+ mLoggedIn = !mLoggedIn;
+ writeLoginValue(mLoggedIn);
+ Log.i(TAG, getString(mLoggedIn ? R.string.logged_in_info : R.string.logged_out_info));
+ }
+
+ /**
+ * Dummy function to save whether the user is logged in.
+ */
+ private void writeLoginValue(boolean loggedIn) {
+ final SharedPreferences sharedPreferences =
+ getActivity().getSharedPreferences(getString(R.string.app_name),
+ getActivity().MODE_PRIVATE);
+ sharedPreferences.edit().putBoolean(getString(R.string.key_logged_in), loggedIn).commit();
+ }
+
+ /**
+ * Dummy function to determine whether the user is logged in.
+ */
+ private boolean readLoginValue() {
+ final SharedPreferences sharedPreferences =
+ getActivity().getSharedPreferences(getString(R.string.app_name),
+ getActivity().MODE_PRIVATE);
+ return sharedPreferences.getBoolean(getString(R.string.key_logged_in), false);
+ }
+
+}
+
+
diff --git a/samples/browseable/SwipeRefreshLayoutBasic/res/values-v11/template-styles.xml b/samples/browseable/SwipeRefreshLayoutBasic/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/SwipeRefreshLayoutBasic/res/values-v11/template-styles.xml
+++ b/samples/browseable/SwipeRefreshLayoutBasic/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/SwipeRefreshLayoutBasic/res/values-v21/base-template-styles.xml b/samples/browseable/SwipeRefreshLayoutBasic/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/SwipeRefreshLayoutBasic/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/SwipeRefreshLayoutBasic/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/SwipeRefreshLayoutBasic/res/values/template-styles.xml b/samples/browseable/SwipeRefreshLayoutBasic/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/SwipeRefreshLayoutBasic/res/values/template-styles.xml
+++ b/samples/browseable/SwipeRefreshLayoutBasic/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/SwipeRefreshListFragment/res/values-v11/template-styles.xml b/samples/browseable/SwipeRefreshListFragment/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/SwipeRefreshListFragment/res/values-v11/template-styles.xml
+++ b/samples/browseable/SwipeRefreshListFragment/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/SwipeRefreshListFragment/res/values-v21/base-template-styles.xml b/samples/browseable/SwipeRefreshListFragment/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/SwipeRefreshListFragment/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/SwipeRefreshListFragment/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/SwipeRefreshListFragment/res/values/template-styles.xml b/samples/browseable/SwipeRefreshListFragment/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/SwipeRefreshListFragment/res/values/template-styles.xml
+++ b/samples/browseable/SwipeRefreshListFragment/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/SwipeRefreshMultipleViews/res/values-v11/template-styles.xml b/samples/browseable/SwipeRefreshMultipleViews/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/SwipeRefreshMultipleViews/res/values-v11/template-styles.xml
+++ b/samples/browseable/SwipeRefreshMultipleViews/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/SwipeRefreshMultipleViews/res/values-v21/base-template-styles.xml b/samples/browseable/SwipeRefreshMultipleViews/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/SwipeRefreshMultipleViews/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/SwipeRefreshMultipleViews/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/SwipeRefreshMultipleViews/res/values/template-styles.xml b/samples/browseable/SwipeRefreshMultipleViews/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/SwipeRefreshMultipleViews/res/values/template-styles.xml
+++ b/samples/browseable/SwipeRefreshMultipleViews/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/SynchronizedNotifications/Application/AndroidManifest.xml b/samples/browseable/SynchronizedNotifications/Application/AndroidManifest.xml
index 1737c7da0..04a69e01b 100644
--- a/samples/browseable/SynchronizedNotifications/Application/AndroidManifest.xml
+++ b/samples/browseable/SynchronizedNotifications/Application/AndroidManifest.xml
@@ -23,7 +23,7 @@
android:versionName="1.0">
+ android:targetSdkVersion="23" />
-
+
diff --git a/samples/browseable/SynchronizedNotifications/Application/res/values-v21/base-template-styles.xml b/samples/browseable/SynchronizedNotifications/Application/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/SynchronizedNotifications/Application/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/SynchronizedNotifications/Application/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/SynchronizedNotifications/Application/src/com.example.android.wearable.synchronizednotifications/DismissListener.java b/samples/browseable/SynchronizedNotifications/Application/src/com.example.android.wearable.synchronizednotifications/DismissListener.java
index 8d5cca440..46218794d 100644
--- a/samples/browseable/SynchronizedNotifications/Application/src/com.example.android.wearable.synchronizednotifications/DismissListener.java
+++ b/samples/browseable/SynchronizedNotifications/Application/src/com.example.android.wearable.synchronizednotifications/DismissListener.java
@@ -31,7 +31,6 @@ import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer;
-import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;
diff --git a/samples/browseable/SynchronizedNotifications/Application/src/com.example.android.wearable.synchronizednotifications/SynchronizedNotificationsFragment.java b/samples/browseable/SynchronizedNotifications/Application/src/com.example.android.wearable.synchronizednotifications/SynchronizedNotificationsFragment.java
index 240af9b27..837c7ad2b 100644
--- a/samples/browseable/SynchronizedNotifications/Application/src/com.example.android.wearable.synchronizednotifications/SynchronizedNotificationsFragment.java
+++ b/samples/browseable/SynchronizedNotifications/Application/src/com.example.android.wearable.synchronizednotifications/SynchronizedNotificationsFragment.java
@@ -19,17 +19,11 @@ package com.example.android.wearable.synchronizednotifications;
import android.app.PendingIntent;
import android.content.Intent;
import android.support.v4.app.Fragment;
-import android.app.Activity;
-import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
-import android.view.LayoutInflater;
import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Toast;
import com.example.android.wearable.synchronizednotifications.common.Constants;
import com.google.android.gms.common.ConnectionResult;
@@ -61,7 +55,7 @@ import java.util.Locale;
public class SynchronizedNotificationsFragment extends Fragment
implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
- private static final String TAG = "SynchronizedNotificationsFragment";
+ private static final String TAG = "SynchronizedFragment";
private GoogleApiClient mGoogleApiClient;
@Override
@@ -87,7 +81,8 @@ public class SynchronizedNotificationsFragment extends Fragment
Constants.WATCH_ONLY_PATH);
return true;
case R.id.btn_different:
- buildMirroredNotifications(getString(R.string.phone_both), getString(R.string.watch_both), now());
+ buildMirroredNotifications(
+ getString(R.string.phone_both), getString(R.string.watch_both), now());
return true;
}
return false;
@@ -110,8 +105,12 @@ public class SynchronizedNotificationsFragment extends Fragment
if (withDismissal) {
Intent dismissIntent = new Intent(Constants.ACTION_DISMISS);
dismissIntent.putExtra(Constants.KEY_NOTIFICATION_ID, Constants.BOTH_ID);
- PendingIntent pendingIntent = PendingIntent
- .getService(this.getActivity(), 0, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent pendingIntent =
+ PendingIntent.getService(
+ this.getActivity(),
+ 0,
+ dismissIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
builder.setDeleteIntent(pendingIntent);
}
NotificationManagerCompat.from(this.getActivity()).notify(notificationId, builder.build());
@@ -127,6 +126,7 @@ public class SynchronizedNotificationsFragment extends Fragment
putDataMapRequest.getDataMap().putString(Constants.KEY_CONTENT, content);
putDataMapRequest.getDataMap().putString(Constants.KEY_TITLE, title);
PutDataRequest request = putDataMapRequest.asPutDataRequest();
+ request.setUrgent();
Wearable.DataApi.putDataItem(mGoogleApiClient, request)
.setResultCallback(new ResultCallback() {
@Override
diff --git a/samples/browseable/SynchronizedNotifications/Wearable/AndroidManifest.xml b/samples/browseable/SynchronizedNotifications/Wearable/AndroidManifest.xml
index f9b0d9c85..5c4d259d7 100644
--- a/samples/browseable/SynchronizedNotifications/Wearable/AndroidManifest.xml
+++ b/samples/browseable/SynchronizedNotifications/Wearable/AndroidManifest.xml
@@ -20,7 +20,7 @@
package="com.example.android.wearable.synchronizednotifications">
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/SynchronizedNotifications/Wearable/src/com.example.android.wearable.synchronizednotifications/NotificationUpdateService.java b/samples/browseable/SynchronizedNotifications/Wearable/src/com.example.android.wearable.synchronizednotifications/NotificationUpdateService.java
index 8b46bf3ae..b5040d4d5 100644
--- a/samples/browseable/SynchronizedNotifications/Wearable/src/com.example.android.wearable.synchronizednotifications/NotificationUpdateService.java
+++ b/samples/browseable/SynchronizedNotifications/Wearable/src/com.example.android.wearable.synchronizednotifications/NotificationUpdateService.java
@@ -35,7 +35,6 @@ import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.DataMap;
import com.google.android.gms.wearable.DataMapItem;
-import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;
diff --git a/samples/browseable/SynchronizedNotifications/Wearable/src/com.example.android.wearable.synchronizednotifications/WearableActivity.java b/samples/browseable/SynchronizedNotifications/Wearable/src/com.example.android.wearable.synchronizednotifications/WearableActivity.java
index 6ef2f1bd2..9d1eff45d 100644
--- a/samples/browseable/SynchronizedNotifications/Wearable/src/com.example.android.wearable.synchronizednotifications/WearableActivity.java
+++ b/samples/browseable/SynchronizedNotifications/Wearable/src/com.example.android.wearable.synchronizednotifications/WearableActivity.java
@@ -18,6 +18,9 @@ package com.example.android.wearable.synchronizednotifications;
import android.app.Activity;
import android.os.Bundle;
+/**
+ * Empty Activity.
+ */
public class WearableActivity extends Activity {
@Override
diff --git a/samples/browseable/TextLinkify/res/values-v11/template-styles.xml b/samples/browseable/TextLinkify/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/TextLinkify/res/values-v11/template-styles.xml
+++ b/samples/browseable/TextLinkify/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/TextLinkify/res/values-v21/base-template-styles.xml b/samples/browseable/TextLinkify/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/TextLinkify/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/TextLinkify/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/TextLinkify/res/values/template-styles.xml b/samples/browseable/TextLinkify/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/TextLinkify/res/values/template-styles.xml
+++ b/samples/browseable/TextLinkify/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/TextSwitcher/res/values-v11/template-styles.xml b/samples/browseable/TextSwitcher/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/TextSwitcher/res/values-v11/template-styles.xml
+++ b/samples/browseable/TextSwitcher/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/TextSwitcher/res/values-v21/base-template-styles.xml b/samples/browseable/TextSwitcher/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/TextSwitcher/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/TextSwitcher/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/TextSwitcher/res/values/template-styles.xml b/samples/browseable/TextSwitcher/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/TextSwitcher/res/values/template-styles.xml
+++ b/samples/browseable/TextSwitcher/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/Timer/AndroidManifest.xml b/samples/browseable/Timer/AndroidManifest.xml
index 364fb5a68..59634fc49 100644
--- a/samples/browseable/Timer/AndroidManifest.xml
+++ b/samples/browseable/Timer/AndroidManifest.xml
@@ -18,7 +18,7 @@
package="com.example.android.wearable.timer" >
+ android:targetSdkVersion="22" />
diff --git a/samples/browseable/WatchFace/Application/AndroidManifest.xml b/samples/browseable/WatchFace/Application/AndroidManifest.xml
index 5433c94f9..d946cdb0f 100644
--- a/samples/browseable/WatchFace/Application/AndroidManifest.xml
+++ b/samples/browseable/WatchFace/Application/AndroidManifest.xml
@@ -18,13 +18,18 @@
package="com.example.android.wearable.watchface" >
+ android:targetSdkVersion="23" />
-
+
+
+
+
+
@@ -55,6 +60,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/WatchFace/Application/res/layout/activity_fit_watch_face_config.xml b/samples/browseable/WatchFace/Application/res/layout/activity_fit_watch_face_config.xml
new file mode 100644
index 000000000..73d14891b
--- /dev/null
+++ b/samples/browseable/WatchFace/Application/res/layout/activity_fit_watch_face_config.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/WatchFace/Application/res/values-v11/template-styles.xml b/samples/browseable/WatchFace/Application/res/values-v11/template-styles.xml
index f261bb5c2..8c1ea66f2 100644
--- a/samples/browseable/WatchFace/Application/res/values-v11/template-styles.xml
+++ b/samples/browseable/WatchFace/Application/res/values-v11/template-styles.xml
@@ -17,6 +17,6 @@
-
+
diff --git a/samples/browseable/WatchFace/Application/res/values-v21/base-template-styles.xml b/samples/browseable/WatchFace/Application/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/WatchFace/Application/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/WatchFace/Application/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/WatchFace/Application/res/values-w820dp/dimens.xml b/samples/browseable/WatchFace/Application/res/values-w820dp/dimens.xml
new file mode 100644
index 000000000..63fc81644
--- /dev/null
+++ b/samples/browseable/WatchFace/Application/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/samples/browseable/WatchFace/Application/res/values/base-strings.xml b/samples/browseable/WatchFace/Application/res/values/base-strings.xml
index 1b346c9b8..49e710098 100644
--- a/samples/browseable/WatchFace/Application/res/values/base-strings.xml
+++ b/samples/browseable/WatchFace/Application/res/values/base-strings.xml
@@ -23,8 +23,13 @@
This sample demonstrates how to create watch faces for android wear and includes a phone app
and a wearable app. The wearable app has a variety of watch faces including analog, digital,
-opengl, calendar, interactive, etc. It also includes a watch-side configuration example.
+opengl, calendar, steps, interactive, etc. It also includes a watch-side configuration example.
The phone app includes a phone-side configuration example.
+
+Additional note on Steps WatchFace Sample, if the user has not installed or setup the Google Fit app
+on their phone and their Wear device has not configured the Google Fit Wear App, then you may get
+zero steps until one of the two is setup. Please note, many Wear devices configure the Google Fit
+Wear App beforehand.
]]>
diff --git a/samples/browseable/WatchFace/Application/res/values/dimens.xml b/samples/browseable/WatchFace/Application/res/values/dimens.xml
new file mode 100644
index 000000000..56dca871d
--- /dev/null
+++ b/samples/browseable/WatchFace/Application/res/values/dimens.xml
@@ -0,0 +1,8 @@
+
+
+
+ 64dp
+ 10dp
+
diff --git a/samples/browseable/WatchFace/Application/res/values/strings.xml b/samples/browseable/WatchFace/Application/res/values/strings.xml
index 6c6834f06..275dcd3f0 100644
--- a/samples/browseable/WatchFace/Application/res/values/strings.xml
+++ b/samples/browseable/WatchFace/Application/res/values/strings.xml
@@ -22,6 +22,8 @@
MinutesSeconds
+ Google Fit
+
No wearable device is currently connected.OK
diff --git a/samples/browseable/WatchFace/Application/res/values/template-styles.xml b/samples/browseable/WatchFace/Application/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/WatchFace/Application/res/values/template-styles.xml
+++ b/samples/browseable/WatchFace/Application/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/WatchFace/Application/src/com.example.android.wearable.watchface/FitDistanceWatchFaceConfigActivity.java b/samples/browseable/WatchFace/Application/src/com.example.android.wearable.watchface/FitDistanceWatchFaceConfigActivity.java
new file mode 100644
index 000000000..1d8e4c9be
--- /dev/null
+++ b/samples/browseable/WatchFace/Application/src/com.example.android.wearable.watchface/FitDistanceWatchFaceConfigActivity.java
@@ -0,0 +1,255 @@
+package com.example.android.wearable.watchface;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GooglePlayServicesUtil;
+import com.google.android.gms.common.Scopes;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Scope;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.fitness.Fitness;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Switch;
+import android.widget.Toast;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Allows users of the Fit WatchFace to tie their Google Fit account to the WatchFace.
+ */
+public class FitDistanceWatchFaceConfigActivity extends Activity implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener {
+
+ private static final String TAG = "FitDistanceConfig";
+
+ // Request code for launching the Intent to resolve authorization.
+ private static final int REQUEST_OAUTH = 1;
+
+ // Shared Preference used to record if the user has enabled Google Fit previously.
+ private static final String PREFS_FIT_ENABLED_BY_USER =
+ "com.example.android.wearable.watchface.preferences.FIT_ENABLED_BY_USER";
+
+ /* Tracks whether an authorization activity is stacking over the current activity, i.e., when
+ * a known auth error is being resolved, such as showing the account chooser or presenting a
+ * consent dialog. This avoids common duplications as might happen on screen rotations, etc.
+ */
+ private static final String EXTRA_AUTH_STATE_PENDING =
+ "com.example.android.wearable.watchface.extra.AUTH_STATE_PENDING";
+
+ private static final long FIT_DISABLE_TIMEOUT_SECS = TimeUnit.SECONDS.toMillis(5);;
+
+ private boolean mResolvingAuthorization;
+
+ private boolean mFitEnabled;
+
+ private GoogleApiClient mGoogleApiClient;
+
+ private Switch mFitAuthSwitch;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_fit_watch_face_config);
+
+ mFitAuthSwitch = (Switch) findViewById(R.id.fit_auth_switch);
+
+ if (savedInstanceState != null) {
+ mResolvingAuthorization =
+ savedInstanceState.getBoolean(EXTRA_AUTH_STATE_PENDING, false);
+ } else {
+ mResolvingAuthorization = false;
+ }
+
+ // Checks if user previously enabled/approved Google Fit.
+ SharedPreferences sharedPreferences = getPreferences(Context.MODE_PRIVATE);
+ mFitEnabled =
+ sharedPreferences.getBoolean(PREFS_FIT_ENABLED_BY_USER, false);
+
+ mGoogleApiClient = new GoogleApiClient.Builder(this)
+ .addApi(Fitness.HISTORY_API)
+ .addApi(Fitness.RECORDING_API)
+ .addApi(Fitness.CONFIG_API)
+ .addScope(new Scope(Scopes.FITNESS_LOCATION_READ_WRITE))
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .build();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ if ((mFitEnabled) && (mGoogleApiClient != null)) {
+
+ mFitAuthSwitch.setChecked(true);
+ mFitAuthSwitch.setEnabled(true);
+
+ mGoogleApiClient.connect();
+
+ } else {
+
+ mFitAuthSwitch.setChecked(false);
+ mFitAuthSwitch.setEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) {
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ bundle.putBoolean(EXTRA_AUTH_STATE_PENDING, mResolvingAuthorization);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ mResolvingAuthorization =
+ savedInstanceState.getBoolean(EXTRA_AUTH_STATE_PENDING, false);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.d(TAG, "onActivityResult()");
+
+ if (requestCode == REQUEST_OAUTH) {
+ mResolvingAuthorization = false;
+
+ if (resultCode == RESULT_OK) {
+ setUserFitPreferences(true);
+
+ if (!mGoogleApiClient.isConnecting() && !mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.connect();
+ }
+ } else {
+ // User cancelled authorization, reset the switch.
+ setUserFitPreferences(false);
+ }
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle connectionHint) {
+ Log.d(TAG, "onConnected: " + connectionHint);
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+
+ if (cause == GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST) {
+ Log.i(TAG, "Connection lost. Cause: Network Lost.");
+ } else if (cause == GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED) {
+ Log.i(TAG, "Connection lost. Reason: Service Disconnected");
+ } else {
+ Log.i(TAG, "onConnectionSuspended: " + cause);
+ }
+
+ mFitAuthSwitch.setChecked(false);
+ mFitAuthSwitch.setEnabled(true);
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ Log.d(TAG, "Connection to Google Fit failed. Cause: " + result.toString());
+
+ if (!result.hasResolution()) {
+ // User cancelled authorization, reset the switch.
+ mFitAuthSwitch.setChecked(false);
+ mFitAuthSwitch.setEnabled(true);
+ // Show the localized error dialog
+ GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(), this, 0).show();
+ return;
+ }
+
+ // Resolve failure if not already trying/authorizing.
+ if (!mResolvingAuthorization) {
+ try {
+ Log.i(TAG, "Attempting to resolve failed GoogleApiClient connection");
+ mResolvingAuthorization = true;
+ result.startResolutionForResult(this, REQUEST_OAUTH);
+ } catch (IntentSender.SendIntentException e) {
+ Log.e(TAG, "Exception while starting resolution activity", e);
+ }
+ }
+ }
+
+ public void onSwitchClicked(View view) {
+
+ boolean userWantsToEnableFit = mFitAuthSwitch.isChecked();
+
+ if (userWantsToEnableFit) {
+
+ Log.d(TAG, "User wants to enable Fit.");
+ if ((mGoogleApiClient != null) && (!mGoogleApiClient.isConnected())) {
+ mGoogleApiClient.connect();
+ }
+
+ } else {
+ Log.d(TAG, "User wants to disable Fit.");
+
+ // Disable switch until disconnect request is finished.
+ mFitAuthSwitch.setEnabled(false);
+
+ PendingResult pendingResult = Fitness.ConfigApi.disableFit(mGoogleApiClient);
+
+ pendingResult.setResultCallback(new ResultCallback() {
+ @Override
+ public void onResult(Status status) {
+
+ if (status.isSuccess()) {
+ Toast.makeText(
+ FitDistanceWatchFaceConfigActivity.this,
+ "Disconnected from Google Fit.",
+ Toast.LENGTH_LONG).show();
+
+ setUserFitPreferences(false);
+
+ mGoogleApiClient.disconnect();
+
+
+ } else {
+ Toast.makeText(
+ FitDistanceWatchFaceConfigActivity.this,
+ "Unable to disconnect from Google Fit. See logcat for details.",
+ Toast.LENGTH_LONG).show();
+
+ // Re-set the switch since auth failed.
+ setUserFitPreferences(true);
+ }
+ }
+ }, FIT_DISABLE_TIMEOUT_SECS, TimeUnit.SECONDS);
+ }
+ }
+
+ private void setUserFitPreferences(boolean userFitPreferences) {
+
+ mFitEnabled = userFitPreferences;
+ SharedPreferences sharedPreferences = getPreferences(Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putBoolean(PREFS_FIT_ENABLED_BY_USER, userFitPreferences);
+ editor.commit();
+
+ mFitAuthSwitch.setChecked(userFitPreferences);
+ mFitAuthSwitch.setEnabled(true);
+ }
+}
diff --git a/samples/browseable/WatchFace/Wearable/AndroidManifest.xml b/samples/browseable/WatchFace/Wearable/AndroidManifest.xml
index c96d73059..ce042e436 100644
--- a/samples/browseable/WatchFace/Wearable/AndroidManifest.xml
+++ b/samples/browseable/WatchFace/Wearable/AndroidManifest.xml
@@ -1,5 +1,6 @@
-
-
+ package="com.example.android.wearable.watchface" >
-
+
-
+
+
+
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name" >
+
+
+
+
+ android:name=".AnalogWatchFaceService"
+ android:label="@string/analog_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_analog" />
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_analog_circular" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_ANALOG" />
+
-
+ android:name=".SweepWatchFaceService"
+ android:label="@string/sweep_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_analog" />
-
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_analog_circular" />
+
+
-
-
+ android:name=".OpenGLWatchFaceService"
+ android:label="@string/opengl_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -134,81 +145,130 @@
+
-
+
+
-
+ android:name=".DigitalWatchFaceService"
+ android:label="@string/digital_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_digital" />
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_digital_circular" />
+ android:name="com.google.android.wearable.watchface.companionConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
+ android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
+ android:value="com.example.android.wearable.watchface.CONFIG_DIGITAL" />
+
+
-
+ and android.intent.category.DEFAULT.
+ -->
+ android:name=".DigitalWatchFaceWearableConfigActivity"
+ android:label="@string/digital_config_name" >
+
+ android:name=".CalendarWatchFaceService"
+ android:label="@string/calendar_name"
+ android:permission="android.permission.BIND_WALLPAPER" >
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview_calendar" />
+ android:name="com.google.android.wearable.watchface.preview_circular"
+ android:resource="@drawable/preview_calendar_circular" />
+
+
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/ic_lock_open_white_24dp.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/ic_lock_open_white_24dp.png
new file mode 100644
index 000000000..6bae68f56
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/ic_lock_open_white_24dp.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance.png
new file mode 100644
index 000000000..a96f35579
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance_circular.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance_circular.png
new file mode 100644
index 000000000..912d85bbb
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_distance_circular.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit.png
new file mode 100644
index 000000000..04b8b5e95
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit_circular.png b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit_circular.png
new file mode 100644
index 000000000..b421e2898
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-hdpi/preview_fit_circular.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-mdpi/ic_lock_open_white_24dp.png b/samples/browseable/WatchFace/Wearable/res/drawable-mdpi/ic_lock_open_white_24dp.png
new file mode 100644
index 000000000..3f47b54cf
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-mdpi/ic_lock_open_white_24dp.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-xhdpi/ic_lock_open_white_24dp.png b/samples/browseable/WatchFace/Wearable/res/drawable-xhdpi/ic_lock_open_white_24dp.png
new file mode 100644
index 000000000..cbe9e1cd0
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-xhdpi/ic_lock_open_white_24dp.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/drawable-xxhdpi/ic_lock_open_white_24dp.png b/samples/browseable/WatchFace/Wearable/res/drawable-xxhdpi/ic_lock_open_white_24dp.png
new file mode 100644
index 000000000..1d1b0f4d3
Binary files /dev/null and b/samples/browseable/WatchFace/Wearable/res/drawable-xxhdpi/ic_lock_open_white_24dp.png differ
diff --git a/samples/browseable/WatchFace/Wearable/res/layout/activity_calendar_watch_face_permission.xml b/samples/browseable/WatchFace/Wearable/res/layout/activity_calendar_watch_face_permission.xml
new file mode 100644
index 000000000..bf0e3f670
--- /dev/null
+++ b/samples/browseable/WatchFace/Wearable/res/layout/activity_calendar_watch_face_permission.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/WatchFace/Wearable/res/values/dimens.xml b/samples/browseable/WatchFace/Wearable/res/values/dimens.xml
index 4973466ec..0b0672b66 100644
--- a/samples/browseable/WatchFace/Wearable/res/values/dimens.xml
+++ b/samples/browseable/WatchFace/Wearable/res/values/dimens.xml
@@ -32,4 +32,15 @@
72dp84dp25dp
+ 40dp
+ 45dp
+ 20dp
+ 25dp
+ 30dp
+ 15dp
+ 25dp
+ 20dp
+ 30dp
+ 80dp
+ 25dp
diff --git a/samples/browseable/WatchFace/Wearable/res/values/strings.xml b/samples/browseable/WatchFace/Wearable/res/values/strings.xml
index 19bc3e7fe..4090995de 100644
--- a/samples/browseable/WatchFace/Wearable/res/values/strings.xml
+++ b/samples/browseable/WatchFace/Wearable/res/values/strings.xml
@@ -25,11 +25,22 @@
Digital watch face configurationAMPM
+
+ Sample Fit Steps
+ Sample Fit Distance
+ AM
+ PM
+ %1$d steps
+ %1$,.2f meters
+
Sample Calendar
+ <br><br><br>WatchFace requires Calendar permission. Click on this WatchFace or visit Settings > Permissions to approve.<br><br><br>You have <b>%1$d</b> meeting in the next 24 hours.<br><br><br>You have <b>%1$d</b> meetings in the next 24 hours.
+ Calendar Permission Activity
+ WatchFace requires Calendar access.Black
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/AnalogWatchFaceService.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/AnalogWatchFaceService.java
index fb86ac70e..9fd73a589 100644
--- a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/AnalogWatchFaceService.java
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/AnalogWatchFaceService.java
@@ -87,7 +87,7 @@ public class AnalogWatchFaceService extends CanvasWatchFaceService {
/* Colors for all hands (hour, minute, seconds, ticks) based on photo loaded. */
private int mWatchHandColor;
- private int mWatchHandHightlightColor;
+ private int mWatchHandHighlightColor;
private int mWatchHandShadowColor;
private Paint mHourPaint;
@@ -151,7 +151,7 @@ public class AnalogWatchFaceService extends CanvasWatchFaceService {
/* Set defaults for colors */
mWatchHandColor = Color.WHITE;
- mWatchHandHightlightColor = Color.RED;
+ mWatchHandHighlightColor = Color.RED;
mWatchHandShadowColor = Color.BLACK;
mHourPaint = new Paint();
@@ -169,7 +169,7 @@ public class AnalogWatchFaceService extends CanvasWatchFaceService {
mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mSecondPaint = new Paint();
- mSecondPaint.setColor(mWatchHandHightlightColor);
+ mSecondPaint.setColor(mWatchHandHighlightColor);
mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
mSecondPaint.setAntiAlias(true);
mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
@@ -193,7 +193,7 @@ public class AnalogWatchFaceService extends CanvasWatchFaceService {
Log.d(TAG, "Palette: " + palette);
}
- mWatchHandHightlightColor = palette.getVibrantColor(Color.RED);
+ mWatchHandHighlightColor = palette.getVibrantColor(Color.RED);
mWatchHandColor = palette.getLightVibrantColor(Color.WHITE);
mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK);
updateWatchHandStyle();
@@ -261,7 +261,7 @@ public class AnalogWatchFaceService extends CanvasWatchFaceService {
} else {
mHourPaint.setColor(mWatchHandColor);
mMinutePaint.setColor(mWatchHandColor);
- mSecondPaint.setColor(mWatchHandHightlightColor);
+ mSecondPaint.setColor(mWatchHandHighlightColor);
mTickAndCirclePaint.setColor(mWatchHandColor);
mHourPaint.setAntiAlias(true);
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFacePermissionActivity.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFacePermissionActivity.java
new file mode 100644
index 000000000..7effd33dd
--- /dev/null
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFacePermissionActivity.java
@@ -0,0 +1,56 @@
+package com.example.android.wearable.watchface;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.wearable.activity.WearableActivity;
+import android.util.Log;
+import android.view.View;
+
+/**
+ * Simple Activity for displaying Calendar Permission Rationale to user.
+ */
+public class CalendarWatchFacePermissionActivity extends WearableActivity {
+
+ private static final String TAG = "PermissionActivity";
+
+ /* Id to identify permission request for calendar. */
+ private static final int PERMISSION_REQUEST_READ_CALENDAR = 1;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_calendar_watch_face_permission);
+ setAmbientEnabled();
+ }
+
+ public void onClickEnablePermission(View view) {
+ Log.d(TAG, "onClickEnablePermission()");
+
+ // On 23+ (M+) devices, GPS permission not granted. Request permission.
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{Manifest.permission.READ_CALENDAR},
+ PERMISSION_REQUEST_READ_CALENDAR);
+
+ }
+
+ /*
+ * Callback received when a permissions request has been completed.
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+
+ Log.d(TAG, "onRequestPermissionsResult()");
+
+ if (requestCode == PERMISSION_REQUEST_READ_CALENDAR) {
+ if ((grantResults.length == 1)
+ && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
+ finish();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFaceService.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFaceService.java
index a8ab95568..98a251cd1 100644
--- a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFaceService.java
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/CalendarWatchFaceService.java
@@ -16,11 +16,13 @@
package com.example.android.wearable.watchface;
+import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -30,8 +32,10 @@ import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
+import android.support.v4.app.ActivityCompat;
import android.support.wearable.provider.WearableCalendarContract;
import android.support.wearable.watchface.CanvasWatchFaceService;
+import android.support.wearable.watchface.WatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.text.DynamicLayout;
import android.text.Editable;
@@ -74,31 +78,37 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
final TextPaint mTextPaint = new TextPaint();
int mNumMeetings;
+ private boolean mCalendarPermissionApproved;
+ private String mCalendarNotApprovedMessage;
private AsyncTask mLoadMeetingsTask;
+ private boolean mIsReceiverRegistered;
+
/** Handler to load the meetings once a minute in interactive mode. */
final Handler mLoadMeetingsHandler = new Handler() {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MSG_LOAD_MEETINGS:
+
cancelLoadMeetingTask();
- mLoadMeetingsTask = new LoadMeetingsTask();
- mLoadMeetingsTask.execute();
+
+ // Loads meetings.
+ if (mCalendarPermissionApproved) {
+ mLoadMeetingsTask = new LoadMeetingsTask();
+ mLoadMeetingsTask.execute();
+ }
break;
}
}
};
- private boolean mIsReceiverRegistered;
-
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_PROVIDER_CHANGED.equals(intent.getAction())
&& WearableCalendarContract.CONTENT_URI.equals(intent.getData())) {
- cancelLoadMeetingTask();
mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
}
}
@@ -106,29 +116,59 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
@Override
public void onCreate(SurfaceHolder holder) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCreate");
- }
super.onCreate(holder);
+ Log.d(TAG, "onCreate");
+
+ mCalendarNotApprovedMessage =
+ getResources().getString(R.string.calendar_permission_not_approved);
+
+ /* Accepts tap events to allow permission changes by user. */
setWatchFaceStyle(new WatchFaceStyle.Builder(CalendarWatchFaceService.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
+ .setAcceptsTapEvents(true)
.build());
mTextPaint.setColor(FOREGROUND_COLOR);
mTextPaint.setTextSize(TEXT_SIZE);
- mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
+ // Enables app to handle 23+ (M+) style permissions.
+ mCalendarPermissionApproved =
+ ActivityCompat.checkSelfPermission(
+ getApplicationContext(),
+ Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED;
+
+ if (mCalendarPermissionApproved) {
+ mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
+ }
}
@Override
public void onDestroy() {
mLoadMeetingsHandler.removeMessages(MSG_LOAD_MEETINGS);
- cancelLoadMeetingTask();
super.onDestroy();
}
+ /*
+ * Captures tap event (and tap type) and increments correct tap type total.
+ */
+ @Override
+ public void onTapCommand(int tapType, int x, int y, long eventTime) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Tap Command: " + tapType);
+ }
+
+ // Ignore lint error (fixed in wearable support library 1.4)
+ if (tapType == WatchFaceService.TAP_TYPE_TAP && !mCalendarPermissionApproved) {
+ Intent permissionIntent = new Intent(
+ getApplicationContext(),
+ CalendarWatchFacePermissionActivity.class);
+ permissionIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(permissionIntent);
+ }
+ }
+
@Override
public void onDraw(Canvas canvas, Rect bounds) {
// Create or update mLayout if necessary.
@@ -141,8 +181,13 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
// Update the contents of mEditable.
mEditable.clear();
- mEditable.append(Html.fromHtml(getResources().getQuantityString(
- R.plurals.calendar_meetings, mNumMeetings, mNumMeetings)));
+
+ if (mCalendarPermissionApproved) {
+ mEditable.append(Html.fromHtml(getResources().getQuantityString(
+ R.plurals.calendar_meetings, mNumMeetings, mNumMeetings)));
+ } else {
+ mEditable.append(Html.fromHtml(mCalendarNotApprovedMessage));
+ }
// Draw the text on a solid background.
canvas.drawColor(BACKGROUND_COLOR);
@@ -151,15 +196,24 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
@Override
public void onVisibilityChanged(boolean visible) {
+ Log.d(TAG, "onVisibilityChanged()");
super.onVisibilityChanged(visible);
if (visible) {
- IntentFilter filter = new IntentFilter(Intent.ACTION_PROVIDER_CHANGED);
- filter.addDataScheme("content");
- filter.addDataAuthority(WearableCalendarContract.AUTHORITY, null);
- registerReceiver(mBroadcastReceiver, filter);
- mIsReceiverRegistered = true;
- mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
+ // Enables app to handle 23+ (M+) style permissions.
+ mCalendarPermissionApproved = ActivityCompat.checkSelfPermission(
+ getApplicationContext(),
+ Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED;
+
+ if (mCalendarPermissionApproved) {
+ IntentFilter filter = new IntentFilter(Intent.ACTION_PROVIDER_CHANGED);
+ filter.addDataScheme("content");
+ filter.addDataAuthority(WearableCalendarContract.AUTHORITY, null);
+ registerReceiver(mBroadcastReceiver, filter);
+ mIsReceiverRegistered = true;
+
+ mLoadMeetingsHandler.sendEmptyMessage(MSG_LOAD_MEETINGS);
+ }
} else {
if (mIsReceiverRegistered) {
unregisterReceiver(mBroadcastReceiver);
@@ -204,9 +258,9 @@ public class CalendarWatchFaceService extends CanvasWatchFaceService {
final Cursor cursor = getContentResolver().query(builder.build(),
null, null, null, null);
int numMeetings = cursor.getCount();
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Num meetings: " + numMeetings);
- }
+
+ Log.d(TAG, "Num meetings: " + numMeetings);
+
return numMeetings;
}
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/DigitalWatchFaceUtil.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/DigitalWatchFaceUtil.java
index 1c4af700b..e13440d23 100644
--- a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/DigitalWatchFaceUtil.java
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/DigitalWatchFaceUtil.java
@@ -165,6 +165,7 @@ public final class DigitalWatchFaceUtil {
*/
public static void putConfigDataItem(GoogleApiClient googleApiClient, DataMap newConfig) {
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(PATH_WITH_FEATURE);
+ putDataMapRequest.setUrgent();
DataMap configToPut = putDataMapRequest.getDataMap();
configToPut.putAll(newConfig);
Wearable.DataApi.putDataItem(googleApiClient, putDataMapRequest.asPutDataRequest())
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitDistanceWatchFaceService.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitDistanceWatchFaceService.java
new file mode 100644
index 000000000..b29a1902d
--- /dev/null
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitDistanceWatchFaceService.java
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.watchface;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.wearable.watchface.CanvasWatchFaceService;
+import android.support.wearable.watchface.WatchFaceStyle;
+import android.text.format.DateFormat;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.WindowInsets;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.Scopes;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Scope;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.fitness.Fitness;
+import com.google.android.gms.fitness.FitnessStatusCodes;
+import com.google.android.gms.fitness.data.DataPoint;
+import com.google.android.gms.fitness.data.DataType;
+import com.google.android.gms.fitness.data.Field;
+import com.google.android.gms.fitness.result.DailyTotalResult;
+
+import java.util.Calendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Displays the user's daily distance total via Google Fit. Distance is polled initially when the
+ * Google API Client successfully connects and once a minute after that via the onTimeTick callback.
+ * If you want more frequent updates, you will want to add your own Handler.
+ *
+ * Authentication IS a requirement to request distance from Google Fit on Wear. Otherwise, distance
+ * will always come back as zero (or stay at whatever the distance was prior to you
+ * de-authorizing watchface).
+ *
+ * In ambient mode, the seconds are replaced with an AM/PM indicator.
+ *
+ * On devices with low-bit ambient mode, the text is drawn without anti-aliasing. On devices which
+ * require burn-in protection, the hours are drawn in normal rather than bold.
+ *
+ */
+public class FitDistanceWatchFaceService extends CanvasWatchFaceService {
+
+ private static final String TAG = "DistanceWatchFace";
+
+ private static final Typeface BOLD_TYPEFACE =
+ Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
+ private static final Typeface NORMAL_TYPEFACE =
+ Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
+
+ /**
+ * Update rate in milliseconds for active mode (non-ambient).
+ */
+ private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends CanvasWatchFaceService.Engine implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ ResultCallback {
+
+ private static final int BACKGROUND_COLOR = Color.BLACK;
+ private static final int TEXT_HOURS_MINS_COLOR = Color.WHITE;
+ private static final int TEXT_SECONDS_COLOR = Color.GRAY;
+ private static final int TEXT_AM_PM_COLOR = Color.GRAY;
+ private static final int TEXT_COLON_COLOR = Color.GRAY;
+ private static final int TEXT_DISTANCE_COUNT_COLOR = Color.GRAY;
+
+ private static final String COLON_STRING = ":";
+
+ private static final int MSG_UPDATE_TIME = 0;
+
+ /* Handler to update the time periodically in interactive mode. */
+ private final Handler mUpdateTimeHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_UPDATE_TIME:
+ Log.v(TAG, "updating time");
+ invalidate();
+ if (shouldUpdateTimeHandlerBeRunning()) {
+ long timeMs = System.currentTimeMillis();
+ long delayMs =
+ ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
+ mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
+ }
+ break;
+ }
+ }
+ };
+
+ /**
+ * Handles time zone and locale changes.
+ */
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mCalendar.setTimeZone(TimeZone.getDefault());
+ invalidate();
+ }
+ };
+
+ /**
+ * Unregistering an unregistered receiver throws an exception. Keep track of the
+ * registration state to prevent that.
+ */
+ private boolean mRegisteredReceiver = false;
+
+ private Paint mHourPaint;
+ private Paint mMinutePaint;
+ private Paint mSecondPaint;
+ private Paint mAmPmPaint;
+ private Paint mColonPaint;
+ private Paint mDistanceCountPaint;
+
+ private float mColonWidth;
+
+ private Calendar mCalendar;
+
+ private float mXOffset;
+ private float mXDistanceOffset;
+ private float mYOffset;
+ private float mLineHeight;
+
+ private String mAmString;
+ private String mPmString;
+
+
+ /**
+ * Whether the display supports fewer bits for each color in ambient mode. When true, we
+ * disable anti-aliasing in ambient mode.
+ */
+ private boolean mLowBitAmbient;
+
+ /*
+ * Google API Client used to make Google Fit requests for step data.
+ */
+ private GoogleApiClient mGoogleApiClient;
+
+ private boolean mDistanceRequested;
+
+ private float mDistanceTotal = 0;
+
+ @Override
+ public void onCreate(SurfaceHolder holder) {
+ Log.d(TAG, "onCreate");
+
+ super.onCreate(holder);
+
+ mDistanceRequested = false;
+ mGoogleApiClient = new GoogleApiClient.Builder(FitDistanceWatchFaceService.this)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .addApi(Fitness.HISTORY_API)
+ .addApi(Fitness.RECORDING_API)
+ .addScope(new Scope(Scopes.FITNESS_LOCATION_READ))
+ // When user has multiple accounts, useDefaultAccount() allows Google Fit to
+ // associated with the main account for steps. It also replaces the need for
+ // a scope request.
+ .useDefaultAccount()
+ .build();
+
+ setWatchFaceStyle(new WatchFaceStyle.Builder(FitDistanceWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setShowSystemUiTime(false)
+ .build());
+
+ Resources resources = getResources();
+
+ mYOffset = resources.getDimension(R.dimen.fit_y_offset);
+ mLineHeight = resources.getDimension(R.dimen.fit_line_height);
+ mAmString = resources.getString(R.string.fit_am);
+ mPmString = resources.getString(R.string.fit_pm);
+
+ mHourPaint = createTextPaint(TEXT_HOURS_MINS_COLOR, BOLD_TYPEFACE);
+ mMinutePaint = createTextPaint(TEXT_HOURS_MINS_COLOR);
+ mSecondPaint = createTextPaint(TEXT_SECONDS_COLOR);
+ mAmPmPaint = createTextPaint(TEXT_AM_PM_COLOR);
+ mColonPaint = createTextPaint(TEXT_COLON_COLOR);
+ mDistanceCountPaint = createTextPaint(TEXT_DISTANCE_COUNT_COLOR);
+
+ mCalendar = Calendar.getInstance();
+
+ }
+
+ @Override
+ public void onDestroy() {
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ super.onDestroy();
+ }
+
+ private Paint createTextPaint(int color) {
+ return createTextPaint(color, NORMAL_TYPEFACE);
+ }
+
+ private Paint createTextPaint(int color, Typeface typeface) {
+ Paint paint = new Paint();
+ paint.setColor(color);
+ paint.setTypeface(typeface);
+ paint.setAntiAlias(true);
+ return paint;
+ }
+
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ Log.d(TAG, "onVisibilityChanged: " + visible);
+
+ super.onVisibilityChanged(visible);
+
+ if (visible) {
+ mGoogleApiClient.connect();
+
+ registerReceiver();
+
+ // Update time zone and date formats, in case they changed while we weren't visible.
+ mCalendar.setTimeZone(TimeZone.getDefault());
+ } else {
+ unregisterReceiver();
+
+ if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ // Whether the timer should be running depends on whether we're visible (as well as
+ // whether we're in ambient mode), so we may need to start or stop the timer.
+ updateTimer();
+ }
+
+
+ private void registerReceiver() {
+ if (mRegisteredReceiver) {
+ return;
+ }
+ mRegisteredReceiver = true;
+ IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+ FitDistanceWatchFaceService.this.registerReceiver(mReceiver, filter);
+ }
+
+ private void unregisterReceiver() {
+ if (!mRegisteredReceiver) {
+ return;
+ }
+ mRegisteredReceiver = false;
+ FitDistanceWatchFaceService.this.unregisterReceiver(mReceiver);
+ }
+
+ @Override
+ public void onApplyWindowInsets(WindowInsets insets) {
+ Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square"));
+
+ super.onApplyWindowInsets(insets);
+
+ // Load resources that have alternate values for round watches.
+ Resources resources = FitDistanceWatchFaceService.this.getResources();
+ boolean isRound = insets.isRound();
+ mXOffset = resources.getDimension(isRound
+ ? R.dimen.fit_x_offset_round : R.dimen.fit_x_offset);
+ mXDistanceOffset =
+ resources.getDimension(
+ isRound ?
+ R.dimen.fit_steps_or_distance_x_offset_round :
+ R.dimen.fit_steps_or_distance_x_offset);
+ float textSize = resources.getDimension(isRound
+ ? R.dimen.fit_text_size_round : R.dimen.fit_text_size);
+ float amPmSize = resources.getDimension(isRound
+ ? R.dimen.fit_am_pm_size_round : R.dimen.fit_am_pm_size);
+
+ mHourPaint.setTextSize(textSize);
+ mMinutePaint.setTextSize(textSize);
+ mSecondPaint.setTextSize(textSize);
+ mAmPmPaint.setTextSize(amPmSize);
+ mColonPaint.setTextSize(textSize);
+ mDistanceCountPaint.setTextSize(
+ resources.getDimension(R.dimen.fit_steps_or_distance_text_size));
+
+ mColonWidth = mColonPaint.measureText(COLON_STRING);
+ }
+
+ @Override
+ public void onPropertiesChanged(Bundle properties) {
+ super.onPropertiesChanged(properties);
+
+ boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
+ mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : BOLD_TYPEFACE);
+
+ mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
+
+ Log.d(TAG, "onPropertiesChanged: burn-in protection = " + burnInProtection
+ + ", low-bit ambient = " + mLowBitAmbient);
+
+ }
+
+ @Override
+ public void onTimeTick() {
+ super.onTimeTick();
+ Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
+ getTotalDistance();
+ invalidate();
+ }
+
+ @Override
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ super.onAmbientModeChanged(inAmbientMode);
+ Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
+
+ if (mLowBitAmbient) {
+ boolean antiAlias = !inAmbientMode;;
+ mHourPaint.setAntiAlias(antiAlias);
+ mMinutePaint.setAntiAlias(antiAlias);
+ mSecondPaint.setAntiAlias(antiAlias);
+ mAmPmPaint.setAntiAlias(antiAlias);
+ mColonPaint.setAntiAlias(antiAlias);
+ mDistanceCountPaint.setAntiAlias(antiAlias);
+ }
+ invalidate();
+
+ // Whether the timer should be running depends on whether we're in ambient mode (as well
+ // as whether we're visible), so we may need to start or stop the timer.
+ updateTimer();
+ }
+
+ private String formatTwoDigitNumber(int hour) {
+ return String.format("%02d", hour);
+ }
+
+ private String getAmPmString(int amPm) {
+ return amPm == Calendar.AM ? mAmString : mPmString;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas, Rect bounds) {
+ long now = System.currentTimeMillis();
+ mCalendar.setTimeInMillis(now);
+ boolean is24Hour = DateFormat.is24HourFormat(FitDistanceWatchFaceService.this);
+
+ // Draw the background.
+ canvas.drawColor(BACKGROUND_COLOR);
+
+ // Draw the hours.
+ float x = mXOffset;
+ String hourString;
+ if (is24Hour) {
+ hourString = formatTwoDigitNumber(mCalendar.get(Calendar.HOUR_OF_DAY));
+ } else {
+ int hour = mCalendar.get(Calendar.HOUR);
+ if (hour == 0) {
+ hour = 12;
+ }
+ hourString = String.valueOf(hour);
+ }
+ canvas.drawText(hourString, x, mYOffset, mHourPaint);
+ x += mHourPaint.measureText(hourString);
+
+ // Draw first colon (between hour and minute).
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+
+ x += mColonWidth;
+
+ // Draw the minutes.
+ String minuteString = formatTwoDigitNumber(mCalendar.get(Calendar.MINUTE));
+ canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
+ x += mMinutePaint.measureText(minuteString);
+
+ // In interactive mode, draw a second colon followed by the seconds.
+ // Otherwise, if we're in 12-hour mode, draw AM/PM
+ if (!isInAmbientMode()) {
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+
+ x += mColonWidth;
+ canvas.drawText(formatTwoDigitNumber(
+ mCalendar.get(Calendar.SECOND)), x, mYOffset, mSecondPaint);
+ } else if (!is24Hour) {
+ x += mColonWidth;
+ canvas.drawText(getAmPmString(
+ mCalendar.get(Calendar.AM_PM)), x, mYOffset, mAmPmPaint);
+ }
+
+ // Only render distance if there is no peek card, so they do not bleed into each other
+ // in ambient mode.
+ if (getPeekCardPosition().isEmpty()) {
+ canvas.drawText(
+ getString(R.string.fit_distance, mDistanceTotal),
+ mXDistanceOffset,
+ mYOffset + mLineHeight,
+ mDistanceCountPaint);
+ }
+ }
+
+ /**
+ * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
+ * or stops it if it shouldn't be running but currently is.
+ */
+ private void updateTimer() {
+ Log.d(TAG, "updateTimer");
+
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ if (shouldUpdateTimeHandlerBeRunning()) {
+ mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
+ }
+ }
+
+ /**
+ * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
+ * only run when we're visible and in interactive mode.
+ */
+ private boolean shouldUpdateTimeHandlerBeRunning() {
+ return isVisible() && !isInAmbientMode();
+ }
+
+ private void getTotalDistance() {
+
+ Log.d(TAG, "getTotalDistance()");
+
+ if ((mGoogleApiClient != null)
+ && (mGoogleApiClient.isConnected())
+ && (!mDistanceRequested)) {
+
+ mDistanceRequested = true;
+
+ PendingResult distanceResult =
+ Fitness.HistoryApi.readDailyTotal(
+ mGoogleApiClient,
+ DataType.TYPE_DISTANCE_DELTA);
+
+ distanceResult.setResultCallback(this);
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle connectionHint) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnected: " + connectionHint);
+
+ mDistanceRequested = false;
+
+ // Subscribe covers devices that do not have Google Fit installed.
+ subscribeToDistance();
+
+ getTotalDistance();
+ }
+
+ /*
+ * Subscribes to distance.
+ */
+ private void subscribeToDistance() {
+
+ if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnecting())) {
+
+ Fitness.RecordingApi.subscribe(mGoogleApiClient, DataType.TYPE_DISTANCE_DELTA)
+ .setResultCallback(new ResultCallback() {
+ @Override
+ public void onResult(Status status) {
+ if (status.isSuccess()) {
+ if (status.getStatusCode()
+ == FitnessStatusCodes.SUCCESS_ALREADY_SUBSCRIBED) {
+ Log.i(TAG, "Existing subscription for activity detected.");
+ } else {
+ Log.i(TAG, "Successfully subscribed!");
+ }
+ } else {
+ Log.i(TAG, "There was a problem subscribing.");
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionSuspended: " + cause);
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionFailed: " + result);
+ }
+
+ @Override
+ public void onResult(DailyTotalResult dailyTotalResult) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onResult(): " + dailyTotalResult);
+
+ mDistanceRequested = false;
+
+ if (dailyTotalResult.getStatus().isSuccess()) {
+
+ List points = dailyTotalResult.getTotal().getDataPoints();
+
+ if (!points.isEmpty()) {
+ mDistanceTotal = points.get(0).getValue(Field.FIELD_DISTANCE).asFloat();
+ Log.d(TAG, "distance updated: " + mDistanceTotal);
+ }
+ } else {
+ Log.e(TAG, "onResult() failed! " + dailyTotalResult.getStatus().getStatusMessage());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitStepsWatchFaceService.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitStepsWatchFaceService.java
new file mode 100644
index 000000000..1f7b298f2
--- /dev/null
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/FitStepsWatchFaceService.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.watchface;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.fitness.Fitness;
+import com.google.android.gms.fitness.FitnessStatusCodes;
+import com.google.android.gms.fitness.data.DataPoint;
+import com.google.android.gms.fitness.data.DataType;
+import com.google.android.gms.fitness.data.Field;
+import com.google.android.gms.fitness.result.DailyTotalResult;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.wearable.watchface.CanvasWatchFaceService;
+import android.support.wearable.watchface.WatchFaceStyle;
+import android.text.format.DateFormat;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.WindowInsets;
+
+import java.util.Calendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The step count watch face shows user's daily step total via Google Fit (matches Google Fit app).
+ * Steps are polled initially when the Google API Client successfully connects and once a minute
+ * after that via the onTimeTick callback. If you want more frequent updates, you will want to add
+ * your own Handler.
+ *
+ * Authentication is not a requirement to request steps from Google Fit on Wear.
+ *
+ * In ambient mode, the seconds are replaced with an AM/PM indicator.
+ *
+ * On devices with low-bit ambient mode, the text is drawn without anti-aliasing. On devices which
+ * require burn-in protection, the hours are drawn in normal rather than bold.
+ *
+ */
+public class FitStepsWatchFaceService extends CanvasWatchFaceService {
+
+ private static final String TAG = "StepCountWatchFace";
+
+ private static final Typeface BOLD_TYPEFACE =
+ Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
+ private static final Typeface NORMAL_TYPEFACE =
+ Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
+
+ /**
+ * Update rate in milliseconds for active mode (non-ambient).
+ */
+ private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
+
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+
+ private class Engine extends CanvasWatchFaceService.Engine implements
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener,
+ ResultCallback {
+
+ private static final int BACKGROUND_COLOR = Color.BLACK;
+ private static final int TEXT_HOURS_MINS_COLOR = Color.WHITE;
+ private static final int TEXT_SECONDS_COLOR = Color.GRAY;
+ private static final int TEXT_AM_PM_COLOR = Color.GRAY;
+ private static final int TEXT_COLON_COLOR = Color.GRAY;
+ private static final int TEXT_STEP_COUNT_COLOR = Color.GRAY;
+
+ private static final String COLON_STRING = ":";
+
+ private static final int MSG_UPDATE_TIME = 0;
+
+ /* Handler to update the time periodically in interactive mode. */
+ private final Handler mUpdateTimeHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_UPDATE_TIME:
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "updating time");
+ }
+ invalidate();
+ if (shouldUpdateTimeHandlerBeRunning()) {
+ long timeMs = System.currentTimeMillis();
+ long delayMs =
+ ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
+ mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
+ }
+ break;
+ }
+ }
+ };
+
+ /**
+ * Handles time zone and locale changes.
+ */
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mCalendar.setTimeZone(TimeZone.getDefault());
+ invalidate();
+ }
+ };
+
+ /**
+ * Unregistering an unregistered receiver throws an exception. Keep track of the
+ * registration state to prevent that.
+ */
+ private boolean mRegisteredReceiver = false;
+
+ private Paint mHourPaint;
+ private Paint mMinutePaint;
+ private Paint mSecondPaint;
+ private Paint mAmPmPaint;
+ private Paint mColonPaint;
+ private Paint mStepCountPaint;
+
+ private float mColonWidth;
+
+ private Calendar mCalendar;
+
+ private float mXOffset;
+ private float mXStepsOffset;
+ private float mYOffset;
+ private float mLineHeight;
+
+ private String mAmString;
+ private String mPmString;
+
+
+ /**
+ * Whether the display supports fewer bits for each color in ambient mode. When true, we
+ * disable anti-aliasing in ambient mode.
+ */
+ private boolean mLowBitAmbient;
+
+ /*
+ * Google API Client used to make Google Fit requests for step data.
+ */
+ private GoogleApiClient mGoogleApiClient;
+
+ private boolean mStepsRequested;
+
+ private int mStepsTotal = 0;
+
+ @Override
+ public void onCreate(SurfaceHolder holder) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate");
+ }
+
+ super.onCreate(holder);
+
+ mStepsRequested = false;
+ mGoogleApiClient = new GoogleApiClient.Builder(FitStepsWatchFaceService.this)
+ .addConnectionCallbacks(this)
+ .addOnConnectionFailedListener(this)
+ .addApi(Fitness.HISTORY_API)
+ .addApi(Fitness.RECORDING_API)
+ // When user has multiple accounts, useDefaultAccount() allows Google Fit to
+ // associated with the main account for steps. It also replaces the need for
+ // a scope request.
+ .useDefaultAccount()
+ .build();
+
+ setWatchFaceStyle(new WatchFaceStyle.Builder(FitStepsWatchFaceService.this)
+ .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
+ .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
+ .setShowSystemUiTime(false)
+ .build());
+
+ Resources resources = getResources();
+
+ mYOffset = resources.getDimension(R.dimen.fit_y_offset);
+ mLineHeight = resources.getDimension(R.dimen.fit_line_height);
+ mAmString = resources.getString(R.string.fit_am);
+ mPmString = resources.getString(R.string.fit_pm);
+
+ mHourPaint = createTextPaint(TEXT_HOURS_MINS_COLOR, BOLD_TYPEFACE);
+ mMinutePaint = createTextPaint(TEXT_HOURS_MINS_COLOR);
+ mSecondPaint = createTextPaint(TEXT_SECONDS_COLOR);
+ mAmPmPaint = createTextPaint(TEXT_AM_PM_COLOR);
+ mColonPaint = createTextPaint(TEXT_COLON_COLOR);
+ mStepCountPaint = createTextPaint(TEXT_STEP_COUNT_COLOR);
+
+ mCalendar = Calendar.getInstance();
+
+ }
+
+ @Override
+ public void onDestroy() {
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ super.onDestroy();
+ }
+
+ private Paint createTextPaint(int color) {
+ return createTextPaint(color, NORMAL_TYPEFACE);
+ }
+
+ private Paint createTextPaint(int color, Typeface typeface) {
+ Paint paint = new Paint();
+ paint.setColor(color);
+ paint.setTypeface(typeface);
+ paint.setAntiAlias(true);
+ return paint;
+ }
+
+ @Override
+ public void onVisibilityChanged(boolean visible) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onVisibilityChanged: " + visible);
+ }
+ super.onVisibilityChanged(visible);
+
+ if (visible) {
+ mGoogleApiClient.connect();
+
+ registerReceiver();
+
+ // Update time zone and date formats, in case they changed while we weren't visible.
+ mCalendar.setTimeZone(TimeZone.getDefault());
+ } else {
+ unregisterReceiver();
+
+ if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
+ mGoogleApiClient.disconnect();
+ }
+ }
+
+ // Whether the timer should be running depends on whether we're visible (as well as
+ // whether we're in ambient mode), so we may need to start or stop the timer.
+ updateTimer();
+ }
+
+
+ private void registerReceiver() {
+ if (mRegisteredReceiver) {
+ return;
+ }
+ mRegisteredReceiver = true;
+ IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+ FitStepsWatchFaceService.this.registerReceiver(mReceiver, filter);
+ }
+
+ private void unregisterReceiver() {
+ if (!mRegisteredReceiver) {
+ return;
+ }
+ mRegisteredReceiver = false;
+ FitStepsWatchFaceService.this.unregisterReceiver(mReceiver);
+ }
+
+ @Override
+ public void onApplyWindowInsets(WindowInsets insets) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square"));
+ }
+ super.onApplyWindowInsets(insets);
+
+ // Load resources that have alternate values for round watches.
+ Resources resources = FitStepsWatchFaceService.this.getResources();
+ boolean isRound = insets.isRound();
+ mXOffset = resources.getDimension(isRound
+ ? R.dimen.fit_x_offset_round : R.dimen.fit_x_offset);
+ mXStepsOffset = resources.getDimension(isRound
+ ? R.dimen.fit_steps_or_distance_x_offset_round : R.dimen.fit_steps_or_distance_x_offset);
+ float textSize = resources.getDimension(isRound
+ ? R.dimen.fit_text_size_round : R.dimen.fit_text_size);
+ float amPmSize = resources.getDimension(isRound
+ ? R.dimen.fit_am_pm_size_round : R.dimen.fit_am_pm_size);
+
+ mHourPaint.setTextSize(textSize);
+ mMinutePaint.setTextSize(textSize);
+ mSecondPaint.setTextSize(textSize);
+ mAmPmPaint.setTextSize(amPmSize);
+ mColonPaint.setTextSize(textSize);
+ mStepCountPaint.setTextSize(resources.getDimension(R.dimen.fit_steps_or_distance_text_size));
+
+ mColonWidth = mColonPaint.measureText(COLON_STRING);
+ }
+
+ @Override
+ public void onPropertiesChanged(Bundle properties) {
+ super.onPropertiesChanged(properties);
+
+ boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
+ mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : BOLD_TYPEFACE);
+
+ mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onPropertiesChanged: burn-in protection = " + burnInProtection
+ + ", low-bit ambient = " + mLowBitAmbient);
+ }
+ }
+
+ @Override
+ public void onTimeTick() {
+ super.onTimeTick();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
+ }
+
+ getTotalSteps();
+ invalidate();
+ }
+
+ @Override
+ public void onAmbientModeChanged(boolean inAmbientMode) {
+ super.onAmbientModeChanged(inAmbientMode);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
+ }
+
+ if (mLowBitAmbient) {
+ boolean antiAlias = !inAmbientMode;;
+ mHourPaint.setAntiAlias(antiAlias);
+ mMinutePaint.setAntiAlias(antiAlias);
+ mSecondPaint.setAntiAlias(antiAlias);
+ mAmPmPaint.setAntiAlias(antiAlias);
+ mColonPaint.setAntiAlias(antiAlias);
+ mStepCountPaint.setAntiAlias(antiAlias);
+ }
+ invalidate();
+
+ // Whether the timer should be running depends on whether we're in ambient mode (as well
+ // as whether we're visible), so we may need to start or stop the timer.
+ updateTimer();
+ }
+
+ private String formatTwoDigitNumber(int hour) {
+ return String.format("%02d", hour);
+ }
+
+ private String getAmPmString(int amPm) {
+ return amPm == Calendar.AM ? mAmString : mPmString;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas, Rect bounds) {
+ long now = System.currentTimeMillis();
+ mCalendar.setTimeInMillis(now);
+ boolean is24Hour = DateFormat.is24HourFormat(FitStepsWatchFaceService.this);
+
+ // Draw the background.
+ canvas.drawColor(BACKGROUND_COLOR);
+
+ // Draw the hours.
+ float x = mXOffset;
+ String hourString;
+ if (is24Hour) {
+ hourString = formatTwoDigitNumber(mCalendar.get(Calendar.HOUR_OF_DAY));
+ } else {
+ int hour = mCalendar.get(Calendar.HOUR);
+ if (hour == 0) {
+ hour = 12;
+ }
+ hourString = String.valueOf(hour);
+ }
+ canvas.drawText(hourString, x, mYOffset, mHourPaint);
+ x += mHourPaint.measureText(hourString);
+
+ // Draw first colon (between hour and minute).
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+
+ x += mColonWidth;
+
+ // Draw the minutes.
+ String minuteString = formatTwoDigitNumber(mCalendar.get(Calendar.MINUTE));
+ canvas.drawText(minuteString, x, mYOffset, mMinutePaint);
+ x += mMinutePaint.measureText(minuteString);
+
+ // In interactive mode, draw a second colon followed by the seconds.
+ // Otherwise, if we're in 12-hour mode, draw AM/PM
+ if (!isInAmbientMode()) {
+ canvas.drawText(COLON_STRING, x, mYOffset, mColonPaint);
+
+ x += mColonWidth;
+ canvas.drawText(formatTwoDigitNumber(
+ mCalendar.get(Calendar.SECOND)), x, mYOffset, mSecondPaint);
+ } else if (!is24Hour) {
+ x += mColonWidth;
+ canvas.drawText(getAmPmString(
+ mCalendar.get(Calendar.AM_PM)), x, mYOffset, mAmPmPaint);
+ }
+
+ // Only render steps if there is no peek card, so they do not bleed into each other
+ // in ambient mode.
+ if (getPeekCardPosition().isEmpty()) {
+ canvas.drawText(
+ getString(R.string.fit_steps, mStepsTotal),
+ mXStepsOffset,
+ mYOffset + mLineHeight,
+ mStepCountPaint);
+ }
+ }
+
+ /**
+ * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
+ * or stops it if it shouldn't be running but currently is.
+ */
+ private void updateTimer() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "updateTimer");
+ }
+ mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
+ if (shouldUpdateTimeHandlerBeRunning()) {
+ mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
+ }
+ }
+
+ /**
+ * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
+ * only run when we're visible and in interactive mode.
+ */
+ private boolean shouldUpdateTimeHandlerBeRunning() {
+ return isVisible() && !isInAmbientMode();
+ }
+
+ private void getTotalSteps() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "getTotalSteps()");
+ }
+
+ if ((mGoogleApiClient != null)
+ && (mGoogleApiClient.isConnected())
+ && (!mStepsRequested)) {
+
+ mStepsRequested = true;
+
+ PendingResult stepsResult =
+ Fitness.HistoryApi.readDailyTotal(
+ mGoogleApiClient,
+ DataType.TYPE_STEP_COUNT_DELTA);
+
+ stepsResult.setResultCallback(this);
+ }
+ }
+
+ @Override
+ public void onConnected(Bundle connectionHint) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnected: " + connectionHint);
+ }
+ mStepsRequested = false;
+
+ // The subscribe step covers devices that do not have Google Fit installed.
+ subscribeToSteps();
+
+ getTotalSteps();
+ }
+
+ /*
+ * Subscribes to step count (for phones that don't have Google Fit app).
+ */
+ private void subscribeToSteps() {
+ Fitness.RecordingApi.subscribe(mGoogleApiClient, DataType.TYPE_STEP_COUNT_DELTA)
+ .setResultCallback(new ResultCallback() {
+ @Override
+ public void onResult(Status status) {
+ if (status.isSuccess()) {
+ if (status.getStatusCode()
+ == FitnessStatusCodes.SUCCESS_ALREADY_SUBSCRIBED) {
+ Log.i(TAG, "Existing subscription for activity detected.");
+ } else {
+ Log.i(TAG, "Successfully subscribed!");
+ }
+ } else {
+ Log.i(TAG, "There was a problem subscribing.");
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionSuspended: " + cause);
+ }
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onConnectionFailed: " + result);
+ }
+ }
+
+ @Override
+ public void onResult(DailyTotalResult dailyTotalResult) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "mGoogleApiAndFitCallbacks.onResult(): " + dailyTotalResult);
+ }
+
+ mStepsRequested = false;
+
+ if (dailyTotalResult.getStatus().isSuccess()) {
+
+ List points = dailyTotalResult.getTotal().getDataPoints();;
+
+ if (!points.isEmpty()) {
+ mStepsTotal = points.get(0).getValue(Field.FIELD_STEPS).asInt();
+ Log.d(TAG, "steps updated: " + mStepsTotal);
+ }
+ } else {
+ Log.e(TAG, "onResult() failed! " + dailyTotalResult.getStatus().getStatusMessage());
+ }
+ }
+ }
+}
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/InteractiveWatchFaceService.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/InteractiveWatchFaceService.java
index 1a6f25b56..7a50208a1 100644
--- a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/InteractiveWatchFaceService.java
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/InteractiveWatchFaceService.java
@@ -30,7 +30,7 @@ import android.view.SurfaceHolder;
import android.view.WindowInsets;
/**
- * Demostrates interactive watch face capabilities, i.e., touching the display and registering
+ * Demonstrates interactive watch face capabilities, i.e., touching the display and registering
* three different events: touch, touch-cancel and tap. The watch face UI will show the count of
* these events as they occur. See the {@code onTapCommand} below.
*/
diff --git a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/SweepWatchFaceService.java b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/SweepWatchFaceService.java
index 0ba2ab938..a94097fe2 100644
--- a/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/SweepWatchFaceService.java
+++ b/samples/browseable/WatchFace/Wearable/src/com.example.android.wearable.watchface/SweepWatchFaceService.java
@@ -78,7 +78,7 @@ public class SweepWatchFaceService extends CanvasWatchFaceService {
/* Colors for all hands (hour, minute, seconds, ticks) based on photo loaded. */
private int mWatchHandColor;
- private int mWatchHandHightlightColor;
+ private int mWatchHandHighlightColor;
private int mWatchHandShadowColor;
private Paint mHourPaint;
@@ -123,7 +123,7 @@ public class SweepWatchFaceService extends CanvasWatchFaceService {
/* Set defaults for colors */
mWatchHandColor = Color.WHITE;
- mWatchHandHightlightColor = Color.RED;
+ mWatchHandHighlightColor = Color.RED;
mWatchHandShadowColor = Color.BLACK;
mHourPaint = new Paint();
@@ -141,7 +141,7 @@ public class SweepWatchFaceService extends CanvasWatchFaceService {
mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mSecondPaint = new Paint();
- mSecondPaint.setColor(mWatchHandHightlightColor);
+ mSecondPaint.setColor(mWatchHandHighlightColor);
mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
mSecondPaint.setAntiAlias(true);
mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
@@ -165,7 +165,7 @@ public class SweepWatchFaceService extends CanvasWatchFaceService {
Log.d(TAG, "Palette: " + palette);
}
- mWatchHandHightlightColor = palette.getVibrantColor(Color.RED);
+ mWatchHandHighlightColor = palette.getVibrantColor(Color.RED);
mWatchHandColor = palette.getLightVibrantColor(Color.WHITE);
mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK);
updateWatchHandStyle();
@@ -226,7 +226,7 @@ public class SweepWatchFaceService extends CanvasWatchFaceService {
} else {
mHourPaint.setColor(mWatchHandColor);
mMinutePaint.setColor(mWatchHandColor);
- mSecondPaint.setColor(mWatchHandHightlightColor);
+ mSecondPaint.setColor(mWatchHandHighlightColor);
mTickAndCirclePaint.setColor(mWatchHandColor);
mHourPaint.setAntiAlias(true);
diff --git a/samples/browseable/WatchFace/_index.jd b/samples/browseable/WatchFace/_index.jd
index 8b779d0cd..4367419fa 100644
--- a/samples/browseable/WatchFace/_index.jd
+++ b/samples/browseable/WatchFace/_index.jd
@@ -7,7 +7,12 @@ sample.group=Wearable
This sample demonstrates how to create watch faces for android wear and includes a phone app
and a wearable app. The wearable app has a variety of watch faces including analog, digital,
-opengl, calendar, interactive, etc. It also includes a watch-side configuration example.
+opengl, calendar, steps, interactive, etc. It also includes a watch-side configuration example.
The phone app includes a phone-side configuration example.
+
+Additional note on Steps WatchFace Sample, if the user has not installed or setup the Google Fit app
+on their phone and their Wear device has not configured the Google Fit Wear App, then you may get
+zero steps until one of the two is setup. Please note, many Wear devices configure the Google Fit
+Wear App beforehand.
+
+A sample that shows how you can record voice using the microphone on a wearable and
+play the recorded voice or an mp3 file, if the wearable device has a built-in speaker.
+
+This sample doesn't have any companion phone app so you need to install this directly
+on your watch (using "adb").
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/circle.xml b/samples/browseable/WearSpeakerSample/res/drawable/circle.xml
new file mode 100644
index 000000000..df4abe529
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/circle.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_120dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_120dp.xml
new file mode 100644
index 000000000..0971d96de
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_120dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_32dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_32dp.xml
new file mode 100644
index 000000000..70de799c6
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_audiotrack_32dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_120dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_120dp.xml
new file mode 100644
index 000000000..15e798a29
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_120dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_32dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_32dp.xml
new file mode 100644
index 000000000..c9417dd2b
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_mic_32dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_120dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_120dp.xml
new file mode 100644
index 000000000..e87660d2f
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_120dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_32dp.xml b/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_32dp.xml
new file mode 100644
index 000000000..9dd86787d
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/drawable/ic_play_arrow_32dp.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/samples/browseable/WearSpeakerSample/res/layout/main_activity.xml b/samples/browseable/WearSpeakerSample/res/layout/main_activity.xml
new file mode 100644
index 000000000..7e004ad42
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/layout/main_activity.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/browseable/WearSpeakerSample/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/WearSpeakerSample/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..cde69bccc
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/samples/browseable/WearSpeakerSample/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/WearSpeakerSample/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c133a0cbd
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/samples/browseable/WearSpeakerSample/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/WearSpeakerSample/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..bfa42f0e7
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/samples/browseable/WearSpeakerSample/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/WearSpeakerSample/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..324e72cdd
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/samples/browseable/WearSpeakerSample/res/raw/sound.mp3 b/samples/browseable/WearSpeakerSample/res/raw/sound.mp3
new file mode 100644
index 000000000..94e3d0e53
Binary files /dev/null and b/samples/browseable/WearSpeakerSample/res/raw/sound.mp3 differ
diff --git a/samples/browseable/WearSpeakerSample/res/values/colors.xml b/samples/browseable/WearSpeakerSample/res/values/colors.xml
new file mode 100644
index 000000000..e9b8605f2
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/values/colors.xml
@@ -0,0 +1,21 @@
+
+
+
+ #FFF3E0
+ #FFF3E0
+ #FF9100
+ #E65100
+ #FFD180
+ #E65100
+
\ No newline at end of file
diff --git a/samples/browseable/WearSpeakerSample/res/values/strings.xml b/samples/browseable/WearSpeakerSample/res/values/strings.xml
new file mode 100644
index 000000000..cc342b5cb
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/res/values/strings.xml
@@ -0,0 +1,18 @@
+
+
+
+ Wear Speaker Sample
+ Recording Audio permission is required, exiting now!
+ Speaker is not supported
+
diff --git a/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/MainActivity.java b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/MainActivity.java
new file mode 100644
index 000000000..e7a4870fe
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/MainActivity.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.speaker;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.wearable.activity.WearableActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * We first get the required permission to use the MIC. If it is granted, then we continue with
+ * the application and present the UI with three icons: a MIC icon (if pressed, user can record up
+ * to 10 seconds), a Play icon (if clicked, it wil playback the recorded audio file) and a music
+ * note icon (if clicked, it plays an MP3 file that is included in the app).
+ */
+public class MainActivity extends WearableActivity implements UIAnimation.UIStateListener,
+ SoundRecorder.OnVoicePlaybackStateChangedListener {
+
+ private static final String TAG = "MainActivity";
+ private static final int PERMISSIONS_REQUEST_CODE = 100;
+ private static final long COUNT_DOWN_MS = TimeUnit.SECONDS.toMillis(10);
+ private static final long MILLIS_IN_SECOND = TimeUnit.SECONDS.toMillis(1);
+ private static final String VOICE_FILE_NAME = "audiorecord.pcm";
+ private MediaPlayer mMediaPlayer;
+ private AppState mState = AppState.READY;
+ private UIAnimation.UIState mUiState = UIAnimation.UIState.HOME;
+ private SoundRecorder mSoundRecorder;
+
+ private UIAnimation mUIAnimation;
+ private ProgressBar mProgressBar;
+ private CountDownTimer mCountDownTimer;
+
+ enum AppState {
+ READY, PLAYING_VOICE, PLAYING_MUSIC, RECORDING
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+ mProgressBar = (ProgressBar) findViewById(R.id.progress);
+ mProgressBar.setMax((int) (COUNT_DOWN_MS / MILLIS_IN_SECOND));
+ setAmbientEnabled();
+ }
+
+ private void setProgressBar(long progressInMillis) {
+ mProgressBar.setProgress((int) (progressInMillis / MILLIS_IN_SECOND));
+ }
+
+ @Override
+ public void onUIStateChanged(UIAnimation.UIState state) {
+ Log.d(TAG, "UI State is: " + state);
+ if (mUiState == state) {
+ return;
+ }
+ switch (state) {
+ case MUSIC_UP:
+ mState = AppState.PLAYING_MUSIC;
+ mUiState = state;
+ playMusic();
+ break;
+ case MIC_UP:
+ mState = AppState.RECORDING;
+ mUiState = state;
+ mSoundRecorder.startRecording();
+ setProgressBar(COUNT_DOWN_MS);
+ mCountDownTimer = new CountDownTimer(COUNT_DOWN_MS, MILLIS_IN_SECOND) {
+ @Override
+ public void onTick(long millisUntilFinished) {
+ mProgressBar.setVisibility(View.VISIBLE);
+ setProgressBar(millisUntilFinished);
+ Log.d(TAG, "Time Left: " + millisUntilFinished / MILLIS_IN_SECOND);
+ }
+
+ @Override
+ public void onFinish() {
+ mProgressBar.setProgress(0);
+ mProgressBar.setVisibility(View.INVISIBLE);
+ mSoundRecorder.stopRecording();
+ mUIAnimation.transitionToHome();
+ mUiState = UIAnimation.UIState.HOME;
+ mState = AppState.READY;
+ mCountDownTimer = null;
+ }
+ };
+ mCountDownTimer.start();
+ break;
+ case SOUND_UP:
+ mState = AppState.PLAYING_VOICE;
+ mUiState = state;
+ mSoundRecorder.startPlay();
+ break;
+ case HOME:
+ switch (mState) {
+ case PLAYING_MUSIC:
+ mState = AppState.READY;
+ mUiState = state;
+ stopMusic();
+ break;
+ case PLAYING_VOICE:
+ mState = AppState.READY;
+ mUiState = state;
+ mSoundRecorder.stopPlaying();
+ break;
+ case RECORDING:
+ mState = AppState.READY;
+ mUiState = state;
+ mSoundRecorder.stopRecording();
+ if (mCountDownTimer != null) {
+ mCountDownTimer.cancel();
+ mCountDownTimer = null;
+ }
+ mProgressBar.setVisibility(View.INVISIBLE);
+ setProgressBar(COUNT_DOWN_MS);
+ break;
+ }
+ break;
+ }
+ }
+
+ /**
+ * Plays back the MP3 file embedded in the application
+ */
+ private void playMusic() {
+ if (mMediaPlayer == null) {
+ mMediaPlayer = MediaPlayer.create(this, R.raw.sound);
+ mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ // we need to transition to the READY/Home state
+ Log.d(TAG, "Music Finished");
+ mUIAnimation.transitionToHome();
+ }
+ });
+ }
+ mMediaPlayer.start();
+ }
+
+ /**
+ * Stops the playback of the MP3 file.
+ */
+ private void stopMusic() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+ }
+
+ /**
+ * Checks the permission that this app needs and if it has not been granted, it will
+ * prompt the user to grant it, otherwise it shuts down the app.
+ */
+ private void checkPermissions() {
+ boolean recordAudioPermissionGranted =
+ ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (recordAudioPermissionGranted) {
+ start();
+ } else {
+ ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.RECORD_AUDIO},
+ PERMISSIONS_REQUEST_CODE);
+ }
+
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ String permissions[], int[] grantResults) {
+ if (requestCode == PERMISSIONS_REQUEST_CODE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ start();
+ } else {
+ // Permission has been denied before. At this point we should show a dialog to
+ // user and explain why this permission is needed and direct him to go to the
+ // Permissions settings for the app in the System settings. For this sample, we
+ // simply exit to get to the important part.
+ Toast.makeText(this, R.string.exiting_for_permissions, Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }
+ }
+
+ /**
+ * Starts the main flow of the application.
+ */
+ private void start() {
+ mSoundRecorder = new SoundRecorder(this, VOICE_FILE_NAME, this);
+ int[] thumbResources = new int[] {R.id.mic, R.id.play, R.id.music};
+ ImageView[] thumbs = new ImageView[3];
+ for(int i=0; i < 3; i++) {
+ thumbs[i] = (ImageView) findViewById(thumbResources[i]);
+ }
+ View containerView = findViewById(R.id.container);
+ ImageView expandedView = (ImageView) findViewById(R.id.expanded);
+ int animationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
+ mUIAnimation = new UIAnimation(containerView, thumbs, expandedView, animationDuration,
+ this);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (speakerIsSupported()) {
+ checkPermissions();
+ } else {
+ findViewById(R.id.container2).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Toast.makeText(MainActivity.this, R.string.no_speaker_supported,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (mSoundRecorder != null) {
+ mSoundRecorder.cleanup();
+ mSoundRecorder = null;
+ }
+ if (mCountDownTimer != null) {
+ mCountDownTimer.cancel();
+ }
+
+ if (mMediaPlayer != null) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onPlaybackStopped() {
+ mUIAnimation.transitionToHome();
+ mUiState = UIAnimation.UIState.HOME;
+ mState = AppState.READY;
+ }
+
+ /**
+ * Determines if the wear device has a built-in speaker and if it is supported. Speaker, even if
+ * physically present, is only supported in Android M+ on a wear device..
+ */
+ public final boolean speakerIsSupported() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PackageManager packageManager = getPackageManager();
+ // The results from AudioManager.getDevices can't be trusted unless the device
+ // advertises FEATURE_AUDIO_OUTPUT.
+ if (!packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) {
+ return false;
+ }
+ AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+ for (AudioDeviceInfo device : devices) {
+ if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/SoundRecorder.java b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/SoundRecorder.java
new file mode 100644
index 000000000..a45bdd27c
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/SoundRecorder.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.speaker;
+
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioTrack;
+import android.media.MediaRecorder;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * A helper class to provide methods to record audio input from the MIC to the internal storage
+ * and to playback the same recorded audio file.
+ */
+public class SoundRecorder {
+
+ private static final String TAG = "SoundRecorder";
+ private static final int RECORDING_RATE = 8000; // can go up to 44K, if needed
+ private static final int CHANNEL_IN = AudioFormat.CHANNEL_IN_MONO;
+ private static final int CHANNELS_OUT = AudioFormat.CHANNEL_OUT_MONO;
+ private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
+ private static int BUFFER_SIZE = AudioRecord
+ .getMinBufferSize(RECORDING_RATE, CHANNEL_IN, FORMAT);
+
+ private final String mOutputFileName;
+ private final AudioManager mAudioManager;
+ private final Handler mHandler;
+ private final Context mContext;
+ private State mState = State.IDLE;
+
+ private OnVoicePlaybackStateChangedListener mListener;
+ private AsyncTask mRecordingAsyncTask;
+ private AsyncTask mPlayingAsyncTask;
+
+ enum State {
+ IDLE, RECORDING, PLAYING
+ }
+
+ public SoundRecorder(Context context, String outputFileName,
+ OnVoicePlaybackStateChangedListener listener) {
+ mOutputFileName = outputFileName;
+ mListener = listener;
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mHandler = new Handler(Looper.getMainLooper());
+ mContext = context;
+ }
+
+ /**
+ * Starts recording from the MIC.
+ */
+ public void startRecording() {
+ if (mState != State.IDLE) {
+ Log.w(TAG, "Requesting to start recording while state was not IDLE");
+ return;
+ }
+
+ mRecordingAsyncTask = new AsyncTask() {
+
+ private AudioRecord mAudioRecord;
+
+ @Override
+ protected void onPreExecute() {
+ mState = State.RECORDING;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
+ RECORDING_RATE, CHANNEL_IN, FORMAT, BUFFER_SIZE * 3);
+ BufferedOutputStream bufferedOutputStream = null;
+ try {
+ bufferedOutputStream = new BufferedOutputStream(
+ mContext.openFileOutput(mOutputFileName, Context.MODE_PRIVATE));
+ byte[] buffer = new byte[BUFFER_SIZE];
+ mAudioRecord.startRecording();
+ while (!isCancelled()) {
+ int read = mAudioRecord.read(buffer, 0, buffer.length);
+ bufferedOutputStream.write(buffer, 0, read);
+ }
+ } catch (IOException | NullPointerException | IndexOutOfBoundsException e) {
+ Log.e(TAG, "Failed to record data: " + e);
+ } finally {
+ if (bufferedOutputStream != null) {
+ try {
+ bufferedOutputStream.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ mAudioRecord.release();
+ mAudioRecord = null;
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ mState = State.IDLE;
+ mRecordingAsyncTask = null;
+ }
+
+ @Override
+ protected void onCancelled() {
+ if (mState == State.RECORDING) {
+ Log.d(TAG, "Stopping the recording ...");
+ mState = State.IDLE;
+ } else {
+ Log.w(TAG, "Requesting to stop recording while state was not RECORDING");
+ }
+ mRecordingAsyncTask = null;
+ }
+ };
+
+ mRecordingAsyncTask.execute();
+ }
+
+ public void stopRecording() {
+ if (mRecordingAsyncTask != null) {
+ mRecordingAsyncTask.cancel(true);
+ }
+ }
+
+ public void stopPlaying() {
+ if (mPlayingAsyncTask != null) {
+ mPlayingAsyncTask.cancel(true);
+ }
+ }
+
+ /**
+ * Starts playback of the recorded audio file.
+ */
+ public void startPlay() {
+ if (mState != State.IDLE) {
+ Log.w(TAG, "Requesting to play while state was not IDLE");
+ return;
+ }
+
+ if (!new File(mContext.getFilesDir(), mOutputFileName).exists()) {
+ // there is no recording to play
+ if (mListener != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onPlaybackStopped();
+ }
+ });
+ }
+ return;
+ }
+ final int intSize = AudioTrack.getMinBufferSize(RECORDING_RATE, CHANNELS_OUT, FORMAT);
+
+ mPlayingAsyncTask = new AsyncTask() {
+
+ private AudioTrack mAudioTrack;
+
+ @Override
+ protected void onPreExecute() {
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
+ mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC), 0 /* flags */);
+ mState = State.PLAYING;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, RECORDING_RATE,
+ CHANNELS_OUT, FORMAT, intSize, AudioTrack.MODE_STREAM);
+ byte[] buffer = new byte[intSize * 2];
+ FileInputStream in = null;
+ BufferedInputStream bis = null;
+ mAudioTrack.setVolume(AudioTrack.getMaxVolume());
+ mAudioTrack.play();
+ try {
+ in = mContext.openFileInput(mOutputFileName);
+ bis = new BufferedInputStream(in);
+ int read;
+ while (!isCancelled() && (read = bis.read(buffer, 0, buffer.length)) > 0) {
+ mAudioTrack.write(buffer, 0, read);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read the sound file into a byte array", e);
+ } finally {
+ try {
+ if (in != null) {
+ in.close();
+ }
+ if (bis != null) {
+ bis.close();
+ }
+ } catch (IOException e) { /* ignore */}
+
+ mAudioTrack.release();
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Failed to start playback", e);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ cleanup();
+ }
+
+ @Override
+ protected void onCancelled() {
+ cleanup();
+ }
+
+ private void cleanup() {
+ if (mListener != null) {
+ mListener.onPlaybackStopped();
+ }
+ mState = State.IDLE;
+ mPlayingAsyncTask = null;
+ }
+ };
+
+ mPlayingAsyncTask.execute();
+ }
+
+ public interface OnVoicePlaybackStateChangedListener {
+
+ /**
+ * Called when the playback of the audio file ends. This should be called on the UI thread.
+ */
+ void onPlaybackStopped();
+ }
+
+ /**
+ * Cleans up some resources related to {@link AudioTrack} and {@link AudioRecord}
+ */
+ public void cleanup() {
+ Log.d(TAG, "cleanup() is called");
+ stopPlaying();
+ stopRecording();
+ }
+}
diff --git a/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/UIAnimation.java b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/UIAnimation.java
new file mode 100644
index 000000000..7ce2fd534
--- /dev/null
+++ b/samples/browseable/WearSpeakerSample/src/com.example.android.wearable.speaker/UIAnimation.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.wearable.speaker;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+
+/**
+ * A helper class to provide a simple animation when user selects any of the three icons on the
+ * main UI.
+ */
+public class UIAnimation {
+
+ private AnimatorSet mCurrentAnimator;
+ private final int[] mLargeDrawables = new int[]{R.drawable.ic_mic_120dp,
+ R.drawable.ic_play_arrow_120dp, R.drawable.ic_audiotrack_120dp};
+ private final ImageView[] mThumbs;
+ private ImageView expandedImageView;
+ private final View mContainerView;
+ private final int mAnimationDurationTime;
+
+ private UIStateListener mListener;
+ private UIState mState = UIState.HOME;
+
+ public UIAnimation(View containerView, ImageView[] thumbs, ImageView expandedView,
+ int animationDuration, UIStateListener listener) {
+ mContainerView = containerView;
+ mThumbs = thumbs;
+ expandedImageView = expandedView;
+ mAnimationDurationTime = animationDuration;
+ mListener = listener;
+
+ mThumbs[0].setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ zoomImageFromThumb(0);
+ }
+ });
+
+ mThumbs[1].setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ zoomImageFromThumb(1);
+ }
+ });
+
+ mThumbs[2].setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ zoomImageFromThumb(2);
+ }
+ });
+ }
+
+ private void zoomImageFromThumb(final int index) {
+ int imageResId = mLargeDrawables[index];
+ final ImageView thumbView = mThumbs[index];
+ if (mCurrentAnimator != null) {
+ return;
+ }
+
+ expandedImageView.setImageResource(imageResId);
+
+ final Rect startBounds = new Rect();
+ final Rect finalBounds = new Rect();
+ final Point globalOffset = new Point();
+ thumbView.getGlobalVisibleRect(startBounds);
+ mContainerView.getGlobalVisibleRect(finalBounds, globalOffset);
+ startBounds.offset(-globalOffset.x, -globalOffset.y);
+ finalBounds.offset(-globalOffset.x, -globalOffset.y);
+ float startScale;
+ if ((float) finalBounds.width() / finalBounds.height()
+ > (float) startBounds.width() / startBounds.height()) {
+ startScale = (float) startBounds.height() / finalBounds.height();
+ float startWidth = startScale * finalBounds.width();
+ float deltaWidth = (startWidth - startBounds.width()) / 2;
+ startBounds.left -= deltaWidth;
+ startBounds.right += deltaWidth;
+ } else {
+ startScale = (float) startBounds.width() / finalBounds.width();
+ float startHeight = startScale * finalBounds.height();
+ float deltaHeight = (startHeight - startBounds.height()) / 2;
+ startBounds.top -= deltaHeight;
+ startBounds.bottom += deltaHeight;
+ }
+
+ for(int k=0; k < 3; k++) {
+ mThumbs[k].setAlpha(0f);
+ }
+ expandedImageView.setVisibility(View.VISIBLE);
+
+ expandedImageView.setPivotX(0f);
+ expandedImageView.setPivotY(0f);
+
+ AnimatorSet zommInAnimator = new AnimatorSet();
+ zommInAnimator.play(ObjectAnimator
+ .ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left)).with(
+ ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds
+ .top)).with(
+ ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
+ .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
+ zommInAnimator.setDuration(mAnimationDurationTime);
+ zommInAnimator.setInterpolator(new DecelerateInterpolator());
+ zommInAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentAnimator = null;
+ if (mListener != null) {
+ mState = UIState.getUIState(index);
+ mListener.onUIStateChanged(mState);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCurrentAnimator = null;
+ }
+ });
+ zommInAnimator.start();
+ mCurrentAnimator = zommInAnimator;
+
+ final float startScaleFinal = startScale;
+ expandedImageView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mCurrentAnimator != null) {
+ return;
+ }
+ AnimatorSet zoomOutAnimator = new AnimatorSet();
+ zoomOutAnimator.play(ObjectAnimator
+ .ofFloat(expandedImageView, View.X, startBounds.left))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.Y, startBounds.top))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.SCALE_X, startScaleFinal))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.SCALE_Y, startScaleFinal));
+ zoomOutAnimator.setDuration(mAnimationDurationTime);
+ zoomOutAnimator.setInterpolator(new DecelerateInterpolator());
+ zoomOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ for (int k = 0; k < 3; k++) {
+ mThumbs[k].setAlpha(1f);
+ }
+ expandedImageView.setVisibility(View.GONE);
+ mCurrentAnimator = null;
+ if (mListener != null) {
+ mState = UIState.HOME;
+ mListener.onUIStateChanged(mState);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ thumbView.setAlpha(1f);
+ expandedImageView.setVisibility(View.GONE);
+ mCurrentAnimator = null;
+ }
+ });
+ zoomOutAnimator.start();
+ mCurrentAnimator = zoomOutAnimator;
+ }
+ });
+ }
+
+ public enum UIState {
+ MIC_UP(0), SOUND_UP(1), MUSIC_UP(2), HOME(3);
+ private int mState;
+
+ UIState(int state) {
+ mState = state;
+ }
+
+ static UIState getUIState(int state) {
+ for(UIState uiState : values()) {
+ if (uiState.mState == state) {
+ return uiState;
+ }
+ }
+ return null;
+ }
+ }
+
+ public interface UIStateListener {
+ void onUIStateChanged(UIState state);
+ }
+
+ public void transitionToHome() {
+ if (mState == UIState.HOME) {
+ return;
+ }
+ expandedImageView.callOnClick();
+
+ }
+}
diff --git a/samples/browseable/XYZTouristAttractions/Application/AndroidManifest.xml b/samples/browseable/XYZTouristAttractions/Application/AndroidManifest.xml
index 76f0198d9..9d88b3981 100644
--- a/samples/browseable/XYZTouristAttractions/Application/AndroidManifest.xml
+++ b/samples/browseable/XYZTouristAttractions/Application/AndroidManifest.xml
@@ -16,17 +16,21 @@
-->
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.android.xyztouristattractions">
+
+
+ android:theme="@style/XYZAppTheme"
+ android:fullBackupContent="true">
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/samples/browseable/XYZTouristAttractions/Application/res/layout/fragment_main.xml b/samples/browseable/XYZTouristAttractions/Application/res/layout/fragment_main.xml
index 94d110ab6..c92a2a819 100644
--- a/samples/browseable/XYZTouristAttractions/Application/res/layout/fragment_main.xml
+++ b/samples/browseable/XYZTouristAttractions/Application/res/layout/fragment_main.xml
@@ -16,6 +16,7 @@
+ android:scrollbars="vertical"
+ app:layoutManager="GridLayoutManager"
+ app:spanCount="@integer/list_columns" />
-
+
diff --git a/samples/browseable/XYZTouristAttractions/Application/res/values-v21/base-template-styles.xml b/samples/browseable/XYZTouristAttractions/Application/res/values-v21/base-template-styles.xml
index d838f9829..c778e4f98 100644
--- a/samples/browseable/XYZTouristAttractions/Application/res/values-v21/base-template-styles.xml
+++ b/samples/browseable/XYZTouristAttractions/Application/res/values-v21/base-template-styles.xml
@@ -18,7 +18,7 @@
-
diff --git a/samples/browseable/XYZTouristAttractions/Application/res/values/strings.xml b/samples/browseable/XYZTouristAttractions/Application/res/values/strings.xml
index 5f3ee145c..fede01e8c 100644
--- a/samples/browseable/XYZTouristAttractions/Application/res/values/strings.xml
+++ b/samples/browseable/XYZTouristAttractions/Application/res/values/strings.xml
@@ -45,5 +45,7 @@ appear further down your stream
Toggle Geofence TriggerShow on Map
+ Allow this app to use your location to show distance to attractions?
+ Let\'s do it!
diff --git a/samples/browseable/XYZTouristAttractions/Application/res/values/template-styles.xml b/samples/browseable/XYZTouristAttractions/Application/res/values/template-styles.xml
index 74086d293..6e7d593dd 100644
--- a/samples/browseable/XYZTouristAttractions/Application/res/values/template-styles.xml
+++ b/samples/browseable/XYZTouristAttractions/Application/res/values/template-styles.xml
@@ -18,7 +18,7 @@
-
+
diff --git a/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/provider/TouristAttractions.java b/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/provider/TouristAttractions.java
index 50be36268..62ddbf9a3 100644
--- a/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/provider/TouristAttractions.java
+++ b/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/provider/TouristAttractions.java
@@ -18,7 +18,6 @@ package com.example.android.xyztouristattractions.provider;
import android.net.Uri;
-import com.example.android.xyztouristattractions.BuildConfig;
import com.example.android.xyztouristattractions.common.Attraction;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.maps.model.LatLng;
@@ -126,11 +125,8 @@ public class TouristAttractions {
public static String getClosestCity(LatLng curLatLng) {
if (curLatLng == null) {
- // In debug build still return a city so some data is displayed
- if (BuildConfig.DEBUG) {
- return TEST_CITY;
- }
- return null;
+ // If location is unknown return test city so some data is shown
+ return TEST_CITY;
}
double minDistance = 0;
diff --git a/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/service/UtilityService.java b/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/service/UtilityService.java
index 4112a6565..3122d567d 100644
--- a/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/service/UtilityService.java
+++ b/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/service/UtilityService.java
@@ -150,6 +150,11 @@ public class UtilityService extends IntentService {
*/
private void addGeofencesInternal() {
Log.v(TAG, ACTION_ADD_GEOFENCES);
+
+ if (!Utils.checkFineLocationPermission(this)) {
+ return;
+ }
+
GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.build();
@@ -202,6 +207,11 @@ public class UtilityService extends IntentService {
*/
private void requestLocationInternal() {
Log.v(TAG, ACTION_REQUEST_LOCATION);
+
+ if (!Utils.checkFineLocationPermission(this)) {
+ return;
+ }
+
GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.build();
@@ -358,7 +368,7 @@ public class UtilityService extends IntentService {
.setSmallIcon(R.drawable.ic_stat_maps_pin_drop)
.setContentIntent(pendingIntent)
.setDeleteIntent(deletePendingIntent)
- .setColor(getResources().getColor(R.color.colorPrimary))
+ .setColor(getResources().getColor(R.color.colorPrimary, getTheme()))
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setAutoCancel(true);
@@ -466,6 +476,7 @@ public class UtilityService extends IntentService {
dataMap.getDataMap().putDataMapArrayList(Constants.EXTRA_ATTRACTIONS, attractionsData);
dataMap.getDataMap().putLong(Constants.EXTRA_TIMESTAMP, new Date().getTime());
PutDataRequest request = dataMap.asPutDataRequest();
+ request.setUrgent();
// Send the data over
DataApi.DataItemResult result =
diff --git a/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/ui/AttractionListActivity.java b/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/ui/AttractionListActivity.java
index 8d2908c2a..8c23f3dda 100644
--- a/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/ui/AttractionListActivity.java
+++ b/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/ui/AttractionListActivity.java
@@ -16,11 +16,16 @@
package com.example.android.xyztouristattractions.ui;
+import android.Manifest;
+import android.content.pm.PackageManager;
import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
+import android.view.View;
import android.widget.Toast;
import com.example.android.xyztouristattractions.R;
@@ -31,7 +36,10 @@ import com.example.android.xyztouristattractions.service.UtilityService;
* The main tourist attraction activity screen which contains a list of
* attractions sorted by distance.
*/
-public class AttractionListActivity extends AppCompatActivity {
+public class AttractionListActivity extends AppCompatActivity implements
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final int PERMISSION_REQ = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -44,7 +52,23 @@ public class AttractionListActivity extends AppCompatActivity {
.commit();
}
- UtilityService.addGeofences(this);
+ // Check fine location permission has been granted
+ if (!Utils.checkFineLocationPermission(this)) {
+ // See if user has denied permission in the past
+ if (ActivityCompat.shouldShowRequestPermissionRationale(
+ this, Manifest.permission.ACCESS_FINE_LOCATION)) {
+ // Show a simple snackbar explaining the request instead
+ showPermissionSnackbar();
+ } else {
+ // Otherwise request permission from user
+ if (savedInstanceState == null) {
+ requestFineLocationPermission();
+ }
+ }
+ } else {
+ // Otherwise permission is granted (which is always the case on pre-M devices)
+ fineLocationPermissionGranted();
+ }
}
@Override
@@ -87,6 +111,51 @@ public class AttractionListActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
+ /**
+ * Permissions request result callback
+ */
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ switch (requestCode) {
+ case PERMISSION_REQ:
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ fineLocationPermissionGranted();
+ }
+ }
+ }
+
+ /**
+ * Request the fine location permission from the user
+ */
+ private void requestFineLocationPermission() {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQ);
+ }
+
+ /**
+ * Run when fine location permission has been granted
+ */
+ private void fineLocationPermissionGranted() {
+ UtilityService.addGeofences(this);
+ UtilityService.requestLocation(this);
+ }
+
+ /**
+ * Show a permission explanation snackbar
+ */
+ private void showPermissionSnackbar() {
+ Snackbar.make(
+ findViewById(R.id.container), R.string.permission_explanation, Snackbar.LENGTH_LONG)
+ .setAction(R.string.permission_explanation_action, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ requestFineLocationPermission();
+ }
+ })
+ .show();
+ }
+
/**
* Show a basic debug dialog to provide more info on the built-in debug
* options.
diff --git a/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/ui/AttractionListFragment.java b/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/ui/AttractionListFragment.java
index 28f912743..71439b14c 100644
--- a/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/ui/AttractionListFragment.java
+++ b/samples/browseable/XYZTouristAttractions/Application/src/com.example.android.xyztouristattractions/ui/AttractionListFragment.java
@@ -23,7 +23,6 @@ import android.location.Location;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
-import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -60,6 +59,7 @@ public class AttractionListFragment extends Fragment {
private AttractionAdapter mAdapter;
private LatLng mLatestLocation;
private int mImageSize;
+ private boolean mItemClicked;
public AttractionListFragment() {}
@@ -79,8 +79,6 @@ public class AttractionListFragment extends Fragment {
(AttractionsRecyclerView) view.findViewById(android.R.id.list);
recyclerView.setEmptyView(view.findViewById(android.R.id.empty));
recyclerView.setHasFixedSize(true);
- recyclerView.setLayoutManager(new GridLayoutManager(
- getActivity(), getResources().getInteger(R.integer.list_columns)));
recyclerView.setAdapter(mAdapter);
return view;
@@ -89,6 +87,7 @@ public class AttractionListFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
+ mItemClicked = false;
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
mBroadcastReceiver, UtilityService.getLocationUpdatedIntentFilter());
}
@@ -189,9 +188,12 @@ public class AttractionListFragment extends Fragment {
@Override
public void onItemClick(View view, int position) {
- View heroView = view.findViewById(android.R.id.icon);
- DetailActivity.launch(
- getActivity(), mAdapter.mAttractionList.get(position).name, heroView);
+ if (!mItemClicked) {
+ mItemClicked = true;
+ View heroView = view.findViewById(android.R.id.icon);
+ DetailActivity.launch(
+ getActivity(), mAdapter.mAttractionList.get(position).name, heroView);
+ }
}
}
diff --git a/samples/browseable/XYZTouristAttractions/Shared/src/com.example.android.xyztouristattractions.common/Utils.java b/samples/browseable/XYZTouristAttractions/Shared/src/com.example.android.xyztouristattractions.common/Utils.java
index 70e05bf28..6fa512990 100644
--- a/samples/browseable/XYZTouristAttractions/Shared/src/com.example.android.xyztouristattractions.common/Utils.java
+++ b/samples/browseable/XYZTouristAttractions/Shared/src/com.example.android.xyztouristattractions.common/Utils.java
@@ -16,13 +16,16 @@
package com.example.android.xyztouristattractions.common;
+import android.Manifest;
import android.content.Context;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.graphics.Rect;
import android.preference.PreferenceManager;
+import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.Display;
@@ -53,6 +56,15 @@ public class Utils {
private static final String DISTANCE_KM_POSTFIX = "km";
private static final String DISTANCE_M_POSTFIX = "m";
+ /**
+ * Check if the app has access to fine location permission. On pre-M
+ * devices this will always return true.
+ */
+ public static boolean checkFineLocationPermission(Context context) {
+ return PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(
+ context, Manifest.permission.ACCESS_FINE_LOCATION);
+ }
+
/**
* Calculate distance between two LatLng points and format it nicely for
* display. As this is a sample, it only statically supports metric units.
@@ -90,6 +102,10 @@ public class Utils {
* Fetch the location from app preferences.
*/
public static LatLng getLocation(Context context) {
+ if (!checkFineLocationPermission(context)) {
+ return null;
+ }
+
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
Long lat = prefs.getLong(PREFERENCES_LAT, Long.MAX_VALUE);
Long lng = prefs.getLong(PREFERENCES_LNG, Long.MAX_VALUE);
diff --git a/samples/browseable/XYZTouristAttractions/Wearable/AndroidManifest.xml b/samples/browseable/XYZTouristAttractions/Wearable/AndroidManifest.xml
index 80d0c920f..d353b2906 100644
--- a/samples/browseable/XYZTouristAttractions/Wearable/AndroidManifest.xml
+++ b/samples/browseable/XYZTouristAttractions/Wearable/AndroidManifest.xml
@@ -16,10 +16,14 @@
-->
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.android.xyztouristattractions">
+
+
+
diff --git a/samples/browseable/XYZTouristAttractions/Wearable/res/layout/gridpager_action.xml b/samples/browseable/XYZTouristAttractions/Wearable/res/layout/gridpager_action.xml
index ac01509fd..45495ad6a 100644
--- a/samples/browseable/XYZTouristAttractions/Wearable/res/layout/gridpager_action.xml
+++ b/samples/browseable/XYZTouristAttractions/Wearable/res/layout/gridpager_action.xml
@@ -26,4 +26,4 @@
android:text="@string/action_open"
android:maxLines="1"
android:color="@color/colorPrimary"
- app:rippleColor="@color/colorAccent" />
+ app:buttonRippleColor="@color/colorAccent" />
diff --git a/samples/browseable/XYZTouristAttractions/Wearable/src/com.example.android.xyztouristattractions/service/ListenerService.java b/samples/browseable/XYZTouristAttractions/Wearable/src/com.example.android.xyztouristattractions/service/ListenerService.java
index d2282511d..ea071f053 100644
--- a/samples/browseable/XYZTouristAttractions/Wearable/src/com.example.android.xyztouristattractions/service/ListenerService.java
+++ b/samples/browseable/XYZTouristAttractions/Wearable/src/com.example.android.xyztouristattractions/service/ListenerService.java
@@ -17,11 +17,12 @@
package com.example.android.xyztouristattractions.service;
import android.app.Notification;
-import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import com.example.android.xyztouristattractions.R;
@@ -39,7 +40,6 @@ import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;
import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@@ -106,21 +106,20 @@ public class ListenerService extends WearableListenerService {
PendingIntent deletePendingIntent = PendingIntent.getService(
this, 0, UtilityService.getClearRemoteNotificationsIntent(this), 0);
- Notification notification = new Notification.Builder(this)
+ Notification notification = new NotificationCompat.Builder(this)
.setContentText(getResources().getQuantityString(
R.plurals.attractions_found, count, count))
.setSmallIcon(R.mipmap.ic_launcher)
.setDeleteIntent(deletePendingIntent)
- .addAction(R.drawable.ic_full_explore,
+ .addAction(new NotificationCompat.Action.Builder(R.drawable.ic_full_explore,
getString(R.string.action_explore),
- pendingIntent)
- .extend(new Notification.WearableExtender()
+ pendingIntent).build())
+ .extend(new NotificationCompat.WearableExtender()
.setBackground(bitmap)
)
.build();
- NotificationManager notificationManager =
- (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(Constants.WEAR_NOTIFICATION_ID, notification);
googleApiClient.disconnect();
diff --git a/samples/samples_source.prop_template b/samples/samples_source.prop_template
index 3d4fac528..1524e33b6 100644
--- a/samples/samples_source.prop_template
+++ b/samples/samples_source.prop_template
@@ -1,4 +1,4 @@
Pkg.UserSrc=false
-Pkg.Revision=3
+Pkg.Revision=5
AndroidVersion.ApiLevel=${PLATFORM_SDK_VERSION}
AndroidVersion.CodeName=${PLATFORM_VERSION_CODENAME}