summaryrefslogtreecommitdiff
path: root/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
blob: 30f24399fdba2b5cc283e8479b98310178ca2e1a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package com.android.server.deviceconfig;

import static com.android.server.deviceconfig.Flags.enableSimPinReplay;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AlarmManager;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.PowerManager;
import android.os.RecoverySystem;
import android.os.SystemClock;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.concurrent.TimeUnit;

/**
 * Reboot scheduler for applying aconfig flags.
 *
 * <p>If device is password protected, uses <a
 * href="https://source.android.com/docs/core/ota/resume-on-reboot">Resume on Reboot</a> to reboot
 * the device, otherwise proceeds with regular reboot.
 *
 * @hide
 */
final class UnattendedRebootManager {
  private static final int DEFAULT_REBOOT_WINDOW_START_TIME_HOUR = 3;
  private static final int DEFAULT_REBOOT_WINDOW_END_TIME_HOUR = 5;

  private static final int DEFAULT_REBOOT_FREQUENCY_DAYS = 2;

  private static final String TAG = "UnattendedRebootManager";

  static final String REBOOT_REASON = "unattended,flaginfra";

  @VisibleForTesting
  static final String ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED =
      "com.android.server.deviceconfig.RESUME_ON_REBOOOT_LSKF_CAPTURED";

  @VisibleForTesting
  static final String ACTION_TRIGGER_REBOOT = "com.android.server.deviceconfig.TRIGGER_REBOOT";

  private final Context mContext;

  private boolean mLskfCaptured;

  private final UnattendedRebootManagerInjector mInjector;

  private final SimPinReplayManager mSimPinReplayManager;

  private static class InjectorImpl implements UnattendedRebootManagerInjector {
    InjectorImpl() {
      /*no op*/
    }

    public long now() {
      return System.currentTimeMillis();
    }

    public ZoneId zoneId() {
      return ZoneId.systemDefault();
    }

    @Override
    public long elapsedRealtime() {
      return SystemClock.elapsedRealtime();
    }

    public int getRebootStartTime() {
      return DEFAULT_REBOOT_WINDOW_START_TIME_HOUR;
    }

    public int getRebootEndTime() {
      return DEFAULT_REBOOT_WINDOW_END_TIME_HOUR;
    }

    public int getRebootFrequency() {
      return DEFAULT_REBOOT_FREQUENCY_DAYS;
    }

    public void setRebootAlarm(Context context, long rebootTimeMillis) {
      AlarmManager alarmManager = context.getSystemService(AlarmManager.class);
      alarmManager.setExact(
          AlarmManager.RTC_WAKEUP, rebootTimeMillis, createTriggerRebootPendingIntent(context));
    }

    public void triggerRebootOnNetworkAvailable(Context context) {
      final ConnectivityManager connectivityManager =
          context.getSystemService(ConnectivityManager.class);
      NetworkRequest request =
          new NetworkRequest.Builder()
              .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
              .build();
      connectivityManager.requestNetwork(request, createTriggerRebootPendingIntent(context));
    }

    public int rebootAndApply(@NonNull Context context, @NonNull String reason, boolean slotSwitch)
        throws IOException {
      return RecoverySystem.rebootAndApply(context, reason, slotSwitch);
    }

    public void prepareForUnattendedUpdate(
        @NonNull Context context, @NonNull String updateToken, @Nullable IntentSender intentSender)
        throws IOException {
      RecoverySystem.prepareForUnattendedUpdate(context, updateToken, intentSender);
    }

    public boolean isPreparedForUnattendedUpdate(@NonNull Context context) throws IOException {
      return RecoverySystem.isPreparedForUnattendedUpdate(context);
    }

    public void regularReboot(Context context) {
      PowerManager powerManager = context.getSystemService(PowerManager.class);
      powerManager.reboot(REBOOT_REASON);
    }

    private static PendingIntent createTriggerRebootPendingIntent(Context context) {
      return PendingIntent.getBroadcast(
          context,
          /* requestCode= */ 0,
          new Intent(ACTION_TRIGGER_REBOOT),
          PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
    }
  }

  @VisibleForTesting
  UnattendedRebootManager(
      Context context,
      UnattendedRebootManagerInjector injector,
      SimPinReplayManager simPinReplayManager) {
    mContext = context;
    mInjector = injector;
    mSimPinReplayManager = simPinReplayManager;

    mContext.registerReceiver(
        new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
            mLskfCaptured = true;
          }
        },
        new IntentFilter(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED),
        Context.RECEIVER_EXPORTED);

    // Do not export receiver so that tests don't trigger reboot.
    mContext.registerReceiver(
        new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
            tryRebootOrSchedule();
          }
        },
        new IntentFilter(ACTION_TRIGGER_REBOOT),
        Context.RECEIVER_NOT_EXPORTED);
  }

  UnattendedRebootManager(Context context) {
    this(context, new InjectorImpl(), new SimPinReplayManager(context));
  }

  public void prepareUnattendedReboot() {
    Log.i(TAG, "Preparing for Unattended Reboot");
    // RoR only supported on devices with screen lock.
    if (!isDeviceSecure(mContext)) {
      return;
    }
    PendingIntent pendingIntent =
        PendingIntent.getBroadcast(
            mContext,
            /* requestCode= */ 0,
            new Intent(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED),
            PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);

    try {
      mInjector.prepareForUnattendedUpdate(
          mContext, /* updateToken= */ "", pendingIntent.getIntentSender());
    } catch (IOException e) {
      Log.i(TAG, "prepareForUnattendedReboot failed with exception" + e.getLocalizedMessage());
    }
  }

  public void scheduleReboot() {
    // Reboot the next day at the reboot start time.
    LocalDateTime timeToReboot =
        Instant.ofEpochMilli(mInjector.now())
            .atZone(mInjector.zoneId())
            .toLocalDate()
            .plusDays(mInjector.getRebootFrequency())
            .atTime(mInjector.getRebootStartTime(), /* minute= */ 12);
    long rebootTimeMillis = timeToReboot.atZone(mInjector.zoneId()).toInstant().toEpochMilli();
    Log.v(TAG, "Scheduling unattended reboot at time " + timeToReboot);

    if (timeToReboot.isBefore(
        LocalDateTime.ofInstant(Instant.ofEpochMilli(mInjector.now()), mInjector.zoneId()))) {
      Log.w(TAG, "Reboot time has already passed.");
      return;
    }

    mInjector.setRebootAlarm(mContext, rebootTimeMillis);
  }

  @VisibleForTesting
  void tryRebootOrSchedule() {
    Log.v(TAG, "Attempting unattended reboot");

    // Has enough time passed since reboot?
    if (TimeUnit.MILLISECONDS.toDays(mInjector.elapsedRealtime())
        < mInjector.getRebootFrequency()) {
      Log.v(
          TAG,
          "Device has already been rebooted in that last "
              + mInjector.getRebootFrequency()
              + " days.");
      scheduleReboot();
      return;
    }
    // Is RoR is supported?
    if (!isDeviceSecure(mContext)) {
      Log.v(TAG, "Device is not secure. Proceed with regular reboot");
      mInjector.regularReboot(mContext);
      return;
    }
    // Is RoR prepared?
    if (!isPreparedForUnattendedReboot()) {
      Log.v(TAG, "Lskf is not captured, reschedule reboot.");
      prepareUnattendedReboot();
      scheduleReboot();
      return;
    }
    // Is network connected?
    // TODO(b/305259443): Use after-boot network connectivity projection
    if (!isNetworkConnected(mContext)) {
      Log.i(TAG, "Network is not connected, reschedule reboot.");
      mInjector.triggerRebootOnNetworkAvailable(mContext);
      return;
    }
    // Is current time between reboot window?
    int currentHour =
        Instant.ofEpochMilli(mInjector.now())
            .atZone(mInjector.zoneId())
            .toLocalDateTime()
            .getHour();
    if (currentHour < mInjector.getRebootStartTime()
        || currentHour >= mInjector.getRebootEndTime()) {
      Log.v(TAG, "Reboot requested outside of reboot window, reschedule reboot.");
      prepareUnattendedReboot();
      scheduleReboot();
      return;
    }
    // Is preparing for SIM PIN replay successful?
    if (enableSimPinReplay() && !mSimPinReplayManager.prepareSimPinReplay()) {
      Log.w(TAG, "Sim Pin Replay failed, reschedule reboot");
      scheduleReboot();
    }

    // Proceed with RoR.
    Log.v(TAG, "Rebooting device to apply device config flags.");
    try {
      int success = mInjector.rebootAndApply(mContext, REBOOT_REASON, /* slotSwitch= */ false);
      if (success != 0) {
        // If reboot is not successful, reschedule.
        Log.w(TAG, "Unattended reboot failed, reschedule reboot.");
        scheduleReboot();
      }
    } catch (IOException e) {
      Log.e(TAG, e.getLocalizedMessage());
      scheduleReboot();
    }
  }

  private boolean isPreparedForUnattendedReboot() {
    try {
      boolean isPrepared = mInjector.isPreparedForUnattendedUpdate(mContext);
      if (isPrepared != mLskfCaptured) {
        Log.w(TAG, "isPrepared != mLskfCaptured. Received " + isPrepared);
      }
      return isPrepared;
    } catch (IOException e) {
      Log.w(TAG, e.getLocalizedMessage());
      return mLskfCaptured;
    }
  }

  /** Returns true if the device has screen lock. */
  private static boolean isDeviceSecure(Context context) {
    KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
    if (keyguardManager == null) {
      // Unknown if device is locked, proceed with RoR anyway.
      Log.w(TAG, "Keyguard manager is null, proceeding with RoR anyway.");
      return true;
    }
    return keyguardManager.isDeviceSecure();
  }

  private static boolean isNetworkConnected(Context context) {
    final ConnectivityManager connectivityManager =
        context.getSystemService(ConnectivityManager.class);
    if (connectivityManager == null) {
      Log.w(TAG, "ConnectivityManager is null");
      return false;
    }
    Network activeNetwork = connectivityManager.getActiveNetwork();
    NetworkCapabilities networkCapabilities =
        connectivityManager.getNetworkCapabilities(activeNetwork);
    return networkCapabilities != null
        && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
  }
}