1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.wearable.watchface;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.opengl.GLES20;
24 import android.opengl.Matrix;
25 import android.support.wearable.watchface.Gles2WatchFaceService;
26 import android.support.wearable.watchface.WatchFaceStyle;
27 import android.util.Log;
28 import android.view.Gravity;
29 import android.view.SurfaceHolder;
30 
31 import java.util.Calendar;
32 import java.util.TimeZone;
33 import java.util.concurrent.TimeUnit;
34 
35 /**
36  * Sample watch face using OpenGL. The watch face is rendered using
37  * {@link Gles2ColoredTriangleList}s. The camera moves around in interactive mode and stops moving
38  * when the watch enters ambient mode.
39  */
40 public class OpenGLWatchFaceService extends Gles2WatchFaceService {
41 
42     private static final String TAG = "OpenGLWatchFaceService";
43 
44     /** Expected frame rate in interactive mode. */
45     private static final long FPS = 60;
46 
47     /** Z distance from the camera to the watchface. */
48     private static final float EYE_Z = -2.3f;
49 
50     /** How long each frame is displayed at expected frame rate. */
51     private static final long FRAME_PERIOD_MS = TimeUnit.SECONDS.toMillis(1) / FPS;
52 
53     @Override
onCreateEngine()54     public Engine onCreateEngine() {
55         return new Engine();
56     }
57 
58     private class Engine extends Gles2WatchFaceService.Engine {
59         /** Cycle time before the camera motion repeats. */
60         private static final long CYCLE_PERIOD_SECONDS = 5;
61 
62         /** Number of camera angles to precompute. */
63         private final int mNumCameraAngles = (int) (CYCLE_PERIOD_SECONDS * FPS);
64 
65         /** Projection transformation matrix. Converts from 3D to 2D. */
66         private final float[] mProjectionMatrix = new float[16];
67 
68         /**
69          * View transformation matrices to use in interactive mode. Converts from world to camera-
70          * relative coordinates. One matrix per camera position.
71          */
72         private final float[][] mViewMatrices = new float[mNumCameraAngles][16];
73 
74         /** The view transformation matrix to use in ambient mode */
75         private final float[] mAmbientViewMatrix = new float[16];
76 
77         /**
78          * Model transformation matrices. Converts from model-relative coordinates to world
79          * coordinates. One matrix per degree of rotation.
80          */
81         private final float[][] mModelMatrices = new float[360][16];
82 
83         /**
84          * Products of {@link #mViewMatrices} and {@link #mProjectionMatrix}. One matrix per camera
85          * position.
86          */
87         private final float[][] mVpMatrices = new float[mNumCameraAngles][16];
88 
89         /** The product of {@link #mAmbientViewMatrix} and {@link #mProjectionMatrix} */
90         private final float[] mAmbientVpMatrix = new float[16];
91 
92         /**
93          * Product of {@link #mModelMatrices}, {@link #mViewMatrices}, and
94          * {@link #mProjectionMatrix}.
95          */
96         private final float[] mMvpMatrix = new float[16];
97 
98         /** Triangles for the 4 major ticks. These are grouped together to speed up rendering. */
99         private Gles2ColoredTriangleList mMajorTickTriangles;
100 
101         /** Triangles for the 8 minor ticks. These are grouped together to speed up rendering. */
102         private Gles2ColoredTriangleList mMinorTickTriangles;
103 
104         /** Triangle for the second hand. */
105         private Gles2ColoredTriangleList mSecondHandTriangle;
106 
107         /** Triangle for the minute hand. */
108         private Gles2ColoredTriangleList mMinuteHandTriangle;
109 
110         /** Triangle for the hour hand. */
111         private Gles2ColoredTriangleList mHourHandTriangle;
112 
113         private Calendar mCalendar = Calendar.getInstance();
114 
115         /** Whether we've registered {@link #mTimeZoneReceiver}. */
116         private boolean mRegisteredTimeZoneReceiver;
117 
118         private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
119             @Override
120             public void onReceive(Context context, Intent intent) {
121                 mCalendar.setTimeZone(TimeZone.getDefault());
122                 invalidate();
123             }
124         };
125 
126         @Override
onCreate(SurfaceHolder surfaceHolder)127         public void onCreate(SurfaceHolder surfaceHolder) {
128             if (Log.isLoggable(TAG, Log.DEBUG)) {
129                 Log.d(TAG, "onCreate");
130             }
131             super.onCreate(surfaceHolder);
132             setWatchFaceStyle(new WatchFaceStyle.Builder(OpenGLWatchFaceService.this)
133                     .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
134                     .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
135                     .setStatusBarGravity(Gravity.RIGHT | Gravity.TOP)
136                     .setHotwordIndicatorGravity(Gravity.LEFT | Gravity.TOP)
137                     .setShowSystemUiTime(false)
138                     .build());
139         }
140 
141         @Override
onGlContextCreated()142         public void onGlContextCreated() {
143             if (Log.isLoggable(TAG, Log.DEBUG)) {
144                 Log.d(TAG, "onGlContextCreated");
145             }
146             super.onGlContextCreated();
147 
148             // Create program for drawing triangles.
149             Gles2ColoredTriangleList.Program triangleProgram =
150                     new Gles2ColoredTriangleList.Program();
151 
152             // We only draw triangles which all use the same program so we don't need to switch
153             // programs mid-frame. This means we can tell OpenGL to use this program only once
154             // rather than having to do so for each frame. This makes OpenGL draw faster.
155             triangleProgram.use();
156 
157             // Create triangles for the ticks.
158             mMajorTickTriangles = createMajorTicks(triangleProgram);
159             mMinorTickTriangles = createMinorTicks(triangleProgram);
160 
161             // Create triangles for the hands.
162             mSecondHandTriangle = createHand(
163                     triangleProgram,
164                     0.02f /* width */,
165                     1.0f /* height */,
166                     new float[]{
167                             1.0f /* red */,
168                             0.0f /* green */,
169                             0.0f /* blue */,
170                             1.0f /* alpha */
171                     }
172             );
173             mMinuteHandTriangle = createHand(
174                     triangleProgram,
175                     0.06f /* width */,
176                     1f /* height */,
177                     new float[]{
178                             0.7f /* red */,
179                             0.7f /* green */,
180                             0.7f /* blue */,
181                             1.0f /* alpha */
182                     }
183             );
184             mHourHandTriangle = createHand(
185                     triangleProgram,
186                     0.1f /* width */,
187                     0.6f /* height */,
188                     new float[]{
189                             0.9f /* red */,
190                             0.9f /* green */,
191                             0.9f /* blue */,
192                             1.0f /* alpha */
193                     }
194             );
195 
196             // Precompute the clock angles.
197             for (int i = 0; i < mModelMatrices.length; ++i) {
198                 Matrix.setRotateM(mModelMatrices[i], 0, i, 0, 0, 1);
199             }
200 
201             // Precompute the camera angles.
202             for (int i = 0; i < mNumCameraAngles; ++i) {
203                 // Set the camera position (View matrix). When active, move the eye around to show
204                 // off that this is 3D.
205                 final float cameraAngle = (float) (((float) i) / mNumCameraAngles * 2 * Math.PI);
206                 final float eyeX = (float) Math.cos(cameraAngle);
207                 final float eyeY = (float) Math.sin(cameraAngle);
208                 Matrix.setLookAtM(mViewMatrices[i],
209                         0, // dest index
210                         eyeX, eyeY, EYE_Z, // eye
211                         0, 0, 0, // center
212                         0, 1, 0); // up vector
213             }
214 
215             Matrix.setLookAtM(mAmbientViewMatrix,
216                     0, // dest index
217                     0, 0, EYE_Z, // eye
218                     0, 0, 0, // center
219                     0, 1, 0); // up vector
220         }
221 
222         @Override
onGlSurfaceCreated(int width, int height)223         public void onGlSurfaceCreated(int width, int height) {
224             if (Log.isLoggable(TAG, Log.DEBUG)) {
225                 Log.d(TAG, "onGlSurfaceCreated: " + width + " x " + height);
226             }
227             super.onGlSurfaceCreated(width, height);
228 
229             // Update the projection matrix based on the new aspect ratio.
230             final float aspectRatio = (float) width / height;
231             Matrix.frustumM(mProjectionMatrix,
232                     0 /* offset */,
233                     -aspectRatio /* left */,
234                     aspectRatio /* right */,
235                     -1 /* bottom */,
236                     1 /* top */,
237                     2 /* near */,
238                     7 /* far */);
239 
240             // Precompute the products of Projection and View matrices for each camera angle.
241             for (int i = 0; i < mNumCameraAngles; ++i) {
242                 Matrix.multiplyMM(mVpMatrices[i], 0, mProjectionMatrix, 0, mViewMatrices[i], 0);
243             }
244 
245             Matrix.multiplyMM(mAmbientVpMatrix, 0, mProjectionMatrix, 0, mAmbientViewMatrix, 0);
246         }
247 
248         /**
249          * Creates a triangle for a hand on the watch face.
250          *
251          * @param program program for drawing triangles
252          * @param width width of base of triangle
253          * @param length length of triangle
254          * @param color color in RGBA order, each in the range [0, 1]
255          */
createHand(Gles2ColoredTriangleList.Program program, float width, float length, float[] color)256         private Gles2ColoredTriangleList createHand(Gles2ColoredTriangleList.Program program,
257                 float width, float length, float[] color) {
258             // Create the data for the VBO.
259             float[] triangleCoords = new float[]{
260                     // in counterclockwise order:
261                     0, length, 0,   // top
262                     -width / 2, 0, 0,   // bottom left
263                     width / 2, 0, 0    // bottom right
264             };
265             return new Gles2ColoredTriangleList(program, triangleCoords, color);
266         }
267 
268         /**
269          * Creates a triangle list for the major ticks on the watch face.
270          *
271          * @param program program for drawing triangles
272          */
createMajorTicks( Gles2ColoredTriangleList.Program program)273         private Gles2ColoredTriangleList createMajorTicks(
274                 Gles2ColoredTriangleList.Program program) {
275             // Create the data for the VBO.
276             float[] trianglesCoords = new float[9 * 4];
277             for (int i = 0; i < 4; i++) {
278                 float[] triangleCoords = getMajorTickTriangleCoords(i);
279                 System.arraycopy(triangleCoords, 0, trianglesCoords, i * 9, triangleCoords.length);
280             }
281 
282             return new Gles2ColoredTriangleList(program, trianglesCoords,
283                     new float[]{
284                             1.0f /* red */,
285                             1.0f /* green */,
286                             1.0f /* blue */,
287                             1.0f /* alpha */
288                     }
289             );
290         }
291 
292         /**
293          * Creates a triangle list for the minor ticks on the watch face.
294          *
295          * @param program program for drawing triangles
296          */
createMinorTicks( Gles2ColoredTriangleList.Program program)297         private Gles2ColoredTriangleList createMinorTicks(
298                 Gles2ColoredTriangleList.Program program) {
299             // Create the data for the VBO.
300             float[] trianglesCoords = new float[9 * (12 - 4)];
301             int index = 0;
302             for (int i = 0; i < 12; i++) {
303                 if (i % 3 == 0) {
304                     // This is where a major tick goes, so skip it.
305                     continue;
306                 }
307                 float[] triangleCoords = getMinorTickTriangleCoords(i);
308                 System.arraycopy(triangleCoords, 0, trianglesCoords, index, triangleCoords.length);
309                 index += 9;
310             }
311 
312             return new Gles2ColoredTriangleList(program, trianglesCoords,
313                     new float[]{
314                             0.5f /* red */,
315                             0.5f /* green */,
316                             0.5f /* blue */,
317                             1.0f /* alpha */
318                     }
319             );
320         }
321 
getMajorTickTriangleCoords(int index)322         private float[] getMajorTickTriangleCoords(int index) {
323             return getTickTriangleCoords(0.03f /* width */, 0.09f /* length */,
324                     index * 360 / 4 /* angleDegrees */);
325         }
326 
getMinorTickTriangleCoords(int index)327         private float[] getMinorTickTriangleCoords(int index) {
328             return getTickTriangleCoords(0.02f /* width */, 0.06f /* length */,
329                     index * 360 / 12 /* angleDegrees */);
330         }
331 
getTickTriangleCoords(float width, float length, int angleDegrees)332         private float[] getTickTriangleCoords(float width, float length, int angleDegrees) {
333             // Create the data for the VBO.
334             float[] coords = new float[]{
335                     // in counterclockwise order:
336                     0, 1, 0,   // top
337                     width / 2, length + 1, 0,   // bottom left
338                     -width / 2, length + 1, 0    // bottom right
339             };
340 
341             rotateCoords(coords, angleDegrees);
342             return coords;
343         }
344 
345         /**
346          * Destructively rotates the given coordinates in the XY plane about the origin by the given
347          * angle.
348          *
349          * @param coords flattened 3D coordinates
350          * @param angleDegrees angle in degrees clockwise when viewed from negative infinity on the
351          *                     Z axis
352          */
rotateCoords(float[] coords, int angleDegrees)353         private void rotateCoords(float[] coords, int angleDegrees) {
354             double angleRadians = Math.toRadians(angleDegrees);
355             double cos = Math.cos(angleRadians);
356             double sin = Math.sin(angleRadians);
357             for (int i = 0; i < coords.length; i += 3) {
358                 float x = coords[i];
359                 float y = coords[i + 1];
360                 coords[i] = (float) (cos * x - sin * y);
361                 coords[i + 1] = (float) (sin * x + cos * y);
362             }
363         }
364 
365         @Override
onAmbientModeChanged(boolean inAmbientMode)366         public void onAmbientModeChanged(boolean inAmbientMode) {
367             if (Log.isLoggable(TAG, Log.DEBUG)) {
368                 Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
369             }
370             super.onAmbientModeChanged(inAmbientMode);
371             invalidate();
372         }
373 
374         @Override
onVisibilityChanged(boolean visible)375         public void onVisibilityChanged(boolean visible) {
376             if (Log.isLoggable(TAG, Log.DEBUG)) {
377                 Log.d(TAG, "onVisibilityChanged: " + visible);
378             }
379             super.onVisibilityChanged(visible);
380             if (visible) {
381                 registerReceiver();
382 
383                 // Update time zone in case it changed while we were detached.
384                 mCalendar.setTimeZone(TimeZone.getDefault());
385 
386                 invalidate();
387             } else {
388                 unregisterReceiver();
389             }
390         }
391 
registerReceiver()392         private void registerReceiver() {
393             if (mRegisteredTimeZoneReceiver) {
394                 return;
395             }
396             mRegisteredTimeZoneReceiver = true;
397             IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
398             OpenGLWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
399         }
400 
unregisterReceiver()401         private void unregisterReceiver() {
402             if (!mRegisteredTimeZoneReceiver) {
403                 return;
404             }
405             mRegisteredTimeZoneReceiver = false;
406             OpenGLWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
407         }
408 
409         @Override
onTimeTick()410         public void onTimeTick() {
411             super.onTimeTick();
412             if (Log.isLoggable(TAG, Log.DEBUG)) {
413                 Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
414             }
415             invalidate();
416         }
417 
418         @Override
onDraw()419         public void onDraw() {
420             if (Log.isLoggable(TAG, Log.VERBOSE)) {
421                 Log.v(TAG, "onDraw");
422             }
423             super.onDraw();
424             final float[] vpMatrix;
425 
426             // Draw background color and select the appropriate view projection matrix. The
427             // background should always be black in ambient mode. The view projection matrix used is
428             // overhead in ambient. In interactive mode, it's tilted depending on the current time.
429             if (isInAmbientMode()) {
430                 GLES20.glClearColor(0, 0, 0, 1);
431                 vpMatrix = mAmbientVpMatrix;
432             } else {
433                 GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1);
434                 final int cameraIndex =
435                         (int) ((System.currentTimeMillis() / FRAME_PERIOD_MS) % mNumCameraAngles);
436                 vpMatrix = mVpMatrices[cameraIndex];
437             }
438             GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
439 
440             // Compute angle indices for the three hands.
441             mCalendar.setTimeInMillis(System.currentTimeMillis());
442             float seconds =
443                     mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f;
444             float minutes = mCalendar.get(Calendar.MINUTE) + seconds / 60f;
445             float hours = mCalendar.get(Calendar.HOUR) + minutes / 60f;
446             final int secIndex = (int) (seconds / 60f * 360f);
447             final int minIndex = (int) (minutes / 60f * 360f);
448             final int hoursIndex = (int) (hours / 12f * 360f);
449 
450             // Draw triangles from back to front. Don't draw the second hand in ambient mode.
451 
452             // Combine the model matrix with the projection and camera view.
453             Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[hoursIndex], 0);
454 
455             // Draw the triangle.
456             mHourHandTriangle.draw(mMvpMatrix);
457 
458             // Combine the model matrix with the projection and camera view.
459             Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[minIndex], 0);
460 
461             // Draw the triangle.
462             mMinuteHandTriangle.draw(mMvpMatrix);
463             if (!isInAmbientMode()) {
464                 // Combine the model matrix with the projection and camera view.
465                 Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[secIndex], 0);
466 
467                 // Draw the triangle.
468                 mSecondHandTriangle.draw(mMvpMatrix);
469             }
470 
471             // Draw the major and minor ticks.
472             mMajorTickTriangles.draw(vpMatrix);
473             mMinorTickTriangles.draw(vpMatrix);
474 
475             // Draw every frame as long as we're visible and in interactive mode.
476             if (isVisible() && !isInAmbientMode()) {
477                 invalidate();
478             }
479         }
480     }
481 }
482