Merge "Create "trusted chain" mechanism for AttributionSource" into sc-dev
This commit is contained in:
@@ -741,6 +741,13 @@ public class AppOpsManager {
|
||||
@TestApi
|
||||
public static final int ATTRIBUTION_FLAG_RECEIVER = 0x4;
|
||||
|
||||
/**
|
||||
* Attribution chain flag: Specifies that all attribution sources in the chain were trusted.
|
||||
* Must only be set by system server.
|
||||
* @hide
|
||||
*/
|
||||
public static final int ATTRIBUTION_FLAG_TRUSTED = 0x8;
|
||||
|
||||
/**
|
||||
* No attribution flags.
|
||||
* @hide
|
||||
@@ -760,7 +767,8 @@ public class AppOpsManager {
|
||||
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
|
||||
ATTRIBUTION_FLAG_ACCESSOR,
|
||||
ATTRIBUTION_FLAG_INTERMEDIARY,
|
||||
ATTRIBUTION_FLAG_RECEIVER
|
||||
ATTRIBUTION_FLAG_RECEIVER,
|
||||
ATTRIBUTION_FLAG_TRUSTED
|
||||
})
|
||||
public @interface AttributionFlags {}
|
||||
|
||||
|
||||
@@ -19,9 +19,11 @@ package android.permission;
|
||||
import static android.Manifest.permission_group.CAMERA;
|
||||
import static android.Manifest.permission_group.LOCATION;
|
||||
import static android.Manifest.permission_group.MICROPHONE;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_FLAGS_NONE;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED;
|
||||
import static android.app.AppOpsManager.AttributionFlags;
|
||||
import static android.app.AppOpsManager.OPSTR_CAMERA;
|
||||
import static android.app.AppOpsManager.OPSTR_COARSE_LOCATION;
|
||||
@@ -180,7 +182,10 @@ public class PermissionUsageHelper implements AppOpsManager.OnOpActiveChangedLis
|
||||
public void onOpActiveChanged(@NonNull String op, int uid, @NonNull String packageName,
|
||||
@Nullable String attributionTag, boolean active, @AttributionFlags int attributionFlags,
|
||||
int attributionChainId) {
|
||||
if ((attributionFlags & ATTRIBUTION_FLAGS_NONE) != 0) {
|
||||
if (attributionChainId == ATTRIBUTION_CHAIN_ID_NONE
|
||||
|| attributionFlags == ATTRIBUTION_FLAGS_NONE
|
||||
|| (attributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) {
|
||||
// If this is not a chain, or it is untrusted, return
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ import javax.inject.Inject;
|
||||
*/
|
||||
@SysUISingleton
|
||||
public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsController,
|
||||
AppOpsManager.OnOpActiveChangedInternalListener,
|
||||
AppOpsManager.OnOpActiveChangedListener,
|
||||
AppOpsManager.OnOpNotedListener, IndividualSensorPrivacyController.Callback,
|
||||
Dumpable {
|
||||
|
||||
@@ -359,11 +359,29 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon
|
||||
mBGHandler.post(() -> notifySuscribersWorker(code, uid, packageName, active));
|
||||
}
|
||||
|
||||
/**
|
||||
* Required to override, delegate to other. Should not be called.
|
||||
*/
|
||||
public void onOpActiveChanged(String op, int uid, String packageName, boolean active) {
|
||||
onOpActiveChanged(op, uid, packageName, null, active,
|
||||
AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE);
|
||||
}
|
||||
|
||||
// Get active app ops, and check if their attributions are trusted
|
||||
@Override
|
||||
public void onOpActiveChanged(int code, int uid, String packageName, boolean active) {
|
||||
public void onOpActiveChanged(String op, int uid, String packageName, String attributionTag,
|
||||
boolean active, int attributionFlags, int attributionChainId) {
|
||||
int code = AppOpsManager.strOpToOp(op);
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s", code, uid, packageName,
|
||||
Boolean.toString(active)));
|
||||
Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s,%d,%d)", code, uid, packageName,
|
||||
Boolean.toString(active), attributionChainId, attributionFlags));
|
||||
}
|
||||
if (attributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE
|
||||
&& attributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE
|
||||
&& (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR) == 0
|
||||
&& (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_TRUSTED) == 0) {
|
||||
// if this attribution chain isn't trusted, and this isn't the accessor, do not show it.
|
||||
return;
|
||||
}
|
||||
boolean activeChanged = updateActives(code, uid, packageName, active);
|
||||
if (!activeChanged) return; // early return
|
||||
|
||||
@@ -77,6 +77,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
private static final int TEST_UID = UserHandle.getUid(0, 0);
|
||||
private static final int TEST_UID_OTHER = UserHandle.getUid(1, 0);
|
||||
private static final int TEST_UID_NON_USER_SENSITIVE = UserHandle.getUid(2, 0);
|
||||
private static final int TEST_CHAIN_ID = 1;
|
||||
|
||||
@Mock
|
||||
private AppOpsManager mAppOpsManager;
|
||||
@@ -162,7 +163,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
new int[]{AppOpsManager.OP_RECORD_AUDIO, AppOpsManager.OP_FINE_LOCATION},
|
||||
mCallback);
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mController.onOpNoted(AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME,
|
||||
TEST_ATTRIBUTION_NAME, AppOpsManager.OP_FLAG_SELF, AppOpsManager.MODE_ALLOWED);
|
||||
mTestableLooper.processAllMessages();
|
||||
@@ -174,7 +175,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
public void addCallback_notIncludedCode() {
|
||||
mController.addCallback(new int[]{AppOpsManager.OP_FINE_LOCATION}, mCallback);
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
verify(mCallback, never()).onActiveStateChanged(
|
||||
anyInt(), anyInt(), anyString(), anyBoolean());
|
||||
@@ -185,7 +186,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
|
||||
mController.removeCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
verify(mCallback, never()).onActiveStateChanged(
|
||||
anyInt(), anyInt(), anyString(), anyBoolean());
|
||||
@@ -196,7 +197,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
|
||||
mController.removeCallback(new int[]{AppOpsManager.OP_CAMERA}, mCallback);
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
verify(mCallback).onActiveStateChanged(AppOpsManager.OP_RECORD_AUDIO,
|
||||
TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
@@ -204,18 +205,18 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void getActiveItems_sameDetails() {
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO,
|
||||
TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO,
|
||||
TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
assertEquals(1, mController.getActiveAppOps().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getActiveItems_differentDetails() {
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO,
|
||||
TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_CAMERA,
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_CAMERA,
|
||||
TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mController.onOpNoted(AppOpsManager.OP_FINE_LOCATION,
|
||||
TEST_UID, TEST_PACKAGE_NAME, TEST_ATTRIBUTION_NAME,
|
||||
@@ -225,9 +226,9 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void getActiveItemsForUser() {
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO,
|
||||
TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_CAMERA,
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_CAMERA,
|
||||
TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mController.onOpNoted(AppOpsManager.OP_FINE_LOCATION,
|
||||
TEST_UID, TEST_PACKAGE_NAME, TEST_ATTRIBUTION_NAME,
|
||||
@@ -242,11 +243,11 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
public void systemAndExemptedRolesAreIgnored() {
|
||||
assumeFalse(mExemptedRolePkgName == null || mExemptedRolePkgName.equals(""));
|
||||
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO,
|
||||
TEST_UID_NON_USER_SENSITIVE, mExemptedRolePkgName, true);
|
||||
assertEquals(0, mController.getActiveAppOpsForUser(
|
||||
UserHandle.getUserId(TEST_UID_NON_USER_SENSITIVE), false).size());
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO,
|
||||
TEST_UID_NON_USER_SENSITIVE, SYSTEM_PKG, true);
|
||||
assertEquals(0, mController.getActiveAppOpsForUser(
|
||||
UserHandle.getUserId(TEST_UID_NON_USER_SENSITIVE), false).size());
|
||||
@@ -257,7 +258,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
assumeFalse(mExemptedRolePkgName == null || mExemptedRolePkgName.equals(""));
|
||||
|
||||
mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO,
|
||||
TEST_UID_NON_USER_SENSITIVE, mExemptedRolePkgName, true);
|
||||
|
||||
mTestableLooper.processAllMessages();
|
||||
@@ -279,8 +280,8 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mController.setBGHandler(mMockHandler);
|
||||
|
||||
mController.setListening(true);
|
||||
mController.onOpActiveChanged(AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME,
|
||||
true);
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_FINE_LOCATION, TEST_UID,
|
||||
TEST_PACKAGE_NAME, true);
|
||||
mController.onOpNoted(AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME,
|
||||
TEST_ATTRIBUTION_NAME, AppOpsManager.OP_FLAG_SELF, AppOpsManager.MODE_ALLOWED);
|
||||
assertFalse(mController.getActiveAppOps().isEmpty());
|
||||
@@ -320,6 +321,36 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
verify(mMockHandler, times(2)).scheduleRemoval(any(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUntrustedChainUsagesDiscarded() {
|
||||
assertTrue(mController.getActiveAppOps().isEmpty());
|
||||
mController.setBGHandler(mMockHandler);
|
||||
|
||||
//untrusted receiver access
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID,
|
||||
TEST_PACKAGE_NAME, TEST_ATTRIBUTION_NAME, true,
|
||||
AppOpsManager.ATTRIBUTION_FLAG_RECEIVER, TEST_CHAIN_ID);
|
||||
//untrusted intermediary access
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID,
|
||||
TEST_PACKAGE_NAME, TEST_ATTRIBUTION_NAME, true,
|
||||
AppOpsManager.ATTRIBUTION_FLAG_INTERMEDIARY, TEST_CHAIN_ID);
|
||||
assertTrue(mController.getActiveAppOps().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrustedChainUsagesKept() {
|
||||
//untrusted accessor access
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID,
|
||||
TEST_PACKAGE_NAME, TEST_ATTRIBUTION_NAME, true,
|
||||
AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR, TEST_CHAIN_ID);
|
||||
//trusted access
|
||||
mController.onOpActiveChanged(AppOpsManager.OPSTR_CAMERA, TEST_UID,
|
||||
TEST_PACKAGE_NAME, TEST_ATTRIBUTION_NAME, true,
|
||||
AppOpsManager.ATTRIBUTION_FLAG_RECEIVER | AppOpsManager.ATTRIBUTION_FLAG_TRUSTED,
|
||||
TEST_CHAIN_ID);
|
||||
assertEquals(2, mController.getActiveAppOps().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActiveOpNotRemovedAfterNoted() throws InterruptedException {
|
||||
// Replaces the timeout delay with 5 ms
|
||||
@@ -329,7 +360,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mController.setBGHandler(testHandler);
|
||||
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
|
||||
mController.onOpNoted(AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME,
|
||||
TEST_ATTRIBUTION_NAME, AppOpsManager.OP_FLAG_SELF, AppOpsManager.MODE_ALLOWED);
|
||||
@@ -364,7 +395,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
TEST_ATTRIBUTION_NAME, AppOpsManager.OP_FLAG_SELF, AppOpsManager.MODE_ALLOWED);
|
||||
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
|
||||
mTestableLooper.processAllMessages();
|
||||
List<AppOpItem> list = mController.getActiveAppOps();
|
||||
@@ -375,7 +406,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
assertEquals(2, list.size());
|
||||
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, false);
|
||||
AppOpsManager.OPSTR_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, false);
|
||||
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
@@ -393,7 +424,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
TEST_ATTRIBUTION_NAME, AppOpsManager.OP_FLAG_SELF, AppOpsManager.MODE_ALLOWED);
|
||||
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
|
||||
mTestableLooper.processAllMessages();
|
||||
verify(mCallback).onActiveStateChanged(
|
||||
@@ -405,7 +436,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mController.addCallback(new int[]{AppOpsManager.OP_FINE_LOCATION}, mCallback);
|
||||
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
|
||||
mController.onOpNoted(AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME,
|
||||
TEST_ATTRIBUTION_NAME, AppOpsManager.OP_FLAG_SELF, AppOpsManager.MODE_ALLOWED);
|
||||
@@ -421,7 +452,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
verify(mCallback, never())
|
||||
@@ -434,7 +465,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertTrue(mController.getActiveAppOps().isEmpty());
|
||||
@@ -446,7 +477,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_PHONE_CALL_MICROPHONE, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertTrue(mController.getActiveAppOps().isEmpty());
|
||||
@@ -461,7 +492,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_CAMERA, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
verify(mCallback).onActiveStateChanged(
|
||||
@@ -477,7 +508,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
|
||||
mTestableLooper.processAllMessages();
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
|
||||
|
||||
mTestableLooper.processAllMessages();
|
||||
mRecordingCallback.onRecordingConfigChanged(Collections.emptyList());
|
||||
@@ -493,7 +524,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
|
||||
mTestableLooper.processAllMessages();
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
AudioRecordingConfiguration mockARC = mock(AudioRecordingConfiguration.class);
|
||||
@@ -516,7 +547,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mCallback);
|
||||
mTestableLooper.processAllMessages();
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
List<AppOpItem> list = mController.getActiveAppOps();
|
||||
assertEquals(1, list.size());
|
||||
@@ -526,7 +557,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
// Add a camera op, and disable the microphone. The camera op should be the only op returned
|
||||
mController.onSensorBlockedChanged(MICROPHONE, true);
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
list = mController.getActiveAppOps();
|
||||
assertEquals(1, list.size());
|
||||
@@ -550,7 +581,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mCallback);
|
||||
mTestableLooper.processAllMessages();
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_PHONE_CALL_MICROPHONE, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
List<AppOpItem> list = mController.getActiveAppOps();
|
||||
assertEquals(1, list.size());
|
||||
@@ -560,7 +591,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
// Add a camera op, and disable the microphone. The camera op should be the only op returned
|
||||
mController.onSensorBlockedChanged(MICROPHONE, true);
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
list = mController.getActiveAppOps();
|
||||
assertEquals(1, list.size());
|
||||
@@ -583,7 +614,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mCallback);
|
||||
mTestableLooper.processAllMessages();
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
List<AppOpItem> list = mController.getActiveAppOps();
|
||||
assertEquals(1, list.size());
|
||||
@@ -593,7 +624,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
// Add an audio op, and disable the camera. The audio op should be the only op returned
|
||||
mController.onSensorBlockedChanged(CAMERA, true);
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
list = mController.getActiveAppOps();
|
||||
assertEquals(1, list.size());
|
||||
@@ -616,7 +647,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
mCallback);
|
||||
mTestableLooper.processAllMessages();
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_PHONE_CALL_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_PHONE_CALL_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
List<AppOpItem> list = mController.getActiveAppOps();
|
||||
assertEquals(1, list.size());
|
||||
@@ -626,7 +657,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
||||
// Add an audio op, and disable the camera. The audio op should be the only op returned
|
||||
mController.onSensorBlockedChanged(CAMERA, true);
|
||||
mController.onOpActiveChanged(
|
||||
AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
|
||||
mTestableLooper.processAllMessages();
|
||||
list = mController.getActiveAppOps();
|
||||
assertEquals(1, list.size());
|
||||
|
||||
@@ -20,6 +20,7 @@ import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_CAMERA;
|
||||
import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_LOCATION;
|
||||
import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED;
|
||||
import static android.app.AppOpsManager.CALL_BACK_ON_SWITCHED_OP;
|
||||
import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG;
|
||||
import static android.app.AppOpsManager.FILTER_BY_OP_NAMES;
|
||||
@@ -3332,7 +3333,8 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
verifyIncomingPackage(proxiedPackageName, UserHandle.getUserId(proxiedUid));
|
||||
verifyIncomingPackage(proxyPackageName, UserHandle.getUserId(proxyUid));
|
||||
|
||||
skipProxyOperation = resolveSkipProxyOperation(skipProxyOperation, attributionSource);
|
||||
skipProxyOperation = skipProxyOperation
|
||||
&& isCallerAndAttributionTrusted(attributionSource);
|
||||
|
||||
String resolveProxyPackageName = AppOpsManager.resolvePackageName(proxyUid,
|
||||
proxyPackageName);
|
||||
@@ -3849,7 +3851,8 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
verifyIncomingPackage(proxyPackageName, UserHandle.getUserId(proxyUid));
|
||||
verifyIncomingPackage(proxiedPackageName, UserHandle.getUserId(proxiedUid));
|
||||
|
||||
skipProxyOperation = resolveSkipProxyOperation(skipProxyOperation, attributionSource);
|
||||
boolean isCallerTrusted = isCallerAndAttributionTrusted(attributionSource);
|
||||
skipProxyOperation = isCallerTrusted && skipProxyOperation;
|
||||
|
||||
String resolvedProxyPackageName = AppOpsManager.resolvePackageName(proxyUid,
|
||||
proxyPackageName);
|
||||
@@ -3858,11 +3861,15 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
proxiedPackageName);
|
||||
}
|
||||
|
||||
final boolean isChainTrusted = isCallerTrusted
|
||||
&& attributionChainId != ATTRIBUTION_CHAIN_ID_NONE
|
||||
&& ((proxyAttributionFlags & ATTRIBUTION_FLAG_TRUSTED) != 0
|
||||
|| (proxiedAttributionFlags & ATTRIBUTION_FLAG_TRUSTED) != 0);
|
||||
final boolean isSelfBlame = Binder.getCallingUid() == proxiedUid;
|
||||
final boolean isProxyTrusted = mContext.checkPermission(
|
||||
Manifest.permission.UPDATE_APP_OPS_STATS, -1, proxyUid)
|
||||
== PackageManager.PERMISSION_GRANTED || isSelfBlame
|
||||
|| attributionChainId != ATTRIBUTION_CHAIN_ID_NONE;
|
||||
|| isChainTrusted;
|
||||
|
||||
String resolvedProxiedPackageName = AppOpsManager.resolvePackageName(proxiedUid,
|
||||
proxiedPackageName);
|
||||
@@ -4051,7 +4058,8 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
|
||||
final IBinder proxiedToken = attributionSource.getNextToken();
|
||||
|
||||
skipProxyOperation = resolveSkipProxyOperation(skipProxyOperation, attributionSource);
|
||||
skipProxyOperation = skipProxyOperation
|
||||
&& isCallerAndAttributionTrusted(attributionSource);
|
||||
|
||||
verifyIncomingProxyUid(attributionSource);
|
||||
verifyIncomingOp(code);
|
||||
@@ -4334,11 +4342,7 @@ public class AppOpsService extends IAppOpsService.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean resolveSkipProxyOperation(boolean requestsSkipProxyOperation,
|
||||
@NonNull AttributionSource attributionSource) {
|
||||
if (!requestsSkipProxyOperation) {
|
||||
return false;
|
||||
}
|
||||
private boolean isCallerAndAttributionTrusted(@NonNull AttributionSource attributionSource) {
|
||||
if (attributionSource.getUid() != Binder.getCallingUid()
|
||||
&& attributionSource.isTrusted(mContext)) {
|
||||
return true;
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.android.server.appop;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED;
|
||||
import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG;
|
||||
import static android.app.AppOpsManager.FILTER_BY_OP_NAMES;
|
||||
import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME;
|
||||
@@ -359,7 +360,8 @@ final class DiscreteRegistry {
|
||||
for (int opEventNum = 0; opEventNum < nOpEvents; opEventNum++) {
|
||||
DiscreteOpEvent event = opEvents.get(opEventNum);
|
||||
if (event == null
|
||||
|| event.mAttributionChainId == ATTRIBUTION_CHAIN_ID_NONE) {
|
||||
|| event.mAttributionChainId == ATTRIBUTION_CHAIN_ID_NONE
|
||||
|| (event.mAttributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ import static android.Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY;
|
||||
import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
|
||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
import static android.Manifest.permission.RECORD_AUDIO;
|
||||
import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
|
||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
|
||||
import static android.app.AppOpsManager.ATTRIBUTION_FLAGS_NONE;
|
||||
import static android.app.AppOpsManager.MODE_ALLOWED;
|
||||
import static android.app.AppOpsManager.MODE_ERRORED;
|
||||
import static android.app.AppOpsManager.MODE_IGNORED;
|
||||
@@ -5601,13 +5604,14 @@ public class PermissionManagerService extends IPermissionManager.Stub {
|
||||
@PermissionCheckerManager.PermissionResult
|
||||
public int checkOp(int op, AttributionSourceState attributionSource,
|
||||
String message, boolean forDataDelivery, boolean startDataDelivery) {
|
||||
int result = checkOp(mContext, op, new AttributionSource(attributionSource), message,
|
||||
forDataDelivery, startDataDelivery);
|
||||
int result = checkOp(mContext, op, mPermissionManagerServiceInternal,
|
||||
new AttributionSource(attributionSource), message, forDataDelivery,
|
||||
startDataDelivery);
|
||||
if (result != PermissionChecker.PERMISSION_GRANTED && startDataDelivery) {
|
||||
// Finish any started op if some step in the attribution chain failed.
|
||||
finishDataDelivery(op, attributionSource, /*fromDatasource*/ false);
|
||||
}
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@PermissionCheckerManager.PermissionResult
|
||||
@@ -5731,8 +5735,14 @@ public class PermissionManagerService extends IPermissionManager.Stub {
|
||||
final int op = AppOpsManager.permissionToOpCode(permission);
|
||||
final int attributionChainId =
|
||||
getAttributionChainId(startDataDelivery, attributionSource);
|
||||
final boolean hasChain = attributionChainId != ATTRIBUTION_CHAIN_ID_NONE;
|
||||
AttributionSource current = attributionSource;
|
||||
AttributionSource next = null;
|
||||
// We consider the chain trusted if the start node has UPDATE_APP_OPS_STATS, and
|
||||
// every attributionSource in the chain is registered with the system.
|
||||
final boolean isChainStartTrusted = !hasChain || checkPermission(context,
|
||||
permissionManagerServiceInt, UPDATE_APP_OPS_STATS, current.getUid(),
|
||||
current.getRenouncedPermissions());
|
||||
|
||||
while (true) {
|
||||
final boolean skipCurrentChecks = (fromDatasource || next != null);
|
||||
@@ -5777,13 +5787,17 @@ public class PermissionManagerService extends IPermissionManager.Stub {
|
||||
&& current.equals(attributionSource)
|
||||
&& next != null && next.getNext() == null);
|
||||
final boolean selfAccess = singleReceiverFromDatasource || next == null;
|
||||
final boolean isLinkTrusted = isChainStartTrusted
|
||||
&& (current.isTrusted(context) || current.equals(attributionSource))
|
||||
&& (next == null || next.isTrusted(context));
|
||||
|
||||
final int proxyAttributionFlags = (!skipCurrentChecks)
|
||||
final int proxyAttributionFlags = (!skipCurrentChecks && hasChain)
|
||||
? resolveProxyAttributionFlags(attributionSource, current, fromDatasource,
|
||||
startDataDelivery, selfAccess)
|
||||
: AppOpsManager.ATTRIBUTION_FLAGS_NONE;
|
||||
final int proxiedAttributionFlags = resolveProxiedAttributionFlags(
|
||||
attributionSource, next, fromDatasource, startDataDelivery, selfAccess);
|
||||
startDataDelivery, selfAccess, isLinkTrusted)
|
||||
: ATTRIBUTION_FLAGS_NONE;
|
||||
final int proxiedAttributionFlags = hasChain ? resolveProxiedAttributionFlags(
|
||||
attributionSource, next, fromDatasource, startDataDelivery, selfAccess,
|
||||
isLinkTrusted) : ATTRIBUTION_FLAGS_NONE;
|
||||
|
||||
final int opMode = performOpTransaction(context, op, current, message,
|
||||
forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess,
|
||||
@@ -5838,48 +5852,52 @@ public class PermissionManagerService extends IPermissionManager.Stub {
|
||||
private static @AttributionFlags int resolveProxyAttributionFlags(
|
||||
@NonNull AttributionSource attributionChain,
|
||||
@NonNull AttributionSource current, boolean fromDatasource,
|
||||
boolean startDataDelivery, boolean selfAccess) {
|
||||
boolean startDataDelivery, boolean selfAccess, boolean isTrusted) {
|
||||
return resolveAttributionFlags(attributionChain, current, fromDatasource,
|
||||
startDataDelivery, selfAccess, /*flagsForProxy*/ true);
|
||||
startDataDelivery, selfAccess, isTrusted, /*flagsForProxy*/ true);
|
||||
}
|
||||
|
||||
private static @AttributionFlags int resolveProxiedAttributionFlags(
|
||||
@NonNull AttributionSource attributionChain,
|
||||
@NonNull AttributionSource current, boolean fromDatasource,
|
||||
boolean startDataDelivery, boolean selfAccess) {
|
||||
boolean startDataDelivery, boolean selfAccess, boolean isTrusted) {
|
||||
return resolveAttributionFlags(attributionChain, current, fromDatasource,
|
||||
startDataDelivery, selfAccess, /*flagsForProxy*/ false);
|
||||
startDataDelivery, selfAccess, isTrusted, /*flagsForProxy*/ false);
|
||||
}
|
||||
|
||||
private static @AttributionFlags int resolveAttributionFlags(
|
||||
@NonNull AttributionSource attributionChain,
|
||||
@NonNull AttributionSource current, boolean fromDatasource,
|
||||
boolean startDataDelivery, boolean selfAccess, boolean flagsForProxy) {
|
||||
boolean startDataDelivery, boolean selfAccess, boolean isTrusted,
|
||||
boolean flagsForProxy) {
|
||||
if (current == null || !startDataDelivery) {
|
||||
return AppOpsManager.ATTRIBUTION_FLAGS_NONE;
|
||||
}
|
||||
int trustedFlag = isTrusted
|
||||
? AppOpsManager.ATTRIBUTION_FLAG_TRUSTED : AppOpsManager.ATTRIBUTION_FLAGS_NONE;
|
||||
if (flagsForProxy) {
|
||||
if (selfAccess) {
|
||||
return AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
|
||||
return trustedFlag | AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
|
||||
} else if (!fromDatasource && current.equals(attributionChain)) {
|
||||
return AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
|
||||
return trustedFlag | AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
|
||||
}
|
||||
} else {
|
||||
if (selfAccess) {
|
||||
return AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
|
||||
return trustedFlag | AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
|
||||
} else if (fromDatasource && current.equals(attributionChain.getNext())) {
|
||||
return AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
|
||||
return trustedFlag | AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
|
||||
} else if (current.getNext() == null) {
|
||||
return AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
|
||||
return trustedFlag | AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
|
||||
}
|
||||
}
|
||||
if (fromDatasource && current.equals(attributionChain)) {
|
||||
return AppOpsManager.ATTRIBUTION_FLAGS_NONE;
|
||||
}
|
||||
return AppOpsManager.ATTRIBUTION_FLAG_INTERMEDIARY;
|
||||
return trustedFlag | AppOpsManager.ATTRIBUTION_FLAG_INTERMEDIARY;
|
||||
}
|
||||
|
||||
private static int checkOp(@NonNull Context context, @NonNull int op,
|
||||
@NonNull PermissionManagerServiceInternal permissionManagerServiceInt,
|
||||
@NonNull AttributionSource attributionSource, @Nullable String message,
|
||||
boolean forDataDelivery, boolean startDataDelivery) {
|
||||
if (op < 0 || attributionSource.getPackageName() == null) {
|
||||
@@ -5888,10 +5906,17 @@ public class PermissionManagerService extends IPermissionManager.Stub {
|
||||
|
||||
final int attributionChainId =
|
||||
getAttributionChainId(startDataDelivery, attributionSource);
|
||||
final boolean hasChain = attributionChainId != ATTRIBUTION_CHAIN_ID_NONE;
|
||||
|
||||
AttributionSource current = attributionSource;
|
||||
AttributionSource next = null;
|
||||
|
||||
// We consider the chain trusted if the start node has UPDATE_APP_OPS_STATS, and
|
||||
// every attributionSource in the chain is registered with the system.
|
||||
final boolean isChainStartTrusted = !hasChain || checkPermission(context,
|
||||
permissionManagerServiceInt, UPDATE_APP_OPS_STATS, current.getUid(),
|
||||
current.getRenouncedPermissions());
|
||||
|
||||
while (true) {
|
||||
final boolean skipCurrentChecks = (next != null);
|
||||
next = current.getNext();
|
||||
@@ -5904,14 +5929,17 @@ public class PermissionManagerService extends IPermissionManager.Stub {
|
||||
|
||||
// The access is for oneself if this is the single attribution source in the chain.
|
||||
final boolean selfAccess = (next == null);
|
||||
final boolean isLinkTrusted = isChainStartTrusted
|
||||
&& (current.isTrusted(context) || current.equals(attributionSource))
|
||||
&& (next == null || next.isTrusted(context));
|
||||
|
||||
final int proxyAttributionFlags = (!skipCurrentChecks)
|
||||
final int proxyAttributionFlags = (!skipCurrentChecks && hasChain)
|
||||
? resolveProxyAttributionFlags(attributionSource, current,
|
||||
/*fromDatasource*/ false, startDataDelivery, selfAccess)
|
||||
: AppOpsManager.ATTRIBUTION_FLAGS_NONE;
|
||||
final int proxiedAttributionFlags = resolveProxiedAttributionFlags(
|
||||
/*fromDatasource*/ false, startDataDelivery, selfAccess,
|
||||
isLinkTrusted) : ATTRIBUTION_FLAGS_NONE;
|
||||
final int proxiedAttributionFlags = hasChain ? resolveProxiedAttributionFlags(
|
||||
attributionSource, next, /*fromDatasource*/ false, startDataDelivery,
|
||||
selfAccess);
|
||||
selfAccess, isLinkTrusted) : ATTRIBUTION_FLAGS_NONE;
|
||||
|
||||
final int opMode = performOpTransaction(context, op, current, message,
|
||||
forDataDelivery, startDataDelivery, skipCurrentChecks, selfAccess,
|
||||
|
||||
@@ -29,6 +29,7 @@ import android.content.pm.ServiceInfo;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.permission.PermissionManager;
|
||||
import android.speech.IRecognitionListener;
|
||||
import android.speech.IRecognitionService;
|
||||
import android.speech.IRecognitionServiceManagerCallback;
|
||||
@@ -139,6 +140,10 @@ final class SpeechRecognitionManagerServiceImpl extends
|
||||
@NonNull AttributionSource attributionSource)
|
||||
throws RemoteException {
|
||||
attributionSource.enforceCallingUid();
|
||||
if (!attributionSource.isTrusted(mMaster.getContext())) {
|
||||
mMaster.getContext().getSystemService(PermissionManager.class)
|
||||
.registerAttributionSource(attributionSource);
|
||||
}
|
||||
service.startListening(recognizerIntent, listener, attributionSource);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user