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

Commit 77f18a56 authored by Matt Garnes's avatar Matt Garnes Committed by Gerrit Code Review
Browse files

Allow alarms to be modified by 3rd parties.

- Export the ClockProvider.
- Use the ClockContract in the CM Platform SDK for external reference.
- Use these CM SDK defined permissions:
  - "cyanogenmod.permission.WRITE_ALARMS" - This
    system|signature only permission gives full write access to the
    ClockProvider.
  - "cyanogenmod.permission.READ_ALARMS" - Read only access to
    the ClockProvider.
  - "cyanogenmod.permission.MANAGE_ALARMS" - Access to perform
    a subset of all alarm changes such as turning on or off alarms.
- Protect the provider with the above permissions for read/write.
- Move the logic for updating an Alarm object directly into the Alarm
  class.
- Add a new IntentService to allow turning on and off alarms when
  started by an application with the MANAGE_ALARMS permission.

Depends on: http://review.cyanogenmod.org/#/c/125486

Derived from: http://review.cyanogenmod.org/#/c/103324

Change-Id: I399ce9f1073c6fdcb1711a11095f7c3a4bce2c6a
parent 91ac9cbe
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -51,7 +51,9 @@

        <provider android:name=".provider.ClockProvider"
                android:authorities="com.android.deskclock"
                android:exported="false" />
                android:readPermission="cyanogenmod.permission.READ_ALARMS"
                android:writePermission="cyanogenmod.permission.WRITE_ALARMS"
                android:exported="true" />

        <activity android:name="DeskClock"
                android:label="@string/app_label"
@@ -364,5 +366,14 @@
                <action android:name="share_stopwatch" />
            </intent-filter>
        </service>

        <service android:name="com.android.deskclock.AlarmModificationIntentService"
                android:exported="true"
                android:permission="cyanogenmod.permission.MANAGE_ALARMS"
                android:description="@string/alarm_modification_service_desc">
            <intent-filter>
                <action android:name="cyanogenmod.alarmclock.SET_ALARM_ENABLED" />
            </intent-filter>
        </service>
    </application>
</manifest>
+1 −0
Original line number Diff line number Diff line
@@ -56,4 +56,5 @@
    <!-- No profile selected label -->
    <string name="profile_no_selected">No profile selected</string>

    <string name="alarm_modification_service_desc">A service to allow manipulation of alarms by third parties.</string>
</resources>
+2 −23
Original line number Diff line number Diff line
@@ -1532,15 +1532,6 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements
        }
    }

    private static AlarmInstance setupAlarmInstance(Context context, Alarm alarm) {
        ContentResolver cr = context.getContentResolver();
        AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance());
        newInstance = AlarmInstance.addInstance(cr, newInstance);
        // Register instance to state manager
        AlarmStateManager.registerInstance(context, newInstance, true);
        return newInstance;
    }

    private void asyncDeleteAlarm(final Alarm alarm) {
        final Context context = AlarmClockFragment.this.getActivity().getApplicationContext();
        final AsyncTask<Void, Void, Void> deleteTask = new AsyncTask<Void, Void, Void>() {
@@ -1577,7 +1568,7 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements

                    // Create and add instance to db
                    if (newAlarm.enabled) {
                        return setupAlarmInstance(context, newAlarm);
                        return Alarm.setupAlarmInstance(context, newAlarm);
                    }
                }
                return null;
@@ -1599,19 +1590,7 @@ public abstract class AlarmClockFragment extends DeskClockFragment implements
                new AsyncTask<Void, Void, AlarmInstance>() {
            @Override
            protected AlarmInstance doInBackground(Void ... parameters) {
                Events.sendAlarmEvent(R.string.action_update, R.string.label_deskclock);
                ContentResolver cr = context.getContentResolver();

                // Dismiss all old instances
                AlarmStateManager.deleteAllInstances(context, alarm.id);

                // Update alarm
                Alarm.updateAlarm(cr, alarm);
                if (alarm.enabled) {
                    return setupAlarmInstance(context, alarm);
                }

                return null;
                    return (alarm == null) ? null : alarm.processUpdate(context);
            }

            @Override
+56 −0
Original line number Diff line number Diff line
/**
 * Copyright (c) 2015, The CyanogenMod Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.deskclock;

import android.app.IntentService;
import android.content.Intent;

import android.util.Log;
import com.android.deskclock.provider.Alarm;
import cyanogenmod.alarmclock.CyanogenModAlarmClock;

/**
 * Allows third parties to modify alarms.
 */
public class AlarmModificationIntentService extends IntentService {
    private static final String TAG = "AlarmModificationIntentService";

    public AlarmModificationIntentService() {
        super(TAG);
    }

    public AlarmModificationIntentService(String name) {
        super(name);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        String action = intent.getAction();
        if (CyanogenModAlarmClock.ACTION_SET_ALARM_ENABLED.equals(action)) {
            long alarmId = intent.getLongExtra(CyanogenModAlarmClock.EXTRA_ALARM_ID, -1L);
            boolean enabled = intent.getBooleanExtra(CyanogenModAlarmClock.EXTRA_ENABLED, true);
            if (alarmId != -1L) {
                boolean success = Alarm.setAlarmEnabled(this, alarmId, enabled);
                if (!success) {
                    Log.w(TAG, "Failed to set alarm enabled status - alarmId: " + alarmId
                            + ", enabled: " + enabled);
                }
            } else {
                Log.w(TAG, "Unable to set alarm enabled status, invalid ID");
            }
        }
    }
}
+82 −0
Original line number Diff line number Diff line
@@ -30,7 +30,11 @@ import android.os.ParcelUuid;
import android.os.Parcelable;

import com.android.deskclock.R;
import com.android.deskclock.alarms.AlarmStateManager;
import com.android.deskclock.events.Events;

import cyanogenmod.app.ProfileManager;
import cyanogenmod.alarmclock.ClockContract;

import java.util.Calendar;
import java.util.LinkedList;
@@ -222,6 +226,60 @@ public final class Alarm implements Parcelable, ClockContract.AlarmsColumns {
        return deletedRows == 1;
    }

    /**
     * Set an existing alarm's enabled status, accounting for all required
     * follow up actions after this occurs. These include:
     *  - Delete all existing instances of this alarm
     *  - Update the ringtone URI to be accessible if the alarm is enabled.
     *  - Update the Alarms table to set the enabled flag.
     *  - If enabling the alarm, schedule a new instance for it.
     * @param context A Context to retrieve a ContentResolver.
     * @param alarmId The ID of the alarm to change the enabled state of.
     * @param enabled if true, set the alarm to enabled. Otherwise, disabled the alarm.
     * @return true if the alarm enabled state change was successful.
     */
    public static boolean setAlarmEnabled(Context context, long alarmId, boolean enabled) {
        ContentResolver contentResolver = context.getContentResolver();
        Alarm alarm = getAlarm(contentResolver, alarmId);
        // If this alarm does not exist, we can't update it's enabled status.
        if (alarm == null || alarm.enabled == enabled) {
            return false;
        } else if (alarm.enabled == enabled) {
            // While we didn't "successfully" update anything, the current state
            // is what the caller requested, so return true.
            return true;
        }

        // Set the new enabled state.
        alarm.enabled = enabled;
        // Persist the change and schedule the next instance.
        AlarmInstance nextInstance = alarm.processUpdate(context);

        if (alarm.enabled) {
            // If the alarm's new state is enabled, processUpdate must
            // result in a new instance being created.
            return nextInstance != null;
        } else {
            // processUpdate handled disabling the alarm, return true.
            return true;
        }
    }

    /**
     * Schedule the next instance of this alarm.
     * @param context A Context to retrieve a ContentResolver.
     * @param alarm The alarm to set enabled/disabled.
     * @return The new AlarmInstance that was created.
     */
    public static AlarmInstance setupAlarmInstance(Context context, Alarm alarm) {
        ContentResolver cr = context.getContentResolver();
        AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance());
        newInstance = AlarmInstance.addInstance(cr, newInstance);
        // Register instance to state manager
        AlarmStateManager.registerInstance(context, newInstance, true);
        return newInstance;
    }

    public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() {
        public Alarm createFromParcel(Parcel p) {
            return new Alarm(p);
@@ -391,6 +449,30 @@ public final class Alarm implements Parcelable, ClockContract.AlarmsColumns {
        return nextInstanceTime;
    }

    /**
     * Fully handle the logic to persist changes that have been made to this alarm.
     * Deletes all instances, re-grants any ringtone URI permissions, updates
     * the alarm in the DB, and recreates the next instance of this alarm.
     * @param context A Context to retrieve a ContentResolver.
     * @return The instance that was created, if the alarm is enabled and instance
     * creation was successful. Returns null otherwise.
     */
    public AlarmInstance processUpdate(Context context) {
        Events.sendAlarmEvent(R.string.action_update, R.string.label_deskclock);
        ContentResolver cr = context.getContentResolver();

        // Dismiss all old instances
        AlarmStateManager.deleteAllInstances(context, id);

        // Update alarm
        Alarm.updateAlarm(cr, this);
        if (enabled) {
            return setupAlarmInstance(context, this);
        }

        return null;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Alarm)) return false;
Loading