Java tutorial
/* * Copyright (C) 2010-2014 Eric Hansander * * This file is part of Retro Timer. * * Retro Timer is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Retro Timer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Retro Timer. If not, see <http://www.gnu.org/licenses/>. * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package se.erichansander.retrotimer; import java.lang.ref.WeakReference; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.MediaPlayer.OnErrorListener; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Vibrator; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.text.format.DateFormat; /** * Plays alarm and vibrates. Runs as a service so that it can continue to play * if another activity overrides the TimerAlert view. * * It will play alarm and start vibrating, show a notification that allows * dismissing the alarm, and start the TimerAlert activity, that also allows * dismissing. * * Receives the ALARM_PLAY_ACTION intent when started. */ public class TimerKlaxon extends Service { /* * Comment from the DeskClock app: * * Volume suggested by media team for in-call alarms. */ private static final float IN_CALL_VOLUME = 0.125f; private static final long[] sVibratePattern = new long[] { 500, 500 }; // Handles to stuff we need to interact with private SharedPreferences mPrefs; private Vibrator mVibrator; private MediaPlayer mMediaPlayer; private TelephonyManager mTelephonyManager; private AudioFocusHelper mAudioFocusHelper; private boolean mPlaying = false; private long mAlarmTime = 0; private int mInitialCallState; // Internal messages. Handles alarm timeouts. private static final int TIMEOUT_ID = 1000; private static class TimeoutHandler extends Handler { private final WeakReference<TimerKlaxon> mKlaxonRef; public TimeoutHandler(TimerKlaxon klaxon) { mKlaxonRef = new WeakReference<TimerKlaxon>(klaxon); } @Override public void handleMessage(Message msg) { TimerKlaxon k = mKlaxonRef.get(); switch (msg.what) { case TIMEOUT_ID: if (k != null) { k.handleAlarmSilence(k.mAlarmTime); k.stopSelf(); } break; } } }; private Handler mHandler = new TimeoutHandler(this); private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String ignored) { TinyTracelog.trace("6"); /* * The user might already be in a call when the alarm fires. When we * register onCallStateChanged, we get the initial in-call state * which kills the alarm. Check against the initial call state so we * don't kill the alarm during a call. */ if (state != TelephonyManager.CALL_STATE_IDLE && state != mInitialCallState) { TinyTracelog.trace("6.1"); handleAlarmSilence(mAlarmTime); stopSelf(); } } }; @Override public void onCreate() { TinyTracelog.trace("3"); mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); // Listen for incoming calls to kill the alarm. mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); if (android.os.Build.VERSION.SDK_INT >= 8) { mAudioFocusHelper = new AudioFocusHelper(this); } else { mAudioFocusHelper = null; } WakeLockHolder.acquireScreenCpuWakeLock(this); } @Override public void onDestroy() { TinyTracelog.trace("9"); stop(); cancelTimeoutCountdown(); // Cancel the notification NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(RetroTimer.NOTIF_TRIGGERED_ID); mTelephonyManager.listen(mPhoneStateListener, 0); WakeLockHolder.releaseCpuLock(); } @Override public IBinder onBind(Intent intent) { return null; } @Override public void onStart(Intent intent, int startId) { // For backwards compatibility with pre-API 8 systems onStartCommand(intent, 0, startId); } @Override public int onStartCommand(Intent intent, int flags, int startId) { TinyTracelog.trace("4"); // No intent, tell the system not to restart us. if (intent == null) { TinyTracelog.trace("4.e"); stopSelf(); return START_NOT_STICKY; } boolean ring = mPrefs.getBoolean(RetroTimer.PREF_RING_ON_ALARM, true); boolean vibrate = mPrefs.getBoolean(RetroTimer.PREF_VIBRATE_ON_ALARM, true); long timeoutMillis = mPrefs.getLong(RetroTimer.PREF_ALARM_TIMEOUT_MILLIS, 10 * 1000); mAlarmTime = intent.getLongExtra(RetroTimer.ALARM_TIME_EXTRA, 0); // Close dialogs and window shade sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); // Trigger a notification that, when clicked, will dismiss the alarm. Intent notify = new Intent(RetroTimer.ALARM_DISMISS_ACTION); PendingIntent pendingNotify = PendingIntent.getBroadcast(this, 0, notify, 0); NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this).setContentIntent(pendingNotify) .setDefaults(Notification.DEFAULT_LIGHTS).setOngoing(true) .setSmallIcon(R.drawable.ic_stat_alarm_triggered) .setContentTitle(getString(R.string.notify_triggered_label)) .setContentText(getString(R.string.notify_triggered_text)); /* * Send the notification using the alarm id to easily identify the * correct notification. */ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(RetroTimer.NOTIF_SET_ID); nm.notify(RetroTimer.NOTIF_TRIGGERED_ID, mBuilder.build()); /* * launch UI, explicitly stating that this is not due to user action so * that the current app's notification management is not disturbed */ Intent timerAlert = new Intent(this, TimerAlert.class); timerAlert.putExtra(RetroTimer.ALARM_TIME_EXTRA, mAlarmTime); timerAlert.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION); startActivity(timerAlert); play(ring, vibrate); startTimeoutCountdown(timeoutMillis); // Record the initial call state here so that the new alarm has the // newest state. mInitialCallState = mTelephonyManager.getCallState(); // Update the shared state RetroTimer.clearAlarm(this); return START_STICKY; } /** * Wrap up after the alarm sound has timed out, with no user dismissal * * Will stop playing alarm, stop vibrating and display a notification saying * when the alarm triggered. */ private void handleAlarmSilence(long alarmTime) { TinyTracelog.trace("8"); // Display notification NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); // Launch the TimerSet activity when clicked Intent viewAlarm = new Intent(this, TimerSet.class); viewAlarm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent pintent = PendingIntent.getActivity(this, 0, viewAlarm, 0); // Update the notification to indicate that the alert has been // silenced. NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this).setContentIntent(pintent) .setAutoCancel(true).setSmallIcon(R.drawable.ic_stat_alarm_triggered) .setContentTitle(getString(R.string.notify_silenced_label)).setContentText(getString( R.string.notify_silenced_text, DateFormat.getTimeFormat(this).format(mAlarmTime))); // We have to cancel the original notification since it is in the // ongoing section and we want the "killed" notification to be a plain // notification. nm.cancel(RetroTimer.NOTIF_TRIGGERED_ID); nm.notify(RetroTimer.NOTIF_SILENCED_ID, mBuilder.build()); Intent intent = new Intent(RetroTimer.ALARM_SILENCE_ACTION); intent.putExtra(RetroTimer.ALARM_TIME_EXTRA, alarmTime); sendBroadcast(intent); stopSelf(); } private void play(boolean ring, boolean vibrate) { TinyTracelog.trace("5"); // stop() checks to see if we are already playing. stop(); if (ring) { TinyTracelog.trace("5.1"); mMediaPlayer = new MediaPlayer(); mMediaPlayer.setOnErrorListener(new OnErrorListener() { public boolean onError(MediaPlayer mp, int what, int extra) { TinyTracelog.trace("5.1.e1"); stop(); return true; } }); try { /* * Check if we are in a call. If we are, use the in-call alarm * resource at a low volume to not disrupt the call. */ if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) { TinyTracelog.trace("5.1.1"); mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); setDataSourceFromResource(getResources(), mMediaPlayer, R.raw.in_call_alarm); } else { TinyTracelog.trace("5.1.2"); setDataSourceFromResource(getResources(), mMediaPlayer, R.raw.classic_alarm); } } catch (Exception ex) { // Failed to set data source. Not much we can do to save // the situation though... TinyTracelog.trace("5.1.e2"); } try { final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); // do not play alarms if stream volume is 0 int volume = audioManager.getStreamVolume(AudioManager.STREAM_ALARM); TinyTracelog.trace("5.1.3 " + volume); if (volume != 0) { mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); mMediaPlayer.setLooping(true); mMediaPlayer.prepare(); if (mAudioFocusHelper != null) { mAudioFocusHelper.requestFocus(); } mMediaPlayer.start(); } } catch (Exception ex) { // Failed to play ring tone. Not much we can do to save // the situation though... TinyTracelog.trace("5.1.e3"); } } /* Start the vibrator after everything is ok with the media player */ if (vibrate) { TinyTracelog.trace("5.2"); mVibrator.vibrate(sVibratePattern, 0); } else { mVibrator.cancel(); } mPlaying = true; } private void setDataSourceFromResource(Resources resources, MediaPlayer player, int res) throws java.io.IOException { AssetFileDescriptor afd = resources.openRawResourceFd(res); if (afd != null) { player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); afd.close(); } } /** * Stops alarm audio and vibration */ public void stop() { if (mPlaying) { mPlaying = false; // Stop audio playing if (mMediaPlayer != null) { mMediaPlayer.stop(); if (mAudioFocusHelper != null) { mAudioFocusHelper.abandonFocus(); } mMediaPlayer.release(); mMediaPlayer = null; } // Stop vibrator mVibrator.cancel(); } } /** * Kills alarm audio after timeoutMillis millis, so the alarm won't run all * day. */ private void startTimeoutCountdown(long timeoutMillis) { mHandler.sendMessageDelayed(mHandler.obtainMessage(TIMEOUT_ID), timeoutMillis); } /* Cancels the timeout countdown */ private void cancelTimeoutCountdown() { mHandler.removeMessages(TIMEOUT_ID); } }