Merge "Separates logic from Chip. Chip is notch-aware."

This commit is contained in:
Fabian Kozynski
2018-11-13 14:03:25 +00:00
committed by Android (Google) Code Review
6 changed files with 269 additions and 73 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>)
}
}

View File

@@ -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) {

View File

@@ -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))
}
}