Hello, this is YOSHIMURA, researching on wearable devices at ATL. This time, we are going to make a prototype of the timer with Android.
Architecture
Here we divide the architectures into two parts as in a theory; for a main screen Activity + custom a href=”http://developer.android.com/reference/android/view/View.html” title=”View | Android Developers”>View, and for time tracking and making a sound Service.
Making a main screen
Let’s make another custom view to avoid the concentration of logic in Activity.
Making a time view
We are going to make TimerView.
First, center the layout with TextView. Make sure you explicitly specify Roboto by using the fontFamily property.
res/layout/view_timer.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<?xml version="1.0" encoding="utf-8"?> <frameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <linearLayout android:id="@+id/timer" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center|center_vertical"> <textView android:id="@+id/min" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:fontFamily="sans-serif-thin" android:textSize="96sp" android:text="25" /> <textView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:paddingLeft="12dp" android:paddingRight="12dp" android:fontFamily="sans-serif-thin" android:textSize="96sp" android:text=":" /> <textView android:id="@+id/sec" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:fontFamily="sans-serif-thin" android:textSize="96sp" android:text="00" /> </linearLayout> </frameLayout> |
Let me write down a minimum logic.
src/main/java/com/gmail/altakey/myapplication/TimerView.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package com.gmail.altakey.myapplication; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; public class TimerView extends FrameLayout { private TextView mMinutes; private TextView mSeconds; public TimerView(Context context) { super(context); init(context); } public TimerView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public TimerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(final Context c) { final View v = View.inflate(c, R.layout.view_timer, this); mMinutes = (TextView)v.findViewById(R.id.min); mSeconds = (TextView)v.findViewById(R.id.sec); } public void setRemaining(final long remainingMillis) { final TimerReader reader = new TimerReader(remainingMillis); mMinutes.setText(String.format("%02d", reader.minutes)); mSeconds.setText(String.format("%02d", reader.seconds)); } } |
src/main/java/com/gmail/altakey/myapplication/TimerReader.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.gmail.altakey.myapplication; public class TimerReader { public long seconds; public long minutes; public long remaining; public TimerReader(long remaining) { remaining = ((long)Math.ceil(remaining / 1000.0)) * 1000; seconds = remaining / 1000 % 60; minutes = remaining / 60000; this.remaining = remaining; } public long getElapsed(final long due) { return due - remaining; } } |
If you preview this, it will look like this below.
Making Activity
Once you have finished those, set those into Activity. All you need to do is just fit in the layout.
res/layout/activity_my.xml:
1 2 3 4 5 6 7 |
<?xml version="1.0" encoding="utf-8"?> <com.gmail.altakey.myapplication.TimerView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"/> </p> |
Then, you will see the rendering in this way eventually. This is easy.
Let me put down the logic now. You have 4 steps to go; 1) Run, and at the same time startup the service, 2) Display a menu when tapped, 3) Reset a timer with Reset, and 4) Finish it with Exit.
1) Control service
When Activity is run and onCreate is called as usual, startup the service. From the service by using LocalBroadcastReceiver you will receive the timer display update request.
src/main/java/com/gmail/altakey/myapplication/MainActivity.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
private BroadcastReceiver mReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_my); ... mReceiver = new TickReceiver(v); ... final Intent intent = new Intent(this, TimerService.class); intent.setAction(TimerService.ACTION_START); startService(intent); } ... private static class TickReceiver extends BroadcastReceiver { private TimerView mView; public TickReceiver(final TimerView v) { mView = v; } @Override public void onReceive(Context context, Intent intent) { mView.setRemaining(intent.getLongExtra(TimerService.KEY_REMAINING, 0)); } } |
2) Display a menu
Menu is displayed when it is tapped. You can use just normal a href=”http://developer.android.com/guide/topics/ui/menus.html#options-menu” title=”Menus | Android Developers”>Options Menu at this time. Its appearance is very different from the one in the Glass Menu, but making a good one now is kind of wasting time (you will see the notice next time!), so forget it and just use it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Override protected void onCreate(Bundle savedInstanceState) { ... final TimerView v = (TimerView)findViewById(R.id.timer); v.setOnClickListener(new TapAction()); ... } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.my, menu); return true; } ... private class TapAction implements View.OnClickListener { @Override public void onClick(View view) { openOptionsMenu(); } } |
3) Reset a timer
This shows the process when Reset is called in the Options Menu. Reset the service.
1 2 3 4 5 6 7 8 |
@Override public boolean onOptionsItemSelected(MenuItem item) { final Intent intent = new Intent(this, TimerService.class); switch (item.getItemId()) { case R.id.action_reset: intent.setAction(TimerService.ACTION_RESET); startService(intent); break; |
4) Finish it
This shows the process when Exit is called in the Options Menu. Stop the service and finish the application.
1 2 3 4 5 6 7 8 9 |
@Override public boolean onOptionsItemSelected(MenuItem item) { final Intent intent = new Intent(this, TimerService.class); switch (item.getItemId()) { ... case R.id.action_exit: stopService(intent); finish(); break; |
Once you get to here, the left is the service which processes the timer. One more step to go.
Making a timer service
Just write down as usual in the following way.
src/main/java/com/gmail/altakey/myapplication/TimerService.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
package com.gmail.altakey.myapplication; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.media.AudioManager; import android.media.SoundPool; import android.os.IBinder; import android.os.PowerManager; import android.os.SystemClock; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import java.util.Timer; import java.util.TimerTask; public class TimerService extends Service { public static final String ACTION_START = "start"; public static final String ACTION_RESET = "reset"; public static final String ACTION_TIMEOUT = "timeout"; public static final String ACTION_TICK = "tick"; public static final String KEY_REMAINING = "remaining"; public static final int STATE_RESET = 0; public static final int STATE_RUNNING = 1; public static final int STATE_BREAKING = 2; private static int sState = STATE_RESET; private static long sDueMillis = 0; private PendingIntent mDueIntent = null; private Timer mTimer = null; private Timer mIdleTimer = null; private Ticker mTicker = new Ticker(this); public static int getState() { return sState; } public static long getDueMillis() { return sDueMillis; } public static long getRemaining(long due) { if (due > 0) { return getDueMillis() - SystemClock.elapsedRealtime(); } else { return 25 * 60 * 1000; } } private static String TAG = "com.gmail.altakey.mint.timer.main"; @Override public void onCreate() { super.onCreate(); mTicker.prepare(); Log.d("TS", "created"); } @Override public void onDestroy() { mTicker.cleanup(); reset(); super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { final String action = intent.getAction(); Log.d("TS.oHI", String.format("state: %d, due: %d", sState, sDueMillis)); updateView(); wakeUp(); if (ACTION_START.equals(action)) { start(); } else if (ACTION_RESET.equals(action)) { reset(); start(); } else if (ACTION_TIMEOUT.equals(action)) { proceed(); } if (sState == STATE_RESET) { if (mIdleTimer == null) { mIdleTimer = new Timer(); mIdleTimer.schedule(new TimerTask() { @Override public void run() { stopSelf(); } }, 90000); } } else { if (mIdleTimer != null) { mIdleTimer.cancel(); mIdleTimer.purge(); mIdleTimer = null; } } return START_NOT_STICKY; } private void wakeUp() { final PowerManager pm = (PowerManager)getSystemService(POWER_SERVICE); final PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE | PowerManager.ACQUIRE_CAUSES_WAKEUP, "screen_poke"); try { wl.acquire(); } finally { wl.release(); } } private void startTimer(long intervalMillis, boolean for_break) { final AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE); resetTimer(); sDueMillis = SystemClock.elapsedRealtime() + intervalMillis; final Intent intent = new Intent(this, TimerService.class); intent.setAction(ACTION_TIMEOUT); mDueIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_ONE_SHOT); am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, sDueMillis, mDueIntent); mTimer = new Timer(); mTimer.schedule(new TimerTask() { @Override public void run() { updateView(); mTicker.tick(); } }, 1000, 1000); } private void resetTimer() { final AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE); if (mDueIntent != null) { am.cancel(mDueIntent); mDueIntent = null; } if (mTimer != null) { mTimer.cancel(); mTimer.purge(); mTimer = null; } sDueMillis = 0; updateView(); } private void updateView() { final Intent data = new Intent(ACTION_TICK); data.putExtra(KEY_REMAINING, getRemaining(getDueMillis())); LocalBroadcastManager.getInstance(this).sendBroadcast(data); } private void start() { if (sState == STATE_RESET) { startTimer(25 * 60 * 1000, false); sState = STATE_RUNNING; } } private void reset() { if (sState != STATE_RESET) { resetTimer(); sState = STATE_RESET; } } private void proceed() { mTicker.bell(); if (sState == STATE_RUNNING) { startTimer(5 * 60 * 1000, true); sState = STATE_BREAKING; } else { resetTimer(); sState = STATE_RESET; } } private static class Ticker { private final Context mContext; private SoundPool mPool = null; private int mSoundTick = 0; private int mSoundBell = 0; public Ticker(final Context c) { mContext = c; } public void prepare() { if (mPool == null) { mPool = new SoundPool(2, AudioManager.STREAM_NOTIFICATION, 0); mSoundTick = mPool.load(mContext, R.raw.tick, 1); mSoundBell = mPool.load(mContext, R.raw.ring, 1); } } public void cleanup() { if (mPool != null) { mSoundTick = 0; mSoundBell = 0; mPool.release(); mPool = null; } } public void tick() { if (mPool != null) { mPool.play(mSoundTick, 1.0f, 1.0f, 0, 0, 1.0f); } } public void bell() { if (mPool != null) { mPool.play(mSoundBell, 1.0f, 1.0f, 0, 1, 1.0f); } } } } |
Do not forget a declaration at AndroidManifest.xml.
1 |
<service android:name=".TimerService" /> |
Add resources
res/rawに2つのOgg Vorbisファイルを追加します。
ring.ogg: clicking sound
tick.ogg: ticking sound
Complete a prototype!
Let’s build and execute it. Does it start the countdown as soon as it is run? Does it display a menu when tapped, go back to 25 minutes with Reset, and stop the countdown with EXIT?
Someone who has done development with Eclipse has noticed it already. In Android Studio you can see errors, etc. in advance with underline markers. Even if the errors are still there, you can try to build and run it.
Let’s port this to a Glass actual device.