死ににくい Timer を作る。
Activity は Background になると意外と簡単に死ぬので、Activity が Background になっても死ににくい Timer を作る。
Timer 起動時に Foreground 化した Service を起動し、死ににくい Service に Timer を Anchor する事で Timer を死ににくくする。
それでも Background 時の Activity 自体が死にやすい事に変わりはないのだが、例え Activity が死んでも Timer は生存して Alarm が正常に鳴る(この Timer は Foreground 化した Service と同等の死ににくさを持っている)。
Timer 起動時に Foreground 化した Service により、(通知バーには表示されない場合があるが、通知バーを下ろした)通知領域にアイコンが表示される、その通知領域にアイコンをタップすると Activity が起動する(テストしたい場合は Timer を少し長めにセットして下さい)。
Timer が終了すると Foreground 化した Service も終了し、通知領域のアイコンも消える。
音源は下記「げきばん!」様からダウンロードいたしました。
フリー作品を使用する場合の注意点。
"利用規約"に違反した場合は違法となり罰金などが請求される可能性がありますので、必ず そのサイトの"利用規約"を お読み下さい。
(通常は)著作権を放棄しているものではありません、(通常は)再配布は禁止されています。
(大抵は)商用として使用できません、(大抵は)改変が禁止されています。
(場合によっては)著作権表示が義務付けられている事があります。
げきばん! オリジナルサウンドトラック制作(音楽・効果音・声・MA)
http://soundjewel.symphie.jp/product/gekiban/free/
「げきばん!」様の"利用規約"。
> すべて無料でダウンロードできます
> 商用としてご使用できます
> ご申告は不要です
> JASRACなどへのご申告も不要です
> 著作権表示、作者名のクレジットは不要です
> 編集やエフェクトを加えるなど、改変してご使用できます
> 音楽ファイルを販売するなど、著作権を侵害する使用はできません
再配布については書かれていませんが、(再配布は禁止されているのが通常なので)再配布は禁止と考えた方が無難でしょう。
当方がダウンロードしたファイルは「真夜中のメリーゴーランド:Mayonaka_No_Merry-Go-Round.wav」(変換後のファイル:mayonaka_no_merrygoround.mp3)、「真夜中のメリーゴーランド(トイピアノ):Mayonaka_No_Merry-Go-Round_Toy_Piano.wav」(変換後のファイル:mayonaka_no_merrygoround_toypiano.mp3)、「真夜中のメリーゴーランド(ハープ):Mayonaka_No_Merry-Go-Round_Harp.wav」(変換後のファイル:mayonaka_no_merrygoround_harp.mp3)、「夏の残り香(ピアノ):Natsu_No_Nokoriga_Piano.wav」(変換後のファイル:natsu_no_nokoriga_piano.mp3)、「新しい道:Atarashii_Michi.wav」(変換後のファイル:atarashii_michi.mp3)です。
※「げきばん!」様のサイト内・検索で これらのファイルを検索したい場合は、上記の日本語名で検索して下さい。
※ 音源の変換は「XMedia Recode Version 3.2.6.1」(フリー・ソフト)を使用しています。
※ Android の Resource で使用可能なファイル名は「英小文字、数字、アンダースコア」のみで「英大文字」、「ハイフンや その他の記号」なども使用できないようなので、(データ形式の変換だけでなく)ファイル名も変更しています。
本プログラムで使用する場合は「変換後のファイル」として記述されているファイル名と完全に一致させて下さい。
『<Project名>→app→main→res→raw』フォルダー(存在しない場合は作成してやる)内に上記の「mayonaka_no_merrygoround.mp3、mayonaka_no_merrygoround_toypiano.mp3、mayonaka_no_merrygoround_harp.mp3、natsu_no_nokoriga_piano.mp3、atarashii_michi.mp3」をコピペして下さい。
とりあえず、[New Project]で(1ページ目)[Application name]:「KitchenTimer」、[Company domain]:[example.com]、[Project location]:「C:\Documents and Settings\<User名>\AndroidStudioProjects\<Application name>」と設定(<Application name>は[Application name]で設定した名前と全く同じにして下さい)、(2ページ目)[Phone and Tablet][Minimum SDK]:[API 8:Android 2.2]と設定、(3ページ目)(「Activity」とかもテキストで上書きしてしまうので)「Activity」の設定は とりあえず「Empty Activity」としておく。
(合っているハズなのに)エラーが消えない場合は一旦 Project を閉じてから、ふたたび Project を開いてみて下さい。
『<Project名>→app→main→res→drawable-hdpi』フォルダー(存在しない場合は作成してやる)内に「Android SDK」のデフォルト・アイコン「ic_menu_moreoverflow_normal_holo_light.png」をコピーする。
一応、「アプリの Logo マーク、通知用アイコン」を Android のマスコット・キャラの「Droid君」にしている。
通知用アイコンは白色が推奨されているらしいので、(一部 白ではないが)白を基調としている。
下の方に"drawable-hdpi"フォルダー用の画像ファイル(アプリの Logo マーク:「48x48」Pixel、通知用アイコン:「36x36」Pixel)をアップしてあります。
"drawable-mdpi"フォルダー用は手抜きして作ってないが、解像度が低い端末でも自動的に その端末の解像度に合うように縮小して表示される。
その場合、当然 画質は悪くなるので画質を綺麗にしたい場合は手抜きせずにチャント"drawable-mdpi"フォルダー用の画像ファイル(「32x32」Pixel)を作成してやれば良い。
なお、Activity の Toolbar の UI はスタンダードな「ActionBarActivity」のような感じになっている。
『<Project名>→app→main→java→com.example.kitchentimer→MainActivity(MainActivity.java)』
package com.example.kitchentimer;
import android.os.Build;
import android.os.Bundle;
import android.app.Activity;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.content.Context;
// import android.widget.Toolbar;
import android.view.KeyEvent;
import android.content.res.Resources;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
// import android.app.AlarmManager;
import java.util.Timer;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.io.IOException;
import android.content.pm.PackageManager.NameNotFoundException;
import android.view.View;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.TextView;
import java.util.HashMap;
public class MainActivity extends AppCompatActivity
implements View.OnClickListener {
static final String fsDebugCrest = " 026";
static final String tag = "LogCat.Debug"; // フィルタリング用タグ。
// ↑ 分かりやすければ どんな文字列でも良い。
static final boolean DEBUG = true; // false; //
// ↑ この"DEBUG"は予約語では無い、"Java"において"final"(定数)の場合は
// 全部 大文字で記述するの事が推奨されているようだ。
// 「DEBUG=true」に設定すれば「if(DEBUG)Log.i(〜)」によって"LogCat"に出力される事になる。
MainActivity oMainApp;
Context oBaseContext;
Context oAppContext; // Application Context.
Context oContext; // Activity Context.
Menu oMainMenu;
String vsPackageName;
String vsVersionName;
int iVersionCode;
String vsToolbarTitle;
static final SimpleDateFormat oDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss SSS");
static final int[] WIDGETS =
new int[]{R.id.wbtStart, R.id.wbtCancel};
TextView wtvMessage;
TextView wtvSound;
EditText wetHour, wetMinute, wetSecond;
static final String fsTimerTask_Audio = "TimerAudio";
// ↑ Timer は"TPlayer"と言う名前で1つだけしか作らない。
// HashMap<Integer, String> dm1oMenuItem = new HashMap<Integer, String>( );
// int iSelectMenuItem;
HashMap<Integer, String> dm1oMenuSound = new HashMap<Integer, String>();
int iSelectMenuSound;
int iSelectSound;
int iSoundPlayerPoolId = -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) Log.i(tag, "Activity.onCreate( );");
setContentView(R.layout.activity_main);
oMainApp = this;
// System 共通の Context:別の Application とやりとりするとき。
oBaseContext = getBaseContext();
// Application 固有の Context(Application ごとに Context が変化する):Application 共通のモノが対象。
oAppContext = getApplicationContext();
// Activity 固有の Context(Activity ごとに Context が変化する):Activity に依存モノが対象。
oContext = this;
vsPackageName = getPackageName();
try {
PackageInfo packageInfo = this.getPackageManager().getPackageInfo(
this.getPackageName(), PackageManager.GET_META_DATA);
// 「<Project名>→app→build.gradle」ファイル内の
// 「versionName、versionCode」の設定値を取得。
vsVersionName = packageInfo.versionName;
iVersionCode = packageInfo.versionCode;
} catch (NameNotFoundException e) {
// 例外処理
e.printStackTrace();
}
wtvMessage = (TextView) findViewById(R.id.wtvMessage);
wtvMessage.append("fsDebugCrest=" + fsDebugCrest + ";\n" +
"PackageName=" + vsPackageName + ";\n" +
"VersionName=" + vsVersionName + ";\n" +
"BuildNo=" + String.valueOf(iVersionCode) + ";\n" +
"");
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionbar = getSupportActionBar();
// actionbar.setIcon(R.drawable.ic_menu_info_details);
actionbar.setLogo(R.drawable.x_logo_android022);
// ↑ 実際は Logo マークはアプリ固有の Icon にすべきモノです。
// アプリ固有の Logo マークと言われてもイメージが湧かない方は
// ブラウザの Icon(Logo マーク)をイメージしてみて下さい。
// 下記サイトに主要ブラウザの Icon(Logo マーク)が掲載されています。
// http://freesoft-100.com/pasokon/browser.html
// ちなみに Toolbar に表示する Icon サイズのスタンダードは
// "drawable-mdpi"フォルダー用は「32x32」、
// "drawable-hdpi"フォルダー用は「48x48」だと思われます。
vsToolbarTitle = " " + getString(R.string.app_name) +
fsDebugCrest +
" V" + vsVersionName + // Version.
" R" + String.valueOf(iVersionCode); // Revision.
// ↑ Toolbar に Logo マークを表示させるとタイトルと Logo マークとの間に隙間がないので、
// タイトルの先頭に半角スペースを2つ入れている。
actionbar.setTitle(vsToolbarTitle);
dm1oMenuSound.put(new Integer(R.id.menu_main_settings_MerryGoRound),
"真夜中のメリーゴーランド");
dm1oMenuSound.put(new Integer(R.id.menu_main_settings_MerryGoRound_ToyPiano),
"真夜中のメリーゴーランド\n(ToyPiano)");
dm1oMenuSound.put(new Integer(R.id.menu_main_settings_MerryGoRound_Harp),
"真夜中のメリーゴーランド\n(Harp)");
dm1oMenuSound.put(new Integer(R.id.menu_main_settings_Natsu_No_Nokoriga_Piano),
"夏の残り香\n(Piano)");
dm1oMenuSound.put(new Integer(R.id.menu_main_settings_Atarashii_Michi),
"新しい道");
iSelectMenuSound = R.id.menu_main_settings_MerryGoRound;
iSelectSound = R.raw.mayonaka_no_merrygoround;
// ↑リソース用ファイルの場合、
// ファイル名には「英小文字、数字、アンダースコア」しか使用できないようだ。
if (DEBUG) Log.i(tag, "Activity.onCreate( ) : " +
"dm1oMenuSound.get(new Integer(iSelectMenuSound))=" +
dm1oMenuSound.get(new Integer(iSelectMenuSound)) + "; " +
"");
wtvSound = (TextView) findViewById(R.id.wtvSound);
wtvSound.setText(dm1oMenuSound.get(new Integer(iSelectMenuSound)));
wetHour = (EditText) findViewById(R.id.wetHour);
wetMinute = (EditText) findViewById(R.id.wetMinute);
wetSecond = (EditText) findViewById(R.id.wetSecond);
for (int iWId : WIDGETS) {
View wvw = findViewById(iWId);
wvw.setOnClickListener(this);
}
}
@Override
protected void onRestart() {
super.onRestart();
if (DEBUG) Log.i(tag, "Activity.onRestart( );");
}
@Override
protected void onStart() {
super.onStart();
if (DEBUG) Log.i(tag, "Activity.onStart( );");
}
@Override
protected void onResume() {
super.onResume();
if (DEBUG) Log.i(tag, "Activity.onResume( );");
}
@Override
protected void onPause() {
super.onPause();
if (DEBUG) Log.i(tag, "Activity.onPause( );");
}
@Override
protected void onStop() {
super.onStop();
if (DEBUG) Log.i(tag, "Activity.onStop( );");
}
@Override
protected void onDestroy() {
super.onDestroy();
if (DEBUG) Log.i(tag, "Activity.onDestroy( );");
}
@Override
public void onClick(View v) {
// super.onClick(v);
if (DEBUG) Log.i(tag, "Activity.onClick( );");
Intent si;
long delay = 0;
String s, vsH, vsM, vsS;
long hour, minute, second;
boolean err = false; // true; //
switch (v.getId()) {
case R.id.wbtStart:
if (DEBUG) Log.i(tag, "wbtStart");
hour = 0;
minute = 0;
second = 0;
try {
s = wetHour.getText().toString();
if (!s.equals(""))
hour = Long.parseLong(s);
s = wetMinute.getText().toString();
if (!s.equals(""))
minute = Long.parseLong(s);
s = wetSecond.getText().toString();
if (!s.equals(""))
second = Long.parseLong(s);
} catch (NumberFormatException e) {
// e.printStackTrace( );
wtvMessage.setText("入力エラーです、数字以外の文字が入力されました。\n");
err = true; // false; //
}
if (DEBUG) Log.i(tag, "wbtStart : " +
"hour=" + hour + "; " +
"minute=" + minute + "; " +
"second=" + second + "; " +
"");
if (!err) {
delay = (long) ((hour * 60 + minute) * 60 + second) * 1000; // ms
if (DEBUG) Log.i(tag, "wbtStart : " +
"delay=" + delay + "; " +
"");
if (0 < delay) {
CreateTimer(fsTimerTask_Audio, delay);
}
}
break;
case R.id.wbtCancel:
if (DEBUG) Log.i(tag, "wbtCancel");
wetHour.setText("");
wetMinute.setText("");
wetSecond.setText("");
try {
if (DEBUG) Log.i(tag, "wbtCancel : " +
"dm1oTimerTask.get(fsTimerTask_Player)=" + MyFSTimerTask.dm1oTimerTask.get(fsTimerTask_Audio) + "; " +
"");
MyFSTimerTask_Audio oFSTT_Audio = (MyFSTimerTask_Audio) MyFSTimerTask.dm1oTimerTask.get(fsTimerTask_Audio);
synchronized (oFSTT_Audio) {
if ( null!=oFSTT_Audio.oMediaPlayer ) {
if ( oFSTT_Audio.oMediaPlayer.isPlaying( ) ) {
oFSTT_Audio.oMediaPlayer.stop( );
}
oFSTT_Audio.oMediaPlayer.release(); // リソースの解放
// ↑リソースの解放は必須のようです。
oFSTT_Audio.oMediaPlayer = null;
}
MyFSTimerTask_Audio.TaskCancel(oAppContext, fsTimerTask_Audio);
}
} catch (NullPointerException e) {
// e.printStackTrace( );
if (DEBUG) Log.i(tag, "MainActivity.CreateTimer( ):NullPointerException;");
}
break;
default:
if (DEBUG) Log.i(tag, "default");
return;
}
return;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
// アクションバー内で使用する為のメニューアイテムを実体化
MenuInflater inflater = getMenuInflater();
// MenuInflater inflater = getSupportMenuInflater();
inflater.inflate(R.menu.main, menu);
// 「R.menu.main」の「menu.main」は 恐らく「res」からの「フォルダー名+ファイル名」
oMainMenu = menu;
// 戻り値に true を返す事で Event が「消費された」事が通知され、
// 他の Event Listener の処理はキャンセルされます。
return true; // false; //
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
if (DEBUG) Log.i(tag, "Activity.onOptionsItemSelected( );");
switch (item.getItemId()) {
case R.id.menu_main_settings:
if (DEBUG) Log.i(tag, "menu_main_settings");
break;
case R.id.menu_main_settings_MerryGoRound:
if (DEBUG) Log.i(tag, "menu_main_settings_MerryGoRound");
iSelectMenuSound = item.getItemId();
iSelectSound = R.raw.mayonaka_no_merrygoround;
wtvSound.setText(dm1oMenuSound.get(new Integer(iSelectMenuSound)));
break;
case R.id.menu_main_settings_MerryGoRound_ToyPiano:
if (DEBUG) Log.i(tag, "menu_main_settings_MerryGoRound_ToyPiano");
iSelectMenuSound = item.getItemId();
iSelectSound = R.raw.mayonaka_no_merrygoround_toypiano;
wtvSound.setText(dm1oMenuSound.get(new Integer(iSelectMenuSound)));
break;
case R.id.menu_main_settings_MerryGoRound_Harp:
if (DEBUG) Log.i(tag, "menu_main_settings_MerryGoRound_Harp");
iSelectMenuSound = item.getItemId();
iSelectSound = R.raw.mayonaka_no_merrygoround_harp;
wtvSound.setText(dm1oMenuSound.get(new Integer(iSelectMenuSound)));
break;
case R.id.menu_main_settings_Natsu_No_Nokoriga_Piano:
if (DEBUG) Log.i(tag, "menu_main_settings_Natsu_No_Nokoriga_Piano");
iSelectMenuSound = item.getItemId();
iSelectSound = R.raw.natsu_no_nokoriga_piano;
wtvSound.setText(dm1oMenuSound.get(new Integer(iSelectMenuSound)));
break;
case R.id.menu_main_settings_Atarashii_Michi:
if (DEBUG) Log.i(tag, "menu_main_settings_Atarashii_Michi");
iSelectMenuSound = item.getItemId();
iSelectSound = R.raw.atarashii_michi;
wtvSound.setText(dm1oMenuSound.get(new Integer(iSelectMenuSound)));
break;
default:
if (DEBUG) Log.i(tag, "default");
// 戻り値に false を返す事で Event は「消費されてない」事が通知され、
// 他の Event Listener の処理は継続されます。
return false; // true; //
}
// 戻り値に true を返す事で Event が「消費された」事が通知され、
// 他の Event Listener の処理はキャンセルされます。
return true; // false; //
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
super.dispatchKeyEvent(event);
if (DEBUG) Log.i(tag, "Activity.dispatchKeyEvent( );");
final int action = event.getAction();
final int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_MENU:
if (DEBUG) Log.i(tag, "KeyEvent.KEYCODE_MENU;");
if (action == KeyEvent.ACTION_UP) {
if (DEBUG) Log.i(tag, "KeyEvent.ACTION_UP;");
if (oMainMenu != null) {
if (DEBUG) Log.i(tag, "oMainMenu.performIdentifierAction( );");
oMainMenu.performIdentifierAction(R.id.menu_main_settings, 0);
}
// 戻り値に true を返す事で Event が「消費された」事が通知され、
// 他の Event Listener の処理はキャンセルされます。
return true; // false; //
}
break;
default:
if (DEBUG) Log.i(tag, "KeyEvent:Default");
break;
}
// 戻り値に false を返す事で Event は「消費されてない」事が通知され、
// 他の Event Listener の処理は継続されます。
return false; // true; //
}
public void CreateTimer(String vsN, long delay) {
if (DEBUG) Log.i(tag, "MainActivity.CreateTimer( );");
try {
synchronized (MyFSTimerTask.dm1oTimerTask.get(vsN)) {
if (null != MyFSTimerTask.dm1oTimerTask.get(vsN)) {
// Timer の多重登録時のエラー処理を記述。
// return;
}
}
} catch (NullPointerException e) {
// e.printStackTrace( );
if (DEBUG) Log.i(tag, "MainActivity.CreateTimer( ):NullPointerException;");
}
Timer timer = new Timer();
timer.schedule(new MyFSTimerTask_Audio(getApplicationContext(), timer, vsN, iSelectSound) {
@Override
@SuppressWarnings("deprecation")
public void run() {
if (DEBUG) Log.i(tag, "MyFSTimerTask_Audio.run( ) : " +
"タイマーが実行されました。");
synchronized(this){
String vsDateTime = oDateFormat.format(
new Date(System.currentTimeMillis()));
if (DEBUG) Log.i(tag, "TimerTask_Audio.run( ) : " +
"vsName=" + vsName + "; " +
"vsDateTime=" + vsDateTime + "; " +
"");
// AudioManagerを取得する
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// Alarm の最大音量を取得する
int iVolumeMax = am.getStreamMaxVolume(AudioManager.STREAM_ALARM);
// Alarm の音量を取得する
int iVolume = am.getStreamVolume(AudioManager.STREAM_ALARM);
float vfVolume = (float) iVolume / iVolumeMax;
if (DEBUG) Log.i(tag, "MyFSTimerTask_Audio.run( ) : " +
"vfVolume=" + vfVolume + "; " +
"");
float vfLeftVolume = vfVolume;
float vfRightVolume = vfVolume;
lSoundRunning = true; // false; //
try {
if (null!=oMediaPlayer) {
if( oMediaPlayer.isPlaying( ) ) {
oMediaPlayer.stop( );
}
oMediaPlayer.release(); // リソースの解放
// ↑リソースの解放は必須のようです。
oMediaPlayer = null;
}
Resources res = getResources();
String rneme = res.getResourceName(iSoundId);
String rtype = res.getResourceTypeName(iSoundId);
String rentry = res.getResourceEntryName(iSoundId);
String rfile = "android.resource://" + getPackageName( ) + "/" + iSoundId;
if (DEBUG) Log.i(tag, "MyFSTimerTask_Audio.run( ) : " +
"rneme=" + rneme + "; " +
"rtype=" + rtype + "; " +
"rentry=" + rentry + "; " +
"rfile=" + rfile + "; " +
"");
Uri uri = Uri.parse(rfile);
// oMediaPlayer = MediaPlayer.create(oAppContext, iSoundId);
// StreamType の設定で「AudioManager.STREAM_MUSIC」を指定すると
// 「MediaPlayer、Ringtone」で音が鳴らない端末があるようです。
// 「MediaPlayer#setAudioStreamType( )」は使わずにデフォルト設定で使う事により、
// 「STREAM_MUSIC」になるようです。
// ただし「Ringtone#setStreamType( )」を使わずにデフォルト設定で使った場合は、
// 恐らくデフォルト値は「STREAM_RING」になると思われます。
// 「音楽:STREAM_MUSIC」以外の「アラーム:STREAM_ALARM、通知:STREAM_NOTIFICATION、
// 「着信:STREAM_RING、システム:STREAM_SYSTEM、通話:STREAM_VOICE_CALL」なら設定 可能だと思われます。
// ただし「Android 4.0」以上は「通知:STREAM_NOTIFICATION、着信:STREAM_RING」のボリュームが連動、
// 「Android 5.0」以上は「通知:STREAM_NOTIFICATION、着信:STREAM_RING、システム:STREAM_SYSTEM」
// のボリュームが連動するようです。
oMediaPlayer = new MediaPlayer();
oMediaPlayer.setDataSource(oAppContext, uri);
// この STREAM の種類は「アラーム音:STREAM_ALARM、ダイヤル音:STREAM_DTMF、
// 通知音:STREAM_NOTIFICATION、着信音:STREAM_RING、システム音:STREAM_SYSTEM、通話:STREAM_VOICE_CALL 」。
// 「setAudioStreamType( )」に上記の STREAM を設定する事により、
// MediaPlayer は その Stream Type に関連付けられ、
// その STREAM に設定されているボリューム(音量)でサウンドを鳴らせる。
// それは その STREAM の音源を参照する訳ではなく、
// あくまで その Stream Type のボリューム(音量)を参照しているだけ。
oMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); // Alarm 音として設定
oMediaPlayer.setOnCompletionListener(this);
oMediaPlayer.setLooping(false); // true;
oMediaPlayer.prepare(); // メディア再生の準備(同期)
oMediaPlayer.start();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace( );
}
lTimerRunning = false; // true; //
// TaskCancel(null, vsName);
}
}
@Override
public synchronized void onCompletion(MediaPlayer mp) {
if (null!=oMediaPlayer) {
oMediaPlayer.release( ); // リソースの解放
// ↑リソースの解放は必須のようです。
oMediaPlayer = null;
}
lSoundRunning = false; // true; //
TaskCancel(null, vsName);
}
}, delay);
}
}
『<Project名>→app→main→java→com.example.kitchentimer→MyService(MyService.java)』
package com.example.kitchentimer;
import android.app.Service;
import android.os.IBinder;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.app.Notification;
import android.support.v7.app.NotificationCompat;
import android.util.Log;
// import android.app.AlarmManager;
import java.util.Timer;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MyService extends Service {
static final String fsDebugCrest = MainActivity.fsDebugCrest;
static final String tag = MainActivity.tag; // フィルタリング用タグ。
// ↑ 分かりやすければ どんな文字列でも良い。
static final boolean DEBUG = MainActivity.DEBUG;
// ↑ この"DEBUG"は予約語では無い、"Java"において"final"(定数)の場合は
// 全部 大文字で記述するの事が推奨されているようだ。
static final int ONGOING_NOTIFICATION = 1;
static Object oAnchor_TimerTask_Array;
// ↑ただのアンカーなので、アクセスする場合は
// MyTimerTask の方の dm1oTimer をアクセスして下さい。
// 下記「timer、delay、interval」は あくまでもテンポラリー。
Timer timer; // = new Timer( );
long delay; // ms
long interval; // ms
@Override
public void onCreate( ){
super.onCreate( );
if (DEBUG) Log.i(tag, "MyService.onCreate( );");
}
@Override
public int onStartCommand(Intent si, int f, int sid) {
super.onStartCommand(si, f, sid);
if(DEBUG)Log.i(tag, "MyService.onStartCommand( ) : "+
"fsDebugCrest="+fsDebugCrest+"; "+
"");
// 良く分からないが、次の PendingIntent は
// Activity の取得の場合は「getActivity(〜)」、
// Activity の取得 以外は「getService(〜)」になると思われる。
SettingForeground(PendingIntent.getActivity(
this, 0, new Intent(this, MainActivity.class),
PendingIntent.FLAG_CANCEL_CURRENT));
return START_NOT_STICKY;
// START_NOT_STICKY:
// サービスを起動するペンディングインテントが存在しない限りサービスは再起動されません 。
// 強制終了によりサービスが終了した場合、勝手な再起動を防ぐ場合にはこれを使用します。
// システムからの勝手な再起動を考慮せずに作成されたサービスにおいて、
// システムからの勝手な再起動を許すと、
// それがバグとなりサービスに異常をきたす事になりなりかねません。
}
@Override
public IBinder onBind(Intent si) {
// super.onBind(si);
return null;
}
public void SettingForeground(PendingIntent pi) {
if(DEBUG)Log.i(tag, "MyService.SettingForeground( );");
Notification notif = new NotificationCompat.Builder(this)
.setContentIntent(pi)
.setAutoCancel(true)
.setContentTitle(getString(R.string.app_name))
.setContentText("Content Text.")
// .setContentInfo("Content Info.")
// .setTicker("Ticker.")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.x_slogo_android031) // SmallIcon を設定しないと通知されない(例外も発生しない)
.build( );
startForeground(ONGOING_NOTIFICATION, notif);
}
@Override
public void onDestroy( ){
super.onDestroy( );
if (DEBUG) Log.i(tag, "MyService.onDestroy( );");
oAnchor_TimerTask_Array = null;
stopForeground(true);
}
}
『<Project名>→app→main→java→com.example.kitchentimer→MyFSTimerTask(MyFSTimerTask.java)』
package com.example.kitchentimer;
import java.util.Timer;
import java.util.TimerTask;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
public abstract class MyFSTimerTask extends TimerTask {
static final String tag = "LogCat.Debug"; // フィルタリング用タグ。
// ↑ 分かりやすければ どんな文字列でも良い。
static final boolean DEBUG = MainActivity.DEBUG;
// ↑ この"DEBUG"は予約語では無い、"Java"において"final"(定数)の場合は
// 全部 大文字で記述するの事が推奨されているようだ。
static Context oAppContext; // Application Context.
// volatile:最適化の抑制.
public volatile String vsName = "";
public volatile Timer oTimer;
public volatile boolean lTimerRunning = false; // true; //
public volatile static HashMap<String, MyFSTimerTask> dm1oTimerTask = new HashMap<String, MyFSTimerTask>( );
// SimpleDateFormat oDateFormat = MainActivity.oDateFormat;
public MyFSTimerTask(Context actx, Timer t, String vsN){
super( );
if (DEBUG) Log.i(tag, "MyTimerTask( );");
synchronized(this) {
lTimerRunning = true; // false; //
vsName = vsN;
oTimer = t;
TaskRemove(vsName);
dm1oTimerTask.put(vsName, this);
oAppContext = actx;
Intent si = new Intent(oAppContext, MyService.class);
// si.putExtra("SubjectKey", "Foreground");
oAppContext.startService(si);
MyService.oAnchor_TimerTask_Array = dm1oTimerTask;
}
if(DEBUG)Log.i(tag, "MyTimerTask( ) : "+
"vsName="+vsName+"; "+
"dm1oTimerTask.size( )="+dm1oTimerTask.size( )+"; "+
"");
}
public static synchronized void TaskCancel(Context actx, String vsN){
if (DEBUG) Log.i(tag, "MyTimerTask.TaskCancel( );");
TaskRemove(vsN);
Check_ServiceCancel(actx);
}
private static synchronized void TaskRemove(String vsN){
if (DEBUG) Log.i(tag, "MyTimerTask.TaskRemove( );");
MyFSTimerTask timertask = dm1oTimerTask.get(vsN);
if( null!=timertask ){
timertask.oTimer.cancel( );
dm1oTimerTask.remove(vsN);
}
}
private static synchronized void Check_ServiceCancel(Context actx){
if (DEBUG) Log.i(tag, "MyTimerTask.Check_ServiceCancel( );");
if( 0==dm1oTimerTask.size( ) ){
if( null!=actx ){
oAppContext = actx;
}
Intent si = new Intent(oAppContext, MyService.class);
oAppContext.stopService(si);
}
}
}
『<Project名>→app→main→java→com.example.kitchentimer→MyFSTimerTask_Audio(MyFSTimerTask_Audio.java)』
package com.example.kitchentimer;
import java.util.Timer;
import android.media.MediaPlayer;
import android.content.Context;
public abstract class MyFSTimerTask_Audio extends MyFSTimerTask
implements MediaPlayer.OnCompletionListener {
int iSoundId;
// volatile:最適化の抑制.
// public volatile int iPoolId = -1;
public volatile boolean lSoundRunning = false; // true; //
public volatile MediaPlayer oMediaPlayer;
// public volatile SoundPool oSoundPlayer;
public MyFSTimerTask_Audio(Context actx, Timer t, String vsN, int iSI){
super(actx, t, vsN);
// lSoundRunning = true; // false; //
iSoundId = iSI;
}
}
『<Project名>→app→main→res→drawable→border.xml』(ファイルを作成)
※「TextView」の枠として使っている。
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ffffff" />
<padding android:top="3dp" android:bottom="3dp"
android:left="7dp" android:right="7dp" />
<stroke android:width="1px" android:color="#000000" />
<corners android:radius="5dp" />
</shape>
『<Project名>→app→main→res→layout→activity_main.xml』
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#ffffff"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:background="#9b7fff"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_margin = "7dp"
android:background="@drawable/border"
android:text="Sound"
android:id="@+id/wtvSound"
android:lines="2" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="時間"
android:id="@+id/wtvHour"
android:layout_weight="1"
android:gravity="center_horizontal"
android:textColor="@android:color/black" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="分"
android:id="@+id/wtvMinute"
android:layout_weight="1"
android:gravity="center_horizontal"
android:textColor="@android:color/black" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="秒"
android:id="@+id/wtvSecond"
android:layout_weight="1"
android:gravity="center_horizontal"
android:textColor="@android:color/black" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:inputType="numberDecimal"
android:ems="10"
android:id="@+id/wetHour"
android:layout_weight="1"
android:gravity="right" />
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:inputType="numberDecimal"
android:ems="10"
android:id="@+id/wetMinute"
android:layout_weight="1"
android:gravity="right" />
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:inputType="numberDecimal"
android:ems="10"
android:id="@+id/wetSecond"
android:layout_weight="1"
android:gravity="right" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Start"
android:id="@+id/wbtStart"
android:layout_weight="1" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Cancel"
android:id="@+id/wbtCancel"
android:layout_weight="1" />
</LinearLayout>
<TextView
android:id="@+id/wtvMessage"
android:layout_width="match_parent"
android:layout_height="0dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_margin = "7dp"
android:background="@drawable/border"
android:layout_weight="1" />
</LinearLayout>
『<Project名>→app→src→main→res→menu→main.xml』(フォルダーとファイルを作成)
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item android:id="@+id/menu_main_settings"
android:title="Settings"
android:icon="@drawable/ic_menu_moreoverflow_normal_holo_light"
app:showAsAction="always">
<!-- ↑ showAsAction:Toolbar に対する表示設定。 -->
<!-- "always":常に表示、"never":常に表示しない、"ifRoom":表示する余裕があれば表示。 -->
<menu>
<item android:id="@+id/menu_main_settings_MerryGoRound"
android:title="メリーゴーランド">
</item>
<item android:id="@+id/menu_main_settings_MerryGoRound_ToyPiano"
android:title="メリーゴーランド(ToyPiano)">
</item>
<item android:id="@+id/menu_main_settings_MerryGoRound_Harp"
android:title="メリーゴーランド(Harp)">
</item>
<item android:id="@+id/menu_main_settings_Natsu_No_Nokoriga_Piano"
android:title="夏の残り香(Piano)">
</item>
<item android:id="@+id/menu_main_settings_Atarashii_Michi"
android:title="新しい道">
</item>
</menu>
</item>
</menu>
『<Project名>→app→src→main→res→values→styles.xml』
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
『<Project名>→app→main→AndroidManifest.xml』
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.kitchentimer">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme" >
<activity android:name=".MainActivity"
android:launchMode="singleInstance" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name="MyService" >
</service>
</application>
</manifest>
|
|