├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── app ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── ic_lap.png │ │ │ │ ├── ic_play.png │ │ │ │ ├── ic_stop.png │ │ │ │ ├── ic_colon.png │ │ │ │ ├── ic_pause.png │ │ │ │ ├── ic_reset.png │ │ │ │ └── ic_timer.png │ │ │ ├── raw │ │ │ │ └── timer_finished.wav │ │ │ ├── xml │ │ │ │ └── voice_trigger_start.xml │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── menu_strings.xml │ │ │ │ └── styles.xml │ │ │ ├── menu │ │ │ │ ├── set_timer.xml │ │ │ │ └── timer.xml │ │ │ ├── values-en-rGB │ │ │ │ └── strings.xml │ │ │ └── layout │ │ │ │ └── card_timer.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── google │ │ │ └── android │ │ │ └── glass │ │ │ └── sample │ │ │ └── timer │ │ │ ├── TimerService.java │ │ │ ├── TimerDrawer.java │ │ │ ├── TimerLiveCardManager.java │ │ │ ├── Timer.java │ │ │ ├── TimerView.java │ │ │ ├── MenuActivity.java │ │ │ └── SetTimerActivity.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── google │ │ └── android │ │ └── glass │ │ └── sample │ │ └── timer │ │ ├── MockMenuActivity.java │ │ ├── MockSetTimerActivity.java │ │ ├── TimerLiveCardManagerTest.java │ │ ├── TimerDrawerTest.java │ │ ├── TimerTest.java │ │ ├── SetTimerActivityTest.java │ │ ├── TimerViewTest.java │ │ └── MenuActivityTest.java └── build.gradle ├── .gitignore ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/gdk-timer-sample/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/gdk-timer-sample/HEAD/app/src/main/res/drawable/ic_lap.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/gdk-timer-sample/HEAD/app/src/main/res/drawable/ic_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/gdk-timer-sample/HEAD/app/src/main/res/drawable/ic_stop.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_colon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/gdk-timer-sample/HEAD/app/src/main/res/drawable/ic_colon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/gdk-timer-sample/HEAD/app/src/main/res/drawable/ic_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/gdk-timer-sample/HEAD/app/src/main/res/drawable/ic_reset.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/gdk-timer-sample/HEAD/app/src/main/res/drawable/ic_timer.png -------------------------------------------------------------------------------- /app/src/main/res/raw/timer_finished.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleglass/gdk-timer-sample/HEAD/app/src/main/res/raw/timer_finished.wav -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Intellij project files 26 | *.iml 27 | *.ipr 28 | *.iws 29 | .idea/ 30 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | repositories { 4 | jcenter() 5 | flatDir { 6 | dirs 'prebuilt-libs' 7 | } 8 | } 9 | 10 | android { 11 | compileSdkVersion "Google Inc.:Glass Development Kit Preview:19" 12 | buildToolsVersion "20.0.0" 13 | 14 | defaultConfig { 15 | minSdkVersion 19 16 | targetSdkVersion 19 17 | } 18 | buildTypes { 19 | release { 20 | runProguard false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | compile fileTree(dir: 'libs', include: ['*.jar']) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/xml/voice_trigger_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | #ff000000 19 | #ffffffff 20 | #ff808080 21 | #ffcc3333 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Timer 2 | ===== 3 | 4 | This sample inserts a live card to the left of the Glass clock that displays a 5 | timer. Tapping the live card presents a menu with options that are displayed 6 | according to the state of the timer: 7 | 8 | - Set timer: set the time for the timer 9 | - Start: start the timer 10 | - Pause: pause the timer 11 | - Resume: resume the timer 12 | - Reset: reset the timer 13 | - Change timer: change the time of the timer 14 | - Stop: remove the timer from the timeline 15 | 16 | ## Getting started 17 | 18 | Check out our documentation to learn how to get started on 19 | https://developers.google.com/glass/gdk/index 20 | 21 | ## Running the sample on Glass 22 | 23 | You can use your IDE to compile and install the sample or use 24 | [`adb`](https://developer.android.com/tools/help/adb.html) 25 | on the command line: 26 | 27 | $ adb install -r TimerSample.apk 28 | 29 | To start the sample, say "ok glass, start a timer" from the Glass clock 30 | screen or use the touch menu. 31 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/android/glass/sample/timer/MockMenuActivity.java: -------------------------------------------------------------------------------- 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.google.android.glass.sample.timer; 18 | 19 | import java.lang.Runnable; 20 | 21 | /** 22 | * Implementation of {@link MenuActivity} for testing. 23 | */ 24 | public class MockMenuActivity extends MenuActivity { 25 | 26 | @Override 27 | protected void post(Runnable runnable) { 28 | runnable.run(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/res/menu/set_timer.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values-en-rGB/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 19 | "Timer" 20 | "Timer finished" 21 | "Swipe forward to set the timer" 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 20 | Timer 21 | 22 | 23 | Timer finished 24 | 25 | Swipe forward to set the timer 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/menu_strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 19 | 22 | CHANGE_THE_TIMER 23 | PAUSE_THE_TIMER 24 | RESET_THE_TIMER 25 | RESUME_THE_TIMER 26 | SET_THE_TIMER 27 | START_THE_TIMER 28 | STOP_THE_TIMER 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/menu/timer.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 27 | 28 | 34 | 35 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/android/glass/sample/timer/MockSetTimerActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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.google.android.glass.sample.timer; 18 | 19 | import android.content.ComponentName; 20 | import android.content.Intent; 21 | 22 | import java.util.List; 23 | import java.util.ArrayList; 24 | 25 | /** 26 | * Extension of {@link SetTimerActivity} to mock out testable methods. 27 | */ 28 | public class MockSetTimerActivity extends SetTimerActivity { 29 | 30 | int mResultCode; 31 | Intent mResultIntent; 32 | Intent mServiceIntent; 33 | List mPlayedSoundEffects = new ArrayList(); 34 | 35 | boolean mOptionsMenuOpen; 36 | 37 | ComponentName mCallingActivity; 38 | 39 | @Override 40 | protected void setResultInternal(int code, Intent intent) { 41 | mResultCode = code; 42 | mResultIntent = intent; 43 | } 44 | 45 | @Override 46 | protected void playSoundEffect(int soundId) { 47 | mPlayedSoundEffects.add(soundId); 48 | } 49 | 50 | @Override 51 | public ComponentName startService(Intent intent) { 52 | mServiceIntent = intent; 53 | return intent.getComponent(); 54 | } 55 | 56 | @Override 57 | public ComponentName getCallingActivity() { 58 | return mCallingActivity; 59 | } 60 | 61 | @Override 62 | public void openOptionsMenu() { 63 | mOptionsMenuOpen = true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/res/layout/card_timer.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 21 | 27 | 30 | 33 | 36 | 39 | 42 | 43 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/android/glass/sample/timer/TimerLiveCardManagerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * 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, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.glass.sample.timer; 18 | 19 | import com.google.android.glass.timeline.LiveCard; 20 | 21 | import android.test.AndroidTestCase; 22 | import android.test.suitebuilder.annotation.SmallTest; 23 | 24 | import java.util.concurrent.TimeUnit; 25 | 26 | /** 27 | * Unit tests for {@link TimerLiveCardManager}. 28 | */ 29 | @SmallTest 30 | public class TimerLiveCardManagerTest extends AndroidTestCase { 31 | 32 | /** Initial duration of 02h30m00s. */ 33 | private static final long INITIAL_DURATION_MILLIS = TimeUnit.HOURS.toMillis(2) 34 | + TimeUnit.MINUTES.toMillis(30); 35 | 36 | private TimerLiveCardManager mManager; 37 | 38 | @Override 39 | protected void setUp() throws Exception { 40 | super.setUp(); 41 | 42 | mManager = new TimerLiveCardManager(getContext()); 43 | } 44 | 45 | public void testStartNewTimer() { 46 | Timer timer = mManager.startNewTimer(INITIAL_DURATION_MILLIS); 47 | LiveCard liveCard = mManager.getLiveCard(timer); 48 | 49 | assertNotNull(timer); 50 | assertNotNull(liveCard); 51 | assertTrue(timer.isStarted()); 52 | assertEquals(INITIAL_DURATION_MILLIS, timer.getDurationMillis()); 53 | assertTrue(liveCard.isPublished()); 54 | } 55 | 56 | public void testStopTimer() { 57 | Timer timer1 = mManager.startNewTimer(INITIAL_DURATION_MILLIS); 58 | Timer timer2 = mManager.startNewTimer(INITIAL_DURATION_MILLIS); 59 | 60 | assertFalse(mManager.stopTimer(timer1.hashCode())); 61 | assertTrue(mManager.stopTimer(timer2.hashCode())); 62 | assertTrue(mManager.stopTimer(0)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 21 | 22 | 25 | 26 | 30 | 31 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/android/glass/sample/timer/TimerService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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.google.android.glass.sample.timer; 18 | 19 | import android.app.Service; 20 | import android.content.Intent; 21 | import android.os.Binder; 22 | import android.os.IBinder; 23 | 24 | /** 25 | * Service owning the LiveCard living in the timeline. 26 | */ 27 | public class TimerService extends Service { 28 | 29 | /** {@link TimerService} Action: start an existing {@link Timer}. */ 30 | public static final String ACTION_START = "com.google.android.glass.sample.timer.action.START"; 31 | 32 | /** {@link TimerService} Action: stop an existing {@link Timer}. */ 33 | public static final String ACTION_STOP = "com.google.android.glass.sample.timer.action.STOP"; 34 | 35 | /** Timer duration in milliseconds. */ 36 | public static final String EXTRA_DURATION_MILLIS = "duration_millis"; 37 | 38 | /** String extra containing the hashcode of the {@link Timer} to process. */ 39 | public static final String EXTRA_TIMER_HASH_CODE = "timer_hash_code"; 40 | 41 | /** 42 | * Binder giving access to the underlying {@code Timer}. 43 | */ 44 | public static class TimerBinder extends Binder { 45 | private Timer mTimer; 46 | 47 | public TimerBinder(Timer timer) { 48 | mTimer = timer; 49 | } 50 | 51 | public Timer getTimer() { 52 | return mTimer; 53 | } 54 | } 55 | 56 | private final TimerLiveCardManager mManager = new TimerLiveCardManager(this); 57 | 58 | @Override 59 | public IBinder onBind(Intent intent) { 60 | Timer timer = mManager.findTimer(intent.getIntExtra(EXTRA_TIMER_HASH_CODE, 0)); 61 | 62 | if (timer != null) { 63 | return new TimerBinder(timer); 64 | } 65 | return null; 66 | } 67 | 68 | @Override 69 | public int onStartCommand(Intent intent, int flags, int startId) { 70 | handleIntent(intent); 71 | 72 | // Return START_NOT_STICKY to prevent the system from restarting the service if it is killed 73 | // (e.g., due to an error). It doesn't make sense to restart automatically because the timer 74 | // state will have been lost. 75 | return START_NOT_STICKY; 76 | } 77 | 78 | private void handleIntent(Intent intent) { 79 | String action = intent.getAction(); 80 | 81 | if (action.equals(ACTION_START)) { 82 | long durationMillis = intent.getLongExtra(EXTRA_DURATION_MILLIS, 0); 83 | 84 | mManager.startNewTimer(durationMillis); 85 | } else if (action.equals(ACTION_STOP)) { 86 | if (mManager.stopTimer(intent.getIntExtra(EXTRA_TIMER_HASH_CODE, 0))) { 87 | stopSelf(); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/android/glass/sample/timer/TimerDrawer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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.google.android.glass.sample.timer; 18 | 19 | import com.google.android.glass.timeline.DirectRenderingCallback; 20 | 21 | import android.content.Context; 22 | import android.graphics.Canvas; 23 | import android.view.SurfaceHolder; 24 | import android.view.View; 25 | 26 | /** 27 | * SurfaceHolder.Callback used to draw the timer on the timeline {@link LiveCard}. 28 | */ 29 | public class TimerDrawer implements DirectRenderingCallback { 30 | 31 | private SurfaceHolder mHolder; 32 | private boolean mRenderingPaused; 33 | 34 | private final TimerView mView; 35 | private final TimerView.ChangeListener mListener = new TimerView.ChangeListener() { 36 | 37 | @Override 38 | public void onChange() { 39 | if (mHolder != null) { 40 | draw(); 41 | } 42 | } 43 | }; 44 | 45 | public TimerDrawer(Context context, Timer timer) { 46 | mView = new TimerView(context); 47 | mView.setTimer(timer); 48 | mView.setListener(mListener); 49 | } 50 | 51 | public TimerDrawer(TimerView view) { 52 | mView = view; 53 | mView.setListener(mListener); 54 | } 55 | 56 | public Timer getTimer() { 57 | return mView.getTimer(); 58 | } 59 | 60 | @Override 61 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 62 | // Measure and layout the view with the canvas dimensions. 63 | int measuredWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); 64 | int measuredHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); 65 | 66 | mView.measure(measuredWidth, measuredHeight); 67 | mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); 68 | draw(); 69 | } 70 | 71 | @Override 72 | public void surfaceCreated(SurfaceHolder holder) { 73 | // The creation of a new Surface implicitly resumes the rendering. 74 | mRenderingPaused = false; 75 | mHolder = holder; 76 | draw(); 77 | } 78 | 79 | @Override 80 | public void surfaceDestroyed(SurfaceHolder holder) { 81 | mHolder = null; 82 | } 83 | 84 | @Override 85 | public void renderingPaused(SurfaceHolder holder, boolean paused) { 86 | mRenderingPaused = paused; 87 | draw(); 88 | } 89 | 90 | public void draw() { 91 | if (!mRenderingPaused && mHolder != null) { 92 | Canvas canvas; 93 | try { 94 | canvas = mHolder.lockCanvas(); 95 | } catch (Exception e) { 96 | return; 97 | } 98 | if (canvas != null) { 99 | mView.draw(canvas); 100 | mHolder.unlockCanvasAndPost(canvas); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/android/glass/sample/timer/TimerLiveCardManager.java: -------------------------------------------------------------------------------- 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.google.android.glass.sample.timer; 18 | 19 | import com.google.android.glass.timeline.LiveCard; 20 | import com.google.android.glass.timeline.LiveCard.PublishMode; 21 | 22 | import android.app.PendingIntent; 23 | import android.app.Service; 24 | import android.content.Context; 25 | import android.content.Intent; 26 | import android.net.Uri; 27 | 28 | import java.util.HashMap; 29 | 30 | /** 31 | * Class to manage {@link LiveCard} for the {@link TimerService}. 32 | */ 33 | public class TimerLiveCardManager { 34 | 35 | private final Context mContext; 36 | private final HashMap mTimers = new HashMap(); 37 | 38 | public TimerLiveCardManager(Context context) { 39 | mContext = context; 40 | } 41 | 42 | /** Starts a new {@link Timer}/{@link LiveCard} combination with the provided duration. */ 43 | public Timer startNewTimer(long durationMillis) { 44 | Timer timer = new Timer(durationMillis); 45 | TimerDrawer drawer = new TimerDrawer(mContext, timer); 46 | LiveCard liveCard = new LiveCard(mContext, timer.toString()); 47 | 48 | liveCard.setDirectRenderingEnabled(true).getSurfaceHolder().addCallback(drawer); 49 | liveCard.setVoiceActionEnabled(true); 50 | 51 | Intent menuIntent = new Intent(mContext, MenuActivity.class); 52 | menuIntent.setData(Uri.parse("glass.timer:" + timer.hashCode())); 53 | menuIntent.putExtra(TimerService.EXTRA_TIMER_HASH_CODE, timer.hashCode()); 54 | liveCard.setAction(PendingIntent.getActivity(mContext, 0, menuIntent, 0)); 55 | if (mContext instanceof Service) { 56 | liveCard.attach((Service) mContext); 57 | } 58 | liveCard.publish(PublishMode.REVEAL); 59 | timer.start(); 60 | 61 | mTimers.put(timer, liveCard); 62 | return timer; 63 | } 64 | 65 | /** 66 | * Stops the {@link Timer}/{@link LiveCard} and returns whether or not the manager is empty of 67 | * {@link Timer}. 68 | */ 69 | public boolean stopTimer(int timerHashCode) { 70 | Timer timer = findTimer(timerHashCode); 71 | 72 | if (timer != null) { 73 | LiveCard liveCard = mTimers.get(timer); 74 | 75 | liveCard.unpublish(); 76 | timer.reset(); 77 | mTimers.remove(timer); 78 | } 79 | return mTimers.isEmpty(); 80 | } 81 | 82 | /** Returns the {@link LiveCard} associated with this {@link Timer}. */ 83 | public LiveCard getLiveCard(Timer timer) { 84 | return mTimers.get(timer); 85 | } 86 | 87 | /** 88 | * Returns the {@link Timer} identified by the provided {@code timerHashCode}. 89 | */ 90 | public Timer findTimer(int timerHashCode) { 91 | for (Timer timer : mTimers.keySet()) { 92 | if (timer.hashCode() == timerHashCode) { 93 | return timer; 94 | } 95 | } 96 | return null; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/android/glass/sample/timer/Timer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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.google.android.glass.sample.timer; 18 | 19 | import android.os.SystemClock; 20 | 21 | /** 22 | * Model holding the Timer state. 23 | */ 24 | public class Timer { 25 | 26 | /** 27 | * Interface to listen for changes on the {@link Timer}. 28 | */ 29 | public interface TimerListener { 30 | /** Timer has started. */ 31 | public void onStart(); 32 | /** Timer has been paused. */ 33 | public void onPause(); 34 | /** Timer has been reset */ 35 | public void onReset(); 36 | } 37 | 38 | private long mDurationMillis; 39 | private long mStartTimeMillis; 40 | private long mPauseTimeMillis; 41 | 42 | private TimerListener mListener; 43 | 44 | public Timer() { 45 | this(0); 46 | } 47 | 48 | public Timer(long durationMillis) { 49 | setDurationMillis(durationMillis); 50 | } 51 | 52 | /** 53 | * Sets the timer's duration in milliseconds. 54 | */ 55 | public void setDurationMillis(long durationMillis) { 56 | mDurationMillis = durationMillis; 57 | if (mListener != null) { 58 | mListener.onReset(); 59 | } 60 | } 61 | 62 | /** 63 | * Gets the timer's duration in milliseconds. 64 | */ 65 | public long getDurationMillis() { 66 | return mDurationMillis; 67 | } 68 | 69 | /** 70 | * Returns whether or not the timer is running. 71 | */ 72 | public boolean isRunning() { 73 | return mStartTimeMillis > 0 && mPauseTimeMillis == 0; 74 | } 75 | 76 | /** 77 | * Returns whether or not the timer has been started. 78 | */ 79 | public boolean isStarted() { 80 | return mStartTimeMillis > 0; 81 | } 82 | 83 | /** 84 | * Gets the remaining time in milliseconds. 85 | */ 86 | public long getRemainingTimeMillis() { 87 | long remainingTime = mDurationMillis; 88 | 89 | if (mPauseTimeMillis != 0) { 90 | remainingTime -= mPauseTimeMillis - mStartTimeMillis; 91 | } else if (mStartTimeMillis != 0) { 92 | remainingTime -= getElapsedRealtime() - mStartTimeMillis; 93 | } 94 | 95 | return remainingTime; 96 | } 97 | 98 | /** 99 | * Starts the timer. 100 | */ 101 | public void start() { 102 | long elapsedTime = mPauseTimeMillis - mStartTimeMillis; 103 | 104 | mStartTimeMillis = getElapsedRealtime() - elapsedTime; 105 | mPauseTimeMillis = 0; 106 | if (mListener != null) { 107 | mListener.onStart(); 108 | } 109 | } 110 | 111 | /** 112 | * Pauses the timer. 113 | */ 114 | public void pause() { 115 | if (isStarted()) { 116 | mPauseTimeMillis = getElapsedRealtime(); 117 | if (mListener != null) { 118 | mListener.onPause(); 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Resets the timer. 125 | */ 126 | public void reset() { 127 | mStartTimeMillis = 0; 128 | mPauseTimeMillis = 0; 129 | if (mListener != null) { 130 | mListener.onPause(); 131 | mListener.onReset(); 132 | } 133 | } 134 | 135 | /** 136 | * Sets a {@link TimerListener}. 137 | */ 138 | public void setListener(TimerListener listener) { 139 | mListener = listener; 140 | } 141 | 142 | /** 143 | * Returns {@link SystemClock.elapsedRealtime()}, overridable for testing. 144 | */ 145 | protected long getElapsedRealtime() { 146 | return SystemClock.elapsedRealtime(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/android/glass/sample/timer/TimerDrawerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * 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, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.glass.sample.timer; 18 | 19 | import android.test.AndroidTestCase; 20 | 21 | import android.graphics.Canvas; 22 | import android.graphics.Rect; 23 | import android.view.SurfaceHolder; 24 | import android.view.Surface; 25 | import android.test.suitebuilder.annotation.SmallTest; 26 | 27 | /** 28 | * Unit tests for {@link TimerDrawer}. 29 | */ 30 | @SmallTest 31 | public class TimerDrawerTest extends AndroidTestCase { 32 | 33 | private TimerDrawer mDrawer; 34 | private TimerView mView; 35 | 36 | private int mCanvasLockedCount; 37 | private int mCanvasUnlockedCount; 38 | private int mDrawCount; 39 | 40 | /** Simple {@link SurfaceHolder} implementation for testing. */ 41 | private final SurfaceHolder mHolder = new SurfaceHolder() { 42 | Canvas mCanvas = new Canvas(); 43 | 44 | @Override 45 | public void addCallback(SurfaceHolder.Callback callback) { 46 | // Nothing to do here. 47 | } 48 | 49 | @Override 50 | public Surface getSurface() { 51 | return null; 52 | } 53 | 54 | @Override 55 | public Rect getSurfaceFrame() { 56 | return null; 57 | } 58 | 59 | @Override 60 | public boolean isCreating() { 61 | return false; 62 | } 63 | 64 | @Override 65 | public Canvas lockCanvas() { 66 | ++mCanvasLockedCount; 67 | return mCanvas; 68 | } 69 | 70 | @Override 71 | public Canvas lockCanvas(Rect rect) { 72 | return lockCanvas(); 73 | } 74 | 75 | @Override 76 | public void removeCallback(SurfaceHolder.Callback callback) { 77 | // Nothing to do here. 78 | } 79 | 80 | @Override 81 | public void setFixedSize(int width, int height) { 82 | // Nothing to do here. 83 | } 84 | 85 | @Override 86 | public void setFormat(int format) { 87 | // Nothing to do here. 88 | } 89 | 90 | @Override 91 | public void setKeepScreenOn(boolean keepScreenOn) { 92 | // Nothing to do here. 93 | } 94 | 95 | @Override 96 | public void setSizeFromLayout() { 97 | // Nothing to do here. 98 | } 99 | 100 | @Override 101 | public void setType(int type) { 102 | // Nothing to do here. 103 | } 104 | 105 | @Override 106 | public void unlockCanvasAndPost(Canvas canvas) { 107 | assertEquals(mCanvas, canvas); 108 | ++mCanvasUnlockedCount; 109 | } 110 | }; 111 | 112 | @Override 113 | protected void setUp() throws Exception { 114 | super.setUp(); 115 | mView = new TimerView(getContext()) { 116 | 117 | @Override 118 | public void draw(Canvas canvas) { 119 | ++mDrawCount; 120 | } 121 | }; 122 | mDrawer = new TimerDrawer(mView); 123 | 124 | mCanvasLockedCount = 0; 125 | mCanvasUnlockedCount = 0; 126 | mDrawCount = 0; 127 | } 128 | 129 | public void testConstructorSetsListener() { 130 | assertNotNull(mView.getListener()); 131 | } 132 | 133 | public void testSurfaceChanged() { 134 | int width = 640; 135 | int height = 360; 136 | 137 | // Ensure the test is not a no-op. 138 | assertEquals(0, mView.getWidth()); 139 | assertEquals(0, mView.getHeight()); 140 | mDrawer.surfaceChanged(mHolder, 0, width, height); 141 | assertEquals(0, mDrawCount); 142 | assertEquals(width, mView.getWidth()); 143 | assertEquals(height, mView.getHeight()); 144 | } 145 | 146 | public void testSurfaceCreatedDrawsOnce() { 147 | mDrawer.surfaceCreated(mHolder); 148 | assertEquals(1, mDrawCount); 149 | } 150 | 151 | public void testSurfaceCreatedRenderingPaused() { 152 | mDrawer.renderingPaused(mHolder, true); 153 | assertEquals(0, mDrawCount); 154 | mDrawer.surfaceCreated(mHolder); 155 | assertEquals(1, mDrawCount); 156 | } 157 | 158 | public void testSurfaceDestroyed() { 159 | mDrawer.surfaceDestroyed(mHolder); 160 | assertEquals(0, mDrawCount); 161 | } 162 | 163 | public void testRenderingPausedFalseNoSurface() { 164 | mDrawer.renderingPaused(mHolder, false); 165 | assertEquals(0, mDrawCount); 166 | } 167 | 168 | public void testRenderingPausedFalseWithSurface() { 169 | mDrawer.surfaceCreated(mHolder); 170 | assertEquals(1, mDrawCount); 171 | mDrawer.renderingPaused(mHolder, false); 172 | assertEquals(2, mDrawCount); 173 | } 174 | 175 | public void testRenderingPausedTrue() { 176 | mDrawer.surfaceCreated(mHolder); 177 | assertEquals(1, mDrawCount); 178 | // Test that no other calls to mView.draw() occurred. 179 | mDrawer.renderingPaused(mHolder, true); 180 | assertEquals(1, mDrawCount); 181 | } 182 | 183 | public void testDrawProperlyLocksAndUnlocksCanvas() { 184 | // This also calls mDrawer.draw(); 185 | mDrawer.surfaceCreated(mHolder); 186 | assertEquals(1, mDrawCount); 187 | assertEquals(1, mCanvasLockedCount); 188 | assertEquals(1, mCanvasUnlockedCount); 189 | } 190 | 191 | public void testListenerNoSurfaceDoesNotCallDraw() { 192 | mView.getListener().onChange(); 193 | assertEquals(0, mDrawCount); 194 | } 195 | 196 | public void testListenerWithSurfaceCallsDraw() { 197 | mDrawer.surfaceCreated(mHolder); 198 | assertEquals(1, mDrawCount); 199 | mView.getListener().onChange(); 200 | assertEquals(2, mDrawCount); 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/android/glass/sample/timer/TimerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * 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, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.glass.sample.timer; 18 | 19 | import android.os.SystemClock; 20 | import android.test.AndroidTestCase; 21 | import android.test.suitebuilder.annotation.SmallTest; 22 | 23 | import java.util.concurrent.TimeUnit; 24 | 25 | /** 26 | * Unit tests for {@link Timer}. 27 | */ 28 | @SmallTest 29 | public class TimerTest extends AndroidTestCase { 30 | 31 | private static final long INITIAL_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(5); 32 | 33 | private Timer mTimer; 34 | private long mElapsedRealtime; 35 | 36 | private boolean mOnStartCalled; 37 | private boolean mOnPauseCalled; 38 | private boolean mOnResetCalled; 39 | 40 | /** 41 | * Mock {@link Timer.TimerListener} to verify that callbacks are properly called. 42 | */ 43 | private class MockTimerListener implements Timer.TimerListener { 44 | 45 | @Override 46 | public void onStart() { 47 | mOnStartCalled = true; 48 | } 49 | 50 | @Override 51 | public void onPause() { 52 | mOnPauseCalled = true; 53 | } 54 | 55 | @Override 56 | public void onReset() { 57 | mOnResetCalled = true; 58 | } 59 | } 60 | 61 | 62 | @Override 63 | protected void setUp() throws Exception { 64 | super.setUp(); 65 | mTimer = new Timer(INITIAL_DURATION_MILLIS) { 66 | 67 | @Override 68 | protected long getElapsedRealtime() { 69 | return mElapsedRealtime; 70 | } 71 | }; 72 | mTimer.setListener(new MockTimerListener()); 73 | mElapsedRealtime = SystemClock.elapsedRealtime(); 74 | mOnStartCalled = false; 75 | mOnPauseCalled = false; 76 | mOnResetCalled = false; 77 | } 78 | 79 | public void testSetDurationMillisCallsListener() { 80 | long expectedDurationMillis = TimeUnit.SECONDS.toMillis(5); 81 | 82 | assertEquals(INITIAL_DURATION_MILLIS, mTimer.getDurationMillis()); 83 | mTimer.setDurationMillis(expectedDurationMillis); 84 | assertTrue(mOnResetCalled); 85 | assertFalse(mOnStartCalled); 86 | assertFalse(mOnPauseCalled); 87 | assertEquals(expectedDurationMillis, mTimer.getDurationMillis()); 88 | } 89 | 90 | public void testIsRunningNotStarted() { 91 | assertFalse(mTimer.isRunning()); 92 | assertListenersNotCalled(); 93 | } 94 | 95 | public void testIsRunningStartedAndRunning() { 96 | mTimer.start(); 97 | assertTrue(mTimer.isRunning()); 98 | } 99 | 100 | public void testIsRunningStartedAndPaused() { 101 | mTimer.start(); 102 | mElapsedRealtime += TimeUnit.SECONDS.toMillis(1); 103 | mTimer.pause(); 104 | assertFalse(mTimer.isRunning()); 105 | } 106 | 107 | public void testIsStartedNotStarted() { 108 | assertFalse(mTimer.isStarted()); 109 | assertListenersNotCalled(); 110 | } 111 | 112 | public void testIsStartedStartedAndRunning() { 113 | mTimer.start(); 114 | assertTrue(mTimer.isStarted()); 115 | } 116 | 117 | public void testIsStartedStartedAndPaused() { 118 | mTimer.start(); 119 | mElapsedRealtime += TimeUnit.SECONDS.toMillis(1); 120 | mTimer.pause(); 121 | assertTrue(mTimer.isStarted()); 122 | } 123 | 124 | public void testGetRemainingTimeMillisNotStarted() { 125 | assertEquals(INITIAL_DURATION_MILLIS, mTimer.getRemainingTimeMillis()); 126 | assertListenersNotCalled(); 127 | } 128 | 129 | public void testGetRemainingTimeMillisStartedAndRunning() { 130 | long elapsedTime = TimeUnit.SECONDS.toMillis(30); 131 | 132 | mTimer.start(); 133 | mElapsedRealtime += elapsedTime; 134 | assertEquals(INITIAL_DURATION_MILLIS - elapsedTime, mTimer.getRemainingTimeMillis()); 135 | } 136 | 137 | public void testGetRemainingTimeMillisStartedAndRunningNegative() { 138 | long elapsedTime = INITIAL_DURATION_MILLIS + TimeUnit.MINUTES.toMillis(30); 139 | 140 | mTimer.start(); 141 | mElapsedRealtime += elapsedTime; 142 | assertEquals(INITIAL_DURATION_MILLIS - elapsedTime, mTimer.getRemainingTimeMillis()); 143 | } 144 | 145 | public void testGetRemainingTimeMillisStartedAndPaused() { 146 | long elapsedTime = TimeUnit.SECONDS.toMillis(30); 147 | 148 | mTimer.start(); 149 | mElapsedRealtime += elapsedTime; 150 | mTimer.pause(); 151 | mElapsedRealtime += elapsedTime; 152 | assertEquals(INITIAL_DURATION_MILLIS - elapsedTime, mTimer.getRemainingTimeMillis()); 153 | } 154 | 155 | public void testStartCallsListener() { 156 | mTimer.start(); 157 | assertTrue(mTimer.isStarted()); 158 | assertTrue(mTimer.isRunning()); 159 | assertTrue(mOnStartCalled); 160 | assertFalse(mOnResetCalled); 161 | assertFalse(mOnPauseCalled); 162 | } 163 | 164 | public void testPauseCallsListener() { 165 | mTimer.start(); 166 | mOnStartCalled = false; 167 | mElapsedRealtime += TimeUnit.SECONDS.toMillis(5); 168 | mTimer.pause(); 169 | assertTrue(mTimer.isStarted()); 170 | assertFalse(mTimer.isRunning()); 171 | assertTrue(mOnPauseCalled); 172 | assertFalse(mOnStartCalled); 173 | assertFalse(mOnResetCalled); 174 | } 175 | 176 | public void testResetCallsListener() { 177 | mTimer.start(); 178 | mOnStartCalled = false; 179 | mElapsedRealtime += TimeUnit.SECONDS.toMillis(5); 180 | assertTrue(mTimer.isStarted()); 181 | assertTrue(mTimer.isRunning()); 182 | mTimer.reset(); 183 | assertFalse(mTimer.isStarted()); 184 | assertFalse(mTimer.isRunning()); 185 | assertTrue(mOnResetCalled); 186 | assertTrue(mOnPauseCalled); 187 | assertFalse(mOnStartCalled); 188 | } 189 | 190 | private void assertListenersNotCalled() { 191 | assertFalse(mOnStartCalled); 192 | assertFalse(mOnPauseCalled); 193 | assertFalse(mOnResetCalled); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/android/glass/sample/timer/SetTimerActivityTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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.google.android.glass.sample.timer; 18 | 19 | import com.google.android.glass.touchpad.Gesture; 20 | import com.google.android.glass.media.Sounds; 21 | 22 | import android.app.Activity; 23 | import android.content.ComponentName; 24 | import android.content.Intent; 25 | import android.test.ActivityUnitTestCase; 26 | import android.test.UiThreadTest; 27 | import android.test.suitebuilder.annotation.SmallTest; 28 | 29 | import java.util.concurrent.TimeUnit; 30 | 31 | /** 32 | * Unit tests for {@link SetTimerActivity}. 33 | */ 34 | @SmallTest 35 | public class SetTimerActivityTest extends ActivityUnitTestCase { 36 | 37 | private static final long INITIAL_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(5); 38 | 39 | private Intent mActivityIntent; 40 | 41 | public SetTimerActivityTest() { 42 | super(MockSetTimerActivity.class); 43 | } 44 | 45 | @Override 46 | protected void setUp() throws Exception { 47 | super.setUp(); 48 | 49 | mActivityIntent = 50 | new Intent(getInstrumentation().getTargetContext(), SetTimerActivity.class); 51 | mActivityIntent.putExtra(SetTimerActivity.EXTRA_DURATION_MILLIS, INITIAL_DURATION_MILLIS); 52 | } 53 | 54 | @UiThreadTest 55 | public void testOnCreate() { 56 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 57 | 58 | assertEquals( 59 | (float) TimeUnit.MILLISECONDS.toSeconds(INITIAL_DURATION_MILLIS), 60 | activity.getTimeSeconds()); 61 | } 62 | 63 | @UiThreadTest 64 | public void testOnCreateNoDuration() { 65 | mActivityIntent.removeExtra(SetTimerActivity.EXTRA_DURATION_MILLIS); 66 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 67 | 68 | assertEquals(0f, activity.getTimeSeconds()); 69 | } 70 | 71 | @UiThreadTest 72 | public void testOnGestureTapSupported() { 73 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 74 | 75 | assertTrue(activity.onGesture(Gesture.TAP)); 76 | assertEquals(1, activity.mPlayedSoundEffects.size()); 77 | assertEquals(Sounds.TAP, activity.mPlayedSoundEffects.get(0).intValue()); 78 | assertTrue(activity.mOptionsMenuOpen); 79 | } 80 | 81 | @UiThreadTest 82 | public void testOnGestureTapDisallowed() { 83 | mActivityIntent.removeExtra(SetTimerActivity.EXTRA_DURATION_MILLIS); 84 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 85 | 86 | assertTrue(activity.onGesture(Gesture.TAP)); 87 | assertEquals(1, activity.mPlayedSoundEffects.size()); 88 | assertEquals(Sounds.DISALLOWED, activity.mPlayedSoundEffects.get(0).intValue()); 89 | assertFalse(activity.mOptionsMenuOpen); 90 | } 91 | 92 | @UiThreadTest 93 | public void testOnGestureSwipeDownSupported() { 94 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 95 | 96 | assertTrue(activity.onGesture(Gesture.SWIPE_DOWN)); 97 | assertEquals(1, activity.mPlayedSoundEffects.size()); 98 | assertEquals(Sounds.DISMISSED, activity.mPlayedSoundEffects.get(0).intValue()); 99 | 100 | assertTrue(isFinishCalled()); 101 | assertEquals(Activity.RESULT_CANCELED, activity.mResultCode); 102 | assertNull(activity.mResultIntent); 103 | } 104 | 105 | public void testOnOptionsItemSelectedStart() { 106 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 107 | 108 | assertTrue(getInstrumentation().invokeMenuActionSync(activity, R.id.start, 0)); 109 | assertNotNull(activity.mServiceIntent); 110 | assertEquals( 111 | TimerService.class.getName(), 112 | activity.mServiceIntent.getComponent().getClassName()); 113 | assertEquals( 114 | INITIAL_DURATION_MILLIS, 115 | activity.mServiceIntent.getLongExtra(TimerService.EXTRA_DURATION_MILLIS, 0)); 116 | } 117 | 118 | public void testOnOptionsItemSelectedStartWithCallingActivity() { 119 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 120 | activity.mCallingActivity = new ComponentName(activity, MenuActivity.class); 121 | 122 | assertTrue(getInstrumentation().invokeMenuActionSync(activity, R.id.start, 0)); 123 | assertNull(activity.mServiceIntent); 124 | assertEquals(Activity.RESULT_OK, activity.mResultCode); 125 | assertNotNull(activity.mResultIntent); 126 | assertEquals( 127 | INITIAL_DURATION_MILLIS, 128 | activity.mResultIntent.getLongExtra(SetTimerActivity.EXTRA_DURATION_MILLIS, 0)); 129 | assertTrue( 130 | activity.mResultIntent.getBooleanExtra(SetTimerActivity.EXTRA_START_TIMER, true)); 131 | } 132 | 133 | public void testOnOptionsItemSelectedSet() { 134 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 135 | activity.mCallingActivity = new ComponentName(activity, MenuActivity.class); 136 | 137 | assertTrue(getInstrumentation().invokeMenuActionSync(activity, R.id.set, 0)); 138 | assertEquals(Activity.RESULT_OK, activity.mResultCode); 139 | assertNotNull(activity.mResultIntent); 140 | assertEquals( 141 | INITIAL_DURATION_MILLIS, 142 | activity.mResultIntent.getLongExtra(SetTimerActivity.EXTRA_DURATION_MILLIS, 0)); 143 | assertFalse( 144 | activity.mResultIntent.getBooleanExtra(SetTimerActivity.EXTRA_START_TIMER, true)); 145 | } 146 | 147 | public void testOnScroll() { 148 | mActivityIntent.removeExtra(SetTimerActivity.EXTRA_DURATION_MILLIS); 149 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 150 | float seconds = 60; 151 | 152 | activity.onScroll(seconds, seconds, 1); 153 | assertEquals(seconds, activity.getTimeSeconds()); 154 | } 155 | 156 | public void testOnFingerCountChangedNoFling() { 157 | mActivityIntent.removeExtra(SetTimerActivity.EXTRA_DURATION_MILLIS); 158 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 159 | 160 | activity.onScroll(0, 0, 0.2f); 161 | assertEquals(0f, activity.getTimeSeconds()); 162 | activity.onFingerCountChanged(1, 0); 163 | activity.forceEndAnimation(); 164 | assertEquals(0f, activity.getTimeSeconds()); 165 | } 166 | 167 | public void testOnFingerCountChangedFling() { 168 | mActivityIntent.removeExtra(SetTimerActivity.EXTRA_DURATION_MILLIS); 169 | MockSetTimerActivity activity = startActivity(mActivityIntent, null, null); 170 | 171 | activity.onScroll(0, 0, 20); 172 | assertEquals(0f, activity.getTimeSeconds()); 173 | activity.onFingerCountChanged(1, 0); 174 | activity.forceEndAnimation(); 175 | assertEquals(1000f, activity.getTimeSeconds()); 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/android/glass/sample/timer/TimerView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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.google.android.glass.sample.timer; 18 | 19 | import android.content.Context; 20 | import android.media.AudioManager; 21 | import android.media.SoundPool; 22 | import android.os.Handler; 23 | import android.util.Log; 24 | import android.util.AttributeSet; 25 | import android.view.LayoutInflater; 26 | import android.view.View; 27 | import android.widget.FrameLayout; 28 | import android.widget.TextView; 29 | 30 | import java.util.concurrent.TimeUnit; 31 | 32 | /** 33 | * View used to draw a running timer. 34 | */ 35 | public class TimerView extends FrameLayout { 36 | 37 | /** 38 | * Interface to listen for changes on the view layout. 39 | */ 40 | public interface ChangeListener { 41 | /** Notified of a change in the view. */ 42 | public void onChange(); 43 | } 44 | 45 | private static final int SOUND_PRIORITY = 1000; 46 | private static final int MAX_STREAMS = 1; 47 | // Visible for testing. 48 | static final long DELAY_MILLIS = 1000; 49 | 50 | private final SoundPool mSoundPool; 51 | private final int mTimerFinishedSoundId; 52 | 53 | private final TextView mHoursView; 54 | private final TextView mMinutesView; 55 | private final TextView mSecondsView; 56 | private final TextView mTipView; 57 | 58 | private final int mWhiteColor; 59 | private final int mRedColor; 60 | 61 | private final Handler mHandler = new Handler(); 62 | private final Runnable mUpdateTextRunnable = new Runnable() { 63 | 64 | @Override 65 | public void run() { 66 | if (mRunning) { 67 | postDelayed(mUpdateTextRunnable, DELAY_MILLIS); 68 | updateText(); 69 | } 70 | } 71 | }; 72 | 73 | private final Timer.TimerListener mTimerListener = new Timer.TimerListener() { 74 | 75 | @Override 76 | public void onStart() { 77 | mRunning = true; 78 | long delayMillis = Math.abs(mTimer.getRemainingTimeMillis()) % DELAY_MILLIS; 79 | if (delayMillis == 0) { 80 | delayMillis = DELAY_MILLIS; 81 | } 82 | postDelayed(mUpdateTextRunnable, delayMillis); 83 | } 84 | 85 | @Override 86 | public void onPause() { 87 | mRunning = false; 88 | removeCallbacks(mUpdateTextRunnable); 89 | } 90 | 91 | @Override 92 | public void onReset() { 93 | mTipView.setVisibility(View.INVISIBLE); 94 | updateText(mTimer.getRemainingTimeMillis(), mWhiteColor); 95 | } 96 | }; 97 | 98 | private Timer mTimer; 99 | 100 | private boolean mStarted; 101 | private boolean mRunning; 102 | private boolean mRedText; 103 | 104 | private ChangeListener mChangeListener; 105 | 106 | public TimerView(Context context) { 107 | this(context, null, 0); 108 | } 109 | 110 | public TimerView(Context context, AttributeSet attrs) { 111 | this(context, attrs, 0); 112 | } 113 | 114 | public TimerView(Context context, AttributeSet attrs, int style) { 115 | super(context, attrs, style); 116 | 117 | mSoundPool = new SoundPool(MAX_STREAMS, AudioManager.STREAM_MUSIC, 0); 118 | mTimerFinishedSoundId = mSoundPool.load(context, R.raw.timer_finished, SOUND_PRIORITY); 119 | 120 | LayoutInflater.from(context).inflate(R.layout.card_timer, this); 121 | 122 | mHoursView = (TextView) findViewById(R.id.hours); 123 | mMinutesView = (TextView) findViewById(R.id.minutes); 124 | mSecondsView = (TextView) findViewById(R.id.seconds); 125 | mTipView = (TextView) findViewById(R.id.tip); 126 | mTipView.setText(context.getResources().getString(R.string.timer_finished)); 127 | mTipView.setVisibility(View.INVISIBLE); 128 | 129 | mWhiteColor = context.getResources().getColor(R.color.white); 130 | mRedColor = context.getResources().getColor(R.color.red); 131 | } 132 | 133 | /** Sets the {@link Timer} model backing up the view. */ 134 | public void setTimer(Timer timer) { 135 | if (mTimer != null) { 136 | // Remove self from the previous Timer's listener. 137 | mTimer.setListener(null); 138 | } 139 | mTimer = timer; 140 | mTimer.setListener(mTimerListener); 141 | updateText(mTimer.getRemainingTimeMillis(), mWhiteColor); 142 | } 143 | 144 | /** Returns the {@link Timer} model backing up the view. */ 145 | public Timer getTimer() { 146 | return mTimer; 147 | } 148 | 149 | /** 150 | * Sets a {@link ChangeListener}. 151 | */ 152 | public void setListener(ChangeListener listener) { 153 | mChangeListener = listener; 154 | } 155 | 156 | /** 157 | * Returns the set {@link ChangeListener}. 158 | */ 159 | public ChangeListener getListener() { 160 | return mChangeListener; 161 | } 162 | 163 | @Override 164 | public boolean postDelayed(Runnable action, long delayMillis) { 165 | return mHandler.postDelayed(action, delayMillis); 166 | } 167 | 168 | @Override 169 | public boolean removeCallbacks(Runnable action) { 170 | mHandler.removeCallbacks(action); 171 | return true; 172 | } 173 | 174 | /** 175 | * Updates the text from the Timer's value, overridable for testing. 176 | */ 177 | protected void updateText() { 178 | long remainingTimeMillis = mTimer.getRemainingTimeMillis(); 179 | 180 | if (remainingTimeMillis > 0) { 181 | mRedText = false; 182 | // Round up: x001 to (x + 1)000 milliseconds should resolve to x seconds. 183 | remainingTimeMillis -= 1; 184 | remainingTimeMillis += TimeUnit.SECONDS.toMillis(1); 185 | mTipView.setVisibility(View.INVISIBLE); 186 | } else { 187 | mRedText = !mRedText; 188 | remainingTimeMillis = Math.abs(remainingTimeMillis); 189 | mTipView.setVisibility(View.VISIBLE); 190 | } 191 | 192 | if (mRedText) { 193 | // Sync the sound with the red text. 194 | playSound(); 195 | } 196 | 197 | updateText(remainingTimeMillis, mRedText ? mRedColor : mWhiteColor); 198 | } 199 | 200 | /** 201 | * Updates the displayed text with the provided values, overridable for testing. 202 | */ 203 | protected void updateText(long timeMillis, int textColor) { 204 | mHoursView.setText(String.format("%02d", TimeUnit.MILLISECONDS.toHours(timeMillis))); 205 | mHoursView.setTextColor(textColor); 206 | timeMillis %= TimeUnit.HOURS.toMillis(1); 207 | mMinutesView.setText(String.format("%02d", TimeUnit.MILLISECONDS.toMinutes(timeMillis))); 208 | mMinutesView.setTextColor(textColor); 209 | timeMillis %= TimeUnit.MINUTES.toMillis(1); 210 | mSecondsView.setText(String.format("%02d", TimeUnit.MILLISECONDS.toSeconds(timeMillis))); 211 | mSecondsView.setTextColor(textColor); 212 | if (mChangeListener != null) { 213 | mChangeListener.onChange(); 214 | } 215 | } 216 | 217 | /** 218 | * Plays the "timer finishd" sound once, overridable for testing. 219 | */ 220 | protected void playSound() { 221 | mSoundPool.play(mTimerFinishedSoundId, 222 | 1 /* leftVolume */, 223 | 1 /* rightVolume */, 224 | SOUND_PRIORITY, 225 | 0 /* loop */, 226 | 1 /* rate */); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/android/glass/sample/timer/TimerViewTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * 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, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.glass.sample.timer; 18 | 19 | import android.test.AndroidTestCase; 20 | 21 | import android.content.Context; 22 | import android.test.suitebuilder.annotation.SmallTest; 23 | import android.view.View; 24 | import android.widget.TextView; 25 | 26 | import java.util.concurrent.TimeUnit; 27 | 28 | /** 29 | * Unit tests for {@link TimerDrawer}. 30 | */ 31 | @SmallTest 32 | public class TimerViewTest extends AndroidTestCase { 33 | 34 | /** Initial duration of 02h30m00s. */ 35 | private static final long INITIAL_DURATION_MILLIS = TimeUnit.HOURS.toMillis(2) 36 | + TimeUnit.MINUTES.toMillis(30); 37 | private static final String INITIAL_HOURS_COMPONENT = "02"; 38 | private static final String INITIAL_MINUTES_COMPONENT = "30"; 39 | private static final String INITIAL_SECONDS_COMPONENT = "00"; 40 | 41 | private long mRemainingTimeMillis; 42 | 43 | private TimerView mView; 44 | private TextView mHoursView; 45 | private TextView mMinutesView; 46 | private TextView mSecondsView; 47 | private TextView mTipView; 48 | 49 | // Test flags. 50 | private boolean mMockUpdateText; 51 | private boolean mOnChangeCalled; 52 | private boolean mPlaySoundCalled; 53 | private boolean mUpdateTextCalled; 54 | private int mTextColor; 55 | private long mPostedCallbackDelayMillis; 56 | private long mTimeMillis; 57 | private Runnable mPostedCallback; 58 | private Runnable mRemovedCallback; 59 | 60 | /** Extension of {@link Timer} for easier testing. */ 61 | private final Timer mTimer = new Timer() { 62 | @Override 63 | public long getRemainingTimeMillis() { 64 | return mRemainingTimeMillis; 65 | } 66 | }; 67 | 68 | /** Extension of {@link TimerView} for easier testing. */ 69 | private class MockTimerView extends TimerView { 70 | 71 | public MockTimerView(Context context, Timer timer) { 72 | super(context, null, 0); 73 | setTimer(timer); 74 | } 75 | 76 | @Override 77 | public boolean postDelayed(Runnable action, long delayMillis) { 78 | mPostedCallback = action; 79 | mPostedCallbackDelayMillis = delayMillis; 80 | return true; 81 | } 82 | 83 | @Override 84 | public boolean removeCallbacks(Runnable action) { 85 | mRemovedCallback = action; 86 | return true; 87 | } 88 | 89 | @Override 90 | protected void updateText(long timeMillis, int textColor) { 91 | mUpdateTextCalled = true; 92 | mTimeMillis = timeMillis; 93 | mTextColor = textColor; 94 | if (!mMockUpdateText) { 95 | super.updateText(timeMillis, textColor); 96 | } 97 | } 98 | 99 | @Override 100 | protected void playSound() { 101 | mPlaySoundCalled = true; 102 | } 103 | 104 | } 105 | 106 | @Override 107 | protected void setUp() throws Exception { 108 | super.setUp(); 109 | 110 | // Reset the test flags. 111 | mMockUpdateText = true; 112 | mUpdateTextCalled = false; 113 | mPlaySoundCalled = false; 114 | mTimeMillis = 0; 115 | mTextColor = 0; 116 | mPostedCallback = null; 117 | mRemovedCallback = null; 118 | mPostedCallbackDelayMillis = 0; 119 | mOnChangeCalled = false; 120 | 121 | // Initialize test objects. 122 | mRemainingTimeMillis = INITIAL_DURATION_MILLIS; 123 | mView = new MockTimerView(getContext(), mTimer); 124 | mView.setListener(new TimerView.ChangeListener() { 125 | 126 | @Override 127 | public void onChange() { 128 | mOnChangeCalled = true; 129 | } 130 | }); 131 | 132 | // Retrieve the underlying views. 133 | mHoursView = (TextView) mView.findViewById(R.id.hours); 134 | mMinutesView = (TextView) mView.findViewById(R.id.minutes); 135 | mSecondsView = (TextView) mView.findViewById(R.id.seconds); 136 | mTipView = (TextView) mView.findViewById(R.id.tip); 137 | } 138 | 139 | public void testConstructorProperlyInitializeViews() { 140 | assertEquals( 141 | getContext().getResources().getString(R.string.timer_finished), 142 | mTipView.getText()); 143 | assertEquals(View.INVISIBLE, mTipView.getVisibility()); 144 | assertTrue(mUpdateTextCalled); 145 | } 146 | 147 | public void testListenerOnStart() { 148 | mTimer.start(); 149 | assertNotNull(mPostedCallback); 150 | assertEquals(TimerView.DELAY_MILLIS, mPostedCallbackDelayMillis); 151 | } 152 | 153 | public void testListenerOnStartWithNoneDefaultDelay() { 154 | // Remove 700ms from the remaining time. 155 | mRemainingTimeMillis -= 700; 156 | mTimer.start(); 157 | assertNotNull(mPostedCallback); 158 | assertEquals(300, mPostedCallbackDelayMillis); 159 | } 160 | 161 | public void testListenerOnPauseRemovesCallback() { 162 | mTimer.start(); 163 | mTimer.pause(); 164 | assertNotNull(mPostedCallback); 165 | assertNotNull(mRemovedCallback); 166 | assertEquals(mPostedCallback, mRemovedCallback); 167 | } 168 | 169 | public void testListenerOnReset() { 170 | mTimer.reset(); 171 | assertTrue(mUpdateTextCalled); 172 | assertEquals(INITIAL_DURATION_MILLIS, mTimeMillis); 173 | assertEquals(getContext().getResources().getColor(R.color.white), mTextColor); 174 | assertEquals(View.INVISIBLE, mTipView.getVisibility()); 175 | } 176 | 177 | public void testUpdateTextTimerRunning() { 178 | long expectedDurationMillis = INITIAL_DURATION_MILLIS - 1 + TimeUnit.SECONDS.toMillis(1); 179 | mView.updateText(); 180 | assertEquals(expectedDurationMillis, mTimeMillis); 181 | assertEquals(getContext().getResources().getColor(R.color.white), mTextColor); 182 | assertEquals(View.INVISIBLE, mTipView.getVisibility()); 183 | } 184 | 185 | public void testUpdateTextTimerFinished() { 186 | mRemainingTimeMillis = TimeUnit.SECONDS.toMillis(-2); 187 | mView.updateText(); 188 | assertEquals(-mRemainingTimeMillis, mTimeMillis); 189 | assertEquals(getContext().getResources().getColor(R.color.red), mTextColor); 190 | assertEquals(View.VISIBLE, mTipView.getVisibility()); 191 | } 192 | 193 | public void testUpdateTextTimerFinishedChangesTextColor() { 194 | int colorWhite = getContext().getResources().getColor(R.color.white); 195 | int colorRed = getContext().getResources().getColor(R.color.red); 196 | 197 | mRemainingTimeMillis = TimeUnit.SECONDS.toMillis(-2); 198 | mView.updateText(); 199 | assertEquals(colorRed, mTextColor); 200 | mView.updateText(); 201 | assertEquals(colorWhite, mTextColor); 202 | mView.updateText(); 203 | assertEquals(colorRed, mTextColor); 204 | } 205 | 206 | public void testUpdateTextWithArgs() { 207 | int colorRed = getContext().getResources().getColor(R.color.red); 208 | 209 | mMockUpdateText = false; 210 | mView.updateText(INITIAL_DURATION_MILLIS, colorRed); 211 | assertEquals(INITIAL_HOURS_COMPONENT, mHoursView.getText()); 212 | assertEquals(INITIAL_MINUTES_COMPONENT, mMinutesView.getText()); 213 | assertEquals(INITIAL_SECONDS_COMPONENT, mSecondsView.getText()); 214 | assertEquals(colorRed, mHoursView.getTextColors().getDefaultColor()); 215 | assertEquals(colorRed, mMinutesView.getTextColors().getDefaultColor()); 216 | assertEquals(colorRed, mSecondsView.getTextColors().getDefaultColor()); 217 | assertTrue(mOnChangeCalled); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/google/android/glass/sample/timer/MenuActivityTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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.google.android.glass.sample.timer; 18 | 19 | import android.app.Activity; 20 | import android.app.Instrumentation; 21 | import android.content.ComponentName; 22 | import android.content.ContextWrapper; 23 | import android.content.Intent; 24 | import android.content.ServiceConnection; 25 | import android.test.ActivityUnitTestCase; 26 | import android.test.suitebuilder.annotation.SmallTest; 27 | import android.view.Window; 28 | 29 | import java.util.concurrent.TimeUnit; 30 | 31 | /** 32 | * Unit tests for {@link MenuActivity}. 33 | */ 34 | @SmallTest 35 | public class MenuActivityTest extends ActivityUnitTestCase { 36 | 37 | private static final long INITIAL_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(5); 38 | 39 | private Timer mTimer; 40 | private boolean mServiceStopped; 41 | private boolean mBindServiceCalled; 42 | private boolean mUnbindServiceCalled; 43 | private Intent mActivityIntent; 44 | 45 | private TimerService.TimerBinder mTimerBinder; 46 | 47 | public MenuActivityTest() { 48 | super(MockMenuActivity.class); 49 | } 50 | 51 | @Override 52 | protected void setUp() throws Exception { 53 | super.setUp(); 54 | mTimer = new Timer(INITIAL_DURATION_MILLIS); 55 | mTimerBinder = new TimerService.TimerBinder(mTimer); 56 | 57 | // Set a mock context to simulate service binding. 58 | setActivityContext(new ContextWrapper(getInstrumentation().getTargetContext()) { 59 | 60 | @Override 61 | public boolean bindService(Intent service, ServiceConnection conn, int flags) { 62 | assertNotNull(service); 63 | assertNotNull(conn); 64 | assertEquals(TimerService.class.getName(), service.getComponent().getClassName()); 65 | assertTrue(service.hasExtra(TimerService.EXTRA_TIMER_HASH_CODE)); 66 | // Bind the mock service with the Activity. 67 | conn.onServiceConnected(null, mTimerBinder); 68 | mBindServiceCalled = true; 69 | return true; 70 | } 71 | 72 | @Override 73 | public void unbindService(ServiceConnection conn) { 74 | mUnbindServiceCalled = true; 75 | } 76 | 77 | @Override 78 | public ComponentName startService(Intent intent) { 79 | assertEquals(TimerService.class.getName(), intent.getComponent().getClassName()); 80 | assertTrue(intent.hasExtra(TimerService.EXTRA_TIMER_HASH_CODE)); 81 | assertEquals(TimerService.ACTION_STOP, intent.getAction()); 82 | mServiceStopped = true; 83 | return intent.getComponent(); 84 | } 85 | }); 86 | 87 | mActivityIntent = new Intent(getInstrumentation().getTargetContext(), MenuActivity.class); 88 | mActivityIntent.putExtra(TimerService.EXTRA_TIMER_HASH_CODE, mTimer.hashCode()); 89 | } 90 | 91 | public void testOnCreateBindsAndUnbindsService() { 92 | MenuActivity activity = startActivity(mActivityIntent, null, null); 93 | assertTrue(mBindServiceCalled); 94 | assertTrue(mUnbindServiceCalled); 95 | } 96 | 97 | public void testOnActivityResult() { 98 | MenuActivity activity = startActivity(mActivityIntent, null, null); 99 | Intent data = new Intent(); 100 | long expectedDurationMillis = TimeUnit.SECONDS.toMillis(30); 101 | 102 | data.putExtra(SetTimerActivity.EXTRA_DURATION_MILLIS, expectedDurationMillis); 103 | activity.onActivityResult(MenuActivity.SET_TIMER, Activity.RESULT_OK, data); 104 | assertEquals(expectedDurationMillis, mTimer.getDurationMillis()); 105 | assertTrue(isFinishCalled()); 106 | } 107 | 108 | public void testOnActivityResultStartTimer() { 109 | MenuActivity activity = startActivity(mActivityIntent, null, null); 110 | Intent data = new Intent(); 111 | long expectedDurationMillis = TimeUnit.SECONDS.toMillis(30); 112 | 113 | data.putExtra(SetTimerActivity.EXTRA_DURATION_MILLIS, expectedDurationMillis); 114 | data.putExtra(SetTimerActivity.EXTRA_START_TIMER, true); 115 | activity.onActivityResult(MenuActivity.SET_TIMER, Activity.RESULT_OK, data); 116 | assertEquals(expectedDurationMillis, mTimer.getDurationMillis()); 117 | assertTrue(mTimer.isStarted()); 118 | assertTrue(isFinishCalled()); 119 | } 120 | 121 | public void testOnActivityResultCanceled() { 122 | MenuActivity activity = startActivity(mActivityIntent, null, null); 123 | 124 | activity.onActivityResult(MenuActivity.SET_TIMER, Activity.RESULT_CANCELED, null); 125 | assertEquals(INITIAL_DURATION_MILLIS, mTimer.getDurationMillis()); 126 | assertTrue(isFinishCalled()); 127 | } 128 | 129 | public void testOptionsMenuChangeTimer() { 130 | assertOptionsMenu(R.id.change_timer, true); 131 | 132 | Intent startedIntent = getStartedActivityIntent(); 133 | assertNotNull(startedIntent); 134 | assertEquals(SetTimerActivity.class.getName(), startedIntent.getComponent().getClassName()); 135 | assertEquals( 136 | INITIAL_DURATION_MILLIS, 137 | startedIntent.getLongExtra(SetTimerActivity.EXTRA_DURATION_MILLIS, 0)); 138 | assertEquals(MenuActivity.SET_TIMER, getStartedActivityRequest()); 139 | assertFalse(isFinishCalled()); 140 | } 141 | 142 | public void testOptionsMenuStart() { 143 | assertOptionsMenu(R.id.start, true); 144 | assertTrue(isFinishCalled()); 145 | assertTrue(mTimer.isStarted()); 146 | assertTrue(mTimer.isRunning()); 147 | } 148 | 149 | public void testOptionsMenuStartTimerRunning() { 150 | mTimer.start(); 151 | assertOptionsMenu(R.id.start, false); 152 | } 153 | 154 | public void testOptionsMenuStartTimerPaused() { 155 | mTimer.start(); 156 | mTimer.pause(); 157 | assertOptionsMenu(R.id.start, false); 158 | } 159 | 160 | public void testOptionsMenuResume() { 161 | assertOptionsMenu(R.id.resume, false); 162 | } 163 | 164 | public void testOptionsMenuResumeTimerRunning() { 165 | mTimer.start(); 166 | assertOptionsMenu(R.id.resume, false); 167 | } 168 | 169 | public void testOptionsMenuResumeTimerPaused() { 170 | mTimer.start(); 171 | mTimer.pause(); 172 | assertOptionsMenu(R.id.resume, true); 173 | assertTrue(isFinishCalled()); 174 | assertTrue(mTimer.isStarted()); 175 | assertTrue(mTimer.isRunning()); 176 | } 177 | 178 | public void testOptionsMenuPause() { 179 | assertOptionsMenu(R.id.pause, false); 180 | } 181 | 182 | public void testOptionsMenuPauseTimerRunning() { 183 | mTimer.start(); 184 | assertOptionsMenu(R.id.pause, true); 185 | assertTrue(isFinishCalled()); 186 | assertTrue(mTimer.isStarted()); 187 | assertFalse(mTimer.isRunning()); 188 | } 189 | 190 | public void testOptionsMenuPauseTimerPaused() { 191 | mTimer.start(); 192 | mTimer.pause(); 193 | assertOptionsMenu(R.id.pause, false); 194 | } 195 | 196 | public void testOptionsMenuReset() { 197 | assertOptionsMenu(R.id.reset, false); 198 | } 199 | 200 | public void testOptionsMenuResetTimerRunning() { 201 | mTimer.start(); 202 | assertOptionsMenu(R.id.reset, true); 203 | assertTrue(isFinishCalled()); 204 | assertFalse(mTimer.isStarted()); 205 | assertFalse(mTimer.isRunning()); 206 | assertEquals(INITIAL_DURATION_MILLIS, mTimer.getDurationMillis()); 207 | } 208 | 209 | public void testOptionsMenuResetTimerPaused() { 210 | mTimer.start(); 211 | mTimer.reset(); 212 | assertOptionsMenu(R.id.reset, false); 213 | assertTrue(isFinishCalled()); 214 | assertFalse(mTimer.isStarted()); 215 | assertFalse(mTimer.isRunning()); 216 | assertEquals(INITIAL_DURATION_MILLIS, mTimer.getDurationMillis()); 217 | } 218 | 219 | public void testOptionsMenuStop() { 220 | assertOptionsMenu(R.id.stop, true); 221 | assertTrue(isFinishCalled()); 222 | assertTrue(mServiceStopped); 223 | } 224 | 225 | /** A convenience method to assert options menu behavior. */ 226 | private void assertOptionsMenu(int menuId, boolean shouldBeHandled) { 227 | MenuActivity activity = startActivity(mActivityIntent, null, null); 228 | boolean handled = getInstrumentation().invokeMenuActionSync(activity, menuId, 0); 229 | 230 | assertEquals(shouldBeHandled, handled); 231 | activity.onPanelClosed(Window.FEATURE_OPTIONS_PANEL, null); 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | 193 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/android/glass/sample/timer/MenuActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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.google.android.glass.sample.timer; 18 | 19 | import com.google.android.glass.timeline.LiveCard; 20 | import com.google.android.glass.view.WindowUtils; 21 | 22 | import android.app.Activity; 23 | import android.content.ComponentName; 24 | import android.content.Intent; 25 | import android.content.ServiceConnection; 26 | import android.os.Bundle; 27 | import android.os.IBinder; 28 | import android.os.Handler; 29 | import android.view.Menu; 30 | import android.view.MenuInflater; 31 | import android.view.MenuItem; 32 | import android.view.View; 33 | import android.view.Window; 34 | 35 | import java.lang.Runnable; 36 | 37 | /** 38 | * This activity manages the options menu that appears when the user taps on the timer's live 39 | * card or says "ok glass" while the live card is settled. 40 | */ 41 | public class MenuActivity extends Activity { 42 | 43 | /** Request code for setting the timer, visible for testing. */ 44 | static final int SET_TIMER = 100; 45 | 46 | private final Handler mHandler = new Handler(); 47 | 48 | private Timer mTimer; 49 | private boolean mAttachedToWindow; 50 | private boolean mIsMenuClosed; 51 | private boolean mPreparePanelCalled; 52 | private boolean mIsSettingTimer; 53 | 54 | private boolean mFromLiveCardVoice; 55 | 56 | private ServiceConnection mConnection = new ServiceConnection() { 57 | @Override 58 | public void onServiceConnected(ComponentName name, IBinder service) { 59 | if (service instanceof TimerService.TimerBinder) { 60 | mTimer = ((TimerService.TimerBinder) service).getTimer(); 61 | openMenu(); 62 | } 63 | // No need to keep the service bound. 64 | unbindService(this); 65 | } 66 | 67 | @Override 68 | public void onServiceDisconnected(ComponentName name) { 69 | // Nothing to do here. 70 | } 71 | }; 72 | 73 | @Override 74 | protected void onCreate(Bundle savedInstanceState) { 75 | super.onCreate(savedInstanceState); 76 | 77 | mFromLiveCardVoice = getIntent().getBooleanExtra(LiveCard.EXTRA_FROM_LIVECARD_VOICE, false); 78 | if (mFromLiveCardVoice) { 79 | // When activated by voice from a live card, enable voice commands. The menu 80 | // will automatically "jump" ahead to the items (skipping the guard phrase 81 | // that was already said at the live card). 82 | getWindow().requestFeature(WindowUtils.FEATURE_VOICE_COMMANDS); 83 | } 84 | 85 | // Bind to the Timer service to retrive the current timer's data. 86 | Intent serviceIntent = new Intent(this, TimerService.class); 87 | serviceIntent.putExtra( 88 | TimerService.EXTRA_TIMER_HASH_CODE, 89 | getIntent().getIntExtra(TimerService.EXTRA_TIMER_HASH_CODE, 0)); 90 | serviceIntent.setData(getIntent().getData()); 91 | bindService(serviceIntent, mConnection, 0); 92 | } 93 | 94 | @Override 95 | public void onAttachedToWindow() { 96 | super.onAttachedToWindow(); 97 | mAttachedToWindow = true; 98 | openMenu(); 99 | } 100 | 101 | @Override 102 | public void onDetachedFromWindow() { 103 | super.onDetachedFromWindow(); 104 | mAttachedToWindow = false; 105 | } 106 | 107 | @Override 108 | public boolean onCreatePanelMenu(int featureId, Menu menu) { 109 | if (isMyMenu(featureId)) { 110 | getMenuInflater().inflate(R.menu.timer, menu); 111 | return true; 112 | } 113 | return super.onCreatePanelMenu(featureId, menu); 114 | } 115 | 116 | @Override 117 | public boolean onPreparePanel(int featureId, View view, Menu menu) { 118 | mPreparePanelCalled = true; 119 | if (isMyMenu(featureId)) { 120 | if (mTimer == null) { 121 | // Can't prepare the menu as we're not yet bound to a timer. 122 | return false; 123 | } else { 124 | setOptionsMenuState( 125 | menu.findItem(R.id.start), !mTimer.isRunning() && !mTimer.isStarted()); 126 | setOptionsMenuState( 127 | menu.findItem(R.id.resume), !mTimer.isRunning() && mTimer.isStarted()); 128 | setOptionsMenuState( 129 | menu.findItem(R.id.pause), 130 | mTimer.isRunning() && mTimer.getRemainingTimeMillis() > 0); 131 | setOptionsMenuState(menu.findItem(R.id.reset), mTimer.isStarted()); 132 | // Don't reopen menu once we are finishing. This is necessary 133 | // since voice menus reopen themselves while in focus. 134 | return !mIsMenuClosed; 135 | } 136 | } 137 | return super.onPreparePanel(featureId, view, menu); 138 | } 139 | 140 | @Override 141 | public boolean onMenuItemSelected(int featureId, MenuItem item) { 142 | if (!isMyMenu(featureId)) { 143 | return super.onMenuItemSelected(featureId, item); 144 | } 145 | // Handle item selection. 146 | switch (item.getItemId()) { 147 | case R.id.start: 148 | case R.id.resume: 149 | mTimer.start(); 150 | return true; 151 | case R.id.pause: 152 | mTimer.pause(); 153 | return true; 154 | case R.id.reset: 155 | mTimer.reset(); 156 | return true; 157 | case R.id.change_timer: 158 | // Start the new Activity at the end of the message queue for proper options menu 159 | // animation. This is only needed when starting a new Activity or stopping a Service 160 | // that published a LiveCard. 161 | post(new Runnable() { 162 | 163 | @Override 164 | public void run() { 165 | startSetTimerActivity(); 166 | } 167 | }); 168 | mIsSettingTimer = true; 169 | return true; 170 | case R.id.stop: 171 | // Stop the service at the end of the message queue for proper options menu 172 | // animation. This is only needed when starting a new Activity or stopping a Service 173 | // that published a LiveCard. 174 | post(new Runnable() { 175 | 176 | @Override 177 | public void run() { 178 | Intent timerIntent = new Intent(MenuActivity.this, TimerService.class); 179 | 180 | timerIntent.setAction(TimerService.ACTION_STOP); 181 | timerIntent.putExtra( 182 | TimerService.EXTRA_TIMER_HASH_CODE, mTimer.hashCode()); 183 | startService(timerIntent); 184 | } 185 | }); 186 | return true; 187 | default: 188 | return super.onMenuItemSelected(featureId, item); 189 | } 190 | } 191 | 192 | @Override 193 | public void onPanelClosed(int featureId, Menu menu) { 194 | super.onPanelClosed(featureId, menu); 195 | if (isMyMenu(featureId)) { 196 | mIsMenuClosed = true; 197 | if (!mIsSettingTimer) { 198 | // Nothing else to do, closing the Activity. 199 | finish(); 200 | } 201 | } 202 | } 203 | 204 | @Override 205 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 206 | if (resultCode == RESULT_OK && requestCode == SET_TIMER) { 207 | mTimer.reset(); 208 | mTimer.setDurationMillis(data.getLongExtra(SetTimerActivity.EXTRA_DURATION_MILLIS, 0)); 209 | if (data.getBooleanExtra(SetTimerActivity.EXTRA_START_TIMER, false)) { 210 | mTimer.start(); 211 | } 212 | } 213 | finish(); 214 | } 215 | 216 | /** 217 | * Posts a {@link Runnable} at the end of the message loop, overridable for testing. 218 | */ 219 | protected void post(Runnable runnable) { 220 | mHandler.post(runnable); 221 | } 222 | 223 | /** 224 | * Opens the touch or voice menu iff all the conditions are satifisfied. 225 | */ 226 | private void openMenu() { 227 | if (mAttachedToWindow && mTimer != null) { 228 | if (mFromLiveCardVoice) { 229 | if (mPreparePanelCalled) { 230 | // Invalidates the previously prepared voice menu now that we can properly 231 | // prepare it. 232 | getWindow().invalidatePanelMenu(WindowUtils.FEATURE_VOICE_COMMANDS); 233 | } 234 | } else { 235 | // Open the options menu for the touch flow. 236 | openOptionsMenu(); 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * Starts the {@link SetTimerActivity}. 243 | */ 244 | private void startSetTimerActivity() { 245 | Intent setTimerIntent = new Intent(this, SetTimerActivity.class); 246 | 247 | setTimerIntent.putExtra(SetTimerActivity.EXTRA_DURATION_MILLIS, mTimer.getDurationMillis()); 248 | startActivityForResult(setTimerIntent, SET_TIMER); 249 | } 250 | 251 | /** 252 | * Returns {@code true} when the {@code featureId} belongs to the options menu or voice 253 | * menu that are controlled by this menu activity. 254 | */ 255 | private boolean isMyMenu(int featureId) { 256 | return featureId == Window.FEATURE_OPTIONS_PANEL || 257 | featureId == WindowUtils.FEATURE_VOICE_COMMANDS; 258 | } 259 | 260 | /** 261 | * Sets a {@code MenuItem} visible and enabled state. 262 | */ 263 | private static void setOptionsMenuState(MenuItem menuItem, boolean enabled) { 264 | menuItem.setVisible(enabled); 265 | menuItem.setEnabled(enabled); 266 | } 267 | 268 | /** 269 | * Sets all menu items visible and enabled state that are in the given group. 270 | */ 271 | private static void setOptionsMenuGroupState(Menu menu, int groupId, boolean enabled) { 272 | menu.setGroupVisible(groupId, enabled); 273 | menu.setGroupEnabled(groupId, enabled); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/android/glass/sample/timer/SetTimerActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 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.google.android.glass.sample.timer; 18 | 19 | import com.google.android.glass.media.Sounds; 20 | import com.google.android.glass.touchpad.Gesture; 21 | import com.google.android.glass.touchpad.GestureDetector; 22 | import com.google.android.glass.touchpad.GestureDetector.BaseListener; 23 | import com.google.android.glass.touchpad.GestureDetector.FingerListener; 24 | import com.google.android.glass.touchpad.GestureDetector.ScrollListener; 25 | 26 | import android.animation.ValueAnimator; 27 | import android.animation.ValueAnimator.AnimatorUpdateListener; 28 | import android.app.Activity; 29 | import android.content.Context; 30 | import android.content.Intent; 31 | import android.media.AudioManager; 32 | import android.os.Bundle; 33 | import android.view.animation.DecelerateInterpolator; 34 | import android.view.Menu; 35 | import android.view.MenuInflater; 36 | import android.view.MenuItem; 37 | import android.view.MotionEvent; 38 | import android.view.View; 39 | import android.widget.TextView; 40 | 41 | import java.util.concurrent.TimeUnit; 42 | 43 | /** 44 | * Activity to set the timer. 45 | */ 46 | public class SetTimerActivity extends Activity implements BaseListener, ScrollListener, 47 | FingerListener { 48 | 49 | public static final String EXTRA_DURATION_MILLIS = "extra_duration"; 50 | 51 | public static final String EXTRA_START_TIMER = "extra_start_timer"; 52 | 53 | /** Maximum velocity when dragging. */ 54 | private static final float MAX_DRAG_VELOCITY = 1; 55 | 56 | /** Deceleration constant for physics simulation. */ 57 | private static final float DECELERATION_CONSTANT = 0.2f; 58 | 59 | /** Minimum velocity to start the inertial scrolling. */ 60 | private static final float FLING_VELOCITY_CUTOFF = 1; 61 | 62 | /** Exagerate the time it takes to slow down the inertial scrolling. */ 63 | private static final float TIME_LENGTHENING = 12; 64 | 65 | /** Max timer value of 24:59:00. */ 66 | private static final long MAX_TIME_SECONDS = TimeUnit.HOURS.toSeconds(24) 67 | + TimeUnit.MINUTES.toSeconds(59); 68 | 69 | /** Animator for inertial scroll. */ 70 | private ValueAnimator mInertialScrollAnimator; 71 | private float mReleaseVelocity; 72 | 73 | private float mTimeSeconds = 0; 74 | 75 | private TextView mHoursView; 76 | private TextView mMinutesView; 77 | private TextView mSecondsView; 78 | private TextView mTipView; 79 | 80 | private AudioManager mAudioManager; 81 | private GestureDetector mDetector; 82 | 83 | // Options menu flags. 84 | private boolean mShouldFinish; 85 | private boolean mOptionMenuOpen; 86 | 87 | @Override 88 | protected void onCreate(Bundle savedInstanceState) { 89 | super.onCreate(savedInstanceState); 90 | 91 | mTimeSeconds = 92 | TimeUnit.MILLISECONDS.toSeconds(getIntent().getLongExtra(EXTRA_DURATION_MILLIS, 0)); 93 | mDetector = new GestureDetector(this) 94 | .setBaseListener(this) 95 | .setFingerListener(this) 96 | .setScrollListener(this); 97 | 98 | mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 99 | 100 | // Initialize the various views. 101 | setContentView(R.layout.card_timer); 102 | mHoursView = (TextView) findViewById(R.id.hours); 103 | mMinutesView = (TextView) findViewById(R.id.minutes); 104 | mSecondsView = (TextView) findViewById(R.id.seconds); 105 | mTipView = (TextView) findViewById(R.id.tip); 106 | 107 | mSecondsView.setTextColor(getResources().getColor(R.color.gray)); 108 | mSecondsView.setText("00"); 109 | mTipView.setText(getResources().getString(R.string.swipe_to_set_timer)); 110 | updateText(); 111 | 112 | // Initialize the animator use for the intertial scrolling. 113 | mInertialScrollAnimator = new ValueAnimator(); 114 | mInertialScrollAnimator.setInterpolator(new DecelerateInterpolator()); 115 | mInertialScrollAnimator.addUpdateListener(new AnimatorUpdateListener() { 116 | @Override 117 | public void onAnimationUpdate(ValueAnimator animation) { 118 | float value = (Float) animation.getAnimatedValue(); 119 | setTimeSeconds(value); 120 | } 121 | }); 122 | } 123 | 124 | @Override 125 | public void onPause() { 126 | super.onPause(); 127 | mInertialScrollAnimator.cancel(); 128 | } 129 | 130 | @Override 131 | public boolean onCreateOptionsMenu(Menu menu) { 132 | MenuInflater inflater = getMenuInflater(); 133 | inflater.inflate(R.menu.set_timer, menu); 134 | return true; 135 | } 136 | 137 | @Override 138 | public boolean onPrepareOptionsMenu(Menu menu) { 139 | // The "set" menu item should only be visible when called from another Activity. 140 | menu.findItem(R.id.set).setVisible(getCallingActivity() != null); 141 | return true; 142 | } 143 | 144 | @Override 145 | public boolean onOptionsItemSelected(MenuItem item) { 146 | mShouldFinish = true; 147 | switch (item.getItemId()) { 148 | case R.id.start: 149 | if (getCallingActivity() == null) { 150 | startTimer(); 151 | } else { 152 | setTimer(true); 153 | } 154 | return true; 155 | case R.id.set: 156 | setTimer(false); 157 | return true; 158 | default: 159 | mShouldFinish = false; 160 | return super.onOptionsItemSelected(item); 161 | } 162 | } 163 | 164 | @Override 165 | public void onOptionsMenuClosed(Menu menu) { 166 | if (mShouldFinish) { 167 | finish(); 168 | } 169 | mOptionMenuOpen = false; 170 | } 171 | 172 | @Override 173 | public boolean onGenericMotionEvent(MotionEvent event) { 174 | return mDetector.onMotionEvent(event); 175 | } 176 | 177 | @Override 178 | public void onFingerCountChanged(int previousCount, int currentCount) { 179 | boolean wentDown = currentCount > previousCount; 180 | 181 | if (currentCount == 0 && !wentDown && !mOptionMenuOpen) { 182 | // Only fling if the velocity is greater than the cutoff 183 | if (Math.abs(mReleaseVelocity) > FLING_VELOCITY_CUTOFF) { 184 | // Deceleration always in the opposite direction of the velocity 185 | final float deceleration = Math.signum(mReleaseVelocity) * -DECELERATION_CONSTANT; 186 | final float flingTime = -mReleaseVelocity / deceleration * TIME_LENGTHENING; 187 | float totalDelta = mReleaseVelocity * mReleaseVelocity / 2f / -deceleration; 188 | 189 | // Start the animation 190 | mInertialScrollAnimator.cancel(); 191 | mInertialScrollAnimator.setFloatValues( 192 | mTimeSeconds, confineTimeSeconds(mTimeSeconds + totalDelta)); 193 | mInertialScrollAnimator.setDuration((long) flingTime); 194 | mInertialScrollAnimator.start(); 195 | } 196 | } else { 197 | mInertialScrollAnimator.cancel(); 198 | } 199 | } 200 | 201 | @Override 202 | public boolean onScroll(float displacement, float delta, float velocity) { 203 | mReleaseVelocity = velocity; 204 | if (!mOptionMenuOpen) { 205 | addTimeSeconds(delta * Math.min(Math.abs(velocity), MAX_DRAG_VELOCITY)); 206 | } 207 | return true; 208 | } 209 | 210 | @Override 211 | public boolean onGesture(Gesture gesture) { 212 | switch (gesture) { 213 | case TAP: 214 | long timeMinutes = TimeUnit.SECONDS.toMinutes((long) mTimeSeconds); 215 | 216 | if (timeMinutes > 0) { 217 | playSoundEffect(Sounds.TAP); 218 | openOptionsMenu(); 219 | mOptionMenuOpen = true; 220 | } else { 221 | playSoundEffect(Sounds.DISALLOWED); 222 | } 223 | return true; 224 | case SWIPE_DOWN: 225 | setResultInternal(RESULT_CANCELED, null); 226 | playSoundEffect(Sounds.DISMISSED); 227 | finish(); 228 | return true; 229 | default: 230 | return false; 231 | } 232 | } 233 | 234 | /** Starts a new Timer. */ 235 | private void startTimer() { 236 | Intent timerIntent = new Intent(this, TimerService.class); 237 | long timeMinutes = TimeUnit.SECONDS.toMinutes((long) mTimeSeconds); 238 | 239 | timerIntent.setAction(TimerService.ACTION_START); 240 | timerIntent.putExtra( 241 | TimerService.EXTRA_DURATION_MILLIS, TimeUnit.MINUTES.toMillis(timeMinutes)); 242 | startService(timerIntent); 243 | } 244 | 245 | /** Returns the new timer value to the calling Activity. */ 246 | private void setTimer(boolean startTimer) { 247 | Intent resultIntent = new Intent(); 248 | long timeMinutes = TimeUnit.SECONDS.toMinutes((long) mTimeSeconds); 249 | 250 | resultIntent.putExtra(EXTRA_DURATION_MILLIS, TimeUnit.MINUTES.toMillis(timeMinutes)); 251 | resultIntent.putExtra(EXTRA_START_TIMER, startTimer); 252 | setResultInternal(RESULT_OK, resultIntent); 253 | } 254 | 255 | /** Adds {@code delta} seconds to the Timer.*/ 256 | private void addTimeSeconds(float delta) { 257 | setTimeSeconds(mTimeSeconds + delta); 258 | } 259 | 260 | /** Sets the Timer value. */ 261 | private void setTimeSeconds(float timeSeconds) { 262 | float previousTimeSeconds = mTimeSeconds; 263 | 264 | mTimeSeconds = confineTimeSeconds(timeSeconds); 265 | if (TimeUnit.SECONDS.toMinutes((int) previousTimeSeconds) 266 | != TimeUnit.SECONDS.toMinutes((int) mTimeSeconds)) { 267 | playSoundEffect(Sounds.TAP); 268 | updateText(); 269 | } 270 | } 271 | 272 | /** Updates the various {@link TextView} with the current Timer value. */ 273 | private void updateText() { 274 | long hours = TimeUnit.SECONDS.toHours((int) mTimeSeconds); 275 | long minutes = TimeUnit.SECONDS.toMinutes((int) mTimeSeconds % TimeUnit.HOURS.toSeconds(1)); 276 | 277 | mHoursView.setText(String.format("%02d", hours)); 278 | mMinutesView.setText(String.format("%02d", minutes)); 279 | if (hours == 0 && minutes == 0) { 280 | mTipView.setVisibility(View.VISIBLE); 281 | } else { 282 | mTipView.setVisibility(View.INVISIBLE); 283 | } 284 | } 285 | 286 | /** 287 | * Keeps the time between 0 and {@link MAX_TIME_SECONDS}. 288 | */ 289 | private float confineTimeSeconds(float timeSeconds) { 290 | if (timeSeconds < 0) { 291 | timeSeconds = 0; 292 | } else if (timeSeconds > MAX_TIME_SECONDS) { 293 | timeSeconds = MAX_TIME_SECONDS; 294 | } 295 | return timeSeconds; 296 | } 297 | 298 | /** 299 | * Plays a sound effect, overridable for testing. 300 | */ 301 | protected void playSoundEffect(int soundId) { 302 | mAudioManager.playSoundEffect(soundId); 303 | } 304 | 305 | /** 306 | * Sets the {@link Activity} result, overridable for testing. 307 | */ 308 | protected void setResultInternal(int resultCode, Intent resultIntent) { 309 | setResult(resultCode, resultIntent); 310 | } 311 | 312 | /** 313 | * Forces any ongoing animation to immediately 'jump to the end', visible for testing. 314 | * This method must be called from same thread that performs the animation. 315 | */ 316 | void forceEndAnimation() { 317 | if (mInertialScrollAnimator.isRunning()) { 318 | mInertialScrollAnimator.end(); 319 | } 320 | } 321 | 322 | /** Returns the Timer current time, visible for testing. */ 323 | float getTimeSeconds() { 324 | return mTimeSeconds; 325 | } 326 | 327 | } 328 | --------------------------------------------------------------------------------