content: /*
   * Copyright (C) 2017 The Android Open Source 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 android.app.timezone;
  
  import android.annotation.IntDef;
  import android.content.Context;
  import android.os.Handler;
  import android.os.ParcelFileDescriptor;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.util.Log;
  
  import java.io.IOException;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.Arrays;
  
  /**
   * The interface through which a time zone update application interacts with the Android system
   * to handle time zone rule updates.
   *
   * <p>This interface is intended for use with the default APK-based time zone rules update
   * application but it can also be used by OEMs if that mechanism is turned off using configuration.
   * All callers must possess the {@link android.Manifest.permission#UPDATE_TIME_ZONE_RULES} system
   * permission unless otherwise stated.
   *
   * <p>When using the default mechanism, when properly configured the Android system will send a
   * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent with a
   * {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN} extra to the time zone rules updater application
   * when it detects that it or the OEM's APK containing time zone rules data has been modified. The
   * updater application is then responsible for calling one of
   * {@link #requestInstall(ParcelFileDescriptor, byte[], Callback)},
   * {@link #requestUninstall(byte[], Callback)} or
   * {@link #requestNothing(byte[], boolean)}, indicating, respectively, whether a new time zone rules
   * distro should be installed, the current distro should be uninstalled, or there is nothing to do
   * (or that the correct operation could not be determined due to an error). In each case the updater
   * must pass the {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN} value it received from the intent
   * back so the system in the {@code checkToken} parameter.
   *
   * <p>If OEMs want to handle their own time zone rules updates, perhaps via a server-side component
   * rather than an APK, then they should disable the default triggering mechanism in config and are
   * responsible for triggering their own update checks / installs / uninstalls. In this case the
   * "check token" parameter can be left null and there is never any need to call
   * {@link #requestNothing(byte[], boolean)}.
   *
   * <p>OEMs should not mix the default mechanism and their own as this could lead to conflicts and
   * unnecessary checks being triggered.
   *
   * <p>Applications obtain this using {@link android.app.Activity#getSystemService(String)} with
   * {@link Context#TIME_ZONE_RULES_MANAGER_SERVICE}.
   * @hide
   */
  public final class RulesManager {
      private static final String TAG = "timezone.RulesManager";
      private static final boolean DEBUG = false;
  
      /**
       * The action of the intent that the Android system will broadcast when a time zone rules update
       * operation has been successfully staged  (i.e. to be applied next reboot) or unstaged.
       *
       * <p>See {@link #EXTRA_OPERATION_STAGED}
       *
       * <p>This is a protected intent that can only be sent by the system.
       */
      public static final String ACTION_RULES_UPDATE_OPERATION =
              "com.android.intent.action.timezone.RULES_UPDATE_OPERATION";
  
      /**
       * The key for a boolean extra for the {@link #ACTION_RULES_UPDATE_OPERATION} intent used to
       * indicate whether the operation was a "stage" or an "unstage".
       */
      public static final String EXTRA_OPERATION_STAGED = "staged";
  
      @Retention(RetentionPolicy.SOURCE)
      @IntDef(prefix = { "SUCCESS", "ERROR_" }, value = {
              SUCCESS,
              ERROR_UNKNOWN_FAILURE,
              ERROR_OPERATION_IN_PROGRESS
      })
      public @interface ResultCode {}
  
      /**
       * Indicates that an operation succeeded.
       */
      public static final int SUCCESS = 0;
  
      /**
       * Indicates that an install/uninstall cannot be initiated because there is one already in
       * progress.
       */
      public static final int ERROR_OPERATION_IN_PROGRESS = 1;
  
      /**
       * Indicates an install / uninstall did not fully succeed for an unknown reason.
       */
      public static final int ERROR_UNKNOWN_FAILURE = 2;
  
      private final Context mContext;
      private final IRulesManager mIRulesManager;
  
      public RulesManager(Context context) {
          mContext = context;
          mIRulesManager = IRulesManager.Stub.asInterface(
                  ServiceManager.getService(Context.TIME_ZONE_RULES_MANAGER_SERVICE));
      }
  
      /**
       * Returns information about the current time zone rules state such as the IANA version of
       * the system and any currently installed distro. This method allows clients to determine the
       * current device state, perhaps to see if it can be improved; for example by passing the
       * information to a server that may provide a new distro for download.
       *
       * <p>Callers must possess the {@link android.Manifest.permission#QUERY_TIME_ZONE_RULES} system
       * permission.
       */
      public RulesState getRulesState() {
          try {
              logDebug("mIRulesManager.getRulesState()");
              RulesState rulesState = mIRulesManager.getRulesState();
              logDebug("mIRulesManager.getRulesState() returned " + rulesState);
              return rulesState;
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Requests installation of the supplied distro. The distro must have been checked for integrity
       * by the caller or have been received via a trusted mechanism.
       *
       * @param distroFileDescriptor the file descriptor for the distro
       * @param checkToken an optional token provided if the install was triggered in response to a
       *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
       * @param callback the {@link Callback} to receive callbacks related to the installation
       * @return {@link #SUCCESS} if the installation will be attempted
       */
      @ResultCode
      public int requestInstall(
              ParcelFileDescriptor distroFileDescriptor, byte[] checkToken, Callback callback)
              throws IOException {
  
          ICallback iCallback = new CallbackWrapper(mContext, callback);
          try {
              logDebug("mIRulesManager.requestInstall()");
              return mIRulesManager.requestInstall(distroFileDescriptor, checkToken, iCallback);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Requests uninstallation of the currently installed distro (leaving the device with no
       * distro installed).
       *
       * @param checkToken an optional token provided if the uninstall was triggered in response to a
       *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
       * @param callback the {@link Callback} to receive callbacks related to the uninstall
       * @return {@link #SUCCESS} if the uninstallation will be attempted
       */
      @ResultCode
      public int requestUninstall(byte[] checkToken, Callback callback) {
          ICallback iCallback = new CallbackWrapper(mContext, callback);
          try {
              logDebug("mIRulesManager.requestUninstall()");
              return mIRulesManager.requestUninstall(checkToken, iCallback);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /*
       * We wrap incoming binder calls with a private class implementation that
       * redirects them into main-thread actions.  This serializes the backup
       * progress callbacks nicely within the usual main-thread lifecycle pattern.
       */
      private class CallbackWrapper extends ICallback.Stub {
          final Handler mHandler;
          final Callback mCallback;
  
          CallbackWrapper(Context context, Callback callback) {
              mCallback = callback;
              mHandler = new Handler(context.getMainLooper());
          }
  
          // Binder calls into this object just enqueue on the main-thread handler
          @Override
          public void onFinished(int status) {
              logDebug("mCallback.onFinished(status), status=" + status);
              mHandler.post(() -> mCallback.onFinished(status));
          }
      }
  
      /**
       * Requests the system does not modify the currently installed time zone distro, if any. This
       * method records the fact that a time zone check operation triggered by the system is now
       * complete and there was nothing to do. The token passed should be the one presented when the
       * check was triggered.
       *
       * <p>Note: Passing {@code success == false} may result in more checks being triggered. Clients
       * should be careful not to pass false if the failure is unlikely to resolve by itself.
       *
       * @param checkToken an optional token provided if the install was triggered in response to a
       *     {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK} intent
       * @param succeeded true if the check was successful, false if it was not successful but may
       *     succeed if it is retried
       */
      public void requestNothing(byte[] checkToken, boolean succeeded) {
          try {
              logDebug("mIRulesManager.requestNothing() with token=" + Arrays.toString(checkToken));
              mIRulesManager.requestNothing(checkToken, succeeded);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      static void logDebug(String msg) {
          if (DEBUG) {
              Log.v(TAG, msg);
          }
      }
  }
  