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
@@ -1,4 +1,4 @@
package com.philliphsu.clock;
package com.philliphsu.clock2;
import com.google.auto.value.AutoValue;
@@ -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);
}
}
@@ -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;
}
}