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