Require READ_CALL_LOG permission to see phone numbers in phone state.
Incoming and outgoing call phone numbers are visible in the phone state
broadcast and via the PhoneStateListener. To enhance user privacy, change
to require the READ_CALL_LOG permission in order to receive the call
phone numbers.
This means to see phone numbers:
1. android.intent.action.PHONE_STATE - requires READ_PHONE_STATE and
READ_CALL_LOG permission.
2. PhoneStateListener#onCallStateChanged - now required READ_CALL_LOG
permission.
To support this new behavior, added sendBroadcastAsUserMultiplePermissions
method to context to allow sending the broadcast to all users while
requiring the two permissions.
Bug: 78650469
Test: Created PHONE_STATE broadcast receiver in test app and verified that
when no permissions are granted, the phone number is empty for incoming
and outgoing calls.
Test: Granted Phone state permission to test app and verified that phone
number is not populated.
Test: Granted test app read call log permission and verified that phone
number is populated.
Test: Created PhoneStateListener in test app and verified that when no
permissions are granted, phone number is empty for incoming and outgoing.
calls.
Test: Granted read call log permission to test app and verified that both
the incoming and outgoing numbers are populated.
Change-Id: I857ea00cc58a0abbb77960643f361dd6dd9c8b56
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 9a491bc..9511786 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1048,6 +1048,22 @@
}
@Override
+ public void sendBroadcastAsUserMultiplePermissions(Intent intent, UserHandle user,
+ String[] receiverPermissions) {
+ warnIfCallingFromSystemProcess();
+ String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
+ try {
+ intent.prepareToLeaveProcess(this);
+ ActivityManager.getService().broadcastIntent(
+ mMainThread.getApplicationThread(), intent, resolvedType, null,
+ Activity.RESULT_OK, null, null, receiverPermissions, AppOpsManager.OP_NONE,
+ null, false, false, user.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
public void sendBroadcast(Intent intent, String receiverPermission, Bundle options) {
warnIfCallingFromSystemProcess();
String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index ede7ee4..90a94ee 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -1976,6 +1976,33 @@
/**
* Broadcast the given intent to all interested BroadcastReceivers, allowing
+ * an array of required permissions to be enforced. This call is asynchronous; it returns
+ * immediately, and you will continue executing while the receivers are run. No results are
+ * propagated from receivers and receivers can not abort the broadcast. If you want to allow
+ * receivers to propagate results or abort the broadcast, you must send an ordered broadcast
+ * using {@link #sendOrderedBroadcast(Intent, String)}.
+ *
+ * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts.
+ *
+ * @param intent The Intent to broadcast; all receivers matching this
+ * Intent will receive the broadcast.
+ * @param user The user to send the broadcast to.
+ * @param receiverPermissions Array of names of permissions that a receiver must hold
+ * in order to receive your broadcast.
+ * If null or empty, no permissions are required.
+ *
+ * @see android.content.BroadcastReceiver
+ * @see #registerReceiver
+ * @see #sendBroadcast(Intent)
+ * @see #sendOrderedBroadcast(Intent, String)
+ * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle)
+ * @hide
+ */
+ public abstract void sendBroadcastAsUserMultiplePermissions(Intent intent, UserHandle user,
+ String[] receiverPermissions);
+
+ /**
+ * Broadcast the given intent to all interested BroadcastReceivers, allowing
* an optional required permission to be enforced. This
* call is asynchronous; it returns immediately, and you will continue
* executing while the receivers are run. No results are propagated from
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 1867a6d..bae99b8 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -456,6 +456,13 @@
}
/** @hide */
+ @Override
+ public void sendBroadcastAsUserMultiplePermissions(Intent intent, UserHandle user,
+ String[] receiverPermissions) {
+ mBase.sendBroadcastAsUserMultiplePermissions(intent, user, receiverPermissions);
+ }
+
+ /** @hide */
@SystemApi
@Override
public void sendBroadcast(Intent intent, String receiverPermission, Bundle options) {
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 60d11d7..02222cc 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -117,10 +117,10 @@
return (onSubscriptionsChangedListenerCallback != null);
}
- boolean canReadPhoneState() {
+ boolean canReadCallLog() {
try {
- return TelephonyPermissions.checkReadPhoneState(
- context, subId, callerPid, callerUid, callingPackage, "listen");
+ return TelephonyPermissions.checkReadCallLog(
+ context, subId, callerPid, callerUid, callingPackage);
} catch (SecurityException e) {
return false;
}
@@ -667,8 +667,8 @@
}
private String getCallIncomingNumber(Record record, int phoneId) {
- // Hide the number if record's process can't currently read phone state.
- return record.canReadPhoneState() ? mCallIncomingNumber[phoneId] : "";
+ // Only reveal the incoming number if the record has read call log permission.
+ return record.canReadCallLog() ? mCallIncomingNumber[phoneId] : "";
}
private Record add(IBinder binder) {
@@ -729,13 +729,13 @@
}
}
- public void notifyCallState(int state, String incomingNumber) {
+ public void notifyCallState(int state, String phoneNumber) {
if (!checkNotifyPermission("notifyCallState()")) {
return;
}
if (VDBG) {
- log("notifyCallState: state=" + state + " incomingNumber=" + incomingNumber);
+ log("notifyCallState: state=" + state + " phoneNumber=" + phoneNumber);
}
synchronized (mRecords) {
@@ -743,8 +743,10 @@
if (r.matchPhoneStateListenerEvent(PhoneStateListener.LISTEN_CALL_STATE) &&
(r.subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)) {
try {
- String incomingNumberOrEmpty = r.canReadPhoneState() ? incomingNumber : "";
- r.callback.onCallStateChanged(state, incomingNumberOrEmpty);
+ // Ensure the listener has read call log permission; if they do not return
+ // an empty phone number.
+ String phoneNumberOrEmpty = r.canReadCallLog() ? phoneNumber : "";
+ r.callback.onCallStateChanged(state, phoneNumberOrEmpty);
} catch (RemoteException ex) {
mRemoveList.add(r.binder);
}
@@ -755,7 +757,7 @@
// Called only by Telecomm to communicate call state across different phone accounts. So
// there is no need to add a valid subId or slotId.
- broadcastCallStateChanged(state, incomingNumber,
+ broadcastCallStateChanged(state, phoneNumber,
SubscriptionManager.INVALID_PHONE_INDEX,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
}
@@ -1571,9 +1573,6 @@
Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
intent.putExtra(PhoneConstants.STATE_KEY,
PhoneConstantConversions.convertCallState(state).toString());
- if (!TextUtils.isEmpty(incomingNumber)) {
- intent.putExtra(TelephonyManager.EXTRA_INCOMING_NUMBER, incomingNumber);
- }
// If a valid subId was specified, we should fire off a subId-specific state
// change intent and include the subId.
@@ -1589,13 +1588,20 @@
// Wakeup apps for the (SUBSCRIPTION_)PHONE_STATE broadcast.
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+ Intent intentWithPhoneNumber = new Intent(intent);
+ if (!TextUtils.isEmpty(incomingNumber)) {
+ intentWithPhoneNumber.putExtra(TelephonyManager.EXTRA_INCOMING_NUMBER, incomingNumber);
+ }
// Send broadcast twice, once for apps that have PRIVILEGED permission and once for those
// that have the runtime one
- mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
+ mContext.sendBroadcastAsUser(intentWithPhoneNumber, UserHandle.ALL,
android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
android.Manifest.permission.READ_PHONE_STATE,
AppOpsManager.OP_READ_PHONE_STATE);
+ mContext.sendBroadcastAsUserMultiplePermissions(intentWithPhoneNumber, UserHandle.ALL,
+ new String[] { android.Manifest.permission.READ_PHONE_STATE,
+ android.Manifest.permission.READ_CALL_LOG});
}
private void broadcastDataConnectionStateChanged(int state,
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index e2ba4d5..213961c 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -254,6 +254,12 @@
}
@Override
+ public void sendBroadcastAsUserMultiplePermissions(Intent intent, UserHandle user,
+ String[] receiverPermissions) {
+ spiedContext.sendBroadcastAsUserMultiplePermissions(intent, user, receiverPermissions);
+ }
+
+ @Override
public void sendBroadcast(Intent intent, String receiverPermission, Bundle options) {
spiedContext.sendBroadcast(intent, receiverPermission, options);
}
diff --git a/telephony/java/android/telephony/PhoneStateListener.java b/telephony/java/android/telephony/PhoneStateListener.java
index c16701b..da75e04 100644
--- a/telephony/java/android/telephony/PhoneStateListener.java
+++ b/telephony/java/android/telephony/PhoneStateListener.java
@@ -443,7 +443,7 @@
* Callback invoked when device call state changes.
* @param state call state
* @param phoneNumber call phone number. If application does not have
- * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} permission or carrier
+ * {@link android.Manifest.permission#READ_CALL_LOG READ_CALL_LOG} permission or carrier
* privileges (see {@link TelephonyManager#hasCarrierPrivileges}), an empty string will be
* passed as an argument.
*
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index c850004..ff135d5 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -334,10 +334,12 @@
*
* <p>
* The {@link #EXTRA_STATE} extra indicates the new call state.
- * If the new state is RINGING, a second extra
- * {@link #EXTRA_INCOMING_NUMBER} provides the incoming phone number as
- * a String.
- *
+ * If a receiving app has {@link android.Manifest.permission#READ_CALL_LOG} permission, a second
+ * extra {@link #EXTRA_INCOMING_NUMBER} provides the phone number for incoming and outoing calls
+ * as a String. Note: If the receiving app has
+ * {@link android.Manifest.permission#READ_CALL_LOG} and
+ * {@link android.Manifest.permission#READ_PHONE_STATE} permission, it will receive the
+ * broadcast twice; one with the phone number and another without it.
* <p class="note">
* This was a {@link android.content.Context#sendStickyBroadcast sticky}
* broadcast in version 1.0, but it is no longer sticky.
diff --git a/telephony/java/com/android/internal/telephony/TelephonyPermissions.java b/telephony/java/com/android/internal/telephony/TelephonyPermissions.java
index a182f2b..bbe38b7 100644
--- a/telephony/java/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/java/com/android/internal/telephony/TelephonyPermissions.java
@@ -15,6 +15,9 @@
*/
package com.android.internal.telephony;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.Manifest;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -75,7 +78,7 @@
/**
* Check whether the app with the given pid/uid can read phone state.
*
- * <p>This method behaves in one of the following ways:
+ * <p>This method behaves in one of the following ways:
* <ul>
* <li>return true: if the caller has the READ_PRIVILEGED_PHONE_STATE permission, the
* READ_PHONE_STATE runtime permission, or carrier privileges on the given subId.
@@ -132,6 +135,40 @@
}
/**
+ * Check whether the app with the given pid/uid can read the call log.
+ * @return {@code true} if the specified app has the read call log permission and AppOpp granted
+ * to it, {@code false} otherwise.
+ */
+ public static boolean checkReadCallLog(
+ Context context, int subId, int pid, int uid, String callingPackage) {
+ return checkReadCallLog(
+ context, TELEPHONY_SUPPLIER, subId, pid, uid, callingPackage);
+ }
+
+ @VisibleForTesting
+ public static boolean checkReadCallLog(
+ Context context, Supplier<ITelephony> telephonySupplier, int subId, int pid, int uid,
+ String callingPackage) {
+
+ if (context.checkPermission(Manifest.permission.READ_CALL_LOG, pid, uid)
+ != PERMISSION_GRANTED) {
+ // If we don't have the runtime permission, but do have carrier privileges, that
+ // suffices for being able to see the call phone numbers.
+ if (SubscriptionManager.isValidSubscriptionId(subId)) {
+ enforceCarrierPrivilege(telephonySupplier, subId, uid, "readCallLog");
+ return true;
+ }
+ return false;
+ }
+
+ // We have READ_CALL_LOG permission, so return true as long as the AppOps bit hasn't been
+ // revoked.
+ AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ return appOps.noteOp(AppOpsManager.OP_READ_CALL_LOG, uid, callingPackage) ==
+ AppOpsManager.MODE_ALLOWED;
+ }
+
+ /**
* Returns whether the caller can read phone numbers.
*
* <p>Besides apps with the ability to read phone state per {@link #checkReadPhoneState}, the
@@ -204,7 +241,7 @@
public static void enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
Context context, int subId, String message) {
if (context.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE) ==
- PackageManager.PERMISSION_GRANTED) {
+ PERMISSION_GRANTED) {
return;
}
diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java
index 4dfd050..9d260eb 100644
--- a/test-mock/src/android/test/mock/MockContext.java
+++ b/test-mock/src/android/test/mock/MockContext.java
@@ -364,6 +364,13 @@
}
/** @hide */
+ @Override
+ public void sendBroadcastAsUserMultiplePermissions(Intent intent, UserHandle user,
+ String[] receiverPermissions) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** @hide */
@SystemApi
@Override
public void sendBroadcast(Intent intent, String receiverPermission, Bundle options) {
diff --git a/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java b/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
index 2166240..25bd7c0 100644
--- a/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
+++ b/tests/utils/testutils/java/com/android/internal/util/test/BroadcastInterceptingContext.java
@@ -175,6 +175,12 @@
}
@Override
+ public void sendBroadcastAsUserMultiplePermissions(Intent intent, UserHandle user,
+ String[] receiverPermissions) {
+ sendBroadcast(intent);
+ }
+
+ @Override
public void sendBroadcastAsUser(Intent intent, UserHandle user) {
sendBroadcast(intent);
}