@@ -77,7 +77,7 @@ public class MainActivity extends AestheticActivity implements TicksView.OnTickC | |||
playView.setOnClickListener(new View.OnClickListener() { | |||
@Override | |||
public void onClick(View v) { | |||
if (isBound && service != null) { | |||
if (isBound()) { | |||
if (service.isPlaying()) | |||
service.pause(); | |||
else service.play(); | |||
@@ -207,15 +207,6 @@ public class MainActivity extends AestheticActivity implements TicksView.OnTickC | |||
textColorPrimarySubscription.dispose(); | |||
} | |||
@Override | |||
protected void onStart() { | |||
Intent intent = new Intent(this, MetronomeService.class); | |||
startService(intent); | |||
bindService(intent, this, Context.BIND_AUTO_CREATE); | |||
super.onStart(); | |||
} | |||
@Override | |||
protected void onResume() { | |||
super.onResume(); | |||
@@ -228,6 +219,15 @@ public class MainActivity extends AestheticActivity implements TicksView.OnTickC | |||
unsubscribe(); | |||
} | |||
@Override | |||
protected void onStart() { | |||
Intent intent = new Intent(this, MetronomeService.class); | |||
startService(intent); | |||
bindService(intent, this, Context.BIND_AUTO_CREATE); | |||
super.onStart(); | |||
} | |||
@Override | |||
protected void onStop() { | |||
if (isBound) { | |||
@@ -1 +1 @@ | |||
include ':app' | |||
include ':app', ':wear' |
@@ -0,0 +1 @@ | |||
/build |
@@ -0,0 +1,28 @@ | |||
apply plugin: 'com.android.application' | |||
android { | |||
compileSdkVersion 26 | |||
buildToolsVersion "26.0.0" | |||
defaultConfig { | |||
applicationId "james.metronome.wear" | |||
minSdkVersion 21 | |||
targetSdkVersion 26 | |||
versionCode 1 | |||
versionName "1.0" | |||
} | |||
buildTypes { | |||
release { | |||
minifyEnabled false | |||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | |||
} | |||
} | |||
} | |||
dependencies { | |||
compile fileTree(dir: 'libs', include: ['*.jar']) | |||
compile 'com.google.android.support:wearable:2.0.1' | |||
compile 'com.google.android.gms:play-services-wearable:10.2.1' | |||
provided 'com.google.android.wearable:wearable:1.0.0' | |||
} |
@@ -0,0 +1,25 @@ | |||
# Add project specific ProGuard rules here. | |||
# By default, the flags in this file are appended to flags specified | |||
# in /home/james/Android/Sdk/tools/proguard/proguard-android.txt | |||
# You can edit the include path and order by changing the proguardFiles | |||
# directive in build.gradle. | |||
# | |||
# For more details, see | |||
# http://developer.android.com/guide/developing/tools/proguard.html | |||
# Add any project specific keep options here: | |||
# If your project uses WebView with JS, uncomment the following | |||
# and specify the fully qualified class name to the JavaScript interface | |||
# class: | |||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { | |||
# public *; | |||
#} | |||
# Uncomment this to preserve the line number information for | |||
# debugging stack traces. | |||
#-keepattributes SourceFile,LineNumberTable | |||
# If you keep the line number information, uncomment this to | |||
# hide the original source file name. | |||
#-renamesourcefileattribute SourceFile |
@@ -0,0 +1,33 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | |||
package="james.metronome.wear"> | |||
<uses-feature android:name="android.hardware.type.watch" /> | |||
<uses-permission android:name="android.permission.WAKE_LOCK" /> | |||
<uses-permission android:name="android.permission.VIBRATE" /> | |||
<application | |||
android:allowBackup="true" | |||
android:icon="@mipmap/ic_launcher" | |||
android:label="@string/app_name" | |||
android:supportsRtl="true" | |||
android:theme="@android:style/Theme.DeviceDefault"> | |||
<uses-library | |||
android:name="com.google.android.wearable" | |||
android:required="false" /> | |||
<activity | |||
android:name=".MainActivity" | |||
android:label="@string/app_name"> | |||
<intent-filter> | |||
<action android:name="android.intent.action.MAIN" /> | |||
<category android:name="android.intent.category.LAUNCHER" /> | |||
</intent-filter> | |||
</activity> | |||
<service android:name=".MetronomeService" /> | |||
</application> | |||
</manifest> |
@@ -0,0 +1,173 @@ | |||
package james.metronome.wear; | |||
import android.content.ComponentName; | |||
import android.content.Context; | |||
import android.content.Intent; | |||
import android.content.ServiceConnection; | |||
import android.graphics.Color; | |||
import android.os.Bundle; | |||
import android.os.IBinder; | |||
import android.support.wearable.activity.WearableActivity; | |||
import android.support.wearable.view.BoxInsetLayout; | |||
import android.view.View; | |||
import android.widget.ImageView; | |||
import android.widget.SeekBar; | |||
import android.widget.TextView; | |||
import java.util.Locale; | |||
public class MainActivity extends WearableActivity implements ServiceConnection, MetronomeService.TickListener { | |||
private BoxInsetLayout container; | |||
private ImageView vibrationView; | |||
private ImageView playView; | |||
private TextView bpmView; | |||
private SeekBar seekBar; | |||
private MetronomeService service; | |||
private boolean isBound; | |||
@Override | |||
protected void onCreate(Bundle savedInstanceState) { | |||
super.onCreate(savedInstanceState); | |||
setContentView(R.layout.activity_main); | |||
setAmbientEnabled(); | |||
container = findViewById(R.id.container); | |||
vibrationView = findViewById(R.id.vibration); | |||
playView = findViewById(R.id.play); | |||
bpmView = findViewById(R.id.bpm); | |||
seekBar = findViewById(R.id.seekBar); | |||
if (isBound()) { | |||
vibrationView.setImageResource(service.isVibration() ? R.drawable.ic_vibration : R.drawable.ic_sound); | |||
playView.setImageResource(service.isPlaying() ? R.drawable.ic_pause : R.drawable.ic_play); | |||
bpmView.setText(String.format(Locale.getDefault(), getString(R.string.bpm), String.valueOf(service.getBpm()))); | |||
seekBar.setProgress(service.getBpm()); | |||
} | |||
vibrationView.setOnClickListener(new View.OnClickListener() { | |||
@Override | |||
public void onClick(View view) { | |||
service.setVibration(!service.isVibration()); | |||
vibrationView.setImageResource(service.isVibration() ? R.drawable.ic_vibration : R.drawable.ic_sound); | |||
} | |||
}); | |||
playView.setOnClickListener(new View.OnClickListener() { | |||
@Override | |||
public void onClick(View view) { | |||
if (isBound()) { | |||
if (service.isPlaying()) | |||
service.pause(); | |||
else service.play(); | |||
} | |||
} | |||
}); | |||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { | |||
@Override | |||
public void onProgressChanged(SeekBar seekBar, int i, boolean b) { | |||
setBpm(i); | |||
} | |||
@Override | |||
public void onStartTrackingTouch(SeekBar seekBar) { | |||
} | |||
@Override | |||
public void onStopTrackingTouch(SeekBar seekBar) { | |||
} | |||
}); | |||
} | |||
private void setBpm(int bpm) { | |||
if (isBound()) { | |||
service.setBpm(bpm); | |||
bpmView.setText(String.format(Locale.getDefault(), getString(R.string.bpm), String.valueOf(bpm))); | |||
} | |||
} | |||
private boolean isBound() { | |||
return isBound && service != null; | |||
} | |||
@Override | |||
public void onEnterAmbient(Bundle ambientDetails) { | |||
super.onEnterAmbient(ambientDetails); | |||
updateDisplay(); | |||
} | |||
@Override | |||
public void onUpdateAmbient() { | |||
super.onUpdateAmbient(); | |||
updateDisplay(); | |||
} | |||
@Override | |||
public void onExitAmbient() { | |||
updateDisplay(); | |||
super.onExitAmbient(); | |||
} | |||
private void updateDisplay() { | |||
if (isAmbient()) | |||
container.setBackgroundColor(Color.BLACK); | |||
else container.setBackground(null); | |||
} | |||
@Override | |||
protected void onStart() { | |||
Intent intent = new Intent(this, MetronomeService.class); | |||
startService(intent); | |||
bindService(intent, this, Context.BIND_AUTO_CREATE); | |||
super.onStart(); | |||
} | |||
@Override | |||
protected void onStop() { | |||
if (isBound) { | |||
unbindService(this); | |||
isBound = false; | |||
} | |||
super.onStop(); | |||
} | |||
@Override | |||
public void onServiceConnected(ComponentName componentName, IBinder iBinder) { | |||
MetronomeService.LocalBinder binder = (MetronomeService.LocalBinder) iBinder; | |||
service = binder.getService(); | |||
service.setTickListener(this); | |||
isBound = true; | |||
if (vibrationView != null) | |||
vibrationView.setImageResource(service.isVibration() ? R.drawable.ic_vibration : R.drawable.ic_sound); | |||
if (playView != null) | |||
playView.setImageResource(service.isPlaying() ? R.drawable.ic_pause : R.drawable.ic_play); | |||
if (bpmView != null) | |||
bpmView.setText(String.format(Locale.getDefault(), getString(R.string.bpm), String.valueOf(service.getBpm()))); | |||
if (seekBar != null) | |||
seekBar.setProgress(service.getBpm()); | |||
} | |||
@Override | |||
public void onServiceDisconnected(ComponentName componentName) { | |||
isBound = false; | |||
} | |||
@Override | |||
public void onStartTicks() { | |||
playView.setImageResource(R.drawable.ic_pause); | |||
} | |||
@Override | |||
public void onStopTicks() { | |||
playView.setImageResource(R.drawable.ic_play); | |||
} | |||
} |
@@ -0,0 +1,195 @@ | |||
package james.metronome.wear; | |||
import android.app.PendingIntent; | |||
import android.app.Service; | |||
import android.content.Context; | |||
import android.content.Intent; | |||
import android.content.SharedPreferences; | |||
import android.media.AudioAttributes; | |||
import android.media.AudioManager; | |||
import android.media.SoundPool; | |||
import android.os.Binder; | |||
import android.os.Build; | |||
import android.os.Handler; | |||
import android.os.IBinder; | |||
import android.os.VibrationEffect; | |||
import android.os.Vibrator; | |||
import android.preference.PreferenceManager; | |||
import android.support.annotation.Nullable; | |||
import android.support.v4.app.NotificationCompat; | |||
public class MetronomeService extends Service implements Runnable { | |||
public static final String ACTION_PAUSE = "james.metronome.ACTION_PAUSE"; | |||
public static final String PREF_VIBRATION = "vibration"; | |||
public static final String PREF_INTERVAL = "interval"; | |||
private final IBinder binder = new LocalBinder(); | |||
private SharedPreferences prefs; | |||
private int bpm; | |||
private long interval; | |||
private SoundPool soundPool; | |||
private Handler handler; | |||
private int soundId = -1; | |||
private boolean isPlaying; | |||
private boolean isVibration; | |||
private Vibrator vibrator; | |||
private TickListener listener; | |||
@Override | |||
public void onCreate() { | |||
super.onCreate(); | |||
prefs = PreferenceManager.getDefaultSharedPreferences(this); | |||
vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); | |||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | |||
soundPool = new SoundPool.Builder() | |||
.setMaxStreams(1) | |||
.setAudioAttributes(new AudioAttributes.Builder() | |||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) | |||
.build()) | |||
.build(); | |||
} else soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0); | |||
isVibration = prefs.getBoolean(PREF_VIBRATION, true); | |||
if (!isVibration) | |||
soundId = soundPool.load(this, R.raw.click, 1); | |||
interval = prefs.getLong(PREF_INTERVAL, 500); | |||
bpm = toBpm(interval); | |||
handler = new Handler(); | |||
} | |||
@Override | |||
public int onStartCommand(Intent intent, int flags, int startId) { | |||
if (intent != null && intent.getAction() != null) { | |||
switch (intent.getAction()) { | |||
case ACTION_PAUSE: | |||
pause(); | |||
} | |||
} | |||
return START_STICKY; | |||
} | |||
private static int toBpm(long interval) { | |||
return (int) (60000 / interval); | |||
} | |||
private static long toInterval(int bpm) { | |||
return (long) 60000 / bpm; | |||
} | |||
public void play() { | |||
handler.post(this); | |||
isPlaying = true; | |||
Intent intent = new Intent(this, MetronomeService.class); | |||
intent.setAction(ACTION_PAUSE); | |||
startForeground(530, | |||
new NotificationCompat.Builder(this) | |||
.setContentTitle(getString(R.string.notification_title)) | |||
.setContentText(getString(R.string.notification_desc)) | |||
.setSmallIcon(R.drawable.ic_notification) | |||
.setContentIntent(PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)) | |||
.build() | |||
); | |||
if (listener != null) | |||
listener.onStartTicks(); | |||
} | |||
public void pause() { | |||
handler.removeCallbacks(this); | |||
stopForeground(true); | |||
isPlaying = false; | |||
if (listener != null) | |||
listener.onStopTicks(); | |||
} | |||
public void setBpm(int bpm) { | |||
this.bpm = bpm; | |||
interval = toInterval(bpm); | |||
prefs.edit().putLong(PREF_INTERVAL, interval).apply(); | |||
} | |||
public void setVibration(boolean vibration) { | |||
isVibration = vibration; | |||
if (!isVibration) | |||
soundId = soundPool.load(this, R.raw.click, 1); | |||
else soundId = -1; | |||
prefs.edit().putBoolean(PREF_VIBRATION, isVibration).apply(); | |||
} | |||
public boolean isPlaying() { | |||
return isPlaying; | |||
} | |||
public long getInterval() { | |||
return interval; | |||
} | |||
public int getBpm() { | |||
return bpm; | |||
} | |||
public boolean isVibration() { | |||
return isVibration; | |||
} | |||
public void setTickListener(TickListener listener) { | |||
this.listener = listener; | |||
} | |||
@Nullable | |||
@Override | |||
public IBinder onBind(Intent intent) { | |||
return binder; | |||
} | |||
@Override | |||
public boolean onUnbind(Intent intent) { | |||
listener = null; | |||
return super.onUnbind(intent); | |||
} | |||
@Override | |||
public void onDestroy() { | |||
handler.removeCallbacks(this); | |||
super.onDestroy(); | |||
} | |||
@Override | |||
public void run() { | |||
if (isPlaying) { | |||
if (soundId != -1) | |||
soundPool.play(soundId, 1.0f, 1.0f, 0, 0, 1.0f); | |||
else if (Build.VERSION.SDK_INT >= 26) | |||
vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)); | |||
else vibrator.vibrate(50); | |||
handler.postDelayed(this, interval); | |||
} | |||
} | |||
public class LocalBinder extends Binder { | |||
public MetronomeService getService() { | |||
return MetronomeService.this; | |||
} | |||
} | |||
public interface TickListener { | |||
void onStartTicks(); | |||
void onStopTicks(); | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
<vector xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:width="24dp" | |||
android:height="24dp" | |||
android:viewportWidth="24.0" | |||
android:viewportHeight="24.0"> | |||
<path | |||
android:fillColor="#FF000000" | |||
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" /> | |||
</vector> |
@@ -0,0 +1,9 @@ | |||
<vector xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:width="24dp" | |||
android:height="24dp" | |||
android:viewportWidth="24.0" | |||
android:viewportHeight="24.0"> | |||
<path | |||
android:fillColor="#FF000000" | |||
android:pathData="M8,5v14l11,-7z" /> | |||
</vector> |
@@ -0,0 +1,9 @@ | |||
<vector xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:width="24dp" | |||
android:height="24dp" | |||
android:viewportWidth="24.0" | |||
android:viewportHeight="24.0"> | |||
<path | |||
android:fillColor="#FF000000" | |||
android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z" /> | |||
</vector> |
@@ -0,0 +1,9 @@ | |||
<vector xmlns:android="http://schemas.android.com/apk/res/android" | |||
android:width="24dp" | |||
android:height="24dp" | |||
android:viewportWidth="24.0" | |||
android:viewportHeight="24.0"> | |||
<path | |||
android:fillColor="#FF000000" | |||
android:pathData="M0,15h2L2,9L0,9v6zM3,17h2L5,7L3,7v10zM22,9v6h2L24,9h-2zM19,17h2L21,7h-2v10zM16.5,3h-9C6.67,3 6,3.67 6,4.5v15c0,0.83 0.67,1.5 1.5,1.5h9c0.83,0 1.5,-0.67 1.5,-1.5v-15c0,-0.83 -0.67,-1.5 -1.5,-1.5zM16,19L8,19L8,5h8v14z" /> | |||
</vector> |
@@ -0,0 +1,49 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
xmlns:app="http://schemas.android.com/apk/res-auto" | |||
android:id="@+id/container" | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent"> | |||
<LinearLayout | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:orientation="vertical" | |||
app:layout_box="all"> | |||
<ImageView | |||
android:id="@+id/vibration" | |||
android:layout_width="match_parent" | |||
android:layout_height="24dp" | |||
android:layout_gravity="center_horizontal" | |||
android:scaleType="fitCenter" | |||
android:tint="@android:color/white" | |||
android:src="@drawable/ic_vibration" /> | |||
<ImageView | |||
android:id="@+id/play" | |||
android:layout_width="match_parent" | |||
android:layout_height="0dp" | |||
android:layout_weight="1" | |||
android:scaleType="centerInside" | |||
android:tint="@android:color/white" | |||
android:src="@drawable/ic_play" /> | |||
<SeekBar | |||
android:id="@+id/seekBar" | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
android:max="300" /> | |||
<TextView | |||
android:id="@+id/bpm" | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
android:gravity="center" | |||
android:textAlignment="center" | |||
android:textColor="@android:color/white" | |||
android:textSize="14sp" /> | |||
</LinearLayout> | |||
</android.support.wearable.view.BoxInsetLayout> |
@@ -0,0 +1,8 @@ | |||
<resources> | |||
<string name="app_name">Metronome</string> | |||
<string name="bpm">%1$s BPM</string> | |||
<string name="notification_title">Metronome is running…</string> | |||
<string name="notification_desc">Touch to stop.</string> | |||
</resources> |