diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
index 5e952e3c44137..ddefb6a43a6f1 100644
--- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
@@ -21,7 +21,8 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="@dimen/ongoing_appops_chip_margin"
- android:gravity="center_vertical|end"
+ android:gravity="center_vertical|center_horizontal"
+ android:layout_gravity="center_vertical|end"
android:orientation="horizontal"
android:paddingStart="@dimen/ongoing_appops_chip_side_padding"
android:paddingEnd="@dimen/ongoing_appops_chip_side_padding"
@@ -32,13 +33,17 @@
android:id="@+id/icons_container"
android:layout_height="match_parent"
android:layout_width="wrap_content"
- android:gravity="center_vertical|start"
+ android:layout_gravity="center_vertical|start"
+ android:gravity="center_vertical"
/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml
index 007070e3ffba8..e7f2c51d124b1 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml
@@ -27,6 +27,13 @@
android:paddingStart="@dimen/status_bar_padding_start"
android:paddingEnd="@dimen/status_bar_padding_end" >
+
+
+
+
+
+ android:orientation="horizontal"
+ android:gravity="center_vertical|end">
@@ -53,4 +68,5 @@
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:gravity="center_vertical|end" />
+
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
index 3953139d43fdb..fc1baeff706e0 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
@@ -14,21 +14,14 @@
package com.android.systemui.privacy
-import android.app.ActivityManager
-import android.app.AppOpsManager
import android.content.Context
import android.graphics.Color
-import android.os.UserHandle
-import android.os.UserManager
import android.util.AttributeSet
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
-import com.android.systemui.Dependency
import com.android.systemui.R
-import com.android.systemui.appops.AppOpItem
-import com.android.systemui.appops.AppOpsController
class OngoingPrivacyChip @JvmOverloads constructor(
context: Context,
@@ -37,37 +30,15 @@ class OngoingPrivacyChip @JvmOverloads constructor(
defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttrs, defStyleRes) {
- companion object {
- val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
- AppOpsManager.OP_RECORD_AUDIO,
- AppOpsManager.OP_COARSE_LOCATION,
- AppOpsManager.OP_FINE_LOCATION)
- }
-
private lateinit var appName: TextView
private lateinit var iconsContainer: LinearLayout
- private var privacyList = emptyList()
- private val appOpsController = Dependency.get(AppOpsController::class.java)
- private val userManager = context.getSystemService(UserManager::class.java)
- private val currentUser = ActivityManager.getCurrentUser()
- private val currentUserIds = userManager.getProfiles(currentUser).map { it.id }
- private var listening = false
-
- var builder = PrivacyDialogBuilder(context, privacyList)
-
- private val callback = object : AppOpsController.Callback {
- override fun onActiveStateChanged(
- code: Int,
- uid: Int,
- packageName: String,
- active: Boolean
- ) {
- val userId = UserHandle.getUserId(uid)
- if (userId in currentUserIds) {
- updatePrivacyList()
- }
+ var builder = PrivacyDialogBuilder(context, emptyList())
+ var privacyList = emptyList()
+ set(value) {
+ field = value
+ builder = PrivacyDialogBuilder(context, value)
+ updateView()
}
- }
override fun onFinishInflate() {
super.onFinishInflate()
@@ -76,36 +47,6 @@ class OngoingPrivacyChip @JvmOverloads constructor(
iconsContainer = findViewById(R.id.icons_container)
}
- fun setListening(listen: Boolean) {
- if (listening == listen) return
- listening = listen
- if (listening) {
- appOpsController.addCallback(OPS, callback)
- updatePrivacyList()
- } else {
- appOpsController.removeCallback(OPS, callback)
- }
- }
-
- private fun updatePrivacyList() {
- privacyList = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) }
- .mapNotNull { toPrivacyItem(it) }
- builder = PrivacyDialogBuilder(context, privacyList)
- updateView()
- }
-
- private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? {
- val type: PrivacyType = when (appOpItem.code) {
- AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
- AppOpsManager.OP_COARSE_LOCATION -> PrivacyType.TYPE_LOCATION
- AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
- AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
- else -> return null
- }
- val app = PrivacyApplication(appOpItem.packageName, context)
- return PrivacyItem(type, app, appOpItem.timeStarted)
- }
-
// Should only be called if the builder icons or app changed
private fun updateView() {
fun setIcons(dialogBuilder: PrivacyDialogBuilder, iconsContainer: ViewGroup) {
@@ -121,11 +62,9 @@ class OngoingPrivacyChip @JvmOverloads constructor(
}
if (privacyList.isEmpty()) {
- visibility = GONE
return
} else {
generateContentDescription()
- visibility = VISIBLE
setIcons(builder, iconsContainer)
appName.visibility = GONE
builder.app?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
new file mode 100644
index 0000000000000..5141e5055e9be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2018 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 com.android.systemui.privacy
+
+import android.app.ActivityManager
+import android.app.AppOpsManager
+import android.content.Context
+import android.os.Handler
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.systemui.Dependency
+import com.android.systemui.appops.AppOpItem
+import com.android.systemui.appops.AppOpsController
+
+class PrivacyItemController(val context: Context, val callback: Callback) {
+
+ companion object {
+ val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
+ AppOpsManager.OP_RECORD_AUDIO,
+ AppOpsManager.OP_COARSE_LOCATION,
+ AppOpsManager.OP_FINE_LOCATION)
+ }
+
+ private var privacyList = emptyList()
+ private val appOpsController = Dependency.get(AppOpsController::class.java)
+ private val userManager = context.getSystemService(UserManager::class.java)
+ private val currentUser = ActivityManager.getCurrentUser()
+ private val currentUserIds = userManager.getProfiles(currentUser).map { it.id }
+ private val bgHandler = Handler(Dependency.get(Dependency.BG_LOOPER))
+ private val uiHandler = Dependency.get(Dependency.MAIN_HANDLER)
+ private val notifyChanges = Runnable {
+ callback.privacyChanged(privacyList)
+ }
+ private val updateListAndNotifyChanges = Runnable {
+ updatePrivacyList()
+ uiHandler.post(notifyChanges)
+ }
+
+ private var listening = false
+
+ private val cb = object : AppOpsController.Callback {
+ override fun onActiveStateChanged(
+ code: Int,
+ uid: Int,
+ packageName: String,
+ active: Boolean
+ ) {
+ val userId = UserHandle.getUserId(uid)
+ if (userId in currentUserIds) {
+ update()
+ }
+ }
+ }
+
+ private fun update() {
+ bgHandler.post(updateListAndNotifyChanges)
+ }
+
+ fun setListening(listen: Boolean) {
+ if (listening == listen) return
+ listening = listen
+ if (listening) {
+ appOpsController.addCallback(OPS, cb)
+ update()
+ } else {
+ appOpsController.removeCallback(OPS, cb)
+ }
+ }
+
+ private fun updatePrivacyList() {
+ privacyList = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) }
+ .mapNotNull { toPrivacyItem(it) }
+ }
+
+ private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? {
+ val type: PrivacyType = when (appOpItem.code) {
+ AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
+ AppOpsManager.OP_COARSE_LOCATION -> PrivacyType.TYPE_LOCATION
+ AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
+ AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
+ else -> return null
+ }
+ val app = PrivacyApplication(appOpItem.packageName, context)
+ return PrivacyItem(type, app, appOpItem.timeStarted)
+ }
+
+ // Used by containing class to get notified of changes
+ interface Callback {
+ fun privacyChanged(privacyItems: List)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 3ee6195858d62..7929099282588 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -39,12 +39,15 @@ import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
+import android.view.DisplayCutout;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.RelativeLayout;
+import android.widget.Space;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
@@ -57,6 +60,8 @@ import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.privacy.OngoingPrivacyChip;
import com.android.systemui.privacy.OngoingPrivacyDialog;
+import com.android.systemui.privacy.PrivacyItem;
+import com.android.systemui.privacy.PrivacyItemController;
import com.android.systemui.qs.QSDetail.Callback;
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -70,6 +75,7 @@ import com.android.systemui.statusbar.policy.DateView;
import com.android.systemui.statusbar.policy.NextAlarmController;
import com.android.systemui.statusbar.policy.ZenModeController;
+import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -125,9 +131,11 @@ public class QuickStatusBarHeader extends RelativeLayout implements
private Clock mClockView;
private DateView mDateView;
private OngoingPrivacyChip mPrivacyChip;
+ private Space mSpace;
private NextAlarmController mAlarmController;
private ZenModeController mZenController;
+ private PrivacyItemController mPrivacyItemController;
/** Counts how many times the long press tooltip has been shown to the user. */
private int mShownCount;
@@ -138,16 +146,26 @@ public class QuickStatusBarHeader extends RelativeLayout implements
updateStatusText();
}
};
+ private boolean mHasTopCutout = false;
/**
* Runnable for automatically fading out the long press tooltip (as if it were animating away).
*/
private final Runnable mAutoFadeOutTooltipRunnable = () -> hideLongPressTooltip(false);
+ private PrivacyItemController.Callback mPICCallback = new PrivacyItemController.Callback() {
+ @Override
+ public void privacyChanged(List privacyItems) {
+ mPrivacyChip.setPrivacyList(privacyItems);
+ setChipVisibility(!privacyItems.isEmpty());
+ }
+ };
+
public QuickStatusBarHeader(Context context, AttributeSet attrs) {
super(context, attrs);
mAlarmController = Dependency.get(NextAlarmController.class);
mZenController = Dependency.get(ZenModeController.class);
+ mPrivacyItemController = new PrivacyItemController(context, mPICCallback);
mShownCount = getStoredShownCount();
}
@@ -194,6 +212,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mDateView = findViewById(R.id.date);
mPrivacyChip = findViewById(R.id.privacy_chip);
mPrivacyChip.setOnClickListener(this);
+ mSpace = findViewById(R.id.space);
}
private void updateStatusText() {
@@ -208,6 +227,16 @@ public class QuickStatusBarHeader extends RelativeLayout implements
}
}
+ private void setChipVisibility(boolean chipVisible) {
+ mBatteryMeterView.setVisibility(View.VISIBLE);
+ if (chipVisible) {
+ mPrivacyChip.setVisibility(View.VISIBLE);
+ if (mHasTopCutout) mBatteryMeterView.setVisibility(View.GONE);
+ } else {
+ mPrivacyChip.setVisibility(View.GONE);
+ }
+ }
+
private boolean updateRingerStatus() {
boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
CharSequence originalRingerText = mRingerModeTextView.getText();
@@ -411,8 +440,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ DisplayCutout cutout = insets.getDisplayCutout();
Pair padding = PhoneStatusBarView.cornerCutoutMargins(
- insets.getDisplayCutout(), getDisplay());
+ cutout, getDisplay());
if (padding == null) {
mSystemIconsView.setPaddingRelative(
getResources().getDimensionPixelSize(R.dimen.status_bar_padding_start), 0,
@@ -421,6 +451,22 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mSystemIconsView.setPadding(padding.first, 0, padding.second, 0);
}
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpace.getLayoutParams();
+ if (cutout != null) {
+ Rect topCutout = cutout.getBoundingRectTop();
+ if (topCutout.isEmpty()) {
+ mHasTopCutout = false;
+ lp.width = 0;
+ mSpace.setVisibility(View.GONE);
+ } else {
+ mHasTopCutout = true;
+ lp.width = topCutout.width();
+ mSpace.setVisibility(View.VISIBLE);
+ }
+ }
+ mSpace.setLayoutParams(lp);
+ // Decide whether to show BatteryMeterView
+ setChipVisibility(mPrivacyChip.getVisibility() == View.VISIBLE);
return super.onApplyWindowInsets(insets);
}
@@ -437,7 +483,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
return;
}
mHeaderQsPanel.setListening(listening);
- mPrivacyChip.setListening(listening);
+ mPrivacyItemController.setListening(listening);
mListening = listening;
if (listening) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt
new file mode 100644
index 0000000000000..48491d7ac88c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2018 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 com.android.systemui.privacy
+
+import android.app.AppOpsManager
+import android.os.Handler
+import android.support.test.filters.SmallTest
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import com.android.systemui.Dependency
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.appops.AppOpItem
+import com.android.systemui.appops.AppOpsController
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyList
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@RunWithLooper
+class PrivacyItemControllerTest : SysuiTestCase() {
+
+ @Mock
+ private lateinit var appOpsController: AppOpsController
+ @Mock
+ private lateinit var callback: PrivacyItemController.Callback
+
+ private lateinit var testableLooper: TestableLooper
+ private lateinit var privacyItemController: PrivacyItemController
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ testableLooper = TestableLooper.get(this)
+
+ appOpsController = mDependency.injectMockDependency(AppOpsController:: class.java)
+ mDependency.injectTestDependency(Dependency.BG_LOOPER, testableLooper.looper)
+ mDependency.injectTestDependency(Dependency.MAIN_HANDLER, Handler(testableLooper.looper))
+
+ doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, 0, "", 0)))
+ .`when`(appOpsController).getActiveAppOpsForUser(anyInt())
+
+ privacyItemController = PrivacyItemController(mContext, callback)
+ }
+ @Test
+ fun testSetListeningTrue() {
+ privacyItemController.setListening(true)
+ verify(appOpsController).addCallback(eq(PrivacyItemController.OPS),
+ any(AppOpsController.Callback::class.java))
+ testableLooper.processAllMessages()
+ verify(callback).privacyChanged(anyList())
+ }
+
+ @Test
+ fun testSetListeningFalse() {
+ privacyItemController.setListening(true)
+ privacyItemController.setListening(false)
+ verify(appOpsController).removeCallback(eq(PrivacyItemController.OPS),
+ any(AppOpsController.Callback:: class.java))
+ }
+}
\ No newline at end of file