Update prebuilts for mnc-docs
developers/build: fb039d88ed8e9183cae9ce510bf9c907d23ecc50
developers/samples/android: 7c3c09dcf092593adb8c13f8e15260f4da0a799a
Note: This was previously committed on May 18 2016, but wasn't
automerged. Replaying commit to bring browseable back into sync
downstream.
(cherry picked from commit 8e7e496ae9)
Bug: 29127946
Change-Id: I6275f830532c3c6ce006756aecc3c2ae7e06873d
@@ -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.DataItem;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -51,7 +49,6 @@ import com.google.android.gms.wearable.Wearable;
|
||||
* permissions as well.
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity implements
|
||||
NodeApi.NodeListener,
|
||||
ConnectionCallbacks,
|
||||
OnConnectionFailedListener,
|
||||
ActivityCompat.OnRequestPermissionsResultCallback {
|
||||
@@ -66,11 +63,11 @@ public class MainActivity extends AppCompatActivity implements
|
||||
private GoogleApiClient mGoogleApiClient;
|
||||
private boolean mResolvingError = false;
|
||||
|
||||
private TextView mLogTextView;
|
||||
ScrollView mScroller;
|
||||
|
||||
private View mLayout;
|
||||
|
||||
private TextView mLogTextView;
|
||||
private ScrollView mScroller;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -98,10 +95,10 @@ public class MainActivity extends AppCompatActivity implements
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
if (mGoogleApiClient.isConnected()) {
|
||||
Wearable.NodeApi.removeListener(mGoogleApiClient, this);
|
||||
if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
|
||||
mGoogleApiClient.disconnect();
|
||||
}
|
||||
mGoogleApiClient.disconnect();
|
||||
|
||||
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.
|
||||
* 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)
|
||||
}
|
||||
|
||||
|
||||
private void pushCalendarToWear() {
|
||||
startService(new Intent(this, CalendarQueryService.class));
|
||||
}
|
||||
|
||||
public void onDeleteEventsClicked(View view) {
|
||||
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
|
||||
public void onConnected(Bundle connectionHint) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Connected to Google Api Service.");
|
||||
}
|
||||
mResolvingError = false;
|
||||
Wearable.NodeApi.addListener(mGoogleApiClient, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionSuspended(int cause) {
|
||||
// Ignore
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "onConnectionSuspended(): Cause id: " + cause);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -274,9 +260,7 @@ public class MainActivity extends AppCompatActivity implements
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Disconnected from Google Api Service");
|
||||
}
|
||||
if (null != Wearable.NodeApi) {
|
||||
Wearable.NodeApi.removeListener(mGoogleApiClient, this);
|
||||
}
|
||||
|
||||
if (mResolvingError) {
|
||||
// Already attempting to resolve an error.
|
||||
return;
|
||||
@@ -359,4 +343,4 @@ public class MainActivity extends AppCompatActivity implements
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
package="com.example.android.wearable.agendadata" >
|
||||
|
||||
<uses-sdk android:minSdkVersion="20"
|
||||
android:targetSdkVersion="22" />
|
||||
android:targetSdkVersion="23" />
|
||||
|
||||
<uses-feature android:name="android.hardware.type.watch" />
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
<service
|
||||
android:name="com.example.android.wearable.agendadata.HomeListenerService" >
|
||||
<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>
|
||||
</service>
|
||||
|
||||
|
||||
@@ -32,4 +32,4 @@ public final class Constants {
|
||||
public static final String PROFILE_PIC = "profile_pic";
|
||||
|
||||
public static final String EXTRA_SILENT = "silent";
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
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.content.Intent;
|
||||
import android.net.Uri;
|
||||
@@ -26,6 +23,9 @@ import android.os.Bundle;
|
||||
import android.support.wearable.activity.ConfirmationActivity;
|
||||
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.api.GoogleApiClient;
|
||||
import com.google.android.gms.wearable.DataApi;
|
||||
@@ -105,14 +105,23 @@ public class DeleteService extends IntentService implements GoogleApiClient.Conn
|
||||
|
||||
@Override
|
||||
public void onConnected(Bundle connectionHint) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "onConnected: " + connectionHint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionSuspended(int cause) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "onConnectionSuspended: " + cause);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailed(ConnectionResult result) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "onConnectionFailed: " + result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,16 +16,6 @@
|
||||
|
||||
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.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
@@ -37,6 +27,15 @@ import android.text.TextUtils;
|
||||
import android.text.format.DateFormat;
|
||||
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.wearable.Asset;
|
||||
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.DataMap;
|
||||
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.WearableListenerService;
|
||||
|
||||
@@ -63,12 +61,22 @@ public class HomeListenerService extends WearableListenerService {
|
||||
private static int sNotificationId = 1;
|
||||
private GoogleApiClient mGoogleApiClient;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mGoogleApiClient = new GoogleApiClient.Builder(this.getApplicationContext())
|
||||
.addApi(Wearable.API)
|
||||
.build();
|
||||
mGoogleApiClient.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChanged(DataEventBuffer dataEvents) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "onDataChanged: " + dataEvents + " for " + getPackageName());
|
||||
}
|
||||
for (DataEvent event : dataEvents) {
|
||||
|
||||
if (event.getType() == DataEvent.TYPE_DELETED) {
|
||||
deleteDataItem(event.getDataItem());
|
||||
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
|
||||
@@ -77,13 +85,17 @@ public class HomeListenerService extends WearableListenerService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mGoogleApiClient = new GoogleApiClient.Builder(this.getApplicationContext())
|
||||
.addApi(Wearable.API)
|
||||
.build();
|
||||
mGoogleApiClient.connect();
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,26 +166,4 @@ public class HomeListenerService extends WearableListenerService {
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,49 +36,57 @@ public class CameraHelper {
|
||||
public static final int MEDIA_TYPE_VIDEO = 2;
|
||||
|
||||
/**
|
||||
* Iterate over supported camera preview sizes to see which one best fits the
|
||||
* Iterate over supported camera video sizes to see which one best fits the
|
||||
* dimensions of the given view while maintaining the aspect ratio. If none can,
|
||||
* be lenient with the aspect ratio.
|
||||
*
|
||||
* @param sizes Supported camera preview sizes.
|
||||
* @param w The width of the view.
|
||||
* @param h The height of the view.
|
||||
* @return Best match camera preview size to fit in the view.
|
||||
* @param supportedVideoSizes Supported camera video sizes.
|
||||
* @param previewSizes Supported camera preview sizes.
|
||||
* @param w The width of the view.
|
||||
* @param h The height of the view.
|
||||
* @return Best match camera video size to fit in the view.
|
||||
*/
|
||||
public static Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
|
||||
public static Camera.Size getOptimalVideoSize(List<Camera.Size> supportedVideoSizes,
|
||||
List<Camera.Size> previewSizes, int w, int h) {
|
||||
// Use a very small tolerance because we want an exact match.
|
||||
final double ASPECT_TOLERANCE = 0.1;
|
||||
double targetRatio = (double) w / h;
|
||||
if (sizes == null)
|
||||
return null;
|
||||
|
||||
// Supported video sizes list might be null, it means that we are allowed to use the preview
|
||||
// sizes
|
||||
List<Camera.Size> videoSizes;
|
||||
if (supportedVideoSizes != null) {
|
||||
videoSizes = supportedVideoSizes;
|
||||
} else {
|
||||
videoSizes = previewSizes;
|
||||
}
|
||||
Camera.Size optimalSize = null;
|
||||
|
||||
// Start with max value and refine as we iterate over available preview sizes. This is the
|
||||
// Start with max value and refine as we iterate over available video sizes. This is the
|
||||
// minimum difference between view and camera height.
|
||||
double minDiff = Double.MAX_VALUE;
|
||||
|
||||
// Target view height
|
||||
int targetHeight = h;
|
||||
|
||||
// Try to find a preview size that matches aspect ratio and the target view size.
|
||||
// Try to find a video size that matches aspect ratio and the target view size.
|
||||
// Iterate over all available sizes and pick the largest size that can fit in the view and
|
||||
// still maintain the aspect ratio.
|
||||
for (Camera.Size size : sizes) {
|
||||
for (Camera.Size size : videoSizes) {
|
||||
double ratio = (double) size.width / size.height;
|
||||
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
|
||||
continue;
|
||||
if (Math.abs(size.height - targetHeight) < minDiff) {
|
||||
if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
|
||||
optimalSize = size;
|
||||
minDiff = Math.abs(size.height - targetHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot find preview size that matches the aspect ratio, ignore the requirement
|
||||
// Cannot find video size that matches the aspect ratio, ignore the requirement
|
||||
if (optimalSize == null) {
|
||||
minDiff = Double.MAX_VALUE;
|
||||
for (Camera.Size size : sizes) {
|
||||
if (Math.abs(size.height - targetHeight) < minDiff) {
|
||||
for (Camera.Size size : videoSizes) {
|
||||
if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
|
||||
optimalSize = size;
|
||||
minDiff = Math.abs(size.height - targetHeight);
|
||||
}
|
||||
|
||||
@@ -36,49 +36,57 @@ public class CameraHelper {
|
||||
public static final int MEDIA_TYPE_VIDEO = 2;
|
||||
|
||||
/**
|
||||
* Iterate over supported camera preview sizes to see which one best fits the
|
||||
* Iterate over supported camera video sizes to see which one best fits the
|
||||
* dimensions of the given view while maintaining the aspect ratio. If none can,
|
||||
* be lenient with the aspect ratio.
|
||||
*
|
||||
* @param sizes Supported camera preview sizes.
|
||||
* @param w The width of the view.
|
||||
* @param h The height of the view.
|
||||
* @return Best match camera preview size to fit in the view.
|
||||
* @param supportedVideoSizes Supported camera video sizes.
|
||||
* @param previewSizes Supported camera preview sizes.
|
||||
* @param w The width of the view.
|
||||
* @param h The height of the view.
|
||||
* @return Best match camera video size to fit in the view.
|
||||
*/
|
||||
public static Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
|
||||
public static Camera.Size getOptimalVideoSize(List<Camera.Size> supportedVideoSizes,
|
||||
List<Camera.Size> previewSizes, int w, int h) {
|
||||
// Use a very small tolerance because we want an exact match.
|
||||
final double ASPECT_TOLERANCE = 0.1;
|
||||
double targetRatio = (double) w / h;
|
||||
if (sizes == null)
|
||||
return null;
|
||||
|
||||
// Supported video sizes list might be null, it means that we are allowed to use the preview
|
||||
// sizes
|
||||
List<Camera.Size> videoSizes;
|
||||
if (supportedVideoSizes != null) {
|
||||
videoSizes = supportedVideoSizes;
|
||||
} else {
|
||||
videoSizes = previewSizes;
|
||||
}
|
||||
Camera.Size optimalSize = null;
|
||||
|
||||
// Start with max value and refine as we iterate over available preview sizes. This is the
|
||||
// Start with max value and refine as we iterate over available video sizes. This is the
|
||||
// minimum difference between view and camera height.
|
||||
double minDiff = Double.MAX_VALUE;
|
||||
|
||||
// Target view height
|
||||
int targetHeight = h;
|
||||
|
||||
// Try to find a preview size that matches aspect ratio and the target view size.
|
||||
// Try to find a video size that matches aspect ratio and the target view size.
|
||||
// Iterate over all available sizes and pick the largest size that can fit in the view and
|
||||
// still maintain the aspect ratio.
|
||||
for (Camera.Size size : sizes) {
|
||||
for (Camera.Size size : videoSizes) {
|
||||
double ratio = (double) size.width / size.height;
|
||||
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
|
||||
continue;
|
||||
if (Math.abs(size.height - targetHeight) < minDiff) {
|
||||
if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
|
||||
optimalSize = size;
|
||||
minDiff = Math.abs(size.height - targetHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot find preview size that matches the aspect ratio, ignore the requirement
|
||||
// Cannot find video size that matches the aspect ratio, ignore the requirement
|
||||
if (optimalSize == null) {
|
||||
minDiff = Double.MAX_VALUE;
|
||||
for (Camera.Size size : sizes) {
|
||||
if (Math.abs(size.height - targetHeight) < minDiff) {
|
||||
for (Camera.Size size : videoSizes) {
|
||||
if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
|
||||
optimalSize = size;
|
||||
minDiff = Math.abs(size.height - targetHeight);
|
||||
}
|
||||
|
||||
@@ -473,7 +473,7 @@ public class BluetoothChatService {
|
||||
int bytes;
|
||||
|
||||
// Keep listening to the InputStream while connected
|
||||
while (true) {
|
||||
while (mState == STATE_CONNECTED) {
|
||||
try {
|
||||
// Read from the InputStream
|
||||
bytes = mmInStream.read(buffer);
|
||||
|
||||
@@ -275,6 +275,11 @@ public class Camera2BasicFragment extends Fragment
|
||||
*/
|
||||
private boolean mFlashSupported;
|
||||
|
||||
/**
|
||||
* Orientation of the camera sensor
|
||||
*/
|
||||
private int mSensorOrientation;
|
||||
|
||||
/**
|
||||
* 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
|
||||
// coordinate.
|
||||
int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
|
||||
int sensorOrientation =
|
||||
characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
|
||||
//noinspection ConstantConditions
|
||||
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
|
||||
boolean swappedDimensions = false;
|
||||
switch (displayRotation) {
|
||||
case Surface.ROTATION_0:
|
||||
case Surface.ROTATION_180:
|
||||
if (sensorOrientation == 90 || sensorOrientation == 270) {
|
||||
if (mSensorOrientation == 90 || mSensorOrientation == 270) {
|
||||
swappedDimensions = true;
|
||||
}
|
||||
break;
|
||||
case Surface.ROTATION_90:
|
||||
case Surface.ROTATION_270:
|
||||
if (sensorOrientation == 0 || sensorOrientation == 180) {
|
||||
if (mSensorOrientation == 0 || mSensorOrientation == 180) {
|
||||
swappedDimensions = true;
|
||||
}
|
||||
break;
|
||||
@@ -821,7 +826,7 @@ public class Camera2BasicFragment extends Fragment
|
||||
|
||||
// Orientation
|
||||
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
|
||||
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));
|
||||
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
|
||||
|
||||
CameraCaptureSession.CaptureCallback CaptureCallback
|
||||
= new CameraCaptureSession.CaptureCallback() {
|
||||
@@ -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
|
||||
* finished.
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -55,9 +55,9 @@ import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -67,7 +67,10 @@ import java.util.concurrent.TimeUnit;
|
||||
public class Camera2VideoFragment extends Fragment
|
||||
implements View.OnClickListener, FragmentCompat.OnRequestPermissionsResultCallback {
|
||||
|
||||
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
|
||||
private static final int SENSOR_ORIENTATION_DEFAULT_DEGREES = 90;
|
||||
private static final int SENSOR_ORIENTATION_INVERSE_DEGREES = 270;
|
||||
private static final SparseIntArray DEFAULT_ORIENTATIONS = new SparseIntArray();
|
||||
private static final SparseIntArray INVERSE_ORIENTATIONS = new SparseIntArray();
|
||||
|
||||
private static final String TAG = "Camera2VideoFragment";
|
||||
private static final int REQUEST_VIDEO_PERMISSIONS = 1;
|
||||
@@ -79,10 +82,17 @@ public class Camera2VideoFragment extends Fragment
|
||||
};
|
||||
|
||||
static {
|
||||
ORIENTATIONS.append(Surface.ROTATION_0, 90);
|
||||
ORIENTATIONS.append(Surface.ROTATION_90, 0);
|
||||
ORIENTATIONS.append(Surface.ROTATION_180, 270);
|
||||
ORIENTATIONS.append(Surface.ROTATION_270, 180);
|
||||
DEFAULT_ORIENTATIONS.append(Surface.ROTATION_0, 90);
|
||||
DEFAULT_ORIENTATIONS.append(Surface.ROTATION_90, 0);
|
||||
DEFAULT_ORIENTATIONS.append(Surface.ROTATION_180, 270);
|
||||
DEFAULT_ORIENTATIONS.append(Surface.ROTATION_270, 180);
|
||||
}
|
||||
|
||||
static {
|
||||
INVERSE_ORIENTATIONS.append(Surface.ROTATION_0, 270);
|
||||
INVERSE_ORIENTATIONS.append(Surface.ROTATION_90, 180);
|
||||
INVERSE_ORIENTATIONS.append(Surface.ROTATION_180, 90);
|
||||
INVERSE_ORIENTATIONS.append(Surface.ROTATION_270, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,11 +156,6 @@ public class Camera2VideoFragment extends Fragment
|
||||
*/
|
||||
private Size mVideoSize;
|
||||
|
||||
/**
|
||||
* Camera preview.
|
||||
*/
|
||||
private CaptureRequest.Builder mPreviewBuilder;
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return new Camera2VideoFragment();
|
||||
@@ -425,6 +434,7 @@ public class Camera2VideoFragment extends Fragment
|
||||
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
|
||||
StreamConfigurationMap map = characteristics
|
||||
.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
|
||||
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
|
||||
mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class));
|
||||
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
|
||||
width, height, mVideoSize);
|
||||
@@ -454,6 +464,7 @@ public class Camera2VideoFragment extends Fragment
|
||||
private void closeCamera() {
|
||||
try {
|
||||
mCameraOpenCloseLock.acquire();
|
||||
closePreviewSession();
|
||||
if (null != mCameraDevice) {
|
||||
mCameraDevice.close();
|
||||
mCameraDevice = null;
|
||||
@@ -477,22 +488,16 @@ public class Camera2VideoFragment extends Fragment
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setUpMediaRecorder();
|
||||
closePreviewSession();
|
||||
SurfaceTexture texture = mTextureView.getSurfaceTexture();
|
||||
assert texture != null;
|
||||
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
|
||||
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
|
||||
List<Surface> surfaces = new ArrayList<Surface>();
|
||||
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
|
||||
|
||||
Surface previewSurface = new Surface(texture);
|
||||
surfaces.add(previewSurface);
|
||||
mPreviewBuilder.addTarget(previewSurface);
|
||||
|
||||
Surface recorderSurface = mMediaRecorder.getSurface();
|
||||
surfaces.add(recorderSurface);
|
||||
mPreviewBuilder.addTarget(recorderSurface);
|
||||
|
||||
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
|
||||
mCameraDevice.createCaptureSession(Arrays.asList(previewSurface), new CameraCaptureSession.StateCallback() {
|
||||
|
||||
@Override
|
||||
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
|
||||
@@ -510,8 +515,6 @@ public class Camera2VideoFragment extends Fragment
|
||||
}, mBackgroundHandler);
|
||||
} catch (CameraAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,32 +578,96 @@ public class Camera2VideoFragment extends Fragment
|
||||
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
||||
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
|
||||
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
|
||||
mMediaRecorder.setOutputFile(getVideoFile(activity).getAbsolutePath());
|
||||
if (mNextVideoAbsolutePath == null || mNextVideoAbsolutePath.isEmpty()) {
|
||||
mNextVideoAbsolutePath = getVideoFilePath(getActivity());
|
||||
}
|
||||
mMediaRecorder.setOutputFile(mNextVideoAbsolutePath);
|
||||
mMediaRecorder.setVideoEncodingBitRate(10000000);
|
||||
mMediaRecorder.setVideoFrameRate(30);
|
||||
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
|
||||
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
|
||||
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
||||
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
|
||||
int orientation = ORIENTATIONS.get(rotation);
|
||||
mMediaRecorder.setOrientationHint(orientation);
|
||||
switch (mSensorOrientation) {
|
||||
case SENSOR_ORIENTATION_DEFAULT_DEGREES:
|
||||
mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
|
||||
break;
|
||||
case SENSOR_ORIENTATION_INVERSE_DEGREES:
|
||||
mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation));
|
||||
break;
|
||||
}
|
||||
mMediaRecorder.prepare();
|
||||
}
|
||||
|
||||
private File getVideoFile(Context context) {
|
||||
return new File(context.getExternalFilesDir(null), "video.mp4");
|
||||
private String getVideoFilePath(Context context) {
|
||||
return context.getExternalFilesDir(null).getAbsolutePath() + "/"
|
||||
+ System.currentTimeMillis() + ".mp4";
|
||||
}
|
||||
|
||||
private void startRecordingVideo() {
|
||||
if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// UI
|
||||
mButtonVideo.setText(R.string.stop);
|
||||
mIsRecordingVideo = true;
|
||||
closePreviewSession();
|
||||
setUpMediaRecorder();
|
||||
SurfaceTexture texture = mTextureView.getSurfaceTexture();
|
||||
assert texture != null;
|
||||
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
|
||||
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
|
||||
List<Surface> surfaces = new ArrayList<>();
|
||||
|
||||
// Start recording
|
||||
mMediaRecorder.start();
|
||||
} catch (IllegalStateException e) {
|
||||
// Set up Surface for the camera preview
|
||||
Surface previewSurface = new Surface(texture);
|
||||
surfaces.add(previewSurface);
|
||||
mPreviewBuilder.addTarget(previewSurface);
|
||||
|
||||
// Set up Surface for the MediaRecorder
|
||||
mRecorderSurface = mMediaRecorder.getSurface();
|
||||
surfaces.add(mRecorderSurface);
|
||||
mPreviewBuilder.addTarget(mRecorderSurface);
|
||||
|
||||
// Start a capture session
|
||||
// Once the session starts, we can update the UI and start recording
|
||||
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
|
||||
|
||||
@Override
|
||||
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
|
||||
mPreviewSession = cameraCaptureSession;
|
||||
updatePreview();
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// UI
|
||||
mButtonVideo.setText(R.string.stop);
|
||||
mIsRecordingVideo = true;
|
||||
|
||||
// Start recording
|
||||
mMediaRecorder.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
|
||||
Activity activity = getActivity();
|
||||
if (null != activity) {
|
||||
Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}, mBackgroundHandler);
|
||||
} catch (CameraAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void closePreviewSession() {
|
||||
if (mPreviewSession != null) {
|
||||
mPreviewSession.close();
|
||||
mPreviewSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,11 +678,14 @@ public class Camera2VideoFragment extends Fragment
|
||||
// Stop recording
|
||||
mMediaRecorder.stop();
|
||||
mMediaRecorder.reset();
|
||||
|
||||
Activity activity = getActivity();
|
||||
if (null != activity) {
|
||||
Toast.makeText(activity, "Video saved: " + getVideoFile(activity),
|
||||
Toast.makeText(activity, "Video saved: " + mNextVideoAbsolutePath,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
Log.d(TAG, "Video saved: " + mNextVideoAbsolutePath);
|
||||
}
|
||||
mNextVideoAbsolutePath = null;
|
||||
startPreview();
|
||||
}
|
||||
|
||||
@@ -687,4 +757,4 @@ public class Camera2VideoFragment extends Fragment
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,9 @@ import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
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.OnConnectionFailedListener;
|
||||
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.CapabilityApi;
|
||||
import com.google.android.gms.wearable.CapabilityInfo;
|
||||
import com.google.android.gms.wearable.DataApi;
|
||||
import com.google.android.gms.wearable.DataApi.DataItemResult;
|
||||
import com.google.android.gms.wearable.DataEvent;
|
||||
@@ -61,7 +62,6 @@ import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
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
|
||||
* to the paired wearable.
|
||||
*/
|
||||
public class MainActivity extends Activity implements DataApi.DataListener,
|
||||
MessageApi.MessageListener, NodeApi.NodeListener, ConnectionCallbacks,
|
||||
public class MainActivity extends Activity implements
|
||||
CapabilityApi.CapabilityListener,
|
||||
MessageApi.MessageListener,
|
||||
DataApi.DataListener,
|
||||
ConnectionCallbacks,
|
||||
OnConnectionFailedListener {
|
||||
|
||||
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_IMAGE_CAPTURE = 1;
|
||||
|
||||
private static final String START_ACTIVITY_PATH = "/start-activity";
|
||||
private static final String COUNT_PATH = "/count";
|
||||
private static final String IMAGE_PATH = "/image";
|
||||
@@ -100,18 +103,14 @@ public class MainActivity extends Activity implements DataApi.DataListener,
|
||||
private View mStartActivityBtn;
|
||||
|
||||
private DataItemAdapter mDataItemListAdapter;
|
||||
private Handler mHandler;
|
||||
|
||||
// Send DataItems.
|
||||
private ScheduledExecutorService mGeneratorExecutor;
|
||||
private ScheduledFuture<?> mDataItemGeneratorFuture;
|
||||
|
||||
static final int REQUEST_IMAGE_CAPTURE = 1;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle b) {
|
||||
super.onCreate(b);
|
||||
mHandler = new Handler();
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LOGD(TAG, "onCreate");
|
||||
mCameraSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
|
||||
setContentView(R.layout.main_activity);
|
||||
@@ -130,15 +129,6 @@ public class MainActivity extends Activity implements DataApi.DataListener,
|
||||
.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
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
@@ -162,16 +152,25 @@ public class MainActivity extends Activity implements DataApi.DataListener,
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
if (!mResolvingError) {
|
||||
if (!mResolvingError && (mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) {
|
||||
Wearable.DataApi.removeListener(mGoogleApiClient, this);
|
||||
Wearable.MessageApi.removeListener(mGoogleApiClient, this);
|
||||
Wearable.NodeApi.removeListener(mGoogleApiClient, this);
|
||||
Wearable.CapabilityApi.removeListener(mGoogleApiClient, this);
|
||||
mGoogleApiClient.disconnect();
|
||||
}
|
||||
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) {
|
||||
LOGD(TAG, "Google API Client was connected");
|
||||
mResolvingError = false;
|
||||
@@ -179,157 +178,100 @@ public class MainActivity extends Activity implements DataApi.DataListener,
|
||||
mSendPhotoBtn.setEnabled(mCameraSupported);
|
||||
Wearable.DataApi.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) {
|
||||
LOGD(TAG, "Connection to Google API client was suspended");
|
||||
mStartActivityBtn.setEnabled(false);
|
||||
mSendPhotoBtn.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override //OnConnectionFailedListener
|
||||
@Override
|
||||
public void onConnectionFailed(ConnectionResult result) {
|
||||
if (mResolvingError) {
|
||||
// Already attempting to resolve an error.
|
||||
return;
|
||||
} else if (result.hasResolution()) {
|
||||
try {
|
||||
mResolvingError = true;
|
||||
result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
|
||||
} catch (IntentSender.SendIntentException e) {
|
||||
// There was an error with the resolution intent. Try again.
|
||||
mGoogleApiClient.connect();
|
||||
if (!mResolvingError) {
|
||||
|
||||
if (result.hasResolution()) {
|
||||
try {
|
||||
mResolvingError = true;
|
||||
result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
|
||||
} catch (IntentSender.SendIntentException e) {
|
||||
// There was an error with the resolution intent. Try again.
|
||||
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) {
|
||||
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);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (DataEvent event : events) {
|
||||
if (event.getType() == DataEvent.TYPE_CHANGED) {
|
||||
mDataItemListAdapter.add(
|
||||
new Event("DataItem Changed", event.getDataItem().toString()));
|
||||
} else if (event.getType() == DataEvent.TYPE_DELETED) {
|
||||
mDataItemListAdapter.add(
|
||||
new Event("DataItem Deleted", event.getDataItem().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
for (DataEvent event : dataEvents) {
|
||||
if (event.getType() == DataEvent.TYPE_CHANGED) {
|
||||
mDataItemListAdapter.add(
|
||||
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) {
|
||||
LOGD(TAG, "onMessageReceived() A message from watch was received:" + messageEvent
|
||||
.getRequestId() + " " + messageEvent.getPath());
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mDataItemListAdapter.add(new Event("Message from watch", messageEvent.toString()));
|
||||
}
|
||||
});
|
||||
LOGD(TAG, "onMessageReceived() A message from watch was received:"
|
||||
+ messageEvent.getRequestId() + " " + messageEvent.getPath());
|
||||
|
||||
mDataItemListAdapter.add(new Event("Message from watch", messageEvent.toString()));
|
||||
}
|
||||
|
||||
@Override //NodeListener
|
||||
public void onPeerConnected(final Node peer) {
|
||||
LOGD(TAG, "onPeerConnected: " + peer);
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mDataItemListAdapter.add(new Event("Connected", peer.toString()));
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onCapabilityChanged(final CapabilityInfo capabilityInfo) {
|
||||
LOGD(TAG, "onCapabilityChanged: " + capabilityInfo);
|
||||
|
||||
}
|
||||
|
||||
@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()));
|
||||
}
|
||||
});
|
||||
mDataItemListAdapter.add(new Event("onCapabilityChanged", capabilityInfo.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) {
|
||||
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;
|
||||
public void onSendPhotoClick(View view) {
|
||||
if (null != mImageBitmap && mGoogleApiClient.isConnected()) {
|
||||
sendPhoto(toAsset(mImageBitmap));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
String text;
|
||||
|
||||
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;
|
||||
// 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();
|
||||
}
|
||||
|
||||
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
|
||||
* 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) {
|
||||
PutDataMapRequest dataMap = PutDataMapRequest.create(IMAGE_PATH);
|
||||
@@ -454,27 +341,18 @@ public class MainActivity extends Activity implements DataApi.DataListener,
|
||||
.isSuccess());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void onTakePhotoClick(View view) {
|
||||
dispatchTakePictureIntent();
|
||||
}
|
||||
private Collection<String> getNodes() {
|
||||
HashSet<String> results = new HashSet<>();
|
||||
NodeApi.GetConnectedNodesResult nodes =
|
||||
Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await();
|
||||
|
||||
public void onSendPhotoClick(View view) {
|
||||
if (null != mImageBitmap && mGoogleApiClient.isConnected()) {
|
||||
sendPhoto(toAsset(mImageBitmap));
|
||||
for (Node node : nodes.getNodes()) {
|
||||
results.add(node.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
package="com.example.android.wearable.datalayer" >
|
||||
|
||||
<uses-sdk android:minSdkVersion="20"
|
||||
android:targetSdkVersion="22" />
|
||||
android:targetSdkVersion="23" />
|
||||
|
||||
<uses-feature android:name="android.hardware.type.watch" />
|
||||
|
||||
@@ -35,10 +35,15 @@
|
||||
<service
|
||||
android:name=".DataLayerListenerService" >
|
||||
<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>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name">
|
||||
@@ -52,4 +57,4 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -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.DataEventBuffer;
|
||||
import com.google.android.gms.wearable.MessageEvent;
|
||||
import com.google.android.gms.wearable.Node;
|
||||
import com.google.android.gms.wearable.Wearable;
|
||||
import com.google.android.gms.wearable.WearableListenerService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -37,7 +35,7 @@ import java.util.concurrent.TimeUnit;
|
||||
*/
|
||||
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 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) {
|
||||
if (Log.isLoggable(tag, Log.DEBUG)) {
|
||||
Log.d(tag, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,9 @@ import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.wearable.view.DotsPageIndicator;
|
||||
import android.support.wearable.view.FragmentGridPagerAdapter;
|
||||
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.MessageEvent;
|
||||
import com.google.android.gms.wearable.Node;
|
||||
import com.google.android.gms.wearable.NodeApi;
|
||||
import com.google.android.gms.wearable.Wearable;
|
||||
|
||||
import java.io.InputStream;
|
||||
@@ -78,9 +77,12 @@ import java.util.Set;
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public class MainActivity extends Activity implements ConnectionCallbacks,
|
||||
OnConnectionFailedListener, DataApi.DataListener, MessageApi.MessageListener,
|
||||
NodeApi.NodeListener {
|
||||
public class MainActivity extends Activity implements
|
||||
ConnectionCallbacks,
|
||||
OnConnectionFailedListener,
|
||||
DataApi.DataListener,
|
||||
MessageApi.MessageListener,
|
||||
CapabilityApi.CapabilityListener {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
private static final String CAPABILITY_1_NAME = "capability_1";
|
||||
@@ -92,8 +94,8 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
||||
private AssetFragment mAssetFragment;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle b) {
|
||||
super.onCreate(b);
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main_activity);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
setupViews();
|
||||
@@ -112,11 +114,14 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
||||
|
||||
@Override
|
||||
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();
|
||||
Wearable.DataApi.removeListener(mGoogleApiClient, this);
|
||||
Wearable.MessageApi.removeListener(mGoogleApiClient, this);
|
||||
Wearable.NodeApi.removeListener(mGoogleApiClient, this);
|
||||
mGoogleApiClient.disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,7 +129,8 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
||||
LOGD(TAG, "onConnected(): Successfully connected to Google API client");
|
||||
Wearable.DataApi.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
|
||||
@@ -244,13 +250,9 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerConnected(Node node) {
|
||||
mDataFragment.appendItem("Node Connected", node.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerDisconnected(Node node) {
|
||||
mDataFragment.appendItem("Node Disconnected", node.getId());
|
||||
public void onCapabilityChanged(CapabilityInfo capabilityInfo) {
|
||||
LOGD(TAG, "onCapabilityChanged: " + capabilityInfo);
|
||||
mDataFragment.appendItem("onCapabilityChanged", capabilityInfo.toString());
|
||||
}
|
||||
|
||||
private void setupViews() {
|
||||
@@ -312,7 +314,7 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
||||
@Override
|
||||
protected Bitmap doInBackground(Asset... params) {
|
||||
|
||||
if(params.length > 0) {
|
||||
if (params.length > 0) {
|
||||
|
||||
Asset asset = params[0];
|
||||
|
||||
@@ -334,11 +336,11 @@ public class MainActivity extends Activity implements ConnectionCallbacks,
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
|
||||
if(bitmap != null) {
|
||||
if (bitmap != null) {
|
||||
LOGD(TAG, "Setting background image on second page..");
|
||||
moveToPage(1);
|
||||
mAssetFragment.setBackgroundImage(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,49 +36,57 @@ public class CameraHelper {
|
||||
public static final int MEDIA_TYPE_VIDEO = 2;
|
||||
|
||||
/**
|
||||
* Iterate over supported camera preview sizes to see which one best fits the
|
||||
* Iterate over supported camera video sizes to see which one best fits the
|
||||
* dimensions of the given view while maintaining the aspect ratio. If none can,
|
||||
* be lenient with the aspect ratio.
|
||||
*
|
||||
* @param sizes Supported camera preview sizes.
|
||||
* @param w The width of the view.
|
||||
* @param h The height of the view.
|
||||
* @return Best match camera preview size to fit in the view.
|
||||
* @param supportedVideoSizes Supported camera video sizes.
|
||||
* @param previewSizes Supported camera preview sizes.
|
||||
* @param w The width of the view.
|
||||
* @param h The height of the view.
|
||||
* @return Best match camera video size to fit in the view.
|
||||
*/
|
||||
public static Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
|
||||
public static Camera.Size getOptimalVideoSize(List<Camera.Size> supportedVideoSizes,
|
||||
List<Camera.Size> previewSizes, int w, int h) {
|
||||
// Use a very small tolerance because we want an exact match.
|
||||
final double ASPECT_TOLERANCE = 0.1;
|
||||
double targetRatio = (double) w / h;
|
||||
if (sizes == null)
|
||||
return null;
|
||||
|
||||
// Supported video sizes list might be null, it means that we are allowed to use the preview
|
||||
// sizes
|
||||
List<Camera.Size> videoSizes;
|
||||
if (supportedVideoSizes != null) {
|
||||
videoSizes = supportedVideoSizes;
|
||||
} else {
|
||||
videoSizes = previewSizes;
|
||||
}
|
||||
Camera.Size optimalSize = null;
|
||||
|
||||
// Start with max value and refine as we iterate over available preview sizes. This is the
|
||||
// Start with max value and refine as we iterate over available video sizes. This is the
|
||||
// minimum difference between view and camera height.
|
||||
double minDiff = Double.MAX_VALUE;
|
||||
|
||||
// Target view height
|
||||
int targetHeight = h;
|
||||
|
||||
// Try to find a preview size that matches aspect ratio and the target view size.
|
||||
// Try to find a video size that matches aspect ratio and the target view size.
|
||||
// Iterate over all available sizes and pick the largest size that can fit in the view and
|
||||
// still maintain the aspect ratio.
|
||||
for (Camera.Size size : sizes) {
|
||||
for (Camera.Size size : videoSizes) {
|
||||
double ratio = (double) size.width / size.height;
|
||||
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE)
|
||||
continue;
|
||||
if (Math.abs(size.height - targetHeight) < minDiff) {
|
||||
if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
|
||||
optimalSize = size;
|
||||
minDiff = Math.abs(size.height - targetHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot find preview size that matches the aspect ratio, ignore the requirement
|
||||
// Cannot find video size that matches the aspect ratio, ignore the requirement
|
||||
if (optimalSize == null) {
|
||||
minDiff = Double.MAX_VALUE;
|
||||
for (Camera.Size size : sizes) {
|
||||
if (Math.abs(size.height - targetHeight) < minDiff) {
|
||||
for (Camera.Size size : videoSizes) {
|
||||
if (Math.abs(size.height - targetHeight) < minDiff && previewSizes.contains(size)) {
|
||||
optimalSize = size;
|
||||
minDiff = Math.abs(size.height - targetHeight);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import android.widget.Button;
|
||||
|
||||
import com.example.android.common.media.CameraHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@@ -45,6 +46,7 @@ public class MainActivity extends Activity {
|
||||
private Camera mCamera;
|
||||
private TextureView mPreview;
|
||||
private MediaRecorder mMediaRecorder;
|
||||
private File mOutputFile;
|
||||
|
||||
private boolean isRecording = false;
|
||||
private static final String TAG = "Recorder";
|
||||
@@ -71,7 +73,15 @@ public class MainActivity extends Activity {
|
||||
// BEGIN_INCLUDE(stop_release_media_recorder)
|
||||
|
||||
// stop recording and release camera
|
||||
mMediaRecorder.stop(); // stop the recording
|
||||
try {
|
||||
mMediaRecorder.stop(); // stop the recording
|
||||
} catch (RuntimeException e) {
|
||||
// RuntimeException is thrown when stop() is called immediately after start().
|
||||
// In this case the output file is not properly constructed ans should be deleted.
|
||||
Log.d(TAG, "RuntimeException: stop() is called immediately after start()");
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
mOutputFile.delete();
|
||||
}
|
||||
releaseMediaRecorder(); // release the MediaRecorder object
|
||||
mCamera.lock(); // take camera access back from MediaRecorder
|
||||
|
||||
@@ -137,8 +147,9 @@ public class MainActivity extends Activity {
|
||||
// dimensions of our preview surface.
|
||||
Camera.Parameters parameters = mCamera.getParameters();
|
||||
List<Camera.Size> mSupportedPreviewSizes = parameters.getSupportedPreviewSizes();
|
||||
Camera.Size optimalSize = CameraHelper.getOptimalPreviewSize(mSupportedPreviewSizes,
|
||||
mPreview.getWidth(), mPreview.getHeight());
|
||||
List<Camera.Size> mSupportedVideoSizes = parameters.getSupportedVideoSizes();
|
||||
Camera.Size optimalSize = CameraHelper.getOptimalVideoSize(mSupportedVideoSizes,
|
||||
mSupportedPreviewSizes, mPreview.getWidth(), mPreview.getHeight());
|
||||
|
||||
// Use the same size for recording profile.
|
||||
CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
|
||||
@@ -174,8 +185,11 @@ public class MainActivity extends Activity {
|
||||
mMediaRecorder.setProfile(profile);
|
||||
|
||||
// Step 4: Set output file
|
||||
mMediaRecorder.setOutputFile(CameraHelper.getOutputMediaFile(
|
||||
CameraHelper.MEDIA_TYPE_VIDEO).toString());
|
||||
mOutputFile = CameraHelper.getOutputMediaFile(CameraHelper.MEDIA_TYPE_VIDEO);
|
||||
if (mOutputFile == null) {
|
||||
return false;
|
||||
}
|
||||
mMediaRecorder.setOutputFile(mOutputFile.getPath());
|
||||
// END_INCLUDE (configure_media_recorder)
|
||||
|
||||
// Step 5: Prepare configured MediaRecorder
|
||||
@@ -227,4 +241,4 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@
|
||||
android:enabled="true"
|
||||
android:exported="true" >
|
||||
<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>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
android:enabled="true"
|
||||
android:exported="true" >
|
||||
<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>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
@@ -48,7 +48,10 @@
|
||||
</activity>
|
||||
<service android:name=".db.UpdateService">
|
||||
<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>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
package="com.example.android.wearable.speedtracker">
|
||||
|
||||
<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.WAKE_LOCK" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -330,6 +330,13 @@ public class WearableMainActivity extends WearableActivity implements
|
||||
public void onConnected(Bundle bundle) {
|
||||
|
||||
Log.d(TAG, "onConnected()");
|
||||
requestLocation();
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void requestLocation() {
|
||||
Log.d(TAG, "requestLocation()");
|
||||
|
||||
/*
|
||||
* 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)) {
|
||||
Log.i(TAG, "GPS permission granted.");
|
||||
mGpsPermissionApproved = true;
|
||||
|
||||
if(mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
|
||||
requestLocation();
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.i(TAG, "GPS permission NOT granted.");
|
||||
mGpsPermissionApproved = false;
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,10 @@
|
||||
</activity>
|
||||
<service android:name=".DismissListener">
|
||||
<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.BOTH_PATH ('/both') -->
|
||||
<data android:scheme="wear" android:host="*" android:pathPrefix="/both"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action
|
||||
|
||||
@@ -41,8 +41,13 @@
|
||||
</activity>
|
||||
<service android:name=".NotificationUpdateService">
|
||||
<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.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>
|
||||
<action
|
||||
|
||||
@@ -132,6 +132,42 @@
|
||||
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name=".InteractiveWatchFaceService"
|
||||
android:label="@string/interactive_name"
|
||||
@@ -218,7 +254,8 @@
|
||||
</service>
|
||||
<service android:name=".DigitalWatchFaceConfigListenerService" >
|
||||
<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>
|
||||
</service>
|
||||
<service
|
||||
@@ -269,6 +306,35 @@
|
||||
android:name=".CalendarWatchFacePermissionActivity"
|
||||
android:label="@string/title_activity_calendar_watch_face_permission" >
|
||||
</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>
|
||||
|
||||
</manifest>
|
||||
|
||||
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
26
samples/browseable/WatchFace/Wearable/res/values/arrays.xml
Normal 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>
|
||||
@@ -18,6 +18,7 @@
|
||||
<string name="opengl_name">Sample OpenGL</string>
|
||||
<string name="interactive_name">Sample Interactive</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="card_bounds_name">Sample Card Bounds</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="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) -->
|
||||
<string name="color_black">Black</string>
|
||||
<string name="color_blue">Blue</string>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@ public class DigitalWatchFaceConfigListenerService extends WearableListenerServi
|
||||
|
||||
@Override // WearableListenerService
|
||||
public void onMessageReceived(MessageEvent messageEvent) {
|
||||
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "onMessageReceived: " + messageEvent);
|
||||
}
|
||||
|
||||
if (!messageEvent.getPath().equals(DigitalWatchFaceUtil.PATH_WITH_FEATURE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,10 @@ import java.util.concurrent.TimeUnit;
|
||||
*
|
||||
* Authentication IS a requirement to request distance from Google Fit on Wear. Otherwise, distance
|
||||
* will always come back as zero (or stay at whatever the distance was prior to you
|
||||
* de-authorizing watchface).
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
29
samples/browseable/WearDrawers/AndroidManifest.xml
Normal 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>
|
||||
10
samples/browseable/WearDrawers/_index.jd
Normal 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>
|
||||
BIN
samples/browseable/WearDrawers/res/drawable-hdpi/earth.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 388 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
BIN
samples/browseable/WearDrawers/res/drawable-hdpi/jupiter.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
samples/browseable/WearDrawers/res/drawable-hdpi/mars.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
samples/browseable/WearDrawers/res/drawable-hdpi/mercury.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
samples/browseable/WearDrawers/res/drawable-hdpi/neptune.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
samples/browseable/WearDrawers/res/drawable-hdpi/saturn.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
samples/browseable/WearDrawers/res/drawable-hdpi/uranus.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
samples/browseable/WearDrawers/res/drawable-hdpi/venus.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 254 B |
|
After Width: | Height: | Size: 487 B |
|
After Width: | Height: | Size: 717 B |
|
After Width: | Height: | Size: 940 B |
@@ -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"/>
|
||||
@@ -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>
|
||||
BIN
samples/browseable/WearDrawers/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
samples/browseable/WearDrawers/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
samples/browseable/WearDrawers/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
samples/browseable/WearDrawers/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
114
samples/browseable/WearDrawers/res/values/strings.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,16 @@
|
||||
|
||||
<service android:name=".service.ListenerService">
|
||||
<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>
|
||||
</service>
|
||||
|
||||
|
||||
@@ -44,7 +44,14 @@
|
||||
|
||||
<service android:name=".service.ListenerService">
|
||||
<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>
|
||||
</service>
|
||||
|
||||
|
||||