diff options
author | Philip P. Moltmann <moltmann@google.com> | 2020-08-20 19:30:31 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-08-20 19:30:31 +0000 |
commit | 4b867486850c75ae98f07525df6558f6c9063295 (patch) | |
tree | 7701594f15818200c3e48662369c5eb2bf2efa7a | |
parent | 8c21a4b602babcc1bdb20d70b8141e66c79264d6 (diff) | |
parent | fa51a6e20487c3cf4e552614eba47e9e5be70446 (diff) | |
download | PackageInstaller-4b867486850c75ae98f07525df6558f6c9063295.tar.gz |
Merge changes from topic "phone-call-indicators" into rvc-qpr-dev
* changes:
Show some limited system usage in ongoing usage dialog
Update to latests UI mocks.
Listen to real app-ops + small bug fixes
Special phone call handling in ReivewOngoingUsage
-rw-r--r-- | res/layout/ongoing_usage_dialog_content.xml | 17 | ||||
-rw-r--r-- | res/values/overlayable.xml | 4 | ||||
-rw-r--r-- | res/values/strings.xml | 17 | ||||
-rw-r--r-- | res/values/styles.xml | 26 | ||||
-rw-r--r-- | src/com/android/permissioncontroller/permission/data/OpUsageLiveData.kt | 127 | ||||
-rw-r--r-- | src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragment.java | 111 |
6 files changed, 295 insertions, 7 deletions
diff --git a/res/layout/ongoing_usage_dialog_content.xml b/res/layout/ongoing_usage_dialog_content.xml index 219b4f9c0..f9a0f07f0 100644 --- a/res/layout/ongoing_usage_dialog_content.xml +++ b/res/layout/ongoing_usage_dialog_content.xml @@ -35,6 +35,23 @@ android:id="@+id/items_container" style="@style/PermissionUsageDialogItemsContainer"/> + <TextView + android:id="@+id/other_use_header" + android:text="@string/other_use" + style="@style/PermissionUsageDialogOtherUseHeader"/> + + <TextView + android:id="@+id/other_use_content" + style="@style/PermissionUsageDialogOtherUseContent"/> + + <View + android:id="@+id/other_use_inside_spacer" + style="@style/PermissionUsageDialogOtherUseInsideSpacer"/> + + <TextView + android:id="@+id/system_use_content" + style="@style/PermissionUsageDialogSystemUseContent"/> + </LinearLayout> </ScrollView> diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml index 162744295..aa683a43e 100644 --- a/res/values/overlayable.xml +++ b/res/values/overlayable.xml @@ -153,6 +153,10 @@ <item type="style" name="PermissionUsageDialogItemAppName" /> <item type="style" name="PermissionUsageDialogItemPermissionsList" /> <item type="style" name="PermissionUsageDialogItemIconsContainer" /> + <item type="style" name="PermissionUsageDialogOtherUseHeader" /> + <item type="style" name="PermissionUsageDialogOtherUseContent" /> + <item type="style" name="PermissionUsageDialogOtherUseInsideSpacer" /> + <item type="style" name="PermissionUsageDialogSystemUseContent" /> <!-- END ONGOING USAGE DIALOG --> <!-- START REQUEST ROLE DIALOG TITLE --> diff --git a/res/values/strings.xml b/res/values/strings.xml index 490817a2f..e7804ffb2 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -884,6 +884,23 @@ <!-- Label for the button to set an application as the default application [CHAR LIMIT=20] --> <string name="request_role_set_as_default">Set as default</string> + <!-- Message telling the user that a phone call is currently using the microphone [CHAR LIMIT=none] --> + <string name="phone_call_uses_microphone">Microphone is used in <b>phone call</b></string> + <!-- Message telling the user that a phone call is currently using the microphone and the camera [CHAR LIMIT=none] --> + <string name="phone_call_uses_microphone_and_camera">Camera and Microphone are used in <b>video call</b></string> + <!-- Message telling the user that a phone call is currently using the camera [CHAR LIMIT=none] --> + <string name="phone_call_uses_camera">Camera is used in <b>video call</b></string> + + <!-- Message telling the user that a system service is currently using the microphone [CHAR LIMIT=none] --> + <string name="system_uses_microphone">Microphone is accessed using system service</string> + <!-- Message telling the user that a system service is currently using the microphone and the camera [CHAR LIMIT=none] --> + <string name="system_uses_microphone_and_camera">Camera and Microphone are accessed using system service</string> + <!-- Message telling the user that a system service is currently using the camera [CHAR LIMIT=none] --> + <string name="system_uses_camera">Camera is accessed using system service</string> + + <!-- Line above a list of other apps and system service that are currently microphone or camera [CHAR LIMIT=60] --> + <string name="other_use">Other use:</string> + <!-- Action for accepting the Ongoing usage dialog [CHAR LIMIT=10]--> <string name="ongoing_usage_dialog_ok">Got it</string> diff --git a/res/values/styles.xml b/res/values/styles.xml index 496e95cb9..c1c8e2d2f 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -663,6 +663,32 @@ <item name="android:layout_gravity">end</item> </style> + <style name="PermissionUsageDialogOtherUseHeader"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">?android:textAppearanceListItemSecondary</item> + <item name="android:layout_marginStart">16dp</item> + </style> + + <style name="PermissionUsageDialogOtherUseContent"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">?android:textAppearanceListItemSecondary</item> + <item name="android:layout_marginStart">16dp</item> + </style> + + <style name="PermissionUsageDialogOtherUseInsideSpacer"> + <item name="android:layout_width">0dp</item> + <item name="android:layout_height">16dp</item> + </style> + + <style name="PermissionUsageDialogSystemUseContent"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textAppearance">?android:textAppearanceListItemSecondary</item> + <item name="android:layout_marginStart">16dp</item> + </style> + <!-- END ONGOING USAGE DIALOG --> <!-- START REQUEST ROLE DIALOG TITLE --> diff --git a/src/com/android/permissioncontroller/permission/data/OpUsageLiveData.kt b/src/com/android/permissioncontroller/permission/data/OpUsageLiveData.kt new file mode 100644 index 000000000..5101dd7ee --- /dev/null +++ b/src/com/android/permissioncontroller/permission/data/OpUsageLiveData.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2020 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.permissioncontroller.permission.data + +import android.app.AppOpsManager +import android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED +import android.app.Application +import android.os.UserHandle +import com.android.permissioncontroller.PermissionControllerApplication +import kotlinx.coroutines.Job +import java.util.function.Consumer + +/** + * LiveData that loads the last usage of each of a list of app ops for every package. + * + * <p>For app-ops with duration the end of the access is considered. + * + * @param app The current application + * @param opNames The names of the app ops we wish to search for + * @param usageDurationMs how much ago can an access have happened to be considered + */ +// TODO: listen for updates +class OpUsageLiveData( + private val app: Application, + private val opNames: List<String>, + private val usageDurationMs: Long +) : SmartAsyncMediatorLiveData<@JvmSuppressWildcards Map<String, List<OpAccess>>>(), + Consumer<AppOpsManager.HistoricalOps> { + val appOpsManager = app.getSystemService(AppOpsManager::class.java)!! + + override suspend fun loadDataAndPostValue(job: Job) { + val now = System.currentTimeMillis() + val opMap = mutableMapOf<String, MutableList<OpAccess>>() + + val packageOps = appOpsManager.getPackagesForOps(opNames.toTypedArray()) + for (packageOp in packageOps) { + for (opEntry in packageOp.ops) { + val user = UserHandle.getUserHandleForUid(packageOp.uid) + val lastAccessTime: Long = opEntry.getLastAccessTime(OP_FLAGS_ALL_TRUSTED) + + if (lastAccessTime == -1L) { + // There was no access, so skip + continue + } + + var lastAccessDuration = opEntry.getLastDuration(OP_FLAGS_ALL_TRUSTED) + + // Some accesses have no duration + if (lastAccessDuration == -1L) { + lastAccessDuration = 0 + } + + if (opEntry.isRunning || + lastAccessTime + lastAccessDuration > (now - usageDurationMs)) { + val accessList = opMap.getOrPut(opEntry.opStr) { mutableListOf() } + val accessTime = if (opEntry.isRunning) { + -1 + } else { + lastAccessTime + } + accessList.add(OpAccess(packageOp.packageName, user, accessTime)) + } + } + } + + postValue(opMap) + } + + override fun accept(historicalOps: AppOpsManager.HistoricalOps) { + val opMap = mutableMapOf<String, MutableList<OpAccess>>() + for (i in 0 until historicalOps.uidCount) { + val historicalUidOps = historicalOps.getUidOpsAt(i) + val user = UserHandle.getUserHandleForUid(historicalUidOps.uid) + for (j in 0 until historicalUidOps.packageCount) { + val historicalPkgOps = historicalUidOps.getPackageOpsAt(j) + val pkgName = historicalPkgOps.packageName + for (k in 0 until historicalPkgOps.opCount) { + val historicalAttributedOps = historicalPkgOps.getAttributedOpsAt(k) + for (l in 0 until historicalAttributedOps.opCount) { + val historicalOp = historicalAttributedOps.getOpAt(l) + val opName = historicalOp.opName + + val accessList = opMap.getOrPut(opName) { mutableListOf() } + accessList.add(OpAccess(pkgName, user, -1)) + } + } + } + } + } + + override fun onActive() { + super.onActive() + updateAsync() + } + + companion object : DataRepository<Pair<List<String>, Long>, OpUsageLiveData>() { + override fun newValue(key: Pair<List<String>, Long>): OpUsageLiveData { + return OpUsageLiveData(PermissionControllerApplication.get(), key.first, key.second) + } + + operator fun get(ops: List<String>, usageDurationMs: Long): OpUsageLiveData { + return get(ops to usageDurationMs) + } + } +} + +data class OpAccess(val packageName: String?, val user: UserHandle?, val lastAccessTime: Long) { + companion object { + const val IS_RUNNING = -1L + } + + fun isRunning() = lastAccessTime == IS_RUNNING +} diff --git a/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragment.java b/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragment.java index 89e804c16..41b8a21b8 100644 --- a/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragment.java +++ b/src/com/android/permissioncontroller/permission/ui/handheld/ReviewOngoingUsageFragment.java @@ -28,9 +28,12 @@ import static com.android.permissioncontroller.permission.debug.UtilsKt.shouldSh import android.app.AlertDialog; import android.content.Context; import android.content.Intent; +import android.location.LocationManager; import android.os.Bundle; import android.os.UserHandle; +import android.text.Html; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; @@ -40,30 +43,43 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Observer; import androidx.preference.PreferenceFragmentCompat; import com.android.permissioncontroller.PermissionControllerStatsLog; import com.android.permissioncontroller.R; +import com.android.permissioncontroller.permission.data.OpAccess; +import com.android.permissioncontroller.permission.data.OpUsageLiveData; import com.android.permissioncontroller.permission.debug.PermissionUsages; +import com.android.permissioncontroller.permission.model.AppPermissionGroup; import com.android.permissioncontroller.permission.model.AppPermissionUsage; import com.android.permissioncontroller.permission.model.AppPermissionUsage.GroupUsage; -import com.android.permissioncontroller.permission.model.AppPermissionGroup; import com.android.permissioncontroller.permission.model.legacy.PermissionApps; import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp; +import com.android.permissioncontroller.permission.utils.KotlinUtils; import com.android.permissioncontroller.permission.utils.Utils; import java.text.Collator; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * A dialog listing the currently uses of camera, microphone, and location. */ public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat { + // TODO: Replace with OPSTR... APIs + static final String PHONE_CALL = "android:phone_call_microphone"; + static final String VIDEO_CALL = "android:phone_call_camera"; + private @NonNull PermissionUsages mPermissionUsages; + private boolean mPermissionUsagesLoaded; private @Nullable AlertDialog mDialog; + private OpUsageLiveData mOpUsageLiveData; + private @Nullable Map<String, List<OpAccess>> mOpUsage; + private ArraySet<String> mSystemUsage = new ArraySet<>(0); private long mStartTime; /** @@ -89,13 +105,27 @@ public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat { if (shouldShowPermissionsDashboard()) { permissions = new String[] {CAMERA, LOCATION, MICROPHONE}; } + ArrayList<String> appOps = new ArrayList<>(List.of(PHONE_CALL, VIDEO_CALL)); + mOpUsageLiveData = OpUsageLiveData.Companion.get(appOps, numMillis); + mOpUsageLiveData.observe(this, new Observer<Map<String, List<OpAccess>>>() { + @Override + public void onChanged(Map<String, List<OpAccess>> opUsage) { + mOpUsage = opUsage; + mOpUsageLiveData.removeObserver(this); + + if (mPermissionUsagesLoaded) { + onPermissionUsagesLoaded(); + } + } + }); mPermissionUsages.load(null, permissions, mStartTime, Long.MAX_VALUE, PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(), false, false, this::onPermissionUsagesLoaded, false); } private void onPermissionUsagesLoaded() { - if (getActivity() == null) { + mPermissionUsagesLoaded = true; + if (getActivity() == null || mOpUsage == null) { return; } @@ -130,11 +160,13 @@ public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat { } } - if (!Utils.isGroupOrBgGroupUserSensitive(groupUsage.getGroup())) { - continue; + if (Utils.isGroupOrBgGroupUserSensitive(groupUsage.getGroup())) { + usedGroups.add(appGroups.get(groupNum)); + } else if (getContext().getSystemService(LocationManager.class).isProviderPackage( + appUsage.getPackageName()) + && (groupName.equals(CAMERA) || groupName.equals(MICROPHONE))) { + mSystemUsage.add(groupName); } - - usedGroups.add(appGroups.get(groupNum)); } if (!usedGroups.isEmpty()) { @@ -143,7 +175,7 @@ public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat { } } - if (usages.isEmpty()) { + if (usages.isEmpty() && mOpUsage.isEmpty() && mSystemUsage.isEmpty()) { getActivity().finish(); return; } @@ -220,6 +252,71 @@ public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat { } } + TextView otherUseHeader = contentView.requireViewById(R.id.other_use_header); + TextView otherUseContent = contentView.requireViewById(R.id.other_use_content); + TextView systemUseContent = contentView.requireViewById(R.id.system_use_content); + View otherUseSpacer = contentView.requireViewById(R.id.other_use_inside_spacer); + + if (mOpUsage.isEmpty() && mSystemUsage.isEmpty()) { + otherUseHeader.setVisibility(View.GONE); + otherUseContent.setVisibility(View.GONE); + } + + if (numUsages == 0) { + otherUseHeader.setVisibility(View.GONE); + appsList.setVisibility(View.GONE); + } + + if (mOpUsage.isEmpty() || mSystemUsage.isEmpty()) { + otherUseSpacer.setVisibility(View.GONE); + } + + if (mOpUsage.isEmpty()) { + otherUseContent.setVisibility(View.GONE); + } + + if (mSystemUsage.isEmpty()) { + systemUseContent.setVisibility(View.GONE); + } + + if (!mOpUsage.isEmpty()) { + if (mOpUsage.containsKey(VIDEO_CALL) && mOpUsage.containsKey( + PHONE_CALL)) { + otherUseContent.setText( + Html.fromHtml(getString(R.string.phone_call_uses_microphone_and_camera), + 0)); + } else if (mOpUsage.containsKey(VIDEO_CALL)) { + otherUseContent.setText( + Html.fromHtml(getString(R.string.phone_call_uses_camera), 0)); + } else if (mOpUsage.containsKey(PHONE_CALL)) { + otherUseContent.setText( + Html.fromHtml(getString(R.string.phone_call_uses_microphone), 0)); + } + + if (mOpUsage.containsKey(VIDEO_CALL)) { + usedGroups.put(CAMERA, KotlinUtils.INSTANCE.getPermGroupLabel(context, CAMERA)); + } + + if (mOpUsage.containsKey(PHONE_CALL)) { + usedGroups.put(MICROPHONE, + KotlinUtils.INSTANCE.getPermGroupLabel(context, MICROPHONE)); + } + } + + if (!mSystemUsage.isEmpty()) { + if (mSystemUsage.contains(MICROPHONE) && mSystemUsage.contains(CAMERA)) { + systemUseContent.setText(getString(R.string.system_uses_microphone_and_camera)); + } else if (mSystemUsage.contains(CAMERA)) { + systemUseContent.setText(getString(R.string.system_uses_camera)); + } else if (mSystemUsage.contains(MICROPHONE) ) { + systemUseContent.setText(getString(R.string.system_uses_microphone)); + } + + for (String usage : mSystemUsage) { + usedGroups.put(usage, KotlinUtils.INSTANCE.getPermGroupLabel(context, usage)); + } + } + // Add the layout for each app. for (int usageNum = 0; usageNum < numUsages; usageNum++) { Pair<AppPermissionUsage, List<GroupUsage>> usage = usages.get(usageNum); |