From 1c8976b843367145afcacd1890aa553745c27cac Mon Sep 17 00:00:00 2001 From: Hall Liu Date: Tue, 13 Oct 2020 14:37:19 -0700 Subject: Expose requestModemActivityInfo Expose requestModemActivityInfo in TelephonyManager using the Executor + Consumer pattern, and modify clients to use it. Test: atest TelephonyManagerTest#testRequestModemActivityInfo Fixes: 170427831 Change-Id: I7e8134c8058017b888c324a9f85d473fc3cdd8f5 Merged-In: I7e8134c8058017b888c324a9f85d473fc3cdd8f5 --- core/api/current.txt | 5 + core/api/system-current.txt | 9 ++ core/api/test-current.txt | 1 + core/java/android/os/OutcomeReceiver.java | 42 ++++++ .../server/am/BatteryExternalStatsWorker.java | 40 ++++-- .../com/android/server/am/BatteryStatsService.java | 2 +- .../server/stats/pull/StatsPullAtomService.java | 33 ++++- .../java/android/telephony/ModemActivityInfo.java | 8 +- .../java/android/telephony/TelephonyManager.java | 144 +++++++++++++++++++-- 9 files changed, 259 insertions(+), 25 deletions(-) create mode 100644 core/java/android/os/OutcomeReceiver.java diff --git a/core/api/current.txt b/core/api/current.txt index bc61d15f4eb0..0b2c6f4fe696 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -29936,6 +29936,11 @@ package android.os { ctor public OperationCanceledException(String); } + public interface OutcomeReceiver { + method public default void onError(@NonNull E); + method public void onResult(@NonNull R); + } + public final class Parcel { method public void appendFrom(android.os.Parcel, int, int); method @Nullable public android.os.IBinder[] createBinderArray(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 84c602faffe9..3e423e6908fd 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10356,6 +10356,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean rebootRadio(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void reportDefaultNetworkStatus(boolean); method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.MODIFY_PHONE_STATE}) public void requestCellInfoUpdate(@NonNull android.os.WorkSource, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyManager.CellInfoCallback); + method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void requestModemActivityInfo(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void requestNumberVerification(@NonNull android.telephony.PhoneNumberRange, long, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.NumberVerificationCallback); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void resetAllCarrierActions(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void resetCarrierKeysForImsiEncryption(); @@ -10528,6 +10529,14 @@ package android.telephony { field public static final int RESULT_SUCCESS = 0; // 0x0 } + public static class TelephonyManager.ModemActivityInfoException extends java.lang.Exception { + method public int getErrorCode(); + field public static final int ERROR_INVALID_INFO_RECEIVED = 2; // 0x2 + field public static final int ERROR_MODEM_RESPONSE_ERROR = 3; // 0x3 + field public static final int ERROR_PHONE_NOT_AVAILABLE = 1; // 0x1 + field public static final int ERROR_UNKNOWN = 0; // 0x0 + } + public final class ThermalMitigationRequest implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.telephony.DataThrottlingRequest getDataThrottlingRequest(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 95de4e34ed7c..d905bbeba6b5 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1660,6 +1660,7 @@ package android.telephony { method public long getTimestampMillis(); method public long getTransmitDurationMillisAtPowerLevel(int); method @NonNull public android.util.Range getTransmitPowerRange(int); + method public boolean isEmpty(); method public boolean isValid(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; diff --git a/core/java/android/os/OutcomeReceiver.java b/core/java/android/os/OutcomeReceiver.java new file mode 100644 index 000000000000..01b276411446 --- /dev/null +++ b/core/java/android/os/OutcomeReceiver.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 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.os; + +import android.annotation.NonNull; + +/** + * Callback interface intended for use when an asynchronous operation may result in a failure. + * + * This interface may be used in cases where an asynchronous API may complete either with a value + * or with a {@link Throwable} that indicates an error. + * @param The type of the result that's being sent. + * @param The type of the {@link Throwable} that contains more information about the error. + */ +public interface OutcomeReceiver { + /** + * Called when the asynchronous operation succeeds and delivers a result value. + * @param result The value delivered by the asynchronous operation. + */ + void onResult(@NonNull R result); + + /** + * Called when the asynchronous operation fails. The mode of failure is indicated by the + * {@link Throwable} passed as an argument to this method. + * @param error A subclass of {@link Throwable} with more details about the error that occurred. + */ + default void onError(@NonNull E error) {} +} diff --git a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java index 39f79ca2f13b..ef47b1ebc7ec 100644 --- a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java +++ b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java @@ -22,6 +22,7 @@ import android.content.Context; import android.net.wifi.WifiManager; import android.os.BatteryStats; import android.os.Bundle; +import android.os.OutcomeReceiver; import android.os.Parcelable; import android.os.Process; import android.os.ServiceManager; @@ -40,6 +41,7 @@ import com.android.internal.os.BatteryStatsImpl; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.function.pooled.PooledLambda; +import java.util.concurrent.ExecutionException; import libcore.util.EmptyArray; import java.util.concurrent.CompletableFuture; @@ -405,7 +407,7 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync { // We will request data from external processes asynchronously, and wait on a timeout. SynchronousResultReceiver wifiReceiver = null; SynchronousResultReceiver bluetoothReceiver = null; - SynchronousResultReceiver modemReceiver = null; + CompletableFuture modemFuture = CompletableFuture.completedFuture(null); boolean railUpdated = false; if ((updateFlags & BatteryStatsImpl.ExternalStatsSync.UPDATE_WIFI) != 0) { @@ -460,8 +462,22 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync { } if (mTelephony != null) { - modemReceiver = new SynchronousResultReceiver("telephony"); - mTelephony.requestModemActivityInfo(modemReceiver); + CompletableFuture temp = new CompletableFuture<>(); + mTelephony.requestModemActivityInfo(Runnable::run, + new OutcomeReceiver() { + @Override + public void onResult(ModemActivityInfo result) { + temp.complete(result); + } + + @Override + public void onError(TelephonyManager.ModemActivityInfoException e) { + Slog.w(TAG, "error reading modem stats:" + e); + temp.complete(null); + } + }); + modemFuture = temp; } if (!railUpdated) { synchronized (mStats) { @@ -472,7 +488,17 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync { final WifiActivityEnergyInfo wifiInfo = awaitControllerInfo(wifiReceiver); final BluetoothActivityEnergyInfo bluetoothInfo = awaitControllerInfo(bluetoothReceiver); - final ModemActivityInfo modemInfo = awaitControllerInfo(modemReceiver); + ModemActivityInfo modemInfo = null; + try { + modemInfo = modemFuture.get(EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS, + TimeUnit.MILLISECONDS); + } catch (TimeoutException | InterruptedException e) { + Slog.w(TAG, "timeout or interrupt reading modem stats: " + e); + } catch (ExecutionException e) { + Slog.w(TAG, "exception reading modem stats: " + e.getCause()); + } + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); synchronized (mStats) { mStats.addHistoryEventLocked( @@ -519,11 +545,7 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync { } if (modemInfo != null) { - if (modemInfo.isValid()) { - mStats.updateMobileRadioState(modemInfo); - } else { - Slog.w(TAG, "modem info is invalid: " + modemInfo); - } + mStats.updateMobileRadioState(modemInfo); } } diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 580ceca3b197..34ba3e078860 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -1208,7 +1208,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub public void noteModemControllerActivity(ModemActivityInfo info) { enforceCallingPermission(); - if (info == null || !info.isValid()) { + if (info == null) { Slog.e(TAG, "invalid modem data given: " + info); return; } diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index a24a6532290f..71b3e61a9844 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -102,6 +102,7 @@ import android.os.Environment; import android.os.IStoraged; import android.os.IThermalEventListener; import android.os.IThermalService; +import android.os.OutcomeReceiver; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.RemoteException; @@ -172,6 +173,7 @@ import com.android.server.stats.pull.netstats.SubInfo; import com.android.server.storage.DiskStatsFileLogger; import com.android.server.storage.DiskStatsLoggingService; +import java.util.concurrent.ExecutionException; import libcore.io.IoUtils; import org.json.JSONArray; @@ -1731,9 +1733,34 @@ public class StatsPullAtomService extends SystemService { int pullModemActivityInfoLocked(int atomTag, List pulledData) { long token = Binder.clearCallingIdentity(); try { - SynchronousResultReceiver modemReceiver = new SynchronousResultReceiver("telephony"); - mTelephony.requestModemActivityInfo(modemReceiver); - final ModemActivityInfo modemInfo = awaitControllerInfo(modemReceiver); + CompletableFuture modemFuture = new CompletableFuture<>(); + mTelephony.requestModemActivityInfo(Runnable::run, + new OutcomeReceiver() { + @Override + public void onResult(ModemActivityInfo result) { + modemFuture.complete(result); + } + + @Override + public void onError(TelephonyManager.ModemActivityInfoException e) { + Slog.w(TAG, "error reading modem stats:" + e); + modemFuture.complete(null); + } + }); + + ModemActivityInfo modemInfo; + try { + modemInfo = modemFuture.get(EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS, + TimeUnit.MILLISECONDS); + } catch (TimeoutException | InterruptedException e) { + Slog.w(TAG, "timeout or interrupt reading modem stats: " + e); + return StatsManager.PULL_SKIP; + } catch (ExecutionException e) { + Slog.w(TAG, "exception reading modem stats: " + e.getCause()); + return StatsManager.PULL_SKIP; + } + if (modemInfo == null) { return StatsManager.PULL_SKIP; } diff --git a/telephony/java/android/telephony/ModemActivityInfo.java b/telephony/java/android/telephony/ModemActivityInfo.java index 881d85c73b5d..0bf8ce620eb7 100644 --- a/telephony/java/android/telephony/ModemActivityInfo.java +++ b/telephony/java/android/telephony/ModemActivityInfo.java @@ -131,7 +131,7 @@ public final class ModemActivityInfo implements Parcelable { + " mTimestamp=" + mTimestamp + " mSleepTimeMs=" + mSleepTimeMs + " mIdleTimeMs=" + mIdleTimeMs - + " mTxTimeMs[]=" + mTxTimeMs + + " mTxTimeMs[]=" + Arrays.toString(mTxTimeMs) + " mRxTimeMs=" + mRxTimeMs + "}"; } @@ -320,8 +320,6 @@ public final class ModemActivityInfo implements Parcelable { * * @return {@code true} if this {@link ModemActivityInfo} record is valid, * {@code false} otherwise. - * TODO: remove usages of this outside Telephony by always returning a valid (or null) result - * from telephony. * @hide */ @TestApi @@ -332,7 +330,9 @@ public final class ModemActivityInfo implements Parcelable { && (getReceiveTimeMillis() >= 0) && !isEmpty()); } - private boolean isEmpty() { + /** @hide */ + @TestApi + public boolean isEmpty() { boolean isTxPowerEmpty = mTxTimeMs == null || mTxTimeMs.length == 0 || Arrays.stream(mTxTimeMs).allMatch((i) -> i == 0); diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 4189784bf5e0..b8a173ed5aee 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -57,7 +57,9 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.OutcomeReceiver; import android.os.ParcelFileDescriptor; +import android.os.Parcelable; import android.os.PersistableBundle; import android.os.RemoteException; import android.os.ResultReceiver; @@ -176,6 +178,9 @@ public class TelephonyManager { */ public static final String MODEM_ACTIVITY_RESULT_KEY = "controller_activity"; + /** @hide */ + public static final String EXCEPTION_RESULT_KEY = "exception"; + /** * The process name of the Phone app as well as many other apps that use this process name, such * as settings and vendor components. @@ -10855,26 +10860,149 @@ public class TelephonyManager { return null; } + /** + * Exception that may be supplied to the callback provided in {@link #requestModemActivityInfo}. + * @hide + */ + @SystemApi + public static class ModemActivityInfoException extends Exception { + /** Indicates that an unknown error occurred */ + public static final int ERROR_UNKNOWN = 0; + + /** + * Indicates that the modem or phone processes are not available (such as when the device + * is in airplane mode). + */ + public static final int ERROR_PHONE_NOT_AVAILABLE = 1; + + /** + * Indicates that the modem supplied an invalid instance of {@link ModemActivityInfo} + */ + public static final int ERROR_INVALID_INFO_RECEIVED = 2; + + /** + * Indicates that the modem encountered an internal failure when processing the request + * for activity info. + */ + public static final int ERROR_MODEM_RESPONSE_ERROR = 3; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"ERROR_"}, + value = { + ERROR_UNKNOWN, + ERROR_PHONE_NOT_AVAILABLE, + ERROR_INVALID_INFO_RECEIVED, + ERROR_MODEM_RESPONSE_ERROR, + }) + public @interface ModemActivityInfoError {} + + private final int mErrorCode; + + /** @hide */ + public ModemActivityInfoException(@ModemActivityInfoError int errorCode) { + mErrorCode = errorCode; + } + + public @ModemActivityInfoError int getErrorCode() { + return mErrorCode; + } + + @Override + public String toString() { + switch (mErrorCode) { + case ERROR_UNKNOWN: return "ERROR_UNKNOWN"; + case ERROR_PHONE_NOT_AVAILABLE: return "ERROR_PHONE_NOT_AVAILABLE"; + case ERROR_INVALID_INFO_RECEIVED: return "ERROR_INVALID_INFO_RECEIVED"; + case ERROR_MODEM_RESPONSE_ERROR: return "ERROR_MODEM_RESPONSE_ERROR"; + default: return "UNDEFINED"; + } + } + } /** - * Requests the modem activity info. The recipient will place the result - * in `result`. - * @param result The object on which the recipient will send the resulting - * {@link android.telephony.ModemActivityInfo} object with key of - * {@link #MODEM_ACTIVITY_RESULT_KEY}. + * Requests the current modem activity info. + * + * The provided instance of {@link ModemActivityInfo} represents the cumulative activity since + * the last restart of the phone process. + * + * @param callback A callback object to which the result will be delivered. If there was an + * error processing the request, {@link OutcomeReceiver#onError} will be called + * with more details about the error. * @hide */ - public void requestModemActivityInfo(@NonNull ResultReceiver result) { + @SystemApi + @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) + public void requestModemActivityInfo(@NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + // Pass no handler into the receiver, since we're going to be trampolining the call to the + // listener onto the provided executor. + ResultReceiver wrapperResultReceiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle data) { + if (data == null) { + Log.w(TAG, "requestModemActivityInfo: received null bundle"); + sendErrorToListener(ModemActivityInfoException.ERROR_UNKNOWN); + return; + } + data.setDefusable(true); + if (data.containsKey(EXCEPTION_RESULT_KEY)) { + int receivedErrorCode = data.getInt(EXCEPTION_RESULT_KEY); + sendErrorToListener(receivedErrorCode); + return; + } + + if (!data.containsKey(MODEM_ACTIVITY_RESULT_KEY)) { + Log.w(TAG, "requestModemActivityInfo: Bundle did not contain expected key"); + sendErrorToListener(ModemActivityInfoException.ERROR_UNKNOWN); + return; + } + Parcelable receivedResult = data.getParcelable(MODEM_ACTIVITY_RESULT_KEY); + if (!(receivedResult instanceof ModemActivityInfo)) { + Log.w(TAG, "requestModemActivityInfo: Bundle contained something that wasn't " + + "a ModemActivityInfo."); + sendErrorToListener(ModemActivityInfoException.ERROR_UNKNOWN); + return; + } + ModemActivityInfo modemActivityInfo = (ModemActivityInfo) receivedResult; + if (!modemActivityInfo.isValid()) { + Log.w(TAG, "requestModemActivityInfo: Received an invalid ModemActivityInfo"); + sendErrorToListener(ModemActivityInfoException.ERROR_INVALID_INFO_RECEIVED); + return; + } + Log.d(TAG, "requestModemActivityInfo: Sending result to app: " + modemActivityInfo); + sendResultToListener(modemActivityInfo); + } + + private void sendResultToListener(ModemActivityInfo info) { + Binder.withCleanCallingIdentity(() -> + executor.execute(() -> + callback.onResult(info))); + } + + private void sendErrorToListener(int code) { + ModemActivityInfoException e = new ModemActivityInfoException(code); + Binder.withCleanCallingIdentity(() -> + executor.execute(() -> + callback.onError(e))); + } + }; + try { ITelephony service = getITelephony(); if (service != null) { - service.requestModemActivityInfo(result); + service.requestModemActivityInfo(wrapperResultReceiver); return; } } catch (RemoteException e) { Log.e(TAG, "Error calling ITelephony#getModemActivityInfo", e); } - result.send(0, null); + executor.execute(() -> callback.onError( + new ModemActivityInfoException( + ModemActivityInfoException.ERROR_PHONE_NOT_AVAILABLE))); } /** -- cgit v1.2.3