Merge "Separates logic from Chip. Chip is notch-aware."
This commit is contained in:
committed by
Android (Google) Code Review
commit
ab96fe0b43
@@ -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"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/app_name"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:gravity="center_vertical|end"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:gravity="center_vertical"
|
||||
/>
|
||||
</com.android.systemui.privacy.OngoingPrivacyChip>
|
||||
@@ -27,6 +27,13 @@
|
||||
android:paddingStart="@dimen/status_bar_padding_start"
|
||||
android:paddingEnd="@dimen/status_bar_padding_end" >
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical|start" >
|
||||
|
||||
<com.android.systemui.statusbar.policy.Clock
|
||||
android:id="@+id/clock"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -38,13 +45,21 @@
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
|
||||
systemui:showDark="false" />
|
||||
</LinearLayout>
|
||||
|
||||
<android.widget.Space
|
||||
android:id="@+id/space"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical|center_horizontal"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|center_horizontal" />
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical|end">
|
||||
|
||||
<include layout="@layout/ongoing_privacy_chip" />
|
||||
|
||||
@@ -53,4 +68,5 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:gravity="center_vertical|end" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -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<PrivacyItem>()
|
||||
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<PrivacyItem>())
|
||||
var privacyList = emptyList<PrivacyItem>()
|
||||
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 {
|
||||
|
||||
@@ -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<PrivacyItem>()
|
||||
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<PrivacyItem>)
|
||||
}
|
||||
}
|
||||
@@ -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<PrivacyItem> 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<Integer, Integer> 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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user