blob: fe83113b5d5619f5be287720794ee772650ab86f [file] [log] [blame]
/*
* 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);
}
}
}