Setup RingtoneService

This commit is contained in:
Phillip Hsu
2016-05-28 18:44:51 -07:00
parent 7d4b0c0dff
commit 37e7d3dd3b
25 changed files with 763 additions and 45 deletions
@@ -0,0 +1,179 @@
package com.philliphsu.clock2;
import com.google.auto.value.AutoValue;
import java.util.Calendar;
import java.util.GregorianCalendar;
/**
* Created by Phillip Hsu on 5/26/2016.
*/
@AutoValue
public abstract class Alarm {
private static final int MAX_MINUTES_CAN_SNOOZE = 30;
// Define our own day constants because those in the
// Calendar class are not zero-based.
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
public static final int NUM_DAYS = 7;
// =========== MUTABLE ===========
private long snoozingUntilMillis;
private boolean enabled;
// ===============================
public abstract long id(); // TODO: Counter in the repository. Set this field as the repo creates instances.
public abstract int hour();
public abstract int minutes();
@SuppressWarnings("mutable")
// TODO: Consider using an immutable collection instead
public abstract boolean[] recurringDays(); // array itself is immutable, but elements are not
public abstract String label();
public abstract String ringtone();
public abstract boolean vibrates();
/** Initializes a Builder to the same property values as this instance */
public abstract Builder toBuilder();
public static Builder builder() {
// Unfortunately, default values must be provided for generated Builders.
// Fields that were not set when build() is called will throw an exception.
// TODO: How can QualityMatters get away with not setting defaults?????
return new AutoValue_Alarm.Builder()
.id(-1)
.hour(0)
.minutes(0)
.recurringDays(new boolean[NUM_DAYS])
.label("")
.ringtone("")
.vibrates(false);
}
public void snooze(int minutes) {
if (minutes <= 0 || minutes > MAX_MINUTES_CAN_SNOOZE)
throw new IllegalArgumentException("Cannot snooze for "+minutes+" minutes");
snoozingUntilMillis = System.currentTimeMillis() + minutes * 60000;
}
public long snoozingUntil() {
return snoozingUntilMillis;
}
public boolean isSnoozed() {
if (snoozingUntilMillis <= System.currentTimeMillis()) {
snoozingUntilMillis = 0;
return false;
}
return true;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return enabled;
}
public void setRecurring(int day, boolean recurring) {
checkDay(day);
recurringDays()[day] = recurring;
}
public boolean isRecurring(int day) {
checkDay(day);
return recurringDays()[day];
}
public boolean hasRecurrence() {
for (boolean b : recurringDays())
if (b) return true;
return false;
}
public long ringsAt() {
// Always with respect to the current date and time
Calendar calendar = new GregorianCalendar();
calendar.set(Calendar.HOUR_OF_DAY, hour());
calendar.set(Calendar.MINUTE, minutes());
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
// The specified time has passed for today
// TODO: This should be wrapped in an if (!hasRecurrence())?
calendar.add(Calendar.HOUR_OF_DAY, 24);
// TODO: Else, compute ring time for the next closest recurring day
}
/* // Not fully thought out or completed!
// TODO: Compute ring time for the next closest recurring day
for (int i = 0; i < NUM_DAYS; i++) {
// The constants for the days defined in Calendar are
// not zero-based, but we are, so we must add 1.
int day = i + 1; // day for this index
int calendarDay = mCalendar.get(Calendar.DAY_OF_WEEK);
if (mRecurringDays[day]) {
mCalendar.add(Calendar.DAY_OF_WEEK, day);
break;
}
}
*/
return calendar.getTimeInMillis();
}
public long ringsIn() {
return ringsAt() - System.currentTimeMillis();
}
/** @return true if this Alarm will ring in the next {@code hours} hours */
public boolean ringsWithinHours(int hours) {
return ringsIn() <= hours * 3600000;
}
@AutoValue.Builder
public abstract static class Builder {
// Builder is mutable, so these are inherently setter methods.
// By omitting the set- prefix, we reduce the number of changes required to define the Builder
// class after copying and pasting the accessor fields here.
public abstract Builder id(long id);
public abstract Builder hour(int hour);
public abstract Builder minutes(int minutes);
/* // TODO: If using an immutable collection instead, can use its Builder instance
// and provide an "accumulating" method
abstract boolean[] recurringDays();
public final Builder setRecurring(int day, boolean recurs) {
checkDay(day)
recurringDays()[day] = recurs;
return this;
}
*/
public abstract Builder recurringDays(boolean[] recurringDays);
public abstract Builder label(String label);
public abstract Builder ringtone(String ringtone);
public abstract Builder vibrates(boolean vibrates);
// To enforce preconditions, split the build method into two. autoBuild() is hidden from
// callers and is generated. You implement the public build(), which calls the generated
// autoBuild() and performs your desired validations.
/*not public*/abstract Alarm autoBuild();
public Alarm build() {
Alarm alarm = autoBuild();
checkTime(alarm.hour(), alarm.minutes());
return alarm;
}
}
private static void checkTime(int hour, int minutes) {
if (hour < 0 || hour > 23 || minutes < 0 || minutes > 59) {
throw new IllegalStateException("Hour and minutes invalid");
}
}
private static void checkDay(int day) {
if (day < SUNDAY || day > SATURDAY)
throw new IllegalArgumentException("Invalid day " + day);
}
}
@@ -0,0 +1,196 @@
package com.philliphsu.clock2;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.media.RingtoneManager;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.philliphsu.clock2.ringtone.RingtoneActivity;
public class MainActivity extends AppCompatActivity {
/**
* The {@link android.support.v4.view.PagerAdapter} that will provide
* fragments for each of the sections. We use a
* {@link FragmentPagerAdapter} derivative, which will keep every
* loaded fragment in memory. If this becomes too memory intensive, it
* may be best to switch to a
* {@link android.support.v4.app.FragmentStatePagerAdapter}.
*/
private SectionsPagerAdapter mSectionsPagerAdapter;
/**
* The {@link ViewPager} that will host the section contents.
*/
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
scheduleAlarm();
Snackbar.make(view, "Alarm set for 1 minute from now", Snackbar.LENGTH_LONG)
.setAction("Dismiss", new View.OnClickListener() {
@Override
public void onClick(View v) {
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
PendingIntent pi = alarmIntent();
am.cancel(pi);
pi.cancel();
}
}).show();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* A placeholder fragment containing a simple view.
*/
public static class PlaceholderFragment extends Fragment {
/**
* The fragment argument representing the section number for this
* fragment.
*/
private static final String ARG_SECTION_NUMBER = "section_number";
public PlaceholderFragment() {
}
/**
* Returns a new instance of this fragment for the given section
* number.
*/
public static PlaceholderFragment newInstance(int sectionNumber) {
PlaceholderFragment fragment = new PlaceholderFragment();
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
TextView textView = (TextView) rootView.findViewById(R.id.section_label);
textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
return rootView;
}
}
/**
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
// Return a PlaceholderFragment (defined as a static inner class below).
return PlaceholderFragment.newInstance(position + 1);
}
@Override
public int getCount() {
// Show 3 total pages.
return 3;
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "SECTION 1";
case 1:
return "SECTION 2";
case 2:
return "SECTION 3";
}
return null;
}
}
private void scheduleAlarm() {
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
// If there is already an alarm for this Intent scheduled (with the equality of two
// intents being defined by filterEquals(Intent)), then it will be removed and replaced
// by this one. For most of our uses, the relevant criteria for equality will be the
// action, the data, and the class (component). Although not documented, the request code
// of a PendingIntent is also considered to determine equality of two intents.
// todo: get alarm's ring time
am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 60000, alarmIntent());
}
private PendingIntent alarmIntent() {
// TODO: Use appropriate subclass instead
Intent intent = new Intent(this, RingtoneActivity.class)
.setData(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM));
// TODO: Use unique request codes per alarm.
// If a PendingIntent with this request code already exists, then we are likely modifying
// an alarm, so we should cancel the existing intent.
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
}
}
@@ -0,0 +1,77 @@
package com.philliphsu.clock2.alarms;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.alarms.dummy.DummyContent.DummyItem;
import java.util.List;
/**
* {@link RecyclerView.Adapter} that can display a {@link DummyItem} and makes a call to the
* specified {@link AlarmsFragment.OnListFragmentInteractionListener}.
* TODO: Replace the implementation with code for your data type.
*/
public class AlarmsAdapter extends RecyclerView.Adapter<AlarmsAdapter.ViewHolder> {
private final List<DummyItem> mValues;
private final AlarmsFragment.OnListFragmentInteractionListener mListener;
public AlarmsAdapter(List<DummyItem> items, AlarmsFragment.OnListFragmentInteractionListener listener) {
mValues = items;
mListener = listener;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_alarms, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.mItem = mValues.get(position);
holder.mIdView.setText(mValues.get(position).id);
holder.mContentView.setText(mValues.get(position).content);
holder.mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (null != mListener) {
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
mListener.onListFragmentInteraction(holder.mItem);
}
}
});
}
@Override
public int getItemCount() {
return mValues.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
public final View mView;
public final TextView mIdView;
public final TextView mContentView;
public DummyItem mItem;
public ViewHolder(View view) {
super(view);
mView = view;
mIdView = (TextView) view.findViewById(R.id.id);
mContentView = (TextView) view.findViewById(R.id.content);
}
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}
@@ -0,0 +1,108 @@
package com.philliphsu.clock2.alarms;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.alarms.dummy.DummyContent;
import com.philliphsu.clock2.alarms.dummy.DummyContent.DummyItem;
/**
* A fragment representing a list of Items.
* <p/>
* Activities containing this fragment MUST implement the {@link OnListFragmentInteractionListener}
* interface.
*/
public class AlarmsFragment extends Fragment {
// TODO: Customize parameter argument names
private static final String ARG_COLUMN_COUNT = "column-count";
// TODO: Customize parameters
private int mColumnCount = 1;
private OnListFragmentInteractionListener mListener;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public AlarmsFragment() {
}
// TODO: Customize parameter initialization
@SuppressWarnings("unused")
public static AlarmsFragment newInstance(int columnCount) {
AlarmsFragment fragment = new AlarmsFragment();
Bundle args = new Bundle();
args.putInt(ARG_COLUMN_COUNT, columnCount);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_alarms_list, container, false);
// Set the adapter
if (view instanceof RecyclerView) {
Context context = view.getContext();
RecyclerView recyclerView = (RecyclerView) view;
if (mColumnCount <= 1) {
recyclerView.setLayoutManager(new LinearLayoutManager(context));
} else {
recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount));
}
recyclerView.setAdapter(new AlarmsAdapter(DummyContent.ITEMS, mListener));
}
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnListFragmentInteractionListener) {
mListener = (OnListFragmentInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnListFragmentInteractionListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* to the activity and potentially other fragments contained in that
* activity.
* <p/>
* See the Android Training lesson <a href=
* "http://developer.android.com/training/basics/fragments/communicating.html"
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnListFragmentInteractionListener {
// TODO: Update argument type and name
void onListFragmentInteraction(DummyItem item);
}
}
@@ -0,0 +1,72 @@
package com.philliphsu.clock2.alarms.dummy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Helper class for providing sample content for user interfaces created by
* Android template wizards.
* <p/>
* TODO: Replace all uses of this class before publishing your app.
*/
public class DummyContent {
/**
* An array of sample (dummy) items.
*/
public static final List<DummyItem> ITEMS = new ArrayList<DummyItem>();
/**
* A map of sample (dummy) items, by ID.
*/
public static final Map<String, DummyItem> ITEM_MAP = new HashMap<String, DummyItem>();
private static final int COUNT = 25;
static {
// Add some sample items.
for (int i = 1; i <= COUNT; i++) {
addItem(createDummyItem(i));
}
}
private static void addItem(DummyItem item) {
ITEMS.add(item);
ITEM_MAP.put(item.id, item);
}
private static DummyItem createDummyItem(int position) {
return new DummyItem(String.valueOf(position), "Item " + position, makeDetails(position));
}
private static String makeDetails(int position) {
StringBuilder builder = new StringBuilder();
builder.append("Details about Item: ").append(position);
for (int i = 0; i < position; i++) {
builder.append("\nMore details information here.");
}
return builder.toString();
}
/**
* A dummy item representing a piece of content.
*/
public static class DummyItem {
public final String id;
public final String content;
public final String details;
public DummyItem(String id, String content, String details) {
this.id = id;
this.content = content;
this.details = details;
}
@Override
public String toString() {
return content;
}
}
}
@@ -0,0 +1,175 @@
package com.philliphsu.clock2.ringtone;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;
import com.philliphsu.clock2.R;
import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
/**
* An example full-screen activity that shows and hides the system UI (i.e.
* status bar and navigation/system bar) with user interaction.
*
* TODO: Make this abstract and make appropriate subclasses for Alarms and Timers.
* TODO: Use this together with RingtoneService.
*/
public class RingtoneActivity extends AppCompatActivity {
/**
* Whether or not the system UI should be auto-hidden after
* {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds.
*/
private static final boolean AUTO_HIDE = true;
/**
* If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after
* user interaction before hiding the system UI.
*/
private static final int AUTO_HIDE_DELAY_MILLIS = 3000;
/**
* Some older devices needs a small delay between UI widget updates
* and a change of the status and navigation bar.
*/
private static final int UI_ANIMATION_DELAY = 300;
private final Handler mHideHandler = new Handler();
private View mContentView;
private final Runnable mHidePart2Runnable = new Runnable() {
@SuppressLint("InlinedApi")
@Override
public void run() {
// Delayed removal of status and navigation bar
// Note that some of these constants are new as of API 16 (Jelly Bean)
// and API 19 (KitKat). It is safe to use them, as they are inlined
// at compile-time and do nothing on earlier devices.
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
}
};
private View mControlsView;
private final Runnable mShowPart2Runnable = new Runnable() {
@Override
public void run() {
// Delayed display of UI elements
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.show();
}
mControlsView.setVisibility(View.VISIBLE);
}
};
private boolean mVisible;
private final Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
hide();
}
};
/**
* Touch listener to use for in-layout UI controls to delay hiding the
* system UI. This is to prevent the jarring behavior of controls going away
* while interacting with activity UI.
*/
private final View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (AUTO_HIDE) {
delayedHide(AUTO_HIDE_DELAY_MILLIS);
}
return false;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ringtone);
Uri ringtone = checkNotNull(getIntent().getData());
Intent intent = new Intent(this, RingtoneService.class).setData(ringtone);
startService(intent);
mVisible = true;
mControlsView = findViewById(R.id.fullscreen_content_controls);
mContentView = findViewById(R.id.fullscreen_content);
// Set up the user interaction to manually show or hide the system UI.
mContentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
toggle();
}
});
// Upon interacting with UI controls, delay any scheduled hide()
// operations to prevent the jarring behavior of controls going away
// while interacting with the UI.
findViewById(R.id.dummy_button).setOnTouchListener(mDelayHideTouchListener);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Trigger the initial hide() shortly after the activity has been
// created, to briefly hint to the user that UI controls
// are available.
delayedHide(100);
}
private void toggle() {
if (mVisible) {
hide();
} else {
show();
}
}
private void hide() {
// Hide UI first
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
mControlsView.setVisibility(View.GONE);
mVisible = false;
// Schedule a runnable to remove the status and navigation bar after a delay
mHideHandler.removeCallbacks(mShowPart2Runnable);
mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY);
}
@SuppressLint("InlinedApi")
private void show() {
// Show the system bar
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
mVisible = true;
// Schedule a runnable to display UI elements after a delay
mHideHandler.removeCallbacks(mHidePart2Runnable);
mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY);
}
/**
* Schedules a call to hide() in [delay] milliseconds, canceling any
* previously scheduled calls.
*/
private void delayedHide(int delayMillis) {
mHideHandler.removeCallbacks(mHideRunnable);
mHideHandler.postDelayed(mHideRunnable, delayMillis);
}
}
@@ -0,0 +1,119 @@
package com.philliphsu.clock2.ringtone;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.philliphsu.clock2.R;
import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
/**
* Runs in the foreground. While it can still be killed by the system, it stays alive significantly
* longer than if it does not run in the foreground. The longevity should be sufficient for practical
* use. In fact, if the app is used properly, longevity should be a non-issue; realistically, the lifetime
* of the RingtoneService will be tied to that of its RingtoneActivity because users are not likely to
* navigate away from the Activity without making an action. But if they do accidentally navigate away,
* they have plenty of time to make the desired action via the notification.
*/
public class RingtoneService extends Service {
private static final String TAG = "RingtoneService";
private AudioManager mAudioManager;
private Ringtone mRingtone;
private boolean mAutoSilenced = false;
private final Handler mSilenceHandler = new Handler();
private final Runnable mSilenceRunnable = new Runnable() {
@Override
public void run() {
mAutoSilenced = true;
stopSelf();
}
};
// TODO: Apply the setting for "Silence after" here by using an AlarmManager to
// schedule an alarm in the future to stop this service, and also update the foreground
// notification to say "alarm missed" in the case of Alarms or "timer expired" for Timers.
// If Alarms and Timers will have distinct settings for this, then consider doing this
// operation in the respective subclass of this service.
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (mAudioManager == null && mRingtone == null) {
Uri ringtone = checkNotNull(intent.getData());
// TODO: The below call requires a notification, and there is no way to provide one suitable
// for both Alarms and Timers. Consider making this class abstract, and have subclasses
// implement an abstract method that calls startForeground(). You would then call that
// method here instead.
Notification note = new NotificationCompat.Builder(this)
// Required contents
.setSmallIcon(R.mipmap.ic_launcher) // TODO: alarm icon
.setContentTitle("Foreground RingtoneService")
.setContentText("Ringtone is playing in the foreground.")
.build();
startForeground(R.id.ringtone_service_notification, note); // TOneverDO: Pass 0 as the first argument
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// Request audio focus first, so we don't play our ringtone on top of any
// other apps that currently have playback.
int result = mAudioManager.requestAudioFocus(
null, // Playback will likely be short, so don't worry about listening for focus changes
AudioManager.STREAM_ALARM,
// Request permanent focus, as ringing could last several minutes
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mRingtone = RingtoneManager.getRingtone(this, ringtone);
// Deprecated, but the alternative AudioAttributes requires API 21
mRingtone.setStreamType(AudioManager.STREAM_ALARM);
mRingtone.play();
scheduleAutoSilence();
}
}
// If killed while started, don't recreate
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy()");
mRingtone.stop();
mAudioManager.abandonAudioFocus(null); // no listener was set
mSilenceHandler.removeCallbacks(mSilenceRunnable);
if (mAutoSilenced) {
// Post notification that alarm was missed, or timer expired.
// TODO: You should probably do this in the appropriate subclass.
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification note = new NotificationCompat.Builder(this)
.setContentTitle("Missed alarm")
.setContentText("Regular alarm time here")
.setSmallIcon(R.mipmap.ic_launcher)
.build();
nm.notify("tag", 0, note);
}
stopForeground(true);
}
@Override
public IBinder onBind(Intent intent) {
return null; // Binding to this service is not supported
}
private void scheduleAutoSilence() {
// TODO: Read prefs
//SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
int minutes = 2; /*Integer.parseInt(pref.getString(
getString(R.string.key_silence_after),
"15"));*/
mSilenceHandler.postDelayed(mSilenceRunnable, minutes * 60000);
}
}
@@ -0,0 +1,14 @@
package com.philliphsu.clock2.util;
/**
* Created by Phillip Hsu on 5/28/2016.
*/
public final class Preconditions {
private Preconditions() {}
public static <T> T checkNotNull(T obj) {
if (null == obj)
throw new NullPointerException();
return obj;
}
}