Update prebuilts for mnc-docs

am: 7e9f0673a1

Change-Id: I8d28c2cda1f5a39251f508608fec6807cb115a77
This commit is contained in:
Trevor Johns
2016-06-15 01:33:52 +00:00
committed by android-build-merger
72 changed files with 2033 additions and 680 deletions

View File

@@ -41,8 +41,6 @@ import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.DataItem; import com.google.android.gms.wearable.DataItem;
import com.google.android.gms.wearable.DataItemBuffer; import com.google.android.gms.wearable.DataItemBuffer;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable; import com.google.android.gms.wearable.Wearable;
/** /**
@@ -51,7 +49,6 @@ import com.google.android.gms.wearable.Wearable;
* permissions as well. * permissions as well.
*/ */
public class MainActivity extends AppCompatActivity implements public class MainActivity extends AppCompatActivity implements
NodeApi.NodeListener,
ConnectionCallbacks, ConnectionCallbacks,
OnConnectionFailedListener, OnConnectionFailedListener,
ActivityCompat.OnRequestPermissionsResultCallback { ActivityCompat.OnRequestPermissionsResultCallback {
@@ -66,11 +63,11 @@ public class MainActivity extends AppCompatActivity implements
private GoogleApiClient mGoogleApiClient; private GoogleApiClient mGoogleApiClient;
private boolean mResolvingError = false; private boolean mResolvingError = false;
private TextView mLogTextView;
ScrollView mScroller;
private View mLayout; private View mLayout;
private TextView mLogTextView;
private ScrollView mScroller;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -98,10 +95,10 @@ public class MainActivity extends AppCompatActivity implements
@Override @Override
protected void onStop() { protected void onStop() {
if (mGoogleApiClient.isConnected()) { if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
Wearable.NodeApi.removeListener(mGoogleApiClient, this); mGoogleApiClient.disconnect();
} }
mGoogleApiClient.disconnect();
super.onStop(); super.onStop();
} }
@@ -132,10 +129,6 @@ public class MainActivity extends AppCompatActivity implements
} }
private void pushCalendarToWear() {
startService(new Intent(this, CalendarQueryService.class));
}
/* /*
* Requests Calendar and Contact permissions. * Requests Calendar and Contact permissions.
* If the permission has been denied previously, a SnackBar will prompt the user to grant the * If the permission has been denied previously, a SnackBar will prompt the user to grant the
@@ -190,7 +183,9 @@ public class MainActivity extends AppCompatActivity implements
// END_INCLUDE(calendar_and_contact_permissions_request) // END_INCLUDE(calendar_and_contact_permissions_request)
} }
private void pushCalendarToWear() {
startService(new Intent(this, CalendarQueryService.class));
}
public void onDeleteEventsClicked(View view) { public void onDeleteEventsClicked(View view) {
if (mGoogleApiClient.isConnected()) { if (mGoogleApiClient.isConnected()) {
@@ -245,28 +240,19 @@ public class MainActivity extends AppCompatActivity implements
} }
} }
@Override
public void onPeerConnected(Node peer) {
appendLog("Device connected");
}
@Override
public void onPeerDisconnected(Node peer) {
appendLog("Device disconnected");
}
@Override @Override
public void onConnected(Bundle connectionHint) { public void onConnected(Bundle connectionHint) {
if (Log.isLoggable(TAG, Log.DEBUG)) { if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Connected to Google Api Service."); Log.d(TAG, "Connected to Google Api Service.");
} }
mResolvingError = false; mResolvingError = false;
Wearable.NodeApi.addListener(mGoogleApiClient, this);
} }
@Override @Override
public void onConnectionSuspended(int cause) { public void onConnectionSuspended(int cause) {
// Ignore if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onConnectionSuspended(): Cause id: " + cause);
}
} }
@Override @Override
@@ -274,9 +260,7 @@ public class MainActivity extends AppCompatActivity implements
if (Log.isLoggable(TAG, Log.DEBUG)) { if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Disconnected from Google Api Service"); Log.d(TAG, "Disconnected from Google Api Service");
} }
if (null != Wearable.NodeApi) {
Wearable.NodeApi.removeListener(mGoogleApiClient, this);
}
if (mResolvingError) { if (mResolvingError) {
// Already attempting to resolve an error. // Already attempting to resolve an error.
return; return;
@@ -359,4 +343,4 @@ public class MainActivity extends AppCompatActivity implements
} }
}); });
} }
} }

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.agendadata" > package="com.example.android.wearable.agendadata" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="22" /> android:targetSdkVersion="23" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />
@@ -34,7 +34,8 @@
<service <service
android:name="com.example.android.wearable.agendadata.HomeListenerService" > android:name="com.example.android.wearable.agendadata.HomeListenerService" >
<intent-filter> <intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/"/>
</intent-filter> </intent-filter>
</service> </service>

View File

@@ -32,4 +32,4 @@ public final class Constants {
public static final String PROFILE_PIC = "profile_pic"; public static final String PROFILE_PIC = "profile_pic";
public static final String EXTRA_SILENT = "silent"; public static final String EXTRA_SILENT = "silent";
} }

View File

@@ -16,9 +16,6 @@
package com.example.android.wearable.agendadata; package com.example.android.wearable.agendadata;
import static com.example.android.wearable.agendadata.Constants.TAG;
import static com.example.android.wearable.agendadata.Constants.EXTRA_SILENT;
import android.app.IntentService; import android.app.IntentService;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@@ -26,6 +23,9 @@ import android.os.Bundle;
import android.support.wearable.activity.ConfirmationActivity; import android.support.wearable.activity.ConfirmationActivity;
import android.util.Log; import android.util.Log;
import static com.example.android.wearable.agendadata.Constants.TAG;
import static com.example.android.wearable.agendadata.Constants.EXTRA_SILENT;
import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.DataApi;
@@ -105,14 +105,23 @@ public class DeleteService extends IntentService implements GoogleApiClient.Conn
@Override @Override
public void onConnected(Bundle connectionHint) { public void onConnected(Bundle connectionHint) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onConnected: " + connectionHint);
}
} }
@Override @Override
public void onConnectionSuspended(int cause) { public void onConnectionSuspended(int cause) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onConnectionSuspended: " + cause);
}
} }
@Override @Override
public void onConnectionFailed(ConnectionResult result) { public void onConnectionFailed(ConnectionResult result) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onConnectionFailed: " + result);
}
} }
} }

View File

@@ -16,16 +16,6 @@
package com.example.android.wearable.agendadata; package com.example.android.wearable.agendadata;
import static com.example.android.wearable.agendadata.Constants.TAG;
import static com.example.android.wearable.agendadata.Constants.EXTRA_SILENT;
import static com.example.android.wearable.agendadata.Constants.ALL_DAY;
import static com.example.android.wearable.agendadata.Constants.BEGIN;
import static com.example.android.wearable.agendadata.Constants.DESCRIPTION;
import static com.example.android.wearable.agendadata.Constants.END;
import static com.example.android.wearable.agendadata.Constants.PROFILE_PIC;
import static com.example.android.wearable.agendadata.Constants.TITLE;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
@@ -37,6 +27,15 @@ import android.text.TextUtils;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import android.util.Log; import android.util.Log;
import static com.example.android.wearable.agendadata.Constants.ALL_DAY;
import static com.example.android.wearable.agendadata.Constants.BEGIN;
import static com.example.android.wearable.agendadata.Constants.DESCRIPTION;
import static com.example.android.wearable.agendadata.Constants.EXTRA_SILENT;
import static com.example.android.wearable.agendadata.Constants.END;
import static com.example.android.wearable.agendadata.Constants.PROFILE_PIC;
import static com.example.android.wearable.agendadata.Constants.TAG;
import static com.example.android.wearable.agendadata.Constants.TITLE;
import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.Asset;
import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.DataApi;
@@ -45,7 +44,6 @@ import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.DataItem; import com.google.android.gms.wearable.DataItem;
import com.google.android.gms.wearable.DataMap; import com.google.android.gms.wearable.DataMap;
import com.google.android.gms.wearable.DataMapItem; import com.google.android.gms.wearable.DataMapItem;
import com.google.android.gms.wearable.MessageEvent;
import com.google.android.gms.wearable.Wearable; import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService; import com.google.android.gms.wearable.WearableListenerService;
@@ -63,12 +61,22 @@ public class HomeListenerService extends WearableListenerService {
private static int sNotificationId = 1; private static int sNotificationId = 1;
private GoogleApiClient mGoogleApiClient; private GoogleApiClient mGoogleApiClient;
@Override
public void onCreate() {
super.onCreate();
mGoogleApiClient = new GoogleApiClient.Builder(this.getApplicationContext())
.addApi(Wearable.API)
.build();
mGoogleApiClient.connect();
}
@Override @Override
public void onDataChanged(DataEventBuffer dataEvents) { public void onDataChanged(DataEventBuffer dataEvents) {
if (Log.isLoggable(TAG, Log.DEBUG)) { if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onDataChanged: " + dataEvents + " for " + getPackageName()); Log.d(TAG, "onDataChanged: " + dataEvents + " for " + getPackageName());
} }
for (DataEvent event : dataEvents) { for (DataEvent event : dataEvents) {
if (event.getType() == DataEvent.TYPE_DELETED) { if (event.getType() == DataEvent.TYPE_DELETED) {
deleteDataItem(event.getDataItem()); deleteDataItem(event.getDataItem());
} else if (event.getType() == DataEvent.TYPE_CHANGED) { } else if (event.getType() == DataEvent.TYPE_CHANGED) {
@@ -77,13 +85,17 @@ public class HomeListenerService extends WearableListenerService {
} }
} }
@Override /**
public void onCreate() { * Deletes the calendar card associated with the data item.
super.onCreate(); */
mGoogleApiClient = new GoogleApiClient.Builder(this.getApplicationContext()) private void deleteDataItem(DataItem dataItem) {
.addApi(Wearable.API) if (Log.isLoggable(TAG, Log.VERBOSE)) {
.build(); Log.v(TAG, "onDataItemDeleted:DataItem=" + dataItem.getUri());
mGoogleApiClient.connect(); }
Integer notificationId = sNotificationIdByDataItemUri.remove(dataItem.getUri());
if (notificationId != null) {
((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancel(notificationId);
}
} }
/** /**
@@ -154,26 +166,4 @@ public class HomeListenerService extends WearableListenerService {
sNotificationIdByDataItemUri.put(dataItem.getUri(), sNotificationId++); sNotificationIdByDataItemUri.put(dataItem.getUri(), sNotificationId++);
} }
/**
* Deletes the calendar card associated with the data item.
*/
private void deleteDataItem(DataItem dataItem) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "onDataItemDeleted:DataItem=" + dataItem.getUri());
}
Integer notificationId = sNotificationIdByDataItemUri.remove(dataItem.getUri());
if (notificationId != null) {
((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancel(notificationId);
}
}
@Override
public void onMessageReceived(MessageEvent messageEvent) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onMessageReceived: " + messageEvent.getPath()
+ " " + messageEvent.getData() + " for " + getPackageName());
}
}
} }

View File

@@ -36,49 +36,57 @@ public class CameraHelper {
public static final int MEDIA_TYPE_VIDEO = 2; public static final int MEDIA_TYPE_VIDEO = 2;
/** /**
* Iterate over supported camera preview sizes to see which one best fits the * Iterate over supported camera video sizes to see which one best fits the
* dimensions of the given view while maintaining the aspect ratio. If none can, * dimensions of the given view while maintaining the aspect ratio. If none can,
* be lenient with the aspect ratio. * be lenient with the aspect ratio.
* *
* @param sizes Supported camera preview sizes. * @param supportedVideoSizes Supported camera video sizes.
* @param w The width of the view. * @param previewSizes Supported camera preview sizes.
* @param h The height of the view. * @param w The width of the view.
* @return Best match camera preview size to fit in the view. * @param h The height of the view.
* @return Best match camera video size to fit in the view.
*/ */
public static Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) { public static Camera.Size getOptimalVideoSize(List<Camera.Size> supportedVideoSizes,
List<Camera.Size> previewSizes, int w, int h) {
// Use a very small tolerance because we want an exact match. // Use a very small tolerance because we want an exact match.
final double ASPECT_TOLERANCE = 0.1; final double ASPECT_TOLERANCE = 0.1;
double targetRatio = (double) w / h; double targetRatio = (double) w / h;
if (sizes == null)
return null;
// Supported video sizes list might be null, it means that we are allowed to use the preview
// sizes
List<Camera.Size> videoSizes;
if (supportedVideoSizes != null) {
videoSizes = supportedVideoSizes;
} else {
videoSizes = previewSizes;
}
Camera.Size optimalSize = null; Camera.Size optimalSize = null;
// Start with max value and refine as we iterate over available preview sizes. This is the // Start with max value and refine as we iterate over available video sizes. This is the
// minimum difference between view and camera height. // minimum difference between view and camera height.
double minDiff = Double.MAX_VALUE; double minDiff = Double.MAX_VALUE;
// Target view height // Target view height
int targetHeight = h; int targetHeight = h;
// Try to find a preview size that matches aspect ratio and the target view size. // Try to find a video size that matches aspect ratio and the target view size.
// Iterate over all available sizes and pick the largest size that can fit in the view and // Iterate over all available sizes and pick the largest size that can fit in the view and
// still maintain the aspect ratio. // still maintain the aspect ratio.
for (Camera.Size size : sizes) { for (Camera.Size size : videoSizes) {
double ratio = (double) size.width / size.height; double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
continue; continue;
if (Math.abs(size.height - targetHeight) < minDiff) { if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size; optimalSize = size;
minDiff = Math.abs(size.height - targetHeight); minDiff = Math.abs(size.height - targetHeight);
} }
} }
// Cannot find preview size that matches the aspect ratio, ignore the requirement // Cannot find video size that matches the aspect ratio, ignore the requirement
if (optimalSize == null) { if (optimalSize == null) {
minDiff = Double.MAX_VALUE; minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) { for (Camera.Size size : videoSizes) {
if (Math.abs(size.height - targetHeight) < minDiff) { if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size; optimalSize = size;
minDiff = Math.abs(size.height - targetHeight); minDiff = Math.abs(size.height - targetHeight);
} }

View File

@@ -36,49 +36,57 @@ public class CameraHelper {
public static final int MEDIA_TYPE_VIDEO = 2; public static final int MEDIA_TYPE_VIDEO = 2;
/** /**
* Iterate over supported camera preview sizes to see which one best fits the * Iterate over supported camera video sizes to see which one best fits the
* dimensions of the given view while maintaining the aspect ratio. If none can, * dimensions of the given view while maintaining the aspect ratio. If none can,
* be lenient with the aspect ratio. * be lenient with the aspect ratio.
* *
* @param sizes Supported camera preview sizes. * @param supportedVideoSizes Supported camera video sizes.
* @param w The width of the view. * @param previewSizes Supported camera preview sizes.
* @param h The height of the view. * @param w The width of the view.
* @return Best match camera preview size to fit in the view. * @param h The height of the view.
* @return Best match camera video size to fit in the view.
*/ */
public static Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) { public static Camera.Size getOptimalVideoSize(List<Camera.Size> supportedVideoSizes,
List<Camera.Size> previewSizes, int w, int h) {
// Use a very small tolerance because we want an exact match. // Use a very small tolerance because we want an exact match.
final double ASPECT_TOLERANCE = 0.1; final double ASPECT_TOLERANCE = 0.1;
double targetRatio = (double) w / h; double targetRatio = (double) w / h;
if (sizes == null)
return null;
// Supported video sizes list might be null, it means that we are allowed to use the preview
// sizes
List<Camera.Size> videoSizes;
if (supportedVideoSizes != null) {
videoSizes = supportedVideoSizes;
} else {
videoSizes = previewSizes;
}
Camera.Size optimalSize = null; Camera.Size optimalSize = null;
// Start with max value and refine as we iterate over available preview sizes. This is the // Start with max value and refine as we iterate over available video sizes. This is the
// minimum difference between view and camera height. // minimum difference between view and camera height.
double minDiff = Double.MAX_VALUE; double minDiff = Double.MAX_VALUE;
// Target view height // Target view height
int targetHeight = h; int targetHeight = h;
// Try to find a preview size that matches aspect ratio and the target view size. // Try to find a video size that matches aspect ratio and the target view size.
// Iterate over all available sizes and pick the largest size that can fit in the view and // Iterate over all available sizes and pick the largest size that can fit in the view and
// still maintain the aspect ratio. // still maintain the aspect ratio.
for (Camera.Size size : sizes) { for (Camera.Size size : videoSizes) {
double ratio = (double) size.width / size.height; double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
continue; continue;
if (Math.abs(size.height - targetHeight) < minDiff) { if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size; optimalSize = size;
minDiff = Math.abs(size.height - targetHeight); minDiff = Math.abs(size.height - targetHeight);
} }
} }
// Cannot find preview size that matches the aspect ratio, ignore the requirement // Cannot find video size that matches the aspect ratio, ignore the requirement
if (optimalSize == null) { if (optimalSize == null) {
minDiff = Double.MAX_VALUE; minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) { for (Camera.Size size : videoSizes) {
if (Math.abs(size.height - targetHeight) < minDiff) { if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size; optimalSize = size;
minDiff = Math.abs(size.height - targetHeight); minDiff = Math.abs(size.height - targetHeight);
} }

View File

@@ -473,7 +473,7 @@ public class BluetoothChatService {
int bytes; int bytes;
// Keep listening to the InputStream while connected // Keep listening to the InputStream while connected
while (true) { while (mState == STATE_CONNECTED) {
try { try {
// Read from the InputStream // Read from the InputStream
bytes = mmInStream.read(buffer); bytes = mmInStream.read(buffer);

View File

@@ -275,6 +275,11 @@ public class Camera2BasicFragment extends Fragment
*/ */
private boolean mFlashSupported; private boolean mFlashSupported;
/**
* Orientation of the camera sensor
*/
private int mSensorOrientation;
/** /**
* A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. * A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture.
*/ */
@@ -515,19 +520,19 @@ public class Camera2BasicFragment extends Fragment
// Find out if we need to swap dimension to get the preview size relative to sensor // Find out if we need to swap dimension to get the preview size relative to sensor
// coordinate. // coordinate.
int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
int sensorOrientation = //noinspection ConstantConditions
characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
boolean swappedDimensions = false; boolean swappedDimensions = false;
switch (displayRotation) { switch (displayRotation) {
case Surface.ROTATION_0: case Surface.ROTATION_0:
case Surface.ROTATION_180: case Surface.ROTATION_180:
if (sensorOrientation == 90 || sensorOrientation == 270) { if (mSensorOrientation == 90 || mSensorOrientation == 270) {
swappedDimensions = true; swappedDimensions = true;
} }
break; break;
case Surface.ROTATION_90: case Surface.ROTATION_90:
case Surface.ROTATION_270: case Surface.ROTATION_270:
if (sensorOrientation == 0 || sensorOrientation == 180) { if (mSensorOrientation == 0 || mSensorOrientation == 180) {
swappedDimensions = true; swappedDimensions = true;
} }
break; break;
@@ -821,7 +826,7 @@ public class Camera2BasicFragment extends Fragment
// Orientation // Orientation
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation)); captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
CameraCaptureSession.CaptureCallback CaptureCallback CameraCaptureSession.CaptureCallback CaptureCallback
= new CameraCaptureSession.CaptureCallback() { = new CameraCaptureSession.CaptureCallback() {
@@ -843,6 +848,20 @@ public class Camera2BasicFragment extends Fragment
} }
} }
/**
* Retrieves the JPEG orientation from the specified screen rotation.
*
* @param rotation The screen rotation.
* @return The JPEG orientation (one of 0, 90, 270, and 360)
*/
private int getOrientation(int rotation) {
// Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X)
// We have to take that into account and rotate JPEG properly.
// For devices with orientation of 90, we simply return our mapping from ORIENTATIONS.
// For devices with orientation of 270, we need to rotate the JPEG 180 degrees.
return (ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360;
}
/** /**
* Unlock the focus. This method should be called when still image capture sequence is * Unlock the focus. This method should be called when still image capture sequence is
* finished. * finished.

View File

@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application android:allowBackup="true" <application android:allowBackup="true"
android:label="@string/app_name" android:label="@string/app_name"

View File

@@ -55,9 +55,9 @@ import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.Toast; import android.widget.Toast;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@@ -67,7 +67,10 @@ import java.util.concurrent.TimeUnit;
public class Camera2VideoFragment extends Fragment public class Camera2VideoFragment extends Fragment
implements View.OnClickListener, FragmentCompat.OnRequestPermissionsResultCallback { implements View.OnClickListener, FragmentCompat.OnRequestPermissionsResultCallback {
private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); private static final int SENSOR_ORIENTATION_DEFAULT_DEGREES = 90;
private static final int SENSOR_ORIENTATION_INVERSE_DEGREES = 270;
private static final SparseIntArray DEFAULT_ORIENTATIONS = new SparseIntArray();
private static final SparseIntArray INVERSE_ORIENTATIONS = new SparseIntArray();
private static final String TAG = "Camera2VideoFragment"; private static final String TAG = "Camera2VideoFragment";
private static final int REQUEST_VIDEO_PERMISSIONS = 1; private static final int REQUEST_VIDEO_PERMISSIONS = 1;
@@ -79,10 +82,17 @@ public class Camera2VideoFragment extends Fragment
}; };
static { static {
ORIENTATIONS.append(Surface.ROTATION_0, 90); DEFAULT_ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0); DEFAULT_ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270); DEFAULT_ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180); DEFAULT_ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
static {
INVERSE_ORIENTATIONS.append(Surface.ROTATION_0, 270);
INVERSE_ORIENTATIONS.append(Surface.ROTATION_90, 180);
INVERSE_ORIENTATIONS.append(Surface.ROTATION_180, 90);
INVERSE_ORIENTATIONS.append(Surface.ROTATION_270, 0);
} }
/** /**
@@ -146,11 +156,6 @@ public class Camera2VideoFragment extends Fragment
*/ */
private Size mVideoSize; private Size mVideoSize;
/**
* Camera preview.
*/
private CaptureRequest.Builder mPreviewBuilder;
/** /**
* MediaRecorder * MediaRecorder
*/ */
@@ -210,6 +215,10 @@ public class Camera2VideoFragment extends Fragment
} }
}; };
private Integer mSensorOrientation;
private String mNextVideoAbsolutePath;
private CaptureRequest.Builder mPreviewBuilder;
private Surface mRecorderSurface;
public static Camera2VideoFragment newInstance() { public static Camera2VideoFragment newInstance() {
return new Camera2VideoFragment(); return new Camera2VideoFragment();
@@ -425,6 +434,7 @@ public class Camera2VideoFragment extends Fragment
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
StreamConfigurationMap map = characteristics StreamConfigurationMap map = characteristics
.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class)); mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class));
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
width, height, mVideoSize); width, height, mVideoSize);
@@ -454,6 +464,7 @@ public class Camera2VideoFragment extends Fragment
private void closeCamera() { private void closeCamera() {
try { try {
mCameraOpenCloseLock.acquire(); mCameraOpenCloseLock.acquire();
closePreviewSession();
if (null != mCameraDevice) { if (null != mCameraDevice) {
mCameraDevice.close(); mCameraDevice.close();
mCameraDevice = null; mCameraDevice = null;
@@ -477,22 +488,16 @@ public class Camera2VideoFragment extends Fragment
return; return;
} }
try { try {
setUpMediaRecorder(); closePreviewSession();
SurfaceTexture texture = mTextureView.getSurfaceTexture(); SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null; assert texture != null;
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
List<Surface> surfaces = new ArrayList<Surface>();
Surface previewSurface = new Surface(texture); Surface previewSurface = new Surface(texture);
surfaces.add(previewSurface);
mPreviewBuilder.addTarget(previewSurface); mPreviewBuilder.addTarget(previewSurface);
Surface recorderSurface = mMediaRecorder.getSurface(); mCameraDevice.createCaptureSession(Arrays.asList(previewSurface), new CameraCaptureSession.StateCallback() {
surfaces.add(recorderSurface);
mPreviewBuilder.addTarget(recorderSurface);
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override @Override
public void onConfigured(CameraCaptureSession cameraCaptureSession) { public void onConfigured(CameraCaptureSession cameraCaptureSession) {
@@ -510,8 +515,6 @@ public class Camera2VideoFragment extends Fragment
}, mBackgroundHandler); }, mBackgroundHandler);
} catch (CameraAccessException e) { } catch (CameraAccessException e) {
e.printStackTrace(); e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} }
} }
@@ -575,32 +578,96 @@ public class Camera2VideoFragment extends Fragment
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setOutputFile(getVideoFile(activity).getAbsolutePath()); if (mNextVideoAbsolutePath == null || mNextVideoAbsolutePath.isEmpty()) {
mNextVideoAbsolutePath = getVideoFilePath(getActivity());
}
mMediaRecorder.setOutputFile(mNextVideoAbsolutePath);
mMediaRecorder.setVideoEncodingBitRate(10000000); mMediaRecorder.setVideoEncodingBitRate(10000000);
mMediaRecorder.setVideoFrameRate(30); mMediaRecorder.setVideoFrameRate(30);
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight()); mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
int orientation = ORIENTATIONS.get(rotation); switch (mSensorOrientation) {
mMediaRecorder.setOrientationHint(orientation); case SENSOR_ORIENTATION_DEFAULT_DEGREES:
mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
break;
case SENSOR_ORIENTATION_INVERSE_DEGREES:
mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation));
break;
}
mMediaRecorder.prepare(); mMediaRecorder.prepare();
} }
private File getVideoFile(Context context) { private String getVideoFilePath(Context context) {
return new File(context.getExternalFilesDir(null), "video.mp4"); return context.getExternalFilesDir(null).getAbsolutePath() + "/"
+ System.currentTimeMillis() + ".mp4";
} }
private void startRecordingVideo() { private void startRecordingVideo() {
if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
return;
}
try { try {
// UI closePreviewSession();
mButtonVideo.setText(R.string.stop); setUpMediaRecorder();
mIsRecordingVideo = true; SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null;
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
List<Surface> surfaces = new ArrayList<>();
// Start recording // Set up Surface for the camera preview
mMediaRecorder.start(); Surface previewSurface = new Surface(texture);
} catch (IllegalStateException e) { surfaces.add(previewSurface);
mPreviewBuilder.addTarget(previewSurface);
// Set up Surface for the MediaRecorder
mRecorderSurface = mMediaRecorder.getSurface();
surfaces.add(mRecorderSurface);
mPreviewBuilder.addTarget(mRecorderSurface);
// Start a capture session
// Once the session starts, we can update the UI and start recording
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
mPreviewSession = cameraCaptureSession;
updatePreview();
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// UI
mButtonVideo.setText(R.string.stop);
mIsRecordingVideo = true;
// Start recording
mMediaRecorder.start();
}
});
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
Activity activity = getActivity();
if (null != activity) {
Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
}
}
}, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace(); e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private void closePreviewSession() {
if (mPreviewSession != null) {
mPreviewSession.close();
mPreviewSession = null;
} }
} }
@@ -611,11 +678,14 @@ public class Camera2VideoFragment extends Fragment
// Stop recording // Stop recording
mMediaRecorder.stop(); mMediaRecorder.stop();
mMediaRecorder.reset(); mMediaRecorder.reset();
Activity activity = getActivity(); Activity activity = getActivity();
if (null != activity) { if (null != activity) {
Toast.makeText(activity, "Video saved: " + getVideoFile(activity), Toast.makeText(activity, "Video saved: " + mNextVideoAbsolutePath,
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
Log.d(TAG, "Video saved: " + mNextVideoAbsolutePath);
} }
mNextVideoAbsolutePath = null;
startPreview(); startPreview();
} }
@@ -687,4 +757,4 @@ public class Camera2VideoFragment extends Fragment
} }
} }

View File

@@ -22,9 +22,9 @@ import android.content.Intent;
import android.content.IntentSender; import android.content.IntentSender;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -41,8 +41,9 @@ import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.data.FreezableUtils;
import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.Asset;
import com.google.android.gms.wearable.CapabilityApi;
import com.google.android.gms.wearable.CapabilityInfo;
import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.DataApi.DataItemResult; import com.google.android.gms.wearable.DataApi.DataItemResult;
import com.google.android.gms.wearable.DataEvent; import com.google.android.gms.wearable.DataEvent;
@@ -61,7 +62,6 @@ import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -72,17 +72,20 @@ import java.util.concurrent.TimeUnit;
* item every second while it is open. Also allows user to take a photo and send that as an asset * item every second while it is open. Also allows user to take a photo and send that as an asset
* to the paired wearable. * to the paired wearable.
*/ */
public class MainActivity extends Activity implements DataApi.DataListener, public class MainActivity extends Activity implements
MessageApi.MessageListener, NodeApi.NodeListener, ConnectionCallbacks, CapabilityApi.CapabilityListener,
MessageApi.MessageListener,
DataApi.DataListener,
ConnectionCallbacks,
OnConnectionFailedListener { OnConnectionFailedListener {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
/** //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; private static final int REQUEST_RESOLVE_ERROR = 1000;
private static final int REQUEST_IMAGE_CAPTURE = 1;
private static final String START_ACTIVITY_PATH = "/start-activity"; private static final String START_ACTIVITY_PATH = "/start-activity";
private static final String COUNT_PATH = "/count"; private static final String COUNT_PATH = "/count";
private static final String IMAGE_PATH = "/image"; private static final String IMAGE_PATH = "/image";
@@ -100,18 +103,14 @@ public class MainActivity extends Activity implements DataApi.DataListener,
private View mStartActivityBtn; private View mStartActivityBtn;
private DataItemAdapter mDataItemListAdapter; private DataItemAdapter mDataItemListAdapter;
private Handler mHandler;
// Send DataItems. // Send DataItems.
private ScheduledExecutorService mGeneratorExecutor; private ScheduledExecutorService mGeneratorExecutor;
private ScheduledFuture<?> mDataItemGeneratorFuture; private ScheduledFuture<?> mDataItemGeneratorFuture;
static final int REQUEST_IMAGE_CAPTURE = 1;
@Override @Override
public void onCreate(Bundle b) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(b); super.onCreate(savedInstanceState);
mHandler = new Handler();
LOGD(TAG, "onCreate"); LOGD(TAG, "onCreate");
mCameraSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); mCameraSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
setContentView(R.layout.main_activity); setContentView(R.layout.main_activity);
@@ -130,15 +129,6 @@ public class MainActivity extends Activity implements DataApi.DataListener,
.build(); .build();
} }
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
Bundle extras = data.getExtras();
mImageBitmap = (Bitmap) extras.get("data");
mThumbView.setImageBitmap(mImageBitmap);
}
}
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
@@ -162,16 +152,25 @@ public class MainActivity extends Activity implements DataApi.DataListener,
@Override @Override
protected void onStop() { protected void onStop() {
if (!mResolvingError) { if (!mResolvingError && (mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) {
Wearable.DataApi.removeListener(mGoogleApiClient, this); Wearable.DataApi.removeListener(mGoogleApiClient, this);
Wearable.MessageApi.removeListener(mGoogleApiClient, this); Wearable.MessageApi.removeListener(mGoogleApiClient, this);
Wearable.NodeApi.removeListener(mGoogleApiClient, this); Wearable.CapabilityApi.removeListener(mGoogleApiClient, this);
mGoogleApiClient.disconnect(); mGoogleApiClient.disconnect();
} }
super.onStop(); super.onStop();
} }
@Override //ConnectionCallbacks @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
Bundle extras = data.getExtras();
mImageBitmap = (Bitmap) extras.get("data");
mThumbView.setImageBitmap(mImageBitmap);
}
}
@Override
public void onConnected(Bundle connectionHint) { public void onConnected(Bundle connectionHint) {
LOGD(TAG, "Google API Client was connected"); LOGD(TAG, "Google API Client was connected");
mResolvingError = false; mResolvingError = false;
@@ -179,157 +178,100 @@ public class MainActivity extends Activity implements DataApi.DataListener,
mSendPhotoBtn.setEnabled(mCameraSupported); mSendPhotoBtn.setEnabled(mCameraSupported);
Wearable.DataApi.addListener(mGoogleApiClient, this); Wearable.DataApi.addListener(mGoogleApiClient, this);
Wearable.MessageApi.addListener(mGoogleApiClient, this); Wearable.MessageApi.addListener(mGoogleApiClient, this);
Wearable.NodeApi.addListener(mGoogleApiClient, this); Wearable.CapabilityApi.addListener(
mGoogleApiClient, this, Uri.parse("wear://"), CapabilityApi.FILTER_REACHABLE);
} }
@Override //ConnectionCallbacks @Override
public void onConnectionSuspended(int cause) { public void onConnectionSuspended(int cause) {
LOGD(TAG, "Connection to Google API client was suspended"); LOGD(TAG, "Connection to Google API client was suspended");
mStartActivityBtn.setEnabled(false); mStartActivityBtn.setEnabled(false);
mSendPhotoBtn.setEnabled(false); mSendPhotoBtn.setEnabled(false);
} }
@Override //OnConnectionFailedListener @Override
public void onConnectionFailed(ConnectionResult result) { public void onConnectionFailed(ConnectionResult result) {
if (mResolvingError) { if (!mResolvingError) {
// Already attempting to resolve an error.
return; if (result.hasResolution()) {
} else if (result.hasResolution()) { try {
try { mResolvingError = true;
mResolvingError = true; result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR); } catch (IntentSender.SendIntentException e) {
} catch (IntentSender.SendIntentException e) { // There was an error with the resolution intent. Try again.
// There was an error with the resolution intent. Try again. mGoogleApiClient.connect();
mGoogleApiClient.connect(); }
} else {
Log.e(TAG, "Connection to Google API client has failed");
mResolvingError = false;
mStartActivityBtn.setEnabled(false);
mSendPhotoBtn.setEnabled(false);
Wearable.DataApi.removeListener(mGoogleApiClient, this);
Wearable.MessageApi.removeListener(mGoogleApiClient, this);
Wearable.CapabilityApi.removeListener(mGoogleApiClient, this);
} }
} else {
Log.e(TAG, "Connection to Google API client has failed");
mResolvingError = false;
mStartActivityBtn.setEnabled(false);
mSendPhotoBtn.setEnabled(false);
Wearable.DataApi.removeListener(mGoogleApiClient, this);
Wearable.MessageApi.removeListener(mGoogleApiClient, this);
Wearable.NodeApi.removeListener(mGoogleApiClient, this);
} }
} }
@Override //DataListener @Override
public void onDataChanged(DataEventBuffer dataEvents) { public void onDataChanged(DataEventBuffer dataEvents) {
LOGD(TAG, "onDataChanged: " + dataEvents); LOGD(TAG, "onDataChanged: " + dataEvents);
// Need to freeze the dataEvents so they will exist later on the UI thread
final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents); for (DataEvent event : dataEvents) {
runOnUiThread(new Runnable() { if (event.getType() == DataEvent.TYPE_CHANGED) {
@Override mDataItemListAdapter.add(
public void run() { new Event("DataItem Changed", event.getDataItem().toString()));
for (DataEvent event : events) { } else if (event.getType() == DataEvent.TYPE_DELETED) {
if (event.getType() == DataEvent.TYPE_CHANGED) { mDataItemListAdapter.add(
mDataItemListAdapter.add( new Event("DataItem Deleted", event.getDataItem().toString()));
new Event("DataItem Changed", event.getDataItem().toString()));
} else if (event.getType() == DataEvent.TYPE_DELETED) {
mDataItemListAdapter.add(
new Event("DataItem Deleted", event.getDataItem().toString()));
}
}
} }
}); }
} }
@Override //MessageListener @Override
public void onMessageReceived(final MessageEvent messageEvent) { public void onMessageReceived(final MessageEvent messageEvent) {
LOGD(TAG, "onMessageReceived() A message from watch was received:" + messageEvent LOGD(TAG, "onMessageReceived() A message from watch was received:"
.getRequestId() + " " + messageEvent.getPath()); + messageEvent.getRequestId() + " " + messageEvent.getPath());
mHandler.post(new Runnable() {
@Override
public void run() {
mDataItemListAdapter.add(new Event("Message from watch", messageEvent.toString()));
}
});
mDataItemListAdapter.add(new Event("Message from watch", messageEvent.toString()));
} }
@Override //NodeListener @Override
public void onPeerConnected(final Node peer) { public void onCapabilityChanged(final CapabilityInfo capabilityInfo) {
LOGD(TAG, "onPeerConnected: " + peer); LOGD(TAG, "onCapabilityChanged: " + capabilityInfo);
mHandler.post(new Runnable() {
@Override
public void run() {
mDataItemListAdapter.add(new Event("Connected", peer.toString()));
}
});
} mDataItemListAdapter.add(new Event("onCapabilityChanged", capabilityInfo.toString()));
@Override //NodeListener
public void onPeerDisconnected(final Node peer) {
LOGD(TAG, "onPeerDisconnected: " + peer);
mHandler.post(new Runnable() {
@Override
public void run() {
mDataItemListAdapter.add(new Event("Disconnected", peer.toString()));
}
});
} }
/** /**
* A View Adapter for presenting the Event objects in a list * Sets up UI components and their callback handlers.
*/ */
private static class DataItemAdapter extends ArrayAdapter<Event> { private void setupViews() {
mSendPhotoBtn = (Button) findViewById(R.id.sendPhoto);
mThumbView = (ImageView) findViewById(R.id.imageView);
mDataItemList = (ListView) findViewById(R.id.data_item_list);
mStartActivityBtn = findViewById(R.id.start_wearable_activity);
}
private final Context mContext; public void onTakePhotoClick(View view) {
dispatchTakePictureIntent();
}
public DataItemAdapter(Context context, int unusedResource) { public void onSendPhotoClick(View view) {
super(context, unusedResource); if (null != mImageBitmap && mGoogleApiClient.isConnected()) {
mContext = context; sendPhoto(toAsset(mImageBitmap));
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(android.R.layout.two_line_list_item, null);
convertView.setTag(holder);
holder.text1 = (TextView) convertView.findViewById(android.R.id.text1);
holder.text2 = (TextView) convertView.findViewById(android.R.id.text2);
} else {
holder = (ViewHolder) convertView.getTag();
}
Event event = getItem(position);
holder.text1.setText(event.title);
holder.text2.setText(event.text);
return convertView;
}
private class ViewHolder {
TextView text1;
TextView text2;
} }
} }
private class Event { /**
* Sends an RPC to start a fullscreen Activity on the wearable.
*/
public void onStartWearableActivityClick(View view) {
LOGD(TAG, "Generating RPC");
String title; // Trigger an AsyncTask that will query for a list of connected nodes and send a
String text; // "start-activity" message to each connected node.
new StartWearableActivityTask().execute();
public Event(String title, String text) {
this.title = title;
this.text = text;
}
}
private Collection<String> getNodes() {
HashSet<String> results = new HashSet<>();
NodeApi.GetConnectedNodesResult nodes =
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await();
for (Node node : nodes.getNodes()) {
results.add(node.getId());
}
return results;
} }
private void sendStartActivityMessage(String node) { private void sendStartActivityMessage(String node) {
@@ -347,61 +289,6 @@ public class MainActivity extends Activity implements DataApi.DataListener,
); );
} }
private class StartWearableActivityTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... args) {
Collection<String> nodes = getNodes();
for (String node : nodes) {
sendStartActivityMessage(node);
}
return null;
}
}
/**
* Sends an RPC to start a fullscreen Activity on the wearable.
*/
public void onStartWearableActivityClick(View view) {
LOGD(TAG, "Generating RPC");
// Trigger an AsyncTask that will query for a list of connected nodes and send a
// "start-activity" message to each connected node.
new StartWearableActivityTask().execute();
}
/**
* Generates a DataItem based on an incrementing count.
*/
private class DataItemGenerator implements Runnable {
private int count = 0;
@Override
public void run() {
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(COUNT_PATH);
putDataMapRequest.getDataMap().putInt(COUNT_KEY, count++);
putDataMapRequest.setUrgent();
PutDataRequest request = putDataMapRequest.asPutDataRequest();
request.setUrgent();
LOGD(TAG, "Generating DataItem: " + request);
if (!mGoogleApiClient.isConnected()) {
return;
}
Wearable.DataApi.putDataItem(mGoogleApiClient, request)
.setResultCallback(new ResultCallback<DataItemResult>() {
@Override
public void onResult(DataItemResult dataItemResult) {
if (!dataItemResult.getStatus().isSuccess()) {
Log.e(TAG, "ERROR: failed to putDataItem, status code: "
+ dataItemResult.getStatus().getStatusCode());
}
}
});
}
}
/** /**
* Dispatches an {@link android.content.Intent} to take a photo. Result will be returned back * Dispatches an {@link android.content.Intent} to take a photo. Result will be returned back
* in onActivityResult(). * in onActivityResult().
@@ -437,7 +324,7 @@ public class MainActivity extends Activity implements DataApi.DataListener,
} }
/** /**
* Sends the asset that was created form the photo we took by adding it to the Data Item store. * Sends the asset that was created from the photo we took by adding it to the Data Item store.
*/ */
private void sendPhoto(Asset asset) { private void sendPhoto(Asset asset) {
PutDataMapRequest dataMap = PutDataMapRequest.create(IMAGE_PATH); PutDataMapRequest dataMap = PutDataMapRequest.create(IMAGE_PATH);
@@ -454,27 +341,18 @@ public class MainActivity extends Activity implements DataApi.DataListener,
.isSuccess()); .isSuccess());
} }
}); });
} }
public void onTakePhotoClick(View view) { private Collection<String> getNodes() {
dispatchTakePictureIntent(); HashSet<String> results = new HashSet<>();
} NodeApi.GetConnectedNodesResult nodes =
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await();
public void onSendPhotoClick(View view) { for (Node node : nodes.getNodes()) {
if (null != mImageBitmap && mGoogleApiClient.isConnected()) { results.add(node.getId());
sendPhoto(toAsset(mImageBitmap));
} }
}
/** return results;
* Sets up UI components and their callback handlers.
*/
private void setupViews() {
mSendPhotoBtn = (Button) findViewById(R.id.sendPhoto);
mThumbView = (ImageView) findViewById(R.id.imageView);
mDataItemList = (ListView) findViewById(R.id.data_item_list);
mStartActivityBtn = findViewById(R.id.start_wearable_activity);
} }
/** /**
@@ -486,4 +364,96 @@ public class MainActivity extends Activity implements DataApi.DataListener,
} }
} }
} /**
* A View Adapter for presenting the Event objects in a list
*/
private static class DataItemAdapter extends ArrayAdapter<Event> {
private final Context mContext;
public DataItemAdapter(Context context, int unusedResource) {
super(context, unusedResource);
mContext = context;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(android.R.layout.two_line_list_item, null);
convertView.setTag(holder);
holder.text1 = (TextView) convertView.findViewById(android.R.id.text1);
holder.text2 = (TextView) convertView.findViewById(android.R.id.text2);
} else {
holder = (ViewHolder) convertView.getTag();
}
Event event = getItem(position);
holder.text1.setText(event.title);
holder.text2.setText(event.text);
return convertView;
}
private class ViewHolder {
TextView text1;
TextView text2;
}
}
private class Event {
String title;
String text;
public Event(String title, String text) {
this.title = title;
this.text = text;
}
}
private class StartWearableActivityTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... args) {
Collection<String> nodes = getNodes();
for (String node : nodes) {
sendStartActivityMessage(node);
}
return null;
}
}
/**
* Generates a DataItem based on an incrementing count.
*/
private class DataItemGenerator implements Runnable {
private int count = 0;
@Override
public void run() {
PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(COUNT_PATH);
putDataMapRequest.getDataMap().putInt(COUNT_KEY, count++);
PutDataRequest request = putDataMapRequest.asPutDataRequest();
request.setUrgent();
LOGD(TAG, "Generating DataItem: " + request);
if (!mGoogleApiClient.isConnected()) {
return;
}
Wearable.DataApi.putDataItem(mGoogleApiClient, request)
.setResultCallback(new ResultCallback<DataItemResult>() {
@Override
public void onResult(DataItemResult dataItemResult) {
if (!dataItemResult.getStatus().isSuccess()) {
Log.e(TAG, "ERROR: failed to putDataItem, status code: "
+ dataItemResult.getStatus().getStatusCode());
}
}
});
}
}
}

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.datalayer" > package="com.example.android.wearable.datalayer" >
<uses-sdk android:minSdkVersion="20" <uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="22" /> android:targetSdkVersion="23" />
<uses-feature android:name="android.hardware.type.watch" /> <uses-feature android:name="android.hardware.type.watch" />
@@ -35,10 +35,15 @@
<service <service
android:name=".DataLayerListenerService" > android:name=".DataLayerListenerService" >
<intent-filter> <intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <!-- listeners receive events that match the action and data filters -->
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/count"/>
</intent-filter>
<intent-filter>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/start-activity"/>
</intent-filter> </intent-filter>
</service> </service>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name"> android:label="@string/app_name">
@@ -52,4 +57,4 @@
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

View File

@@ -25,11 +25,9 @@ import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.wearable.DataEvent; import com.google.android.gms.wearable.DataEvent;
import com.google.android.gms.wearable.DataEventBuffer; import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.MessageEvent; 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.Wearable;
import com.google.android.gms.wearable.WearableListenerService; import com.google.android.gms.wearable.WearableListenerService;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@@ -37,7 +35,7 @@ import java.util.concurrent.TimeUnit;
*/ */
public class DataLayerListenerService extends WearableListenerService { public class DataLayerListenerService extends WearableListenerService {
private static final String TAG = "DataLayerListenerServic"; private static final String TAG = "DataLayerService";
private static final String START_ACTIVITY_PATH = "/start-activity"; private static final String START_ACTIVITY_PATH = "/start-activity";
private static final String DATA_ITEM_RECEIVED_PATH = "/data-item-received"; private static final String DATA_ITEM_RECEIVED_PATH = "/data-item-received";
@@ -98,19 +96,9 @@ public class DataLayerListenerService extends WearableListenerService {
} }
} }
@Override
public void onPeerConnected(Node peer) {
LOGD(TAG, "onPeerConnected: " + peer);
}
@Override
public void onPeerDisconnected(Node peer) {
LOGD(TAG, "onPeerDisconnected: " + peer);
}
public static void LOGD(final String tag, String message) { public static void LOGD(final String tag, String message) {
if (Log.isLoggable(tag, Log.DEBUG)) { if (Log.isLoggable(tag, Log.DEBUG)) {
Log.d(tag, message); Log.d(tag, message);
} }
} }
} }

View File

@@ -23,9 +23,9 @@ import android.app.Fragment;
import android.app.FragmentManager; import android.app.FragmentManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.support.wearable.view.DotsPageIndicator; import android.support.wearable.view.DotsPageIndicator;
import android.support.wearable.view.FragmentGridPagerAdapter; import android.support.wearable.view.FragmentGridPagerAdapter;
import android.support.wearable.view.GridViewPager; import android.support.wearable.view.GridViewPager;
@@ -54,7 +54,6 @@ import com.google.android.gms.wearable.DataMapItem;
import com.google.android.gms.wearable.MessageApi; import com.google.android.gms.wearable.MessageApi;
import com.google.android.gms.wearable.MessageEvent; import com.google.android.gms.wearable.MessageEvent;
import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable; import com.google.android.gms.wearable.Wearable;
import java.io.InputStream; import java.io.InputStream;
@@ -78,9 +77,12 @@ import java.util.Set;
* </li> * </li>
* </ul> * </ul>
*/ */
public class MainActivity extends Activity implements ConnectionCallbacks, public class MainActivity extends Activity implements
OnConnectionFailedListener, DataApi.DataListener, MessageApi.MessageListener, ConnectionCallbacks,
NodeApi.NodeListener { OnConnectionFailedListener,
DataApi.DataListener,
MessageApi.MessageListener,
CapabilityApi.CapabilityListener {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
private static final String CAPABILITY_1_NAME = "capability_1"; private static final String CAPABILITY_1_NAME = "capability_1";
@@ -92,8 +94,8 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
private AssetFragment mAssetFragment; private AssetFragment mAssetFragment;
@Override @Override
public void onCreate(Bundle b) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(b); super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity); setContentView(R.layout.main_activity);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setupViews(); setupViews();
@@ -112,11 +114,14 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
@Override @Override
protected void onPause() { protected void onPause() {
if ((mGoogleApiClient != null) && mGoogleApiClient.isConnected()) {
Wearable.DataApi.removeListener(mGoogleApiClient, this);
Wearable.MessageApi.removeListener(mGoogleApiClient, this);
Wearable.CapabilityApi.removeListener(mGoogleApiClient, this);
mGoogleApiClient.disconnect();
}
super.onPause(); super.onPause();
Wearable.DataApi.removeListener(mGoogleApiClient, this);
Wearable.MessageApi.removeListener(mGoogleApiClient, this);
Wearable.NodeApi.removeListener(mGoogleApiClient, this);
mGoogleApiClient.disconnect();
} }
@Override @Override
@@ -124,7 +129,8 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
LOGD(TAG, "onConnected(): Successfully connected to Google API client"); LOGD(TAG, "onConnected(): Successfully connected to Google API client");
Wearable.DataApi.addListener(mGoogleApiClient, this); Wearable.DataApi.addListener(mGoogleApiClient, this);
Wearable.MessageApi.addListener(mGoogleApiClient, this); Wearable.MessageApi.addListener(mGoogleApiClient, this);
Wearable.NodeApi.addListener(mGoogleApiClient, this); Wearable.CapabilityApi.addListener(
mGoogleApiClient, this, Uri.parse("wear://"), CapabilityApi.FILTER_REACHABLE);
} }
@Override @Override
@@ -244,13 +250,9 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
} }
@Override @Override
public void onPeerConnected(Node node) { public void onCapabilityChanged(CapabilityInfo capabilityInfo) {
mDataFragment.appendItem("Node Connected", node.getId()); LOGD(TAG, "onCapabilityChanged: " + capabilityInfo);
} mDataFragment.appendItem("onCapabilityChanged", capabilityInfo.toString());
@Override
public void onPeerDisconnected(Node node) {
mDataFragment.appendItem("Node Disconnected", node.getId());
} }
private void setupViews() { private void setupViews() {
@@ -312,7 +314,7 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
@Override @Override
protected Bitmap doInBackground(Asset... params) { protected Bitmap doInBackground(Asset... params) {
if(params.length > 0) { if (params.length > 0) {
Asset asset = params[0]; Asset asset = params[0];
@@ -334,11 +336,11 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
@Override @Override
protected void onPostExecute(Bitmap bitmap) { protected void onPostExecute(Bitmap bitmap) {
if(bitmap != null) { if (bitmap != null) {
LOGD(TAG, "Setting background image on second page.."); LOGD(TAG, "Setting background image on second page..");
moveToPage(1); moveToPage(1);
mAssetFragment.setBackgroundImage(bitmap); mAssetFragment.setBackgroundImage(bitmap);
} }
} }
} }
} }

View File

@@ -36,49 +36,57 @@ public class CameraHelper {
public static final int MEDIA_TYPE_VIDEO = 2; public static final int MEDIA_TYPE_VIDEO = 2;
/** /**
* Iterate over supported camera preview sizes to see which one best fits the * Iterate over supported camera video sizes to see which one best fits the
* dimensions of the given view while maintaining the aspect ratio. If none can, * dimensions of the given view while maintaining the aspect ratio. If none can,
* be lenient with the aspect ratio. * be lenient with the aspect ratio.
* *
* @param sizes Supported camera preview sizes. * @param supportedVideoSizes Supported camera video sizes.
* @param w The width of the view. * @param previewSizes Supported camera preview sizes.
* @param h The height of the view. * @param w The width of the view.
* @return Best match camera preview size to fit in the view. * @param h The height of the view.
* @return Best match camera video size to fit in the view.
*/ */
public static Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) { public static Camera.Size getOptimalVideoSize(List<Camera.Size> supportedVideoSizes,
List<Camera.Size> previewSizes, int w, int h) {
// Use a very small tolerance because we want an exact match. // Use a very small tolerance because we want an exact match.
final double ASPECT_TOLERANCE = 0.1; final double ASPECT_TOLERANCE = 0.1;
double targetRatio = (double) w / h; double targetRatio = (double) w / h;
if (sizes == null)
return null;
// Supported video sizes list might be null, it means that we are allowed to use the preview
// sizes
List<Camera.Size> videoSizes;
if (supportedVideoSizes != null) {
videoSizes = supportedVideoSizes;
} else {
videoSizes = previewSizes;
}
Camera.Size optimalSize = null; Camera.Size optimalSize = null;
// Start with max value and refine as we iterate over available preview sizes. This is the // Start with max value and refine as we iterate over available video sizes. This is the
// minimum difference between view and camera height. // minimum difference between view and camera height.
double minDiff = Double.MAX_VALUE; double minDiff = Double.MAX_VALUE;
// Target view height // Target view height
int targetHeight = h; int targetHeight = h;
// Try to find a preview size that matches aspect ratio and the target view size. // Try to find a video size that matches aspect ratio and the target view size.
// Iterate over all available sizes and pick the largest size that can fit in the view and // Iterate over all available sizes and pick the largest size that can fit in the view and
// still maintain the aspect ratio. // still maintain the aspect ratio.
for (Camera.Size size : sizes) { for (Camera.Size size : videoSizes) {
double ratio = (double) size.width / size.height; double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
continue; continue;
if (Math.abs(size.height - targetHeight) < minDiff) { if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size; optimalSize = size;
minDiff = Math.abs(size.height - targetHeight); minDiff = Math.abs(size.height - targetHeight);
} }
} }
// Cannot find preview size that matches the aspect ratio, ignore the requirement // Cannot find video size that matches the aspect ratio, ignore the requirement
if (optimalSize == null) { if (optimalSize == null) {
minDiff = Double.MAX_VALUE; minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) { for (Camera.Size size : videoSizes) {
if (Math.abs(size.height - targetHeight) < minDiff) { if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
optimalSize = size; optimalSize = size;
minDiff = Math.abs(size.height - targetHeight); minDiff = Math.abs(size.height - targetHeight);
} }

View File

@@ -32,6 +32,7 @@ import android.widget.Button;
import com.example.android.common.media.CameraHelper; import com.example.android.common.media.CameraHelper;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@@ -45,6 +46,7 @@ public class MainActivity extends Activity {
private Camera mCamera; private Camera mCamera;
private TextureView mPreview; private TextureView mPreview;
private MediaRecorder mMediaRecorder; private MediaRecorder mMediaRecorder;
private File mOutputFile;
private boolean isRecording = false; private boolean isRecording = false;
private static final String TAG = "Recorder"; private static final String TAG = "Recorder";
@@ -71,7 +73,15 @@ public class MainActivity extends Activity {
// BEGIN_INCLUDE(stop_release_media_recorder) // BEGIN_INCLUDE(stop_release_media_recorder)
// stop recording and release camera // stop recording and release camera
mMediaRecorder.stop(); // stop the recording try {
mMediaRecorder.stop(); // stop the recording
} catch (RuntimeException e) {
// RuntimeException is thrown when stop() is called immediately after start().
// In this case the output file is not properly constructed ans should be deleted.
Log.d(TAG, "RuntimeException: stop() is called immediately after start()");
//noinspection ResultOfMethodCallIgnored
mOutputFile.delete();
}
releaseMediaRecorder(); // release the MediaRecorder object releaseMediaRecorder(); // release the MediaRecorder object
mCamera.lock(); // take camera access back from MediaRecorder mCamera.lock(); // take camera access back from MediaRecorder
@@ -137,8 +147,9 @@ public class MainActivity extends Activity {
// dimensions of our preview surface. // dimensions of our preview surface.
Camera.Parameters parameters = mCamera.getParameters(); Camera.Parameters parameters = mCamera.getParameters();
List<Camera.Size> mSupportedPreviewSizes = parameters.getSupportedPreviewSizes(); List<Camera.Size> mSupportedPreviewSizes = parameters.getSupportedPreviewSizes();
Camera.Size optimalSize = CameraHelper.getOptimalPreviewSize(mSupportedPreviewSizes, List<Camera.Size> mSupportedVideoSizes = parameters.getSupportedVideoSizes();
mPreview.getWidth(), mPreview.getHeight()); Camera.Size optimalSize = CameraHelper.getOptimalVideoSize(mSupportedVideoSizes,
mSupportedPreviewSizes, mPreview.getWidth(), mPreview.getHeight());
// Use the same size for recording profile. // Use the same size for recording profile.
CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH); CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
@@ -174,8 +185,11 @@ public class MainActivity extends Activity {
mMediaRecorder.setProfile(profile); mMediaRecorder.setProfile(profile);
// Step 4: Set output file // Step 4: Set output file
mMediaRecorder.setOutputFile(CameraHelper.getOutputMediaFile( mOutputFile = CameraHelper.getOutputMediaFile(CameraHelper.MEDIA_TYPE_VIDEO);
CameraHelper.MEDIA_TYPE_VIDEO).toString()); if (mOutputFile == null) {
return false;
}
mMediaRecorder.setOutputFile(mOutputFile.getPath());
// END_INCLUDE (configure_media_recorder) // END_INCLUDE (configure_media_recorder)
// Step 5: Prepare configured MediaRecorder // Step 5: Prepare configured MediaRecorder
@@ -227,4 +241,4 @@ public class MainActivity extends Activity {
} }
} }
} }

View File

@@ -66,7 +66,8 @@
android:enabled="true" android:enabled="true"
android:exported="true" > android:exported="true" >
<intent-filter> <intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/"/>
</intent-filter> </intent-filter>
</service> </service>
</application> </application>

View File

@@ -61,7 +61,8 @@
android:enabled="true" android:enabled="true"
android:exported="true" > android:exported="true" >
<intent-filter> <intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/"/>
</intent-filter> </intent-filter>
</service> </service>
</application> </application>

View File

@@ -48,7 +48,10 @@
</activity> </activity>
<service android:name=".db.UpdateService"> <service android:name=".db.UpdateService">
<intent-filter> <intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <!-- listeners receive events that match the action and data filters -->
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<!-- filters by Constants.PATH. -->
<data android:scheme="wear" android:host="*" android:pathPrefix="/location"/>
</intent-filter> </intent-filter>
</service> </service>
</application> </application>

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.speedtracker"> package="com.example.android.wearable.speedtracker">
<uses-feature android:name="android.hardware.type.watch"/> <uses-feature android:name="android.hardware.type.watch"/>
<uses-feature android:name="android.hardware.location.gps" android:required="true" /> <uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />

View File

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

View File

@@ -330,6 +330,13 @@ public class WearableMainActivity extends WearableActivity implements
public void onConnected(Bundle bundle) { public void onConnected(Bundle bundle) {
Log.d(TAG, "onConnected()"); Log.d(TAG, "onConnected()");
requestLocation();
}
private void requestLocation() {
Log.d(TAG, "requestLocation()");
/* /*
* mGpsPermissionApproved covers 23+ (M+) style permissions. If that is already approved or * mGpsPermissionApproved covers 23+ (M+) style permissions. If that is already approved or
@@ -464,6 +471,11 @@ public class WearableMainActivity extends WearableActivity implements
&& (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
Log.i(TAG, "GPS permission granted."); Log.i(TAG, "GPS permission granted.");
mGpsPermissionApproved = true; mGpsPermissionApproved = true;
if(mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
requestLocation();
}
} else { } else {
Log.i(TAG, "GPS permission NOT granted."); Log.i(TAG, "GPS permission NOT granted.");
mGpsPermissionApproved = false; mGpsPermissionApproved = false;

View File

@@ -1,75 +0,0 @@
/*
* Copyright (C) 2014 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.wearable.speedtracker.ui;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.TextView;
import com.example.android.wearable.speedtracker.R;
/**
* A simple activity that allows the user to start or stop recording of GPS location data.
*/
public class LocationSettingActivity extends Activity {
private static final String PREFS_KEY_SAVE_GPS = "save-gps";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.saving_activity);
TextView textView = (TextView) findViewById(R.id.textView);
textView.setText(getGpsRecordingStatusFromPreferences(this) ? R.string.stop_saving_gps
: R.string.start_saving_gps);
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.submitBtn:
saveGpsRecordingStatusToPreferences(LocationSettingActivity.this,
!getGpsRecordingStatusFromPreferences(this));
break;
case R.id.cancelBtn:
break;
}
finish();
}
/**
* Get the persisted value for whether the app should record the GPS location data or not. If
* there is no prior value persisted, it returns {@code false}.
*/
public static boolean getGpsRecordingStatusFromPreferences(Context context) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
return pref.getBoolean(PREFS_KEY_SAVE_GPS, false);
}
/**
* Persists the user selection to whether save the GPS location data or not.
*/
public static void saveGpsRecordingStatusToPreferences(Context context, boolean value) {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
pref.edit().putBoolean(PREFS_KEY_SAVE_GPS, value).apply();
}
}

View File

@@ -1,105 +0,0 @@
/*
* 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 MyCloudFragment extends Fragment {
private static final String TAG = "MyCloudFragment";
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);
}
}

View File

@@ -42,8 +42,10 @@
</activity> </activity>
<service android:name=".DismissListener"> <service android:name=".DismissListener">
<intent-filter> <intent-filter>
<action <!-- listeners receive events that match the action and data filters -->
android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<!-- filters by Constants.BOTH_PATH ('/both') -->
<data android:scheme="wear" android:host="*" android:pathPrefix="/both"/>
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action <action

View File

@@ -41,8 +41,13 @@
</activity> </activity>
<service android:name=".NotificationUpdateService"> <service android:name=".NotificationUpdateService">
<intent-filter> <intent-filter>
<action <!-- listeners receive events that match the action and data filters -->
android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<!-- filters by Constants.BOTH_PATH ('/both') and
Constants.WATCH_ONLY_PATH ('/watch-only') -->
<data android:scheme="wear" android:host="*" android:pathPrefix="/both"/>
<data android:scheme="wear" android:host="*" android:pathPrefix="/watch-only"/>;
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action <action

View File

@@ -132,6 +132,42 @@
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".ComplicationSimpleWatchFaceService"
android:enabled="true"
android:label="@string/complication_simple"
android:permission="android.permission.BIND_WALLPAPER">
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/watch_face"/>
<meta-data
android:name="com.google.android.wearable.watchface.preview"
android:resource="@drawable/preview_complication_simple"/>
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/preview_complication_simple"/>
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
android:value="com.example.android.wearable.watchface.CONFIG_COMPLICATION_SIMPLE"/>
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService"/>
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE"/>
</intent-filter>
</service>
<activity
android:name=".ComplicationSimpleConfigActivity"
android:label="@string/complication_simple">
<intent-filter>
<action android:name="com.example.android.wearable.watchface.CONFIG_COMPLICATION_SIMPLE"/>
<category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<service <service
android:name=".InteractiveWatchFaceService" android:name=".InteractiveWatchFaceService"
android:label="@string/interactive_name" android:label="@string/interactive_name"
@@ -218,7 +254,8 @@
</service> </service>
<service android:name=".DigitalWatchFaceConfigListenerService" > <service android:name=".DigitalWatchFaceConfigListenerService" >
<intent-filter> <intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data android:scheme="wear" android:host="*" android:pathPrefix="/"/>
</intent-filter> </intent-filter>
</service> </service>
<service <service
@@ -269,6 +306,35 @@
android:name=".CalendarWatchFacePermissionActivity" android:name=".CalendarWatchFacePermissionActivity"
android:label="@string/title_activity_calendar_watch_face_permission" > android:label="@string/title_activity_calendar_watch_face_permission" >
</activity> </activity>
<service
android:name=".provider.RandomNumberProviderService"
android:label="@string/complications_provider_random_number"
android:icon="@drawable/ic_launcher">
<intent-filter>
<action android:name="android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
</intent-filter>
<meta-data
android:name="android.support.wearable.complications.SUPPORTED_TYPES"
android:value="RANGED_VALUE,SHORT_TEXT,LONG_TEXT"/>
<!--
When your complication data provider is active, UPDATE_PERIOD_SECONDS specifies how
often you want the system to check for updates to the data. In this case, the time is
specified to a relatively short 120 seconds, so we can observe the result of this code
lab. In everyday use, developers should consider intervals in the order of minutes.
Also, remember that this is only a guidance for the system. Android Wear may update less
frequently.
If your app needs to push updates instead of updating on a regular schedule, you should
set this value to 0 and use ProviderUpdateRequester.requestUpdate() to trigger an update
request when you need one.
-->
<meta-data
android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
android:value="120"/>
</service>
</application> </application>
</manifest> </manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/dark_grey"
android:paddingTop="4dp">
<TextView
android:id="@+id/header"
android:layout_width="match_parent"
android:gravity="center"
android:text="@string/complication_simple_config_name"
android:textSize="20sp"
android:layout_height="wrap_content"/>
<android.support.wearable.view.WearableListView
android:id="@+id/wearable_list"
android:layout_height="wrap_content"
android:layout_width="match_parent">
</android.support.wearable.view.WearableListView>
</LinearLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textColor="@color/white"
android:textSize="18sp"/>
</LinearLayout>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string-array name="complication_simple_names">
<item>Left dial</item>
<item>Right dial</item>
</string-array>
<array name="complication_simple_icons">
<item>@drawable/complications_left_dial</item>
<item>@drawable/complications_right_dial</item>
</array>
</resources>

View File

@@ -18,6 +18,7 @@
<string name="opengl_name">Sample OpenGL</string> <string name="opengl_name">Sample OpenGL</string>
<string name="interactive_name">Sample Interactive</string> <string name="interactive_name">Sample Interactive</string>
<string name="analog_name">Sample Analog</string> <string name="analog_name">Sample Analog</string>
<string name="complication_simple">Sample Complication Simple</string>
<string name="sweep_name">Sample Sweep</string> <string name="sweep_name">Sample Sweep</string>
<string name="card_bounds_name">Sample Card Bounds</string> <string name="card_bounds_name">Sample Card Bounds</string>
<string name="digital_name">Sample Digital</string> <string name="digital_name">Sample Digital</string>
@@ -42,6 +43,9 @@
<string name="title_activity_calendar_watch_face_permission">Calendar Permission Activity</string> <string name="title_activity_calendar_watch_face_permission">Calendar Permission Activity</string>
<string name="calendar_permission_text">WatchFace requires Calendar access.</string> <string name="calendar_permission_text">WatchFace requires Calendar access.</string>
<string name="complication_simple_config_name">Configuration</string>
<string name="complications_provider_random_number">Random Number</string>
<!-- TODO: this should be shared (needs covering all the samples with Gradle build model) --> <!-- TODO: this should be shared (needs covering all the samples with Gradle build model) -->
<string name="color_black">Black</string> <string name="color_black">Black</string>
<string name="color_blue">Blue</string> <string name="color_blue">Blue</string>

View File

@@ -0,0 +1,191 @@
/*
* 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.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.wearable.complications.ProviderChooserIntent;
import android.support.wearable.view.WearableListView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
/**
* The watch-side config activity for {@link ComplicationSimpleWatchFaceService}, which
* allows for setting complications on the left and right of watch face.
*/
public class ComplicationSimpleConfigActivity extends Activity implements
WearableListView.ClickListener {
private static final String TAG = "CompSimpleConfig";
private static final int PROVIDER_CHOOSER_REQUEST_CODE = 1;
private WearableListView mWearableConfigListView;
private ConfigurationAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_complication_simple_config);
mAdapter = new ConfigurationAdapter(getApplicationContext(), getComplicationItems());
mWearableConfigListView = (WearableListView) findViewById(R.id.wearable_list);
mWearableConfigListView.setAdapter(mAdapter);
mWearableConfigListView.setClickListener(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == PROVIDER_CHOOSER_REQUEST_CODE
&& resultCode == RESULT_OK) {
finish();
}
}
@Override
public void onClick(WearableListView.ViewHolder viewHolder) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onClick()");
}
Integer tag = (Integer) viewHolder.itemView.getTag();
ComplicationItem complicationItem = mAdapter.getItem(tag);
startActivityForResult(ProviderChooserIntent.createProviderChooserIntent(
complicationItem.watchFace,
complicationItem.complicationId,
complicationItem.supportedTypes), PROVIDER_CHOOSER_REQUEST_CODE);
}
private List<ComplicationItem> getComplicationItems() {
ComponentName watchFace = new ComponentName(
getApplicationContext(), ComplicationSimpleWatchFaceService.class);
String[] complicationNames =
getResources().getStringArray(R.array.complication_simple_names);
int[] complicationIds = ComplicationSimpleWatchFaceService.COMPLICATION_IDS;
TypedArray icons = getResources().obtainTypedArray(R.array.complication_simple_icons);
List<ComplicationItem> items = new ArrayList<>();
for (int i = 0; i < complicationIds.length; i++) {
items.add(new ComplicationItem(watchFace,
complicationIds[i],
ComplicationSimpleWatchFaceService.COMPLICATION_SUPPORTED_TYPES[i],
icons.getDrawable(i),
complicationNames[i]));
}
return items;
}
@Override
public void onTopEmptyRegionClick() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onTopEmptyRegionClick()");
}
}
/*
* Inner class representing items of the ConfigurationAdapter (WearableListView.Adapter) class.
*/
private final class ComplicationItem {
ComponentName watchFace;
int complicationId;
int[] supportedTypes;
Drawable icon;
String title;
public ComplicationItem(ComponentName watchFace, int complicationId, int[] supportedTypes,
Drawable icon, String title) {
this.watchFace = watchFace;
this.complicationId = complicationId;
this.supportedTypes = supportedTypes;
this.icon = icon;
this.title = title;
}
}
private static class ConfigurationAdapter extends WearableListView.Adapter {
private Context mContext;
private final LayoutInflater mInflater;
private List<ComplicationItem> mItems;
public ConfigurationAdapter (Context context, List<ComplicationItem> items) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
mItems = items;
}
// Provides a reference to the type of views you're using
public static class ItemViewHolder extends WearableListView.ViewHolder {
private ImageView iconImageView;
private TextView textView;
public ItemViewHolder(View itemView) {
super(itemView);
iconImageView = (ImageView) itemView.findViewById(R.id.icon);
textView = (TextView) itemView.findViewById(R.id.name);
}
}
@Override
public WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Inflate custom layout for list items.
return new ItemViewHolder(
mInflater.inflate(R.layout.activity_complication_simple_list_item, null));
}
@Override
public void onBindViewHolder(WearableListView.ViewHolder holder, int position) {
ItemViewHolder itemHolder = (ItemViewHolder) holder;
ImageView imageView = itemHolder.iconImageView;
imageView.setImageDrawable(mItems.get(position).icon);
TextView textView = itemHolder.textView;
textView.setText(mItems.get(position).title);
holder.itemView.setTag(position);
}
@Override
public int getItemCount() {
return mItems.size();
}
public ComplicationItem getItem(int position) {
return mItems.get(position);
}
}
}

View File

@@ -0,0 +1,744 @@
/*
* 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.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
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.v7.graphics.Palette;
import android.support.wearable.complications.ComplicationData;
import android.support.wearable.complications.ComplicationText;
import android.support.wearable.watchface.CanvasWatchFaceService;
import android.support.wearable.watchface.WatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.SurfaceHolder;
import java.util.Calendar;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
/**
* Demonstrates two simple complications in a watch face.
*/
public class ComplicationSimpleWatchFaceService extends CanvasWatchFaceService {
private static final String TAG = "SimpleComplicationWF";
// Unique IDs for each complication.
private static final int LEFT_DIAL_COMPLICATION = 0;
private static final int RIGHT_DIAL_COMPLICATION = 1;
// Left and right complication IDs as array for Complication API.
public static final int[] COMPLICATION_IDS = {LEFT_DIAL_COMPLICATION, RIGHT_DIAL_COMPLICATION};
// Left and right dial supported types.
public static final int[][] COMPLICATION_SUPPORTED_TYPES = {
{ComplicationData.TYPE_SHORT_TEXT},
{ComplicationData.TYPE_SHORT_TEXT}
};
/*
* Update rate in milliseconds for interactive mode. We update once a second to advance the
* second hand.
*/
private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);
@Override
public Engine onCreateEngine() {
return new Engine();
}
private class Engine extends CanvasWatchFaceService.Engine {
private static final int MSG_UPDATE_TIME = 0;
private static final float COMPLICATION_TEXT_SIZE = 38f;
private static final int COMPLICATION_TAP_BUFFER = 40;
private static final float HOUR_STROKE_WIDTH = 5f;
private static final float MINUTE_STROKE_WIDTH = 3f;
private static final float SECOND_TICK_STROKE_WIDTH = 2f;
private static final float CENTER_GAP_AND_CIRCLE_RADIUS = 4f;
private static final int SHADOW_RADIUS = 6;
private Calendar mCalendar;
private boolean mRegisteredTimeZoneReceiver = false;
private boolean mMuteMode;
private int mWidth;
private int mHeight;
private float mCenterX;
private float mCenterY;
private float mSecondHandLength;
private float mMinuteHandLength;
private float mHourHandLength;
// Colors for all hands (hour, minute, seconds, ticks) based on photo loaded.
private int mWatchHandColor;
private int mWatchHandHighlightColor;
private int mWatchHandShadowColor;
private Paint mHourPaint;
private Paint mMinutePaint;
private Paint mSecondPaint;
private Paint mTickAndCirclePaint;
private Paint mBackgroundPaint;
private Bitmap mBackgroundBitmap;
private Bitmap mGrayBackgroundBitmap;
// Variables for painting Complications
private Paint mComplicationPaint;
/* To properly place each complication, we need their x and y coordinates. While the width
* may change from moment to moment based on the time, the height will not change, so we
* store it as a local variable and only calculate it only when the surface changes
* (onSurfaceChanged()).
*/
private int mComplicationsY;
/* Maps active complication ids to the data for that complication. Note: Data will only be
* present if the user has chosen a provider via the settings activity for the watch face.
*/
private SparseArray<ComplicationData> mActiveComplicationDataSparseArray;
private boolean mAmbient;
private boolean mLowBitAmbient;
private boolean mBurnInProtection;
private Rect mPeekCardBounds = new Rect();
private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mCalendar.setTimeZone(TimeZone.getDefault());
invalidate();
}
};
// Handler to update the time once a second in interactive mode.
private final Handler mUpdateTimeHandler = new Handler() {
@Override
public void handleMessage(Message message) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "updating time");
}
invalidate();
if (shouldTimerBeRunning()) {
long timeMs = System.currentTimeMillis();
long delayMs = INTERACTIVE_UPDATE_RATE_MS
- (timeMs % INTERACTIVE_UPDATE_RATE_MS);
mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
}
}
};
@Override
public void onCreate(SurfaceHolder holder) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCreate");
}
super.onCreate(holder);
mCalendar = Calendar.getInstance();
setWatchFaceStyle(new WatchFaceStyle.Builder(ComplicationSimpleWatchFaceService.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
.setAcceptsTapEvents(true)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.build());
initializeBackground();
initializeComplication();
initializeWatchFace();
}
private void initializeBackground() {
mBackgroundPaint = new Paint();
mBackgroundPaint.setColor(Color.BLACK);
mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg);
}
private void initializeComplication() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "initializeComplications()");
}
mActiveComplicationDataSparseArray = new SparseArray<>(COMPLICATION_IDS.length);
mComplicationPaint = new Paint();
mComplicationPaint.setColor(Color.WHITE);
mComplicationPaint.setTextSize(COMPLICATION_TEXT_SIZE);
mComplicationPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
mComplicationPaint.setAntiAlias(true);
setActiveComplications(COMPLICATION_IDS);
}
private void initializeWatchFace() {
/* Set defaults for colors */
mWatchHandColor = Color.WHITE;
mWatchHandHighlightColor = Color.RED;
mWatchHandShadowColor = Color.BLACK;
mHourPaint = new Paint();
mHourPaint.setColor(mWatchHandColor);
mHourPaint.setStrokeWidth(HOUR_STROKE_WIDTH);
mHourPaint.setAntiAlias(true);
mHourPaint.setStrokeCap(Paint.Cap.ROUND);
mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mMinutePaint = new Paint();
mMinutePaint.setColor(mWatchHandColor);
mMinutePaint.setStrokeWidth(MINUTE_STROKE_WIDTH);
mMinutePaint.setAntiAlias(true);
mMinutePaint.setStrokeCap(Paint.Cap.ROUND);
mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mSecondPaint = new Paint();
mSecondPaint.setColor(mWatchHandHighlightColor);
mSecondPaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
mSecondPaint.setAntiAlias(true);
mSecondPaint.setStrokeCap(Paint.Cap.ROUND);
mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mTickAndCirclePaint = new Paint();
mTickAndCirclePaint.setColor(mWatchHandColor);
mTickAndCirclePaint.setStrokeWidth(SECOND_TICK_STROKE_WIDTH);
mTickAndCirclePaint.setAntiAlias(true);
mTickAndCirclePaint.setStyle(Paint.Style.STROKE);
mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
// Asynchronous call extract colors from background image to improve watch face style.
Palette.from(mBackgroundBitmap).generate(
new Palette.PaletteAsyncListener() {
public void onGenerated(Palette palette) {
/*
* Sometimes, palette is unable to generate a color palette
* so we need to check that we have one.
*/
if (palette != null) {
Log.d("onGenerated", palette.toString());
mWatchHandColor = palette.getVibrantColor(Color.WHITE);
mWatchHandShadowColor = palette.getDarkMutedColor(Color.BLACK);
updateWatchHandStyle();
}
}
});
}
@Override
public void onDestroy() {
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
super.onDestroy();
}
@Override
public void onPropertiesChanged(Bundle properties) {
super.onPropertiesChanged(properties);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
}
mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
}
/*
* Called when there is updated data for a complication id.
*/
@Override
public void onComplicationDataUpdate(
int complicationId, ComplicationData complicationData) {
Log.d(TAG, "onComplicationDataUpdate() id: " + complicationId);
// Adds/updates active complication data in the array.
mActiveComplicationDataSparseArray.put(complicationId, complicationData);
invalidate();
}
@Override
public void onTapCommand(int tapType, int x, int y, long eventTime) {
Log.d(TAG, "OnTapCommand()");
switch (tapType) {
case TAP_TYPE_TAP:
int tappedComplicationId = getTappedComplicationId(x, y);
if (tappedComplicationId != -1) {
onComplicationTap(tappedComplicationId);
}
break;
}
}
/*
* Determines if tap inside a complication area or returns -1.
*/
private int getTappedComplicationId(int x, int y) {
ComplicationData complicationData;
long currentTimeMillis = System.currentTimeMillis();
for (int i = 0; i < COMPLICATION_IDS.length; i++) {
complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]);
if ((complicationData != null)
&& (complicationData.isActive(currentTimeMillis))
&& (complicationData.getType() != ComplicationData.TYPE_NOT_CONFIGURED)
&& (complicationData.getType() != ComplicationData.TYPE_EMPTY)) {
Rect complicationBoundingRect = new Rect(0, 0, 0, 0);
switch (COMPLICATION_IDS[i]) {
case LEFT_DIAL_COMPLICATION:
complicationBoundingRect.set(
0, // left
mComplicationsY - COMPLICATION_TAP_BUFFER, // top
(mWidth / 2), // right
((int) COMPLICATION_TEXT_SIZE // bottom
+ mComplicationsY
+ COMPLICATION_TAP_BUFFER));
break;
case RIGHT_DIAL_COMPLICATION:
complicationBoundingRect.set(
(mWidth / 2), // left
mComplicationsY - COMPLICATION_TAP_BUFFER, // top
mWidth, // right
((int) COMPLICATION_TEXT_SIZE // bottom
+ mComplicationsY
+ COMPLICATION_TAP_BUFFER));
break;
}
if (complicationBoundingRect.width() > 0) {
if (complicationBoundingRect.contains(x, y)) {
return COMPLICATION_IDS[i];
}
} else {
Log.e(TAG, "Not a recognized complication id.");
}
}
}
return -1;
}
/*
* Fires PendingIntent associated with complication (if it has one).
*/
private void onComplicationTap(int complicationId) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onComplicationTap()");
}
ComplicationData complicationData =
mActiveComplicationDataSparseArray.get(complicationId);
if ((complicationData != null) && (complicationData.getTapAction() != null)) {
try {
complicationData.getTapAction().send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "On complication tap action error " + e);
}
invalidate();
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "No PendingIntent for complication " + complicationId + ".");
}
}
}
@Override
public void onTimeTick() {
super.onTimeTick();
invalidate();
}
@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
}
mAmbient = inAmbientMode;
updateWatchHandStyle();
// Updates complication style
mComplicationPaint.setAntiAlias(!inAmbientMode);
// Check and trigger whether or not timer should be running (only in active mode).
updateTimer();
}
private void updateWatchHandStyle(){
if (mAmbient){
mHourPaint.setColor(Color.WHITE);
mMinutePaint.setColor(Color.WHITE);
mSecondPaint.setColor(Color.WHITE);
mTickAndCirclePaint.setColor(Color.WHITE);
mHourPaint.setAntiAlias(false);
mMinutePaint.setAntiAlias(false);
mSecondPaint.setAntiAlias(false);
mTickAndCirclePaint.setAntiAlias(false);
mHourPaint.clearShadowLayer();
mMinutePaint.clearShadowLayer();
mSecondPaint.clearShadowLayer();
mTickAndCirclePaint.clearShadowLayer();
} else {
mHourPaint.setColor(mWatchHandColor);
mMinutePaint.setColor(mWatchHandColor);
mSecondPaint.setColor(mWatchHandHighlightColor);
mTickAndCirclePaint.setColor(mWatchHandColor);
mHourPaint.setAntiAlias(true);
mMinutePaint.setAntiAlias(true);
mSecondPaint.setAntiAlias(true);
mTickAndCirclePaint.setAntiAlias(true);
mHourPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mMinutePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mSecondPaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
mTickAndCirclePaint.setShadowLayer(SHADOW_RADIUS, 0, 0, mWatchHandShadowColor);
}
}
@Override
public void onInterruptionFilterChanged(int interruptionFilter) {
super.onInterruptionFilterChanged(interruptionFilter);
boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
/* Dim display in mute mode. */
if (mMuteMode != inMuteMode) {
mMuteMode = inMuteMode;
mHourPaint.setAlpha(inMuteMode ? 100 : 255);
mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
mSecondPaint.setAlpha(inMuteMode ? 80 : 255);
invalidate();
}
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
super.onSurfaceChanged(holder, format, width, height);
// Used for complications
mWidth = width;
mHeight = height;
/*
* Find the coordinates of the center point on the screen, and ignore the window
* insets, so that, on round watches with a "chin", the watch face is centered on the
* entire screen, not just the usable portion.
*/
mCenterX = mWidth / 2f;
mCenterY = mHeight / 2f;
/*
* Since the height of the complications text does not change, we only have to
* recalculate when the surface changes.
*/
mComplicationsY = (int) ((mHeight / 2) + (mComplicationPaint.getTextSize() / 2));
/*
* Calculate lengths of different hands based on watch screen size.
*/
mSecondHandLength = (float) (mCenterX * 0.875);
mMinuteHandLength = (float) (mCenterX * 0.75);
mHourHandLength = (float) (mCenterX * 0.5);
/* Scale loaded background image (more efficient) if surface dimensions change. */
float scale = ((float) width) / (float) mBackgroundBitmap.getWidth();
mBackgroundBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
(int) (mBackgroundBitmap.getWidth() * scale),
(int) (mBackgroundBitmap.getHeight() * scale), true);
/*
* Create a gray version of the image only if it will look nice on the device in
* ambient mode. That means we don't want devices that support burn-in
* protection (slight movements in pixels, not great for images going all the way to
* edges) and low ambient mode (degrades image quality).
*
* Also, if your watch face will know about all images ahead of time (users aren't
* selecting their own photos for the watch face), it will be more
* efficient to create a black/white version (png, etc.) and load that when you need it.
*/
if (!mBurnInProtection && !mLowBitAmbient) {
initGrayBackgroundBitmap();
}
}
private void initGrayBackgroundBitmap() {
mGrayBackgroundBitmap = Bitmap.createBitmap(
mBackgroundBitmap.getWidth(),
mBackgroundBitmap.getHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mGrayBackgroundBitmap);
Paint grayPaint = new Paint();
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(0);
ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
grayPaint.setColorFilter(filter);
canvas.drawBitmap(mBackgroundBitmap, 0, 0, grayPaint);
}
@Override
public void onDraw(Canvas canvas, Rect bounds) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onDraw");
}
long now = System.currentTimeMillis();
mCalendar.setTimeInMillis(now);
drawBackground(canvas);
drawComplications(canvas, now);
drawWatchFace(canvas);
}
private void drawBackground(Canvas canvas) {
if (mAmbient && (mLowBitAmbient || mBurnInProtection)) {
canvas.drawColor(Color.BLACK);
} else if (mAmbient) {
canvas.drawBitmap(mGrayBackgroundBitmap, 0, 0, mBackgroundPaint);
} else {
canvas.drawBitmap(mBackgroundBitmap, 0, 0, mBackgroundPaint);
}
}
private void drawComplications(Canvas canvas, long currentTimeMillis) {
ComplicationData complicationData;
for (int i = 0; i < COMPLICATION_IDS.length; i++) {
complicationData = mActiveComplicationDataSparseArray.get(COMPLICATION_IDS[i]);
if ((complicationData != null)
&& (complicationData.isActive(currentTimeMillis))
&& (complicationData.getType() == ComplicationData.TYPE_SHORT_TEXT)) {
ComplicationText mainText = complicationData.getShortText();
ComplicationText subText = complicationData.getShortTitle();
CharSequence complicationMessage =
mainText.getText(getApplicationContext(), currentTimeMillis);
/* In most cases you would want the subText (Title) under the mainText (Text),
* but to keep it simple for the code lab, we are concatenating them all on one
* line.
*/
if (subText != null) {
complicationMessage = TextUtils.concat(
complicationMessage,
" ",
subText.getText(getApplicationContext(), currentTimeMillis));
}
//Log.d(TAG, "Comp id: " + COMPLICATION_IDS[i] + "\t" + complicationMessage);
double textWidth =
mComplicationPaint.measureText(
complicationMessage,
0,
complicationMessage.length());
int complicationsX;
if (COMPLICATION_IDS[i] == LEFT_DIAL_COMPLICATION) {
complicationsX = (int) ((mWidth / 2) - textWidth) / 2;
} else {
// RIGHT_DIAL_COMPLICATION calculations
int offset = (int) ((mWidth / 2) - textWidth) / 2;
complicationsX = (mWidth / 2) + offset;
}
canvas.drawText(
complicationMessage,
0,
complicationMessage.length(),
complicationsX,
mComplicationsY,
mComplicationPaint);
}
}
}
private void drawWatchFace(Canvas canvas) {
/*
* Draw ticks. Usually you will want to bake this directly into the photo, but in
* cases where you want to allow users to select their own photos, this dynamically
* creates them on top of the photo.
*/
float innerTickRadius = mCenterX - 10;
float outerTickRadius = mCenterX;
for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
float innerX = (float) Math.sin(tickRot) * innerTickRadius;
float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
float outerX = (float) Math.sin(tickRot) * outerTickRadius;
float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
canvas.drawLine(mCenterX + innerX, mCenterY + innerY,
mCenterX + outerX, mCenterY + outerY, mTickAndCirclePaint);
}
/*
* These calculations reflect the rotation in degrees per unit of time, e.g.,
* 360 / 60 = 6 and 360 / 12 = 30.
*/
final float seconds =
(mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f);
final float secondsRotation = seconds * 6f;
final float minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f;
final float hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f;
final float hoursRotation = (mCalendar.get(Calendar.HOUR) * 30) + hourHandOffset;
/*
* Save the canvas state before we can begin to rotate it.
*/
canvas.save();
canvas.rotate(hoursRotation, mCenterX, mCenterY);
canvas.drawLine(
mCenterX,
mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
mCenterX,
mCenterY - mHourHandLength,
mHourPaint);
canvas.rotate(minutesRotation - hoursRotation, mCenterX, mCenterY);
canvas.drawLine(
mCenterX,
mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
mCenterX,
mCenterY - mMinuteHandLength,
mMinutePaint);
/*
* Ensure the "seconds" hand is drawn only when we are in interactive mode.
* Otherwise, we only update the watch face once a minute.
*/
if (!mAmbient) {
canvas.rotate(secondsRotation - minutesRotation, mCenterX, mCenterY);
canvas.drawLine(
mCenterX,
mCenterY - CENTER_GAP_AND_CIRCLE_RADIUS,
mCenterX,
mCenterY - mSecondHandLength,
mSecondPaint);
}
canvas.drawCircle(
mCenterX,
mCenterY,
CENTER_GAP_AND_CIRCLE_RADIUS,
mTickAndCirclePaint);
/* Restore the canvas' original orientation. */
canvas.restore();
/* Draw rectangle behind peek card in ambient mode to improve readability. */
if (mAmbient) {
canvas.drawRect(mPeekCardBounds, mBackgroundPaint);
}
}
@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);
if (visible) {
registerReceiver();
// Update time zone in case it changed while we weren't visible.
mCalendar.setTimeZone(TimeZone.getDefault());
invalidate();
} else {
unregisterReceiver();
}
/* Check and trigger whether or not timer should be running (only in active mode). */
updateTimer();
}
@Override
public void onPeekCardPositionUpdate(Rect rect) {
super.onPeekCardPositionUpdate(rect);
mPeekCardBounds.set(rect);
}
private void registerReceiver() {
if (mRegisteredTimeZoneReceiver) {
return;
}
mRegisteredTimeZoneReceiver = true;
IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
ComplicationSimpleWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
}
private void unregisterReceiver() {
if (!mRegisteredTimeZoneReceiver) {
return;
}
mRegisteredTimeZoneReceiver = false;
ComplicationSimpleWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
}
/**
* Starts/stops the {@link #mUpdateTimeHandler} timer based on the state of the watch face.
*/
private void updateTimer() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "updateTimer");
}
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
if (shouldTimerBeRunning()) {
mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
}
}
/**
* Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer
* should only run in active mode.
*/
private boolean shouldTimerBeRunning() {
return isVisible() && !mAmbient;
}
}
}

View File

@@ -40,6 +40,11 @@ public class DigitalWatchFaceConfigListenerService extends WearableListenerServi
@Override // WearableListenerService @Override // WearableListenerService
public void onMessageReceived(MessageEvent messageEvent) { public void onMessageReceived(MessageEvent messageEvent) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onMessageReceived: " + messageEvent);
}
if (!messageEvent.getPath().equals(DigitalWatchFaceUtil.PATH_WITH_FEATURE)) { if (!messageEvent.getPath().equals(DigitalWatchFaceUtil.PATH_WITH_FEATURE)) {
return; return;
} }

View File

@@ -62,7 +62,10 @@ import java.util.concurrent.TimeUnit;
* *
* Authentication IS a requirement to request distance from Google Fit on Wear. Otherwise, distance * 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 * will always come back as zero (or stay at whatever the distance was prior to you
* de-authorizing watchface). * de-authorizing watchface). To authenticate and communicate with Google Fit, you must create a
* project in the Google Developers Console, activate the Fitness API, create an OAuth 2.0
* client ID, and register the public certificate from your app's signed APK. More details can be
* found here: https://developers.google.com/fit/android/get-started#step_3_enable_the_fitness_api
* *
* In ambient mode, the seconds are replaced with an AM/PM indicator. * In ambient mode, the seconds are replaced with an AM/PM indicator.
* *

View File

@@ -0,0 +1,96 @@
package com.example.android.wearable.watchface.provider;
import android.support.wearable.complications.ComplicationData;
import android.support.wearable.complications.ComplicationManager;
import android.support.wearable.complications.ComplicationProviderService;
import android.support.wearable.complications.ComplicationText;
import android.util.Log;
import java.util.Locale;
/**
* Example Watch Face Complication data provider provides a random number on every update.
*/
public class RandomNumberProviderService extends ComplicationProviderService {
private static final String TAG = "RandomNumberProvider";
/*
* Called when a complication has been activated. The method is for any one-time
* (per complication) set-up.
*
* You can continue sending data for the active complicationId until onComplicationDeactivated()
* is called.
*/
@Override
public void onComplicationActivated(
int complicationId, int dataType, ComplicationManager complicationManager) {
Log.d(TAG, "onComplicationActivated(): " + complicationId);
super.onComplicationActivated(complicationId, dataType, complicationManager);
}
/*
* Called when the complication needs updated data from your provider. There are four scenarios
* when this will happen:
*
* 1. An active watch face complication is changed to use this provider
* 2. A complication using this provider becomes active
* 3. The period of time you specified in the manifest has elapsed (UPDATE_PERIOD_SECONDS)
* 4. You triggered an update from your own class via the
* ProviderUpdateRequester.requestUpdate() method.
*/
@Override
public void onComplicationUpdate(
int complicationId, int dataType, ComplicationManager complicationManager) {
Log.d(TAG, "onComplicationUpdate()");
// Retrieve your data, in this case, we simply create a random number to display.
int randomNumber = (int) Math.floor(Math.random() * 10);
String randomNumberText =
String.format(Locale.getDefault(), "%d!", randomNumber);
ComplicationData complicationData = null;
switch (dataType) {
case ComplicationData.TYPE_RANGED_VALUE:
complicationData = new ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE)
.setValue(randomNumber)
.setMinValue(0)
.setMaxValue(10)
.setShortText(ComplicationText.plainText(randomNumberText))
.build();
break;
case ComplicationData.TYPE_SHORT_TEXT:
complicationData = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText(randomNumberText))
.build();
break;
case ComplicationData.TYPE_LONG_TEXT:
complicationData = new ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
.setLongText(
ComplicationText.plainText("Random Number: " + randomNumberText))
.build();
break;
default:
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unexpected complication type " + dataType);
}
}
if (complicationData != null) {
complicationManager.updateComplicationData(complicationId, complicationData);
}
}
/*
* Called when the complication has been deactivated. If you are updating the complication
* manager outside of this class with updates, you will want to update your class to stop.
*/
@Override
public void onComplicationDeactivated(int complicationId) {
Log.d(TAG, "onComplicationDeactivated(): " + complicationId);
super.onComplicationDeactivated(complicationId);
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.example.android.wearable.wear.weardrawers"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.type.watch"/>
<!-- Required for Always-on. -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<!--If you want your app to run on pre-22, then set required to false -->
<uses-library android:name="com.google.android.wearable" android:required="false" />
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,10 @@
page.tags="WearDrawers"
sample.group=Wearable
@jd:body
<p>
A simple sample that demonstrates Navigation and Action Drawers, part of Material Design for Wear.
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

View File

@@ -0,0 +1,20 @@
<!--
Copyright 2016 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.
-->
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"/>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/menu_planet_name"
android:icon="@drawable/ic_info_outline_black_18dp"
android:title="Planet Name"/>
<item android:id="@+id/menu_number_of_moons"
android:icon="@drawable/ic_info_outline_black_18dp"
android:title="Number of Moons" />
<item android:id="@+id/menu_volume"
android:icon="@drawable/ic_info_outline_black_18dp"
android:title="Volume" />
<item android:id="@+id/menu_surface_area"
android:icon="@drawable/ic_info_outline_black_18dp"
android:title="Surface Area" />
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,114 @@
<!--
Copyright 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="app_name">Wearable Drawer Layout</string>
<!--
Reference name for each planet's information array.
-->
<string-array name="planets_array_names">
<item>mercury</item>
<item>venus</item>
<item>earth</item>
<item>mars</item>
<item>jupiter</item>
<item>saturn</item>
<item>uranus</item>
<item>neptune</item>
</string-array>
<!--
Keys for planet array information below:
"Planet Name",
"Navigation icon",
"Drawable for planet"
"Number of Moons",
"Volume"
"Surface Area"
-->
<string-array name="mercury">
<item>Mercury</item>
<item>ic_m_white_48dp</item>
<item>mercury</item>
<item>0 Moons</item>
<item>0.056 x Earth</item>
<item>0.147 x Earth</item>
</string-array>
<string-array name="venus">
<item>Venus</item>
<item>ic_v_white_48dp</item>
<item>venus</item>
<item>0 Moons</item>
<item>0.857 x Earth</item>
<item>0.902 x Earth</item>
</string-array>
<string-array name="earth">
<item>Earth</item>
<item>ic_e_white_48dp</item>
<item>earth</item>
<item>1 moon</item>
<item>1,083,206,916,846 km3</item>
<item>510,064,472 km2</item>
</string-array>
<string-array name="mars">
<item>Mars</item>
<item>ic_m_white_48dp</item>
<item>mars</item>
<item>2 Moons</item>
<item>0.151 x Earth</item>
<item>0.283 x Earth</item>
</string-array>
<string-array name="jupiter">
<item>Jupiter</item>
<item>ic_j_white_48dp</item>
<item>jupiter</item>
<item>67 Moons</item>
<item>1,321.337 x Earth</item>
<item>120.414 x Earth</item>
</string-array>
<string-array name="saturn">
<item>Saturn</item>
<item>ic_s_white_48dp</item>
<item>saturn</item>
<item>62 moons</item>
<item>763.594 x Earth</item>
<item>83.543 x Earth</item>
</string-array>
<string-array name="uranus">
<item>Uranus</item>
<item>ic_u_white_48dp</item>
<item>uranus</item>
<item>27 Moons</item>
<item>63.085 x Earth</item>
<item>15.847 x Earth</item>
</string-array>
<string-array name="neptune">
<item>Neptune</item>
<item>ic_n_white_48dp</item>
<item>neptune</item>
<item>14 Moons</item>
<item>57.723 x Earth</item>
<item>14.980 x Earth</item>
</string-array>
</resources>

View File

@@ -0,0 +1,69 @@
/*
Copyright 2016 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.wear.weardrawers;
/**
* Represents planet for app.
*/
public class Planet {
private String name;
private String navigationIcon;
private String image;
private String moons;
private String volume;
private String surfaceArea;
public Planet(
String name,
String navigationIcon,
String image,
String moons,
String volume,
String surfaceArea) {
this.name = name;
this.navigationIcon = navigationIcon;
this.image = image;
this.moons = moons;
this.volume = volume;
this.surfaceArea = surfaceArea;
}
public String getName() {
return name;
}
public String getNavigationIcon() {
return navigationIcon;
}
public String getImage() {
return image;
}
public String getMoons() {
return moons;
}
public String getVolume() {
return volume;
}
public String getSurfaceArea() {
return surfaceArea;
}
}

View File

@@ -55,7 +55,16 @@
<service android:name=".service.ListenerService"> <service android:name=".service.ListenerService">
<intent-filter> <intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <!-- listeners receive events that match the action and data filters -->
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<!-- filters by Constants.CLEAR_NOTIFICATIONS_PATH -->
<data android:scheme="wear" android:host="*" android:pathPrefix="/clear"/>
<!-- filters by Constants.START_PATH, the prefix for
Constants.START_ATTRACTION_PATH ('/start/attraction') and
Constants.START_NAVIGATION_PATH ('/start/navigation') -->
<data android:scheme="wear" android:host="*" android:pathPrefix="/start"/>
</intent-filter> </intent-filter>
</service> </service>

View File

@@ -44,7 +44,14 @@
<service android:name=".service.ListenerService"> <service android:name=".service.ListenerService">
<intent-filter> <intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" /> <!-- listeners receive events that match the action and data filters -->
<action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
<!-- filters by Constants.EXTRA_ATTRACTIONS -->
<data android:scheme="wear" android:host="*" android:pathPrefix="/attraction"/>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<!-- filters by Constants.CLEAR_NOTIFICATIONS_PATH -->
<data android:scheme="wear" android:host="*" android:pathPrefix="/clear"/>
</intent-filter> </intent-filter>
</service> </service>