*/
public static final String HASH_SALT_MAX_DAYS = "hash_salt_max_days";
+ // Flag related to Privacy Indicators
+
+ /**
+ * Whether the Permissions Hub is showing.
+ */
+ public static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_enabled";
+
private SystemUiDeviceConfigFlags() { }
}
import android.content.Intent
import android.content.IntentFilter
import android.os.Handler
+import android.os.Looper
+import android.os.Message
import android.os.UserHandle
import android.os.UserManager
+import android.provider.DeviceConfig
import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
import com.android.systemui.Dependency.BG_HANDLER_NAME
import com.android.systemui.Dependency.MAIN_HANDLER_NAME
import com.android.systemui.R
import javax.inject.Named
import javax.inject.Singleton
+fun isPermissionsHubEnabled() = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false)
+
@Singleton
class PrivacyItemController @Inject constructor(
val context: Context,
@Named(BG_HANDLER_NAME) private val bgHandler: Handler
) : Dumpable {
- companion object {
+ @VisibleForTesting
+ internal companion object {
val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
AppOpsManager.OP_RECORD_AUDIO,
AppOpsManager.OP_COARSE_LOCATION,
Intent.ACTION_MANAGED_PROFILE_REMOVED)
const val TAG = "PrivacyItemController"
const val SYSTEM_UID = 1000
+ const val MSG_ADD_CALLBACK = 0
+ const val MSG_REMOVE_CALLBACK = 1
+ const val MSG_UPDATE_LISTENING_STATE = 2
}
@VisibleForTesting
val systemApp =
PrivacyApplication(context.getString(R.string.device_services), SYSTEM_UID, context)
private val callbacks = mutableListOf<WeakReference<Callback>>()
+ private val messageHandler = H(WeakReference(this), uiHandler.looper)
private val notifyChanges = Runnable {
val list = privacyList
uiHandler.post(notifyChanges)
}
+ private var indicatorsAvailable = isPermissionsHubEnabled()
+ @VisibleForTesting
+ internal val devicePropertyChangedListener =
+ object : DeviceConfig.OnPropertyChangedListener {
+ override fun onPropertyChanged(namespace: String, name: String, value: String?) {
+ if (DeviceConfig.NAMESPACE_PRIVACY.equals(namespace) &&
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED.equals(name)) {
+ indicatorsAvailable = java.lang.Boolean.parseBoolean(value)
+ messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
+ messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
+ }
+ }
+ }
+
private val cb = object : AppOpsController.Callback {
override fun onActiveStateChanged(
code: Int,
registerReceiver()
}
+ init {
+ DeviceConfig.addOnPropertyChangedListener(
+ DeviceConfig.NAMESPACE_PRIVACY, context.mainExecutor, devicePropertyChangedListener)
+ }
+
private fun unregisterReceiver() {
context.unregisterReceiver(userSwitcherReceiver)
}
bgHandler.post(updateListAndNotifyChanges)
}
- @VisibleForTesting
- internal fun setListening(listen: Boolean) {
+ /**
+ * Updates listening status based on whether there are callbacks and the indicators are enabled
+ *
+ * This is only called from private (add/remove)Callback and from the config listener, all in
+ * main thread.
+ */
+ private fun setListeningState() {
+ val listen = !callbacks.isEmpty() and indicatorsAvailable
if (listening == listen) return
listening = listen
if (listening) {
} else {
appOpsController.removeCallback(OPS, cb)
unregisterReceiver()
+ // Make sure that we remove all indicators and notify listeners if we are not
+ // listening anymore due to indicators being disabled
+ update(false)
}
}
private fun addCallback(callback: WeakReference<Callback>) {
callbacks.add(callback)
- if (callbacks.isNotEmpty() && !listening) setListening(true)
+ if (callbacks.isNotEmpty() && !listening) {
+ messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
+ messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
+ }
// Notify this callback if we didn't set to listening
- else uiHandler.post(NotifyChangesToCallback(callback.get(), privacyList))
+ else if (listening) uiHandler.post(NotifyChangesToCallback(callback.get(), privacyList))
}
private fun removeCallback(callback: WeakReference<Callback>) {
// Removes also if the callback is null
callbacks.removeIf { it.get()?.equals(callback.get()) ?: true }
- if (callbacks.isEmpty()) setListening(false)
+ if (callbacks.isEmpty()) {
+ messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
+ messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
+ }
}
fun addCallback(callback: Callback) {
- addCallback(WeakReference(callback))
+ messageHandler.obtainMessage(MSG_ADD_CALLBACK, callback).sendToTarget()
}
fun removeCallback(callback: Callback) {
- removeCallback(WeakReference(callback))
+ messageHandler.obtainMessage(MSG_REMOVE_CALLBACK, callback).sendToTarget()
}
private fun updatePrivacyList() {
-
+ if (!listening) {
+ privacyList = emptyList()
+ return
+ }
val list = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) }
.mapNotNull { toPrivacyItem(it) }.distinct()
privacyList = list
}
}
}
+
+ private class H(
+ private val outerClass: WeakReference<PrivacyItemController>,
+ looper: Looper
+ ) : Handler(looper) {
+ override fun handleMessage(msg: Message) {
+ super.handleMessage(msg)
+ when (msg.what) {
+ MSG_UPDATE_LISTENING_STATE -> outerClass.get()?.setListeningState()
+
+ MSG_ADD_CALLBACK -> {
+ if (msg.obj !is PrivacyItemController.Callback) return
+ outerClass.get()?.addCallback(
+ WeakReference(msg.obj as PrivacyItemController.Callback))
+ }
+
+ MSG_REMOVE_CALLBACK -> {
+ if (msg.obj !is PrivacyItemController.Callback) return
+ outerClass.get()?.removeCallback(
+ WeakReference(msg.obj as PrivacyItemController.Callback))
+ }
+ else -> {}
+ }
+ }
+ }
}
\ No newline at end of file
import android.os.Handler;
import android.os.Looper;
import android.provider.AlarmClock;
+import android.provider.DeviceConfig;
import android.provider.Settings;
import android.service.notification.ZenModeConfig;
import android.text.format.DateUtils;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.settingslib.Utils;
import com.android.systemui.BatteryMeterView;
import com.android.systemui.DualToneHandler;
import com.android.systemui.privacy.PrivacyDialogBuilder;
import com.android.systemui.privacy.PrivacyItem;
import com.android.systemui.privacy.PrivacyItemController;
+import com.android.systemui.privacy.PrivacyItemControllerKt;
import com.android.systemui.qs.QSDetail.Callback;
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.StatusBarIconController;
private OngoingPrivacyChip mPrivacyChip;
private Space mSpace;
private BatteryMeterView mBatteryRemainingIcon;
+ private boolean mPermissionsHubEnabled;
private PrivacyItemController mPrivacyItemController;
private boolean mHasTopCutout = false;
private boolean mPrivacyChipLogged = false;
+ private final DeviceConfig.OnPropertyChangedListener mPropertyListener =
+ new DeviceConfig.OnPropertyChangedListener() {
+ @Override
+ public void onPropertyChanged(String namespace, String name, String value) {
+ if (DeviceConfig.NAMESPACE_PRIVACY.equals(namespace)
+ && SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED.equals(
+ name)) {
+ mPermissionsHubEnabled = Boolean.valueOf(value);
+ StatusIconContainer iconContainer = findViewById(R.id.statusIcons);
+ iconContainer.setIgnoredSlots(getIgnoredIconSlots());
+ }
+ }
+ };
+
private PrivacyItemController.Callback mPICCallback = new PrivacyItemController.Callback() {
@Override
public void privacyChanged(List<PrivacyItem> privacyItems) {
mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
mRingerModeTextView.setSelected(true);
mNextAlarmTextView.setSelected(true);
+
+ mPermissionsHubEnabled = PrivacyItemControllerKt.isPermissionsHubEnabled();
+ // Change the ignored slots when DeviceConfig flag changes
+ DeviceConfig.addOnPropertyChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
+ mContext.getMainExecutor(), mPropertyListener);
+
}
private List<String> getIgnoredIconSlots() {
com.android.internal.R.string.status_bar_camera));
ignored.add(mContext.getResources().getString(
com.android.internal.R.string.status_bar_microphone));
- ignored.add(mContext.getResources().getString(
- com.android.internal.R.string.status_bar_location));
+ if (mPermissionsHubEnabled) {
+ ignored.add(mContext.getResources().getString(
+ com.android.internal.R.string.status_bar_location));
+ }
return ignored;
}
}
private void setChipVisibility(boolean chipVisible) {
- if (chipVisible) {
+ if (chipVisible && mPermissionsHubEnabled) {
mPrivacyChip.setVisibility(View.VISIBLE);
// Makes sure that the chip is logged as viewed at most once each time QS is opened
// mListening makes sure that the callback didn't return after the user closed QS
import com.android.systemui.UiOffloadThread;
import com.android.systemui.privacy.PrivacyItem;
import com.android.systemui.privacy.PrivacyItemController;
+import com.android.systemui.privacy.PrivacyItemControllerKt;
import com.android.systemui.privacy.PrivacyType;
import com.android.systemui.qs.tiles.DndTile;
import com.android.systemui.qs.tiles.RotationLockTile;
ZenModeController.Callback,
DeviceProvisionedListener,
KeyguardMonitor.Callback,
- PrivacyItemController.Callback {
+ PrivacyItemController.Callback,
+ LocationController.LocationChangeCallback {
private static final String TAG = "PhoneStatusBarPolicy";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
mKeyguardMonitor.addCallback(this);
mPrivacyItemController.addCallback(this);
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
+ mLocationController.addCallback(this);
SysUiServiceProvider.getComponent(mContext, CommandQueue.class).addCallback(this);
}
mIconController.setIconVisibility(mSlotLocation, showLocation);
}
+ @Override
+ public void onLocationActiveChanged(boolean active) {
+ if (!PrivacyItemControllerKt.isPermissionsHubEnabled()) updateLocation();
+ }
+
+ // Updates the status view based on the current state of location requests.
+ private void updateLocation() {
+ if (mLocationController.isLocationActive()) {
+ mIconController.setIconVisibility(mSlotLocation, true);
+ } else {
+ mIconController.setIconVisibility(mSlotLocation, false);
+ }
+ }
+
private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
}
/**
+ * Sets the list of ignored icon slots clearing the current list.
+ * @param slots names of the icons to ignore
+ */
+ public void setIgnoredSlots(List<String> slots) {
+ mIgnoredSlots.clear();
+ addIgnoredSlots(slots);
+ }
+
+ /**
* Layout is happening from end -> start
*/
private void calculateIconTranslations() {
import android.os.Handler
import android.os.UserHandle
import android.os.UserManager
-import androidx.test.filters.SmallTest
+import android.provider.DeviceConfig
+import android.provider.Settings.RESET_MODE_PACKAGE_DEFAULTS
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
import com.android.systemui.Dependency
-import com.android.systemui.Dependency.BG_HANDLER
-import com.android.systemui.Dependency.MAIN_HANDLER
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.appops.AppOpItem
import org.hamcrest.Matchers.hasItem
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
+import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThat
import org.junit.Assert.assertTrue
mContext.addMockSystemService(UserManager::class.java, userManager)
mContext.getOrCreateTestableResources().addOverride(R.string.device_services,
DEVICE_SERVICES_STRING)
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
+ "true", false)
doReturn(listOf(object : UserInfo() {
init {
privacyItemController = PrivacyItemController(mContext)
}
+ @After
+ fun tearDown() {
+ DeviceConfig.resetToDefaults(RESET_MODE_PACKAGE_DEFAULTS, DeviceConfig.NAMESPACE_PRIVACY)
+ }
+
@Test
fun testSetListeningTrueByAddingCallback() {
privacyItemController.addCallback(callback)
+ testableLooper.processAllMessages()
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS),
any(AppOpsController.Callback::class.java))
testableLooper.processAllMessages()
}
@Test
- fun testSetListeningTrue() {
- privacyItemController.setListening(true)
- verify(appOpsController).addCallback(eq(PrivacyItemController.OPS),
+ fun testSetListeningFalseByRemovingLastCallback() {
+ privacyItemController.addCallback(callback)
+ testableLooper.processAllMessages()
+ verify(appOpsController, never()).removeCallback(any(IntArray::class.java),
any(AppOpsController.Callback::class.java))
- }
-
- @Test
- fun testSetListeningFalse() {
- privacyItemController.setListening(true)
- privacyItemController.setListening(false)
+ privacyItemController.removeCallback(callback)
+ testableLooper.processAllMessages()
verify(appOpsController).removeCallback(eq(PrivacyItemController.OPS),
any(AppOpsController.Callback::class.java))
+ verify(callback).privacyChanged(emptyList())
}
@Test
fun testRegisterReceiver_allUsers() {
val spiedContext = spy(mContext)
val itemController = PrivacyItemController(spiedContext)
- itemController.setListening(true)
+ itemController.addCallback(callback)
+ testableLooper.processAllMessages()
verify(spiedContext, atLeastOnce()).registerReceiverAsUser(
eq(itemController.userSwitcherReceiver), eq(UserHandle.ALL), any(), eq(null),
eq(null))
assertEquals(list, privacyList)
assertTrue(list !== privacyList)
}
+
+ @Test
+ fun testNotListeningWhenIndicatorsDisabled() {
+ privacyItemController.devicePropertyChangedListener.onPropertyChanged(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
+ "false")
+ privacyItemController.addCallback(callback)
+ testableLooper.processAllMessages()
+ verify(appOpsController, never()).addCallback(eq(PrivacyItemController.OPS),
+ any(AppOpsController.Callback::class.java))
+ }
}
\ No newline at end of file