1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.deskclock;
18
19import android.app.AlarmManager;
20import android.app.PendingIntent;
21import android.app.backup.BackupAgent;
22import android.app.backup.BackupDataInput;
23import android.app.backup.BackupDataOutput;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.Intent;
27import android.os.ParcelFileDescriptor;
28import android.os.SystemClock;
29import android.support.annotation.NonNull;
30
31import com.android.deskclock.alarms.AlarmStateManager;
32import com.android.deskclock.data.DataModel;
33import com.android.deskclock.provider.Alarm;
34import com.android.deskclock.provider.AlarmInstance;
35
36import java.io.File;
37import java.io.IOException;
38import java.util.Calendar;
39import java.util.List;
40
41public class DeskClockBackupAgent extends BackupAgent {
42
43    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DeskClockBackupAgent");
44
45    public static final String ACTION_COMPLETE_RESTORE =
46            "com.android.deskclock.action.COMPLETE_RESTORE";
47
48    @Override
49    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
50            ParcelFileDescriptor newState) throws IOException { }
51
52    @Override
53    public void onRestore(BackupDataInput data, int appVersionCode,
54            ParcelFileDescriptor newState) throws IOException { }
55
56    @Override
57    public void onRestoreFile(@NonNull ParcelFileDescriptor data, long size, File destination,
58            int type, long mode, long mtime) throws IOException {
59        // The preference file on the backup device may not be the same on the restore device.
60        // Massage the file name here before writing it.
61        if (destination.getName().endsWith("_preferences.xml")) {
62            final String prefFileName = getPackageName() + "_preferences.xml";
63            destination = new File(destination.getParentFile(), prefFileName);
64        }
65
66        super.onRestoreFile(data, size, destination, type, mode, mtime);
67    }
68
69    /**
70     * When this method is called during backup/restore, the application is executing in a
71     * "minimalist" state. Because of this, the application's ContentResolver cannot be used.
72     * Consequently, the work of scheduling alarms on the restore device cannot be done here.
73     * Instead, a future callback to DeskClock is used as a signal to reschedule the alarms. The
74     * future callback may take the form of ACTION_BOOT_COMPLETED if the device is not yet fully
75     * booted (i.e. the restore occurred as part of the setup wizard). If the device is booted, an
76     * ACTION_COMPLETE_RESTORE broadcast is scheduled 10 seconds in the future to give
77     * backup/restore enough time to kill the Clock process. Both of these future callbacks result
78     * in the execution of {@link #processRestoredData(Context)}.
79     */
80    @Override
81    public void onRestoreFinished() {
82        if (Utils.isNOrLater()) {
83            // TODO: migrate restored database and preferences over into
84            // the device-encrypted storage area
85        }
86
87        // Indicate a data restore has been completed.
88        DataModel.getDataModel().setRestoreBackupFinished(true);
89
90        // Create an Intent to send into DeskClock indicating restore is complete.
91        final PendingIntent restoreIntent = PendingIntent.getBroadcast(this, 0,
92                new Intent(ACTION_COMPLETE_RESTORE).setClass(this, AlarmInitReceiver.class),
93                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT);
94
95        // Deliver the Intent 10 seconds from now.
96        final long triggerAtMillis = SystemClock.elapsedRealtime() + 10000;
97
98        // Schedule the Intent delivery in AlarmManager.
99        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
100        alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, restoreIntent);
101
102        LOGGER.i("Waiting for %s to complete the data restore", ACTION_COMPLETE_RESTORE);
103    }
104
105    /**
106     * @param context a context to access resources and services
107     * @return {@code true} if restore data was processed; {@code false} otherwise.
108     */
109    public static boolean processRestoredData(Context context) {
110        // If data was not recently restored, there is nothing to do.
111        if (!DataModel.getDataModel().isRestoreBackupFinished()) {
112            return false;
113        }
114
115        LOGGER.i("processRestoredData() started");
116
117        // Now that alarms have been restored, schedule new instances in AlarmManager.
118        final ContentResolver contentResolver = context.getContentResolver();
119        final List<Alarm> alarms = Alarm.getAlarms(contentResolver, null);
120
121        final Calendar now = Calendar.getInstance();
122        for (Alarm alarm : alarms) {
123            // Remove any instances that may currently exist for the alarm;
124            // these aren't relevant on the restore device and we'll recreate them below.
125            AlarmStateManager.deleteAllInstances(context, alarm.id);
126
127            if (alarm.enabled) {
128                // Create the next alarm instance to schedule.
129                AlarmInstance alarmInstance = alarm.createInstanceAfter(now);
130
131                // Add the next alarm instance to the database.
132                alarmInstance = AlarmInstance.addInstance(contentResolver, alarmInstance);
133
134                // Schedule the next alarm instance in AlarmManager.
135                AlarmStateManager.registerInstance(context, alarmInstance, true);
136                LOGGER.i("DeskClockBackupAgent scheduled alarm instance: %s", alarmInstance);
137            }
138        }
139
140        // Remove the preference to avoid executing this logic multiple times.
141        DataModel.getDataModel().setRestoreBackupFinished(false);
142
143        LOGGER.i("processRestoredData() completed");
144        return true;
145    }
146}
147