Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 95559b29 authored by James Lemieux's avatar James Lemieux
Browse files

Fix remaining dangerous M permissions

android.permission.WRITE_SETTINGS was required to adjust the Setting
that stored the last selected alarm ringtone as the new default. The
value is now stored in SharedPreferences and no longer written to the
Setting. The permission has been removed.

android.permission.READ_EXTERNAL_STORAGE was required to play custom
ringtones located at /sdcard/Alarms via MediaPlayer. MediaPlayer is
no longer used to play those ringtones so the permission has been
removed. Ringtone, which is designed to play custom ringtones without
requiring the permission to read from external storage is now the
playback method. The caveat to this approach is: Ringtone does not
offer control over the volume at which the ringtone is played. Old
MediaPlayer code used to detect when we were in a phone call and
reduce the volume defensively to avoid playing a loud ringtone
directly into a user's ear. In practice, it appears that lower layers
of the audio stack are already handling this behavior and the app
need not request a lower volume itself.

Bug: 20273223
Change-Id: I328d168ff7677506aeae3fdf78c915f82e6cc1c7
parent ac260c00
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -27,10 +27,9 @@
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
    <!-- READ_PHONE_STATE is required to determine when a phone call exists prior to M -->
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application android:label="@string/app_label"
                 android:name=".DeskClockApplication"
+34 −23
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.Loader;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
@@ -41,6 +42,7 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.support.v4.view.ViewCompat;
import android.transition.AutoTransition;
import android.transition.Fade;
@@ -104,6 +106,7 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements

    private static final int REQUEST_CODE_RINGTONE = 1;
    private static final long INVALID_ID = -1;
    private static final String PREF_KEY_DEFAULT_ALARM_RINGTONE_URI = "default_alarm_ringtone_uri";

    // Use transitions only in API 21+
    private static final boolean USE_TRANSITION_FRAMEWORK =
@@ -153,8 +156,7 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements
        if (mSelectedAlarm == null) {
            // If mSelectedAlarm is null then we're creating a new alarm.
            Alarm a = new Alarm();
            a.alert = RingtoneManager.getActualDefaultRingtoneUri(getActivity(),
                    RingtoneManager.TYPE_ALARM);
            a.alert = getDefaultRingtoneUri();
            if (a.alert == null) {
                a.alert = Uri.parse("content://settings/system/alarm_alert");
            }
@@ -288,14 +290,6 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements
        return v;
    }

    private void setUndoBarRightMargin(int margin) {
        FrameLayout.LayoutParams params =
                (FrameLayout.LayoutParams) mUndoBar.getLayoutParams();
        ((FrameLayout.LayoutParams) mUndoBar.getLayoutParams())
            .setMargins(params.leftMargin, params.topMargin, margin, params.bottomMargin);
        mUndoBar.requestLayout();
    }

    @Override
    public void onResume() {
        super.onResume();
@@ -476,13 +470,35 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements
        mSelectedAlarm.alert = uri;

        // Save the last selected ringtone as the default for new alarms
        if (!Alarm.NO_RINGTONE_URI.equals(uri)) {
            RingtoneManager.setActualDefaultRingtoneUri(
                    getActivity(), RingtoneManager.TYPE_ALARM, uri);
        }
        setDefaultRingtoneUri(uri);

        asyncUpdateAlarm(mSelectedAlarm, false);
    }

    private Uri getDefaultRingtoneUri() {
        final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
        final String ringtoneUriString = sp.getString(PREF_KEY_DEFAULT_ALARM_RINGTONE_URI, null);

        final Uri ringtoneUri;
        if (ringtoneUriString != null) {
            ringtoneUri = Uri.parse(ringtoneUriString);
        } else {
            ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(getActivity(),
                    RingtoneManager.TYPE_ALARM);
        }

        return ringtoneUri;
    }

    private void setDefaultRingtoneUri(Uri uri) {
        final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
        if (uri == null) {
            sp.edit().remove(PREF_KEY_DEFAULT_ALARM_RINGTONE_URI).apply();
        } else {
            sp.edit().putString(PREF_KEY_DEFAULT_ALARM_RINGTONE_URI, uri.toString()).apply();
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK) {
@@ -504,8 +520,8 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements

        private long mExpandedId;
        private ItemHolder mExpandedItemHolder;
        private final HashSet<Long> mRepeatChecked = new HashSet<Long>();
        private final HashSet<Long> mSelectedAlarms = new HashSet<Long>();
        private final HashSet<Long> mRepeatChecked = new HashSet<>();
        private final HashSet<Long> mSelectedAlarms = new HashSet<>();
        private Bundle mPreviousDaysOfWeekMap = new Bundle();

        private final boolean mHasVibrator;
@@ -597,10 +613,6 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements
            setDayOrder();
        }

        public void removeSelectedId(int id) {
            mSelectedAlarms.remove(id);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (!getCursor().moveToPosition(position)) {
@@ -988,8 +1000,7 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements
            itemHolder.vibrate.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    final boolean checked = ((CheckBox) v).isChecked();
                    alarm.vibrate = checked;
                    alarm.vibrate = ((CheckBox) v).isChecked();
                    asyncUpdateAlarm(alarm, false);
                }
            });
@@ -1060,7 +1071,7 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements
                Ringtone ringTone = RingtoneManager.getRingtone(mContext, uri);
                if (ringTone == null) {
                    LogUtils.i("No ringtone for uri %s", uri.toString());
                    return title;
                    return null;
                }
                title = ringTone.getTitle(mContext);
                if (title != null) {
+163 −0
Original line number Diff line number Diff line
package com.android.deskclock;

import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;

/**
 * Plays the alarm ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
 * used from the main thread. Consequently, problems controlling the ringtone do not cause ANRs in
 * the main thread of the application.
 */
public class AsyncRingtonePlayer {

    private static final String TAG = "AsyncRingtonePlayer";

    // Message codes used with the ringtone thread.
    private static final int EVENT_PLAY = 1;
    private static final int EVENT_STOP = 2;
    private static final String RINGTONE_URI_KEY = "RINGTONE_URI_KEY";

    /** Handler running on the ringtone thread. */
    private Handler mHandler;

    /** The audio focus manager. Only used by the ringtone thread. */
    private AudioManager mAudioManager;

    /** The current ringtone. Only used by the ringtone thread. */
    private Ringtone mRingtone;

    /** The context. */
    private final Context mContext;

    public AsyncRingtonePlayer(Context context) {
        mContext = context;
    }

    /** Plays the ringtone. */
    public void play(Uri ringtoneUri) {
        LogUtils.d(TAG, "Posting play.");
        postMessage(EVENT_PLAY, ringtoneUri);
    }

    /** Stops playing the ringtone. */
    public void stop() {
        LogUtils.d(TAG, "Posting stop.");
        postMessage(EVENT_STOP, null);
    }

    /**
     * Posts a message to the ringtone-thread handler.
     *
     * @param messageCode The message to post.
     */
    private void postMessage(int messageCode, Uri ringtoneUri) {
        synchronized (this) {
            if (mHandler == null) {
                mHandler = getNewHandler();
            }

            final Message message = mHandler.obtainMessage(messageCode);
            if (ringtoneUri != null) {
                final Bundle bundle = new Bundle();
                bundle.putParcelable(RINGTONE_URI_KEY, ringtoneUri);
                message.setData(bundle);
            }
            message.sendToTarget();
        }
    }

    /**
     * Creates a new ringtone Handler running in its own thread.
     */
    private Handler getNewHandler() {
        final HandlerThread thread = new HandlerThread("ringtone-player");
        thread.start();

        return new Handler(thread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case EVENT_PLAY:
                        final Uri ringtoneUri = msg.getData().getParcelable(RINGTONE_URI_KEY);
                        handlePlay(ringtoneUri);
                        break;
                    case EVENT_STOP:
                        handleStop();
                        break;
                }
            }
        };
    }

    /**
     * Starts the actual playback of the ringtone. Executes on ringtone-thread.
     */
    private void handlePlay(Uri ringtoneUri) {
        if (Looper.getMainLooper() == Looper.myLooper()) {
            LogUtils.e(TAG, "Must not be on the main thread!", new IllegalStateException());
        }

        LogUtils.i(TAG, "Play ringtone.");

        if (mAudioManager == null) {
            mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        }

        // attempt to fetch the specified ringtone
        mRingtone = RingtoneManager.getRingtone(mContext, ringtoneUri);

        if (mRingtone == null) {
            // fall back to the default ringtone
            final Uri alarmRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
            mRingtone = RingtoneManager.getRingtone(mContext, alarmRingtoneUri);
        }

        // if we don't have a ringtone at this point there isn't much recourse
        if (mRingtone == null) {
            LogUtils.i(TAG, "Unable to locate alarm ringtone.");
            return;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mRingtone.setAudioAttributes(new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_ALARM)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .build());
        }

        mAudioManager.requestAudioFocus(null, AudioManager.STREAM_ALARM,
                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
        mRingtone.play();
    }

    /**
     * Stops the playback of the ringtone. Executes on the ringtone-thread.
     */
    private void handleStop() {
        if (Looper.getMainLooper() == Looper.myLooper()) {
            LogUtils.e(TAG, "Must not be on the main thread!", new IllegalStateException());
        }

        LogUtils.i(TAG, "Stop ringtone.");

        if (mRingtone != null && mRingtone.isPlaying()) {
            LogUtils.d(TAG, "Ringtone.stop() invoked.");
            mRingtone.stop();
        }

        if (mAudioManager != null) {
            mAudioManager.abandonAudioFocus(null);
        }
    }
}
+18 −95
Original line number Diff line number Diff line
@@ -17,112 +17,50 @@
package com.android.deskclock.alarms;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnErrorListener;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Vibrator;

import com.android.deskclock.AsyncRingtonePlayer;
import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
import com.android.deskclock.provider.AlarmInstance;

import java.io.IOException;

/**
 * Manages playing ringtone and vibrating the device.
 */
public class AlarmKlaxon {
    private static final long[] sVibratePattern = new long[] { 500, 500 };

    // Volume suggested by media team for in-call alarms.
    private static final float IN_CALL_VOLUME = 0.125f;
public final class AlarmKlaxon {
    private static final long[] sVibratePattern = {500, 500};

    private static boolean sStarted = false;
    private static MediaPlayer sMediaPlayer = null;
    private static AsyncRingtonePlayer sAsyncRingtonePlayer;

    private AlarmKlaxon() {}

    public static void stop(Context context) {
        LogUtils.v("AlarmKlaxon.stop()");

        if (sStarted) {
            sStarted = false;
            // Stop audio playing
            if (sMediaPlayer != null) {
                sMediaPlayer.stop();
                AudioManager audioManager = (AudioManager)
                        context.getSystemService(Context.AUDIO_SERVICE);
                audioManager.abandonAudioFocus(null);
                sMediaPlayer.release();
                sMediaPlayer = null;
            }

            getAsyncRingtonePlayer(context).stop();
            ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).cancel();
        }
    }

    public static void start(final Context context, AlarmInstance instance,
            boolean inTelephoneCall) {
    public static void start(Context context, AlarmInstance instance) {
        LogUtils.v("AlarmKlaxon.start()");
        // Make sure we are stop before starting
        // Make sure we are stopped before starting
        stop(context);

        if (!AlarmInstance.NO_RINGTONE_URI.equals(instance.mRingtone)) {
            Uri alarmNoise = instance.mRingtone;
            // Fall back on the default alarm if the database does not have an
            // alarm stored.
            if (alarmNoise == null) {
                alarmNoise = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
                LogUtils.v("Using default alarm: " + alarmNoise.toString());
            }

            // TODO: Reuse mMediaPlayer instead of creating a new one and/or use RingtoneManager.
            sMediaPlayer = new MediaPlayer();
            sMediaPlayer.setOnErrorListener(new OnErrorListener() {
                @Override
                public boolean onError(MediaPlayer mp, int what, int extra) {
                    LogUtils.e("Error occurred while playing audio. Stopping AlarmKlaxon.");
                    AlarmKlaxon.stop(context);
                    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 (inTelephoneCall) {
                    LogUtils.v("Using the in-call alarm");
                    sMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME);
                    setDataSourceFromResource(context, sMediaPlayer, R.raw.in_call_alarm);
                } else {
                    sMediaPlayer.setDataSource(context, alarmNoise);
                }
                startAlarm(context, sMediaPlayer);
            } catch (Exception ex) {
                LogUtils.e("Use the fallback ringtone, original was " + alarmNoise, ex);
                // The alarmNoise may be on the sd card which could be busy right
                // now. Use the fallback ringtone.
                try {
                    // Must reset the media player to clear the error state.
                    sMediaPlayer.reset();
                    setDataSourceFromResource(context, sMediaPlayer, R.raw.fallbackring);
                    startAlarm(context, sMediaPlayer);
                } catch (Exception ex2) {
                    // At this point we just don't play anything.
                    LogUtils.e("Failed to play fallback ringtone", ex2);
                }
            }
            getAsyncRingtonePlayer(context).play(instance.mRingtone);
        }

        if (instance.mVibrate) {
            Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
            final Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                vibrator.vibrate(sVibratePattern, 0, new AudioAttributes.Builder()
                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                        .setUsage(AudioAttributes.USAGE_ALARM)
                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                        .build());
            } else {
                vibrator.vibrate(sVibratePattern, 0);
@@ -132,26 +70,11 @@ public class AlarmKlaxon {
        sStarted = true;
    }

    // Do the common stuff when starting the alarm.
    private static void startAlarm(Context context, MediaPlayer player) throws IOException {
        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        // do not play alarms if stream volume is 0 (typically because ringer mode is silent).
        if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) {
            player.setAudioStreamType(AudioManager.STREAM_ALARM);
            player.setLooping(true);
            player.prepare();
            audioManager.requestAudioFocus(null,
                    AudioManager.STREAM_ALARM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
            player.start();
        }
    private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
        if (sAsyncRingtonePlayer == null) {
            sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
        }

    private static void setDataSourceFromResource(Context context, MediaPlayer player, int res)
            throws IOException {
        AssetFileDescriptor afd = context.getResources().openRawResourceFd(res);
        if (afd != null) {
            player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            afd.close();
        }
        return sAsyncRingtonePlayer;
    }
}
 No newline at end of file
+1 −2
Original line number Diff line number Diff line
@@ -150,8 +150,7 @@ public class AlarmService extends Service {
        AlarmNotifications.showAlarmNotification(this, mCurrentAlarm);
        mInitialCallState = mTelephonyManager.getCallState();
        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
        final boolean inCall = mInitialCallState != TelephonyManager.CALL_STATE_IDLE;
        AlarmKlaxon.start(this, mCurrentAlarm, inCall);
        AlarmKlaxon.start(this, mCurrentAlarm);
        sendBroadcast(new Intent(ALARM_ALERT_ACTION));
    }