1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.permissioncontroller.permission.ui.handheld.v31; 18 19 import static android.Manifest.permission_group.CAMERA; 20 import static android.Manifest.permission_group.MICROPHONE; 21 22 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED; 23 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS; 24 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM; 25 26 import android.app.Activity; 27 import android.app.AlertDialog; 28 import android.content.Intent; 29 import android.icu.text.ListFormatter; 30 import android.os.Bundle; 31 import android.os.UserHandle; 32 import android.text.Html; 33 import android.util.ArrayMap; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.ImageView; 38 import android.widget.TextView; 39 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 import androidx.lifecycle.ViewModelProvider; 43 import androidx.preference.PreferenceFragmentCompat; 44 45 import com.android.permissioncontroller.PermissionControllerStatsLog; 46 import com.android.permissioncontroller.R; 47 import com.android.permissioncontroller.permission.ui.model.v31.ReviewOngoingUsageViewModel; 48 import com.android.permissioncontroller.permission.ui.model.v31.ReviewOngoingUsageViewModel.PackageAttribution; 49 import com.android.permissioncontroller.permission.ui.model.v31.ReviewOngoingUsageViewModelFactory; 50 import com.android.permissioncontroller.permission.utils.KotlinUtils; 51 import com.android.permissioncontroller.permission.utils.Utils; 52 53 import java.text.Collator; 54 import java.util.ArrayList; 55 import java.util.Collection; 56 import java.util.List; 57 import java.util.Map; 58 import java.util.Set; 59 60 /** 61 * A dialog listing the currently uses of camera, microphone, and location. 62 */ 63 public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat { 64 private static final String LOG_TAG = ReviewOngoingUsageFragment.class.getSimpleName(); 65 66 // TODO: Replace with OPSTR... APIs 67 public static final String PHONE_CALL = "android:phone_call_microphone"; 68 public static final String VIDEO_CALL = "android:phone_call_camera"; 69 70 private @Nullable AlertDialog mDialog; 71 72 private ReviewOngoingUsageViewModel mViewModel; 73 74 // create new ViewModel in onStart, because viewModel is sometimes persisting after finish() 75 // TODO: determine why viewModel is doing this. 76 @Override onStart()77 public void onStart() { 78 super.onStart(); 79 80 ReviewOngoingUsageViewModelFactory factory = 81 new ReviewOngoingUsageViewModelFactory( 82 getArguments().getLong(Intent.EXTRA_DURATION_MILLIS), this, new Bundle()); 83 mViewModel = new ViewModelProvider(this, factory).get(ReviewOngoingUsageViewModel.class); 84 85 mViewModel.getUsages().observe(this, usages -> { 86 if (mViewModel.getUsages().isStale()) { 87 // Prevent stale data from being shown, if Dialog is shown twice in quick succession 88 return; 89 } 90 if (usages == null) { 91 getActivity().finishAfterTransition(); 92 return; 93 } 94 95 if (mDialog == null) { 96 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) 97 .setView(updateDialogView(usages)) 98 .setPositiveButton(R.string.ongoing_usage_dialog_ok, (dialog, which) -> 99 PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED, 100 PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS)) 101 .setOnDismissListener((dialog) -> getActivity().finishAfterTransition()); 102 mDialog = builder.create(); 103 mDialog.show(); 104 } else { 105 updateDialogView(usages); 106 } 107 mViewModel.getUsages().removeObservers(this); 108 }); 109 } 110 111 @Override onPause()112 public void onPause() { 113 super.onPause(); 114 if (mDialog != null && getActivity() != null && !getActivity().isFinishing()) { 115 mDialog.dismiss(); 116 } 117 } 118 119 /** 120 * Get a list of permission labels. 121 * 122 * @param groups map<perm group name, perm group label> 123 * 124 * @return A localized string with the list of permissions 125 */ getListOfPermissionLabels(ArrayMap<String, CharSequence> groups)126 private CharSequence getListOfPermissionLabels(ArrayMap<String, CharSequence> groups) { 127 int numGroups = groups.size(); 128 129 if (numGroups == 1) { 130 return groups.valueAt(0); 131 } else if (numGroups == 2 && groups.containsKey(MICROPHONE) && groups.containsKey(CAMERA)) { 132 // Special case camera + mic permission to be localization friendly 133 return getContext().getString(R.string.permgroup_list_microphone_and_camera); 134 } else { 135 ArrayList<CharSequence> sortedGroups = new ArrayList<>(groups.values()); 136 Collator collator = Collator.getInstance( 137 getResources().getConfiguration().getLocales().get(0)); 138 sortedGroups.sort(collator); 139 140 StringBuilder listBuilder = new StringBuilder(); 141 142 for (int i = 0; i < numGroups; i++) { 143 listBuilder.append(sortedGroups.get(i)); 144 if (i < numGroups - 2) { 145 listBuilder.append(getString(R.string.ongoing_usage_dialog_separator)); 146 } else if (i < numGroups - 1) { 147 listBuilder.append(getString(R.string.ongoing_usage_dialog_last_separator)); 148 } 149 } 150 151 return listBuilder; 152 } 153 } 154 updateDialogView(@onNull ReviewOngoingUsageViewModel.Usages allUsages)155 private @NonNull View updateDialogView(@NonNull ReviewOngoingUsageViewModel.Usages allUsages) { 156 Activity context = getActivity(); 157 158 LayoutInflater inflater = LayoutInflater.from(context); 159 View contentView = inflater.inflate(R.layout.ongoing_usage_dialog_content, null); 160 ViewGroup appsList = contentView.requireViewById(R.id.items_container); 161 Map<PackageAttribution, Set<String>> appUsages = allUsages.getAppUsages(); 162 Collection<String> callUsage = allUsages.getCallUsages(); 163 Map<PackageAttribution, List<CharSequence>> attrLabels = allUsages.getShownAttributions(); 164 165 // Compute all of the permission group labels that were used. 166 ArrayMap<String, CharSequence> usedGroups = new ArrayMap<>(); 167 for (Set<String> accessedPermGroupNames : appUsages.values()) { 168 for (String accessedPermGroupName : accessedPermGroupNames) { 169 usedGroups.put(accessedPermGroupName, KotlinUtils.INSTANCE.getPermGroupLabel( 170 context, accessedPermGroupName).toString()); 171 } 172 } 173 174 TextView otherUseHeader = contentView.requireViewById(R.id.other_use_header); 175 TextView otherUseContent = contentView.requireViewById(R.id.other_use_content); 176 177 boolean hasCallUsage = !callUsage.isEmpty(); 178 boolean hasAppUsages = !appUsages.isEmpty(); 179 180 if (!hasCallUsage) { 181 otherUseHeader.setVisibility(View.GONE); 182 otherUseContent.setVisibility(View.GONE); 183 } 184 185 if (!hasAppUsages) { 186 otherUseHeader.setVisibility(View.GONE); 187 appsList.setVisibility(View.GONE); 188 } 189 190 if (!hasCallUsage) { 191 otherUseContent.setVisibility(View.GONE); 192 } 193 194 if (hasCallUsage) { 195 if (callUsage.contains(VIDEO_CALL) && callUsage.contains(PHONE_CALL)) { 196 otherUseContent.setText( 197 Html.fromHtml(getString(R.string.phone_call_uses_microphone_and_camera), 198 0)); 199 } else if (callUsage.contains(VIDEO_CALL)) { 200 otherUseContent.setText( 201 Html.fromHtml(getString(R.string.phone_call_uses_camera), 0)); 202 } else if (callUsage.contains(PHONE_CALL)) { 203 otherUseContent.setText( 204 Html.fromHtml(getString(R.string.phone_call_uses_microphone), 0)); 205 } 206 207 if (callUsage.contains(VIDEO_CALL)) { 208 usedGroups.put(CAMERA, KotlinUtils.INSTANCE.getPermGroupLabel(context, CAMERA)); 209 } 210 211 if (callUsage.contains(PHONE_CALL)) { 212 usedGroups.put(MICROPHONE, KotlinUtils.INSTANCE.getPermGroupLabel(context, 213 MICROPHONE)); 214 } 215 } 216 217 // Add the layout for each app. 218 for (Map.Entry<PackageAttribution, Set<String>> usage : appUsages.entrySet()) { 219 String packageName = usage.getKey().getPackageName(); 220 UserHandle user = usage.getKey().getUser(); 221 222 Set<String> groups = usage.getValue(); 223 224 View itemView = inflater.inflate(R.layout.ongoing_usage_dialog_item, appsList, false); 225 226 ((TextView) itemView.requireViewById(R.id.app_name)) 227 .setText(KotlinUtils.INSTANCE.getPackageLabel(context.getApplication(), 228 packageName, user)); 229 ((ImageView) itemView.requireViewById(R.id.app_icon)) 230 .setImageDrawable(KotlinUtils.INSTANCE.getBadgedPackageIcon( 231 context.getApplication(), packageName, user)); 232 233 ArrayMap<String, CharSequence> usedGroupsThisApp = new ArrayMap<>(); 234 235 ViewGroup iconFrame = itemView.requireViewById(R.id.icons); 236 CharSequence specialGroupMessage = null; 237 for (String group : groups) { 238 ViewGroup groupView = (ViewGroup) inflater.inflate(R.layout.image_view, null); 239 ((ImageView) groupView.requireViewById(R.id.icon)).setImageDrawable( 240 Utils.applyTint(context, KotlinUtils.INSTANCE.getPermGroupIcon(context, 241 group), android.R.attr.colorControlNormal)); 242 iconFrame.addView(groupView); 243 244 CharSequence groupLabel = KotlinUtils.INSTANCE.getPermGroupLabel(context, group); 245 if (group.equals(MICROPHONE) && attrLabels.containsKey(usage.getKey())) { 246 specialGroupMessage = ListFormatter.getInstance().format( 247 attrLabels.get(usage.getKey())); 248 continue; 249 } 250 251 usedGroupsThisApp.put(group, groupLabel); 252 } 253 iconFrame.setVisibility(View.VISIBLE); 254 255 TextView permissionsList = itemView.requireViewById(R.id.permissionsList); 256 permissionsList.setText(specialGroupMessage != null 257 ? specialGroupMessage : getListOfPermissionLabels(usedGroupsThisApp)); 258 259 itemView.setOnClickListener((v) -> { 260 PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED, 261 PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM); 262 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS); 263 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 264 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); 265 intent.putExtra(Intent.EXTRA_USER, user); 266 context.startActivity(intent); 267 mDialog.dismiss(); 268 }); 269 270 appsList.addView(itemView); 271 } 272 273 ((TextView) contentView.requireViewById(R.id.title)).setText(getTitle(usedGroups)); 274 275 return contentView; 276 } 277 getTitle(ArrayMap<String, CharSequence> usedGroups)278 private CharSequence getTitle(ArrayMap<String, CharSequence> usedGroups) { 279 if (usedGroups.size() == 1 && usedGroups.keyAt(0).equals(MICROPHONE)) { 280 return getString(R.string.ongoing_usage_dialog_title_mic); 281 } else if (usedGroups.size() == 1 && usedGroups.keyAt(0).equals(CAMERA)) { 282 return getString(R.string.ongoing_usage_dialog_title_camera); 283 } else if (usedGroups.size() == 2 && usedGroups.containsKey(MICROPHONE) 284 && usedGroups.containsKey(CAMERA)) { 285 return getString(R.string.ongoing_usage_dialog_title_mic_camera); 286 } else { 287 return getString(R.string.ongoing_usage_dialog_title, 288 getListOfPermissionLabels(usedGroups)); 289 } 290 } 291 292 @Override onCreatePreferences(Bundle bundle, String s)293 public void onCreatePreferences(Bundle bundle, String s) { 294 // empty 295 } 296 } 297