1 /* 2 * Copyright (C) 2020 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; 18 19 import static android.Manifest.permission_group.CAMERA; 20 import static android.Manifest.permission_group.LOCATION; 21 import static android.Manifest.permission_group.MICROPHONE; 22 23 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED; 24 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS; 25 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM; 26 import static com.android.permissioncontroller.permission.debug.UtilsKt.shouldShowPermissionsDashboard; 27 28 import android.app.AlertDialog; 29 import android.content.BroadcastReceiver; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.location.LocationManager; 34 import android.media.AudioManager; 35 import android.os.Bundle; 36 import android.os.UserHandle; 37 import android.text.Html; 38 import android.util.ArrayMap; 39 import android.util.ArraySet; 40 import android.util.Pair; 41 import android.view.LayoutInflater; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.widget.ImageView; 45 import android.widget.TextView; 46 47 import androidx.annotation.NonNull; 48 import androidx.annotation.Nullable; 49 import androidx.lifecycle.Observer; 50 import androidx.preference.PreferenceFragmentCompat; 51 52 import com.android.permissioncontroller.PermissionControllerStatsLog; 53 import com.android.permissioncontroller.R; 54 import com.android.permissioncontroller.permission.data.OpAccess; 55 import com.android.permissioncontroller.permission.data.OpUsageLiveData; 56 import com.android.permissioncontroller.permission.debug.PermissionUsages; 57 import com.android.permissioncontroller.permission.model.AppPermissionGroup; 58 import com.android.permissioncontroller.permission.model.AppPermissionUsage; 59 import com.android.permissioncontroller.permission.model.AppPermissionUsage.GroupUsage; 60 import com.android.permissioncontroller.permission.model.legacy.PermissionApps; 61 import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp; 62 import com.android.permissioncontroller.permission.utils.KotlinUtils; 63 import com.android.permissioncontroller.permission.utils.Utils; 64 65 import java.text.Collator; 66 import java.time.Instant; 67 import java.util.ArrayList; 68 import java.util.List; 69 import java.util.Map; 70 71 /** 72 * A dialog listing the currently uses of camera, microphone, and location. 73 */ 74 public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat { 75 76 // TODO: Replace with OPSTR... APIs 77 static final String PHONE_CALL = "android:phone_call_microphone"; 78 static final String VIDEO_CALL = "android:phone_call_camera"; 79 80 private AudioManager mAudioManager; 81 private @NonNull PermissionUsages mPermissionUsages; 82 private boolean mPermissionUsagesLoaded; 83 private @Nullable AlertDialog mDialog; 84 private OpUsageLiveData mOpUsageLiveData; 85 private @Nullable Map<String, List<OpAccess>> mOpUsage; 86 private ArraySet<String> mSystemUsage = new ArraySet<>(0); 87 private long mStartTime; 88 89 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 90 @Override 91 public void onReceive(Context context, Intent intent) { 92 onPermissionUsagesLoaded(); 93 } 94 }; 95 96 /** 97 * @return A new {@link ReviewOngoingUsageFragment} 98 */ newInstance(long numMillis)99 public static ReviewOngoingUsageFragment newInstance(long numMillis) { 100 ReviewOngoingUsageFragment fragment = new ReviewOngoingUsageFragment(); 101 Bundle arguments = new Bundle(); 102 arguments.putLong(Intent.EXTRA_DURATION_MILLIS, numMillis); 103 fragment.setArguments(arguments); 104 return fragment; 105 } 106 107 @Override onCreate(Bundle savedInstanceState)108 public void onCreate(Bundle savedInstanceState) { 109 super.onCreate(savedInstanceState); 110 111 long numMillis = getArguments().getLong(Intent.EXTRA_DURATION_MILLIS); 112 113 mAudioManager = getContext().getSystemService(AudioManager.class); 114 getContext().registerReceiver(mReceiver, 115 new IntentFilter(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED)); 116 mPermissionUsages = new PermissionUsages(getActivity()); 117 mStartTime = Math.max(System.currentTimeMillis() - numMillis, Instant.EPOCH.toEpochMilli()); 118 String[] permissions = new String[]{CAMERA, MICROPHONE}; 119 if (shouldShowPermissionsDashboard()) { 120 permissions = new String[] {CAMERA, LOCATION, MICROPHONE}; 121 } 122 ArrayList<String> appOps = new ArrayList<>(List.of(PHONE_CALL, VIDEO_CALL)); 123 mOpUsageLiveData = OpUsageLiveData.Companion.get(appOps, numMillis); 124 mOpUsageLiveData.observeStale(this, new Observer<Map<String, List<OpAccess>>>() { 125 @Override 126 public void onChanged(Map<String, List<OpAccess>> opUsage) { 127 if (mOpUsageLiveData.isStale()) { 128 return; 129 } 130 mOpUsage = opUsage; 131 mOpUsageLiveData.removeObserver(this); 132 133 if (mPermissionUsagesLoaded) { 134 onPermissionUsagesLoaded(); 135 } 136 } 137 }); 138 mPermissionUsages.load(null, permissions, mStartTime, Long.MAX_VALUE, 139 PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(), false, false, 140 this::onPermissionUsagesLoaded, false); 141 } 142 onPermissionUsagesLoaded()143 private void onPermissionUsagesLoaded() { 144 mPermissionUsagesLoaded = true; 145 if (getActivity() == null || mOpUsage == null) { 146 return; 147 } 148 149 List<AppPermissionUsage> appPermissionUsages = mPermissionUsages.getUsages(); 150 151 List<Pair<AppPermissionUsage, List<GroupUsage>>> usages = new ArrayList<>(); 152 ArrayList<PermissionApp> permApps = new ArrayList<>(); 153 int numApps = appPermissionUsages.size(); 154 for (int appNum = 0; appNum < numApps; appNum++) { 155 AppPermissionUsage appUsage = appPermissionUsages.get(appNum); 156 157 List<GroupUsage> usedGroups = new ArrayList<>(); 158 List<GroupUsage> appGroups = appUsage.getGroupUsages(); 159 int numGroups = appGroups.size(); 160 for (int groupNum = 0; groupNum < numGroups; groupNum++) { 161 GroupUsage groupUsage = appGroups.get(groupNum); 162 String groupName = groupUsage.getGroup().getName(); 163 164 if (!groupUsage.isRunning()) { 165 if (groupUsage.getLastAccessDuration() == -1) { 166 if (groupUsage.getLastAccessTime() < mStartTime) { 167 continue; 168 } 169 } else { 170 // TODO: Warning: Only works for groups with a single permission as it is 171 // not guaranteed the last access time and duration refer to same permission 172 // in AppPermissionUsage#lastAccessAggregate 173 if (groupUsage.getLastAccessTime() + groupUsage.getLastAccessDuration() 174 < mStartTime) { 175 continue; 176 } 177 } 178 } 179 180 if (Utils.isGroupOrBgGroupUserSensitive(groupUsage.getGroup())) { 181 usedGroups.add(appGroups.get(groupNum)); 182 } else if (getContext().getSystemService(LocationManager.class).isProviderPackage( 183 appUsage.getPackageName()) 184 && (groupName.equals(CAMERA) || groupName.equals(MICROPHONE))) { 185 mSystemUsage.add(groupName); 186 } 187 } 188 189 if (!usedGroups.isEmpty()) { 190 usages.add(Pair.create(appUsage, usedGroups)); 191 permApps.add(appUsage.getApp()); 192 } 193 } 194 195 if (usages.isEmpty() && mOpUsage.isEmpty() && mSystemUsage.isEmpty()) { 196 getActivity().finish(); 197 return; 198 } 199 200 new PermissionApps.AppDataLoader(getActivity(), () -> showDialog(usages)) 201 .execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()])); 202 } 203 showDialog(@onNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages)204 private void showDialog(@NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) { 205 if (mDialog == null || !mDialog.isShowing()) { 206 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) 207 .setView(createDialogView(usages)) 208 .setPositiveButton(R.string.ongoing_usage_dialog_ok, (dialog, which) -> 209 PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED, 210 PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS)) 211 .setOnDismissListener((dialog) -> getActivity().finish()); 212 mDialog = builder.create(); 213 mDialog.show(); 214 } else { 215 mDialog.setView(createDialogView(usages)); 216 } 217 } 218 219 /** 220 * Get a list of permission labels. 221 * 222 * @param groups map<perm group name, perm group label> 223 * 224 * @return A localized string with the list of permissions 225 */ getListOfPermissionLabels(ArrayMap<String, CharSequence> groups)226 private CharSequence getListOfPermissionLabels(ArrayMap<String, CharSequence> groups) { 227 int numGroups = groups.size(); 228 229 if (numGroups == 1) { 230 return groups.valueAt(0); 231 } else if (numGroups == 2 && groups.containsKey(MICROPHONE) && groups.containsKey(CAMERA)) { 232 // Special case camera + mic permission to be localization friendly 233 return getContext().getString(R.string.permgroup_list_microphone_and_camera); 234 } else { 235 // TODO: Use internationalization safe concatenation 236 237 ArrayList<CharSequence> sortedGroups = new ArrayList<>(groups.values()); 238 Collator collator = Collator.getInstance( 239 getResources().getConfiguration().getLocales().get(0)); 240 sortedGroups.sort(collator); 241 242 StringBuilder listBuilder = new StringBuilder(); 243 244 for (int i = 0; i < numGroups; i++) { 245 listBuilder.append(sortedGroups.get(i)); 246 if (i < numGroups - 2) { 247 listBuilder.append(getString(R.string.ongoing_usage_dialog_separator)); 248 } else if (i < numGroups - 1) { 249 listBuilder.append(getString(R.string.ongoing_usage_dialog_last_separator)); 250 } 251 } 252 253 return listBuilder; 254 } 255 } 256 createDialogView( @onNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages)257 private @NonNull View createDialogView( 258 @NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) { 259 Context context = getActivity(); 260 LayoutInflater inflater = LayoutInflater.from(context); 261 View contentView = inflater.inflate(R.layout.ongoing_usage_dialog_content, null); 262 ViewGroup appsList = contentView.requireViewById(R.id.items_container); 263 264 // Compute all of the permission group labels that were used. 265 ArrayMap<String, CharSequence> usedGroups = new ArrayMap<>(); 266 int numUsages = usages.size(); 267 for (int usageNum = 0; usageNum < numUsages; usageNum++) { 268 List<GroupUsage> groups = usages.get(usageNum).second; 269 int numGroups = groups.size(); 270 for (int groupNum = 0; groupNum < numGroups; groupNum++) { 271 AppPermissionGroup group = groups.get(groupNum).getGroup(); 272 if (group.getName().equals(MICROPHONE) && mAudioManager.isMicrophoneMute()) { 273 continue; 274 } 275 usedGroups.put(group.getName(), group.getLabel()); 276 } 277 } 278 279 TextView otherUseHeader = contentView.requireViewById(R.id.other_use_header); 280 TextView otherUseContent = contentView.requireViewById(R.id.other_use_content); 281 TextView systemUseContent = contentView.requireViewById(R.id.system_use_content); 282 View otherUseSpacer = contentView.requireViewById(R.id.other_use_inside_spacer); 283 284 if (mOpUsage.isEmpty() && mSystemUsage.isEmpty()) { 285 otherUseHeader.setVisibility(View.GONE); 286 otherUseContent.setVisibility(View.GONE); 287 } 288 289 if (numUsages == 0) { 290 otherUseHeader.setVisibility(View.GONE); 291 appsList.setVisibility(View.GONE); 292 } 293 294 if (mOpUsage.isEmpty() || mSystemUsage.isEmpty()) { 295 otherUseSpacer.setVisibility(View.GONE); 296 } 297 298 if (mOpUsage.isEmpty()) { 299 otherUseContent.setVisibility(View.GONE); 300 } 301 302 if (mSystemUsage.isEmpty()) { 303 systemUseContent.setVisibility(View.GONE); 304 } 305 306 if (!mOpUsage.isEmpty()) { 307 boolean hasVideo = mOpUsage.containsKey(VIDEO_CALL); 308 boolean hasPhone = mOpUsage.containsKey(PHONE_CALL) 309 && !mAudioManager.isMicrophoneMute(); 310 if (hasVideo && hasPhone) { 311 otherUseContent.setText( 312 Html.fromHtml(getString(R.string.phone_call_uses_microphone_and_camera), 313 0)); 314 } else if (hasVideo && mAudioManager.isMicrophoneMute()) { 315 otherUseContent.setText( 316 Html.fromHtml(getString(R.string.phone_call_uses_camera), 0)); 317 } else if (hasPhone) { 318 otherUseContent.setText( 319 Html.fromHtml(getString(R.string.phone_call_uses_microphone), 0)); 320 } 321 322 if (hasVideo) { 323 usedGroups.put(CAMERA, KotlinUtils.INSTANCE.getPermGroupLabel(context, CAMERA)); 324 if (!mAudioManager.isMicrophoneMute()) { 325 usedGroups.put(MICROPHONE, 326 KotlinUtils.INSTANCE.getPermGroupLabel(context, MICROPHONE)); 327 } 328 } 329 330 if (hasPhone) { 331 usedGroups.put(MICROPHONE, 332 KotlinUtils.INSTANCE.getPermGroupLabel(context, MICROPHONE)); 333 } 334 } 335 336 if (!mSystemUsage.isEmpty()) { 337 if (mSystemUsage.contains(MICROPHONE) && mSystemUsage.contains(CAMERA) 338 && !mAudioManager.isMicrophoneMute()) { 339 systemUseContent.setText(getString(R.string.system_uses_microphone_and_camera)); 340 } else if (mSystemUsage.contains(CAMERA)) { 341 systemUseContent.setText(getString(R.string.system_uses_camera)); 342 } else if (mSystemUsage.contains(MICROPHONE) && !mAudioManager.isMicrophoneMute()) { 343 systemUseContent.setText(getString(R.string.system_uses_microphone)); 344 } 345 346 for (String usage : mSystemUsage) { 347 usedGroups.put(usage, KotlinUtils.INSTANCE.getPermGroupLabel(context, usage)); 348 } 349 } 350 351 // Add the layout for each app. 352 for (int usageNum = 0; usageNum < numUsages; usageNum++) { 353 Pair<AppPermissionUsage, List<GroupUsage>> usage = usages.get(usageNum); 354 PermissionApp app = usage.first.getApp(); 355 List<GroupUsage> groups = usage.second; 356 357 // Check if this uses only mic permission. If the mic is muted, do not show 358 if (groups.size() == 1) { 359 if (groups.get(0).getGroup().getName().equals(MICROPHONE) 360 && mAudioManager.isMicrophoneMute()) { 361 continue; 362 } 363 } 364 365 View itemView = inflater.inflate(R.layout.ongoing_usage_dialog_item, appsList, false); 366 367 ((TextView) itemView.requireViewById(R.id.app_name)).setText(app.getLabel()); 368 ((ImageView) itemView.requireViewById(R.id.app_icon)).setImageDrawable(app.getIcon()); 369 370 ArrayMap<String, CharSequence> usedGroupsThisApp = new ArrayMap<>(); 371 372 ViewGroup iconFrame = itemView.requireViewById(R.id.icons); 373 int numGroups = usages.get(usageNum).second.size(); 374 for (int groupNum = 0; groupNum < numGroups; groupNum++) { 375 AppPermissionGroup group = groups.get(groupNum).getGroup(); 376 if (mAudioManager.isMicrophoneMute() && group.getName().equals(MICROPHONE)) { 377 continue; 378 } 379 380 ViewGroup groupView = (ViewGroup) inflater.inflate(R.layout.image_view, null); 381 ((ImageView) groupView.requireViewById(R.id.icon)).setImageDrawable( 382 Utils.applyTint(context, group.getIconResId(), 383 android.R.attr.colorControlNormal)); 384 iconFrame.addView(groupView); 385 386 usedGroupsThisApp.put(group.getName(), group.getLabel()); 387 } 388 iconFrame.setVisibility(View.VISIBLE); 389 390 TextView permissionsList = itemView.requireViewById(R.id.permissionsList); 391 permissionsList.setText(getListOfPermissionLabels(usedGroupsThisApp)); 392 permissionsList.setVisibility(View.VISIBLE); 393 394 itemView.setOnClickListener((v) -> { 395 String packageName = app.getPackageName(); 396 PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED, 397 PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM); 398 UserHandle user = UserHandle.getUserHandleForUid(app.getUid()); 399 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS); 400 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 401 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); 402 intent.putExtra(Intent.EXTRA_USER, user); 403 context.startActivity(intent); 404 mDialog.dismiss(); 405 }); 406 407 appsList.addView(itemView); 408 } 409 410 ((TextView) contentView.requireViewById(R.id.title)).setText( 411 getString(R.string.ongoing_usage_dialog_title, 412 getListOfPermissionLabels(usedGroups))); 413 414 return contentView; 415 } 416 417 @Override onCreatePreferences(Bundle bundle, String s)418 public void onCreatePreferences(Bundle bundle, String s) { 419 // empty 420 } 421 422 @Override onDestroy()423 public void onDestroy() { 424 getContext().unregisterReceiver(mReceiver); 425 super.onDestroy(); 426 427 } 428 } 429