From 37e7d3dd3bdd17db4c1f684aeb3a6d14c12387d9 Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Sat, 28 May 2016 18:44:51 -0700 Subject: [PATCH] Setup RingtoneService --- app/build.gradle | 2 + .../{clock => clock2}/ApplicationTest.java | 2 +- app/src/main/AndroidManifest.xml | 16 +- .../philliphsu/{clock => clock2}/Alarm.java | 2 +- .../{clock => clock2}/MainActivity.java | 52 +++++- .../clock2/alarms/AlarmsAdapter.java | 77 ++++++++ .../clock2/alarms/AlarmsFragment.java | 108 +++++++++++ .../clock2/alarms/dummy/DummyContent.java | 72 +++++++ .../clock2/ringtone/RingtoneActivity.java | 175 ++++++++++++++++++ .../clock2/ringtone/RingtoneService.java | 119 ++++++++++++ .../philliphsu/clock2/util/Preconditions.java | 14 ++ app/src/main/res/layout/activity_main.xml | 40 ++-- app/src/main/res/layout/activity_ringtone.xml | 50 +++++ app/src/main/res/layout/fragment_alarms.xml | 20 ++ .../main/res/layout/fragment_alarms_list.xml | 14 ++ app/src/main/res/layout/fragment_main.xml | 2 +- app/src/main/res/menu/menu_main.xml | 2 +- app/src/main/res/values/attrs.xml | 12 ++ app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/ids.xml | 4 + app/src/main/res/values/strings.xml | 6 +- app/src/main/res/values/styles.xml | 12 ++ .../{clock => clock2}/AlarmTest.java | 2 +- .../{clock => clock2}/ExampleUnitTest.java | 2 +- 25 files changed, 763 insertions(+), 45 deletions(-) rename app/src/androidTest/java/com/philliphsu/{clock => clock2}/ApplicationTest.java (90%) rename app/src/main/java/com/philliphsu/{clock => clock2}/Alarm.java (99%) rename app/src/main/java/com/philliphsu/{clock => clock2}/MainActivity.java (71%) create mode 100644 app/src/main/java/com/philliphsu/clock2/alarms/AlarmsAdapter.java create mode 100644 app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java create mode 100644 app/src/main/java/com/philliphsu/clock2/alarms/dummy/DummyContent.java create mode 100644 app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java create mode 100644 app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java create mode 100644 app/src/main/java/com/philliphsu/clock2/util/Preconditions.java create mode 100644 app/src/main/res/layout/activity_ringtone.xml create mode 100644 app/src/main/res/layout/fragment_alarms.xml create mode 100644 app/src/main/res/layout/fragment_alarms_list.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/ids.xml rename app/src/test/java/com/philliphsu/{clock => clock2}/AlarmTest.java (99%) rename app/src/test/java/com/philliphsu/{clock => clock2}/ExampleUnitTest.java (89%) diff --git a/app/build.gradle b/app/build.gradle index f948d5f..431ff8d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,4 +30,6 @@ dependencies { apt 'com.google.auto.value:auto-value:1.2' compile 'com.android.support:appcompat-v7:23.2.1' compile 'com.android.support:design:23.2.1' + compile 'com.android.support:support-v4:23.2.1' + compile 'com.android.support:recyclerview-v7:23.2.1' } diff --git a/app/src/androidTest/java/com/philliphsu/clock/ApplicationTest.java b/app/src/androidTest/java/com/philliphsu/clock2/ApplicationTest.java similarity index 90% rename from app/src/androidTest/java/com/philliphsu/clock/ApplicationTest.java rename to app/src/androidTest/java/com/philliphsu/clock2/ApplicationTest.java index 8bcdb38..1a61571 100644 --- a/app/src/androidTest/java/com/philliphsu/clock/ApplicationTest.java +++ b/app/src/androidTest/java/com/philliphsu/clock2/ApplicationTest.java @@ -1,4 +1,4 @@ -package com.philliphsu.clock; +package com.philliphsu.clock2; import android.app.Application; import android.test.ApplicationTestCase; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0b00313..caae57a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - @@ -18,6 +18,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/philliphsu/clock/Alarm.java b/app/src/main/java/com/philliphsu/clock2/Alarm.java similarity index 99% rename from app/src/main/java/com/philliphsu/clock/Alarm.java rename to app/src/main/java/com/philliphsu/clock2/Alarm.java index 39b10f1..051902e 100644 --- a/app/src/main/java/com/philliphsu/clock/Alarm.java +++ b/app/src/main/java/com/philliphsu/clock2/Alarm.java @@ -1,4 +1,4 @@ -package com.philliphsu.clock; +package com.philliphsu.clock2; import com.google.auto.value.AutoValue; diff --git a/app/src/main/java/com/philliphsu/clock/MainActivity.java b/app/src/main/java/com/philliphsu/clock2/MainActivity.java similarity index 71% rename from app/src/main/java/com/philliphsu/clock/MainActivity.java rename to app/src/main/java/com/philliphsu/clock2/MainActivity.java index 4a26458..bb01ce5 100644 --- a/app/src/main/java/com/philliphsu/clock/MainActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/MainActivity.java @@ -1,24 +1,28 @@ -package com.philliphsu.clock; +package com.philliphsu.clock2; -import android.support.design.widget.TabLayout; +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.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; - +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.os.Bundle; +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 { /** @@ -58,8 +62,17 @@ public class MainActivity extends AppCompatActivity { fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) - .setAction("Action", null).show(); + 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(); } }); @@ -159,4 +172,25 @@ public class MainActivity extends AppCompatActivity { 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); + } } diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsAdapter.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsAdapter.java new file mode 100644 index 0000000..52bf88b --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsAdapter.java @@ -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 { + + private final List mValues; + private final AlarmsFragment.OnListFragmentInteractionListener mListener; + + public AlarmsAdapter(List 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() + "'"; + } + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java new file mode 100644 index 0000000..3252879 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -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. + *

+ * 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. + *

+ * See the Android Training lesson Communicating with Other Fragments for more information. + */ + public interface OnListFragmentInteractionListener { + // TODO: Update argument type and name + void onListFragmentInteraction(DummyItem item); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/dummy/DummyContent.java b/app/src/main/java/com/philliphsu/clock2/alarms/dummy/DummyContent.java new file mode 100644 index 0000000..9a2dc3c --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/alarms/dummy/DummyContent.java @@ -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. + *

+ * TODO: Replace all uses of this class before publishing your app. + */ +public class DummyContent { + + /** + * An array of sample (dummy) items. + */ + public static final List ITEMS = new ArrayList(); + + /** + * A map of sample (dummy) items, by ID. + */ + public static final Map ITEM_MAP = new HashMap(); + + 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; + } + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java new file mode 100644 index 0000000..7607dd5 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java @@ -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); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java new file mode 100644 index 0000000..2424438 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java @@ -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); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/util/Preconditions.java b/app/src/main/java/com/philliphsu/clock2/util/Preconditions.java new file mode 100644 index 0000000..7dedac8 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/util/Preconditions.java @@ -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 checkNotNull(T obj) { + if (null == obj) + throw new NullPointerException(); + return obj; + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ce3a239..0b90552 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,42 +1,28 @@ - + - - - - - + android:layout_height="?attr/actionBarSize" + android:background="?attr/colorPrimary" + app:popupTheme="@style/AppTheme.PopupOverlay"> - + + android:layout_height="match_parent"/> - + diff --git a/app/src/main/res/layout/activity_ringtone.xml b/app/src/main/res/layout/activity_ringtone.xml new file mode 100644 index 0000000..f63dac5 --- /dev/null +++ b/app/src/main/res/layout/activity_ringtone.xml @@ -0,0 +1,50 @@ + + + + + + + + + + +