blob: 55ea3dd1d5d5bf3f137c5e00b659c4c608b05434 [file] [log] [blame]
/*
* 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.packageinstaller.role.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.role.RoleManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import com.android.packageinstaller.PermissionControllerStatsLog;
import com.android.packageinstaller.permission.utils.PackageRemovalMonitor;
import com.android.packageinstaller.permission.utils.Utils;
import com.android.packageinstaller.role.model.Role;
import com.android.packageinstaller.role.model.Roles;
import com.android.packageinstaller.role.model.UserDeniedManager;
import com.android.packageinstaller.role.utils.PackageUtils;
import com.android.permissioncontroller.R;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* {@code Fragment} for a role request.
*/
public class RequestRoleFragment extends DialogFragment {
private static final String LOG_TAG = RequestRoleFragment.class.getSimpleName();
private static final String STATE_DONT_ASK_AGAIN = RequestRoleFragment.class.getName()
+ ".state.DONT_ASK_AGAIN";
private String mRoleName;
private String mPackageName;
private Role mRole;
private Adapter mAdapter;
private CheckBox mDontAskAgainCheck;
private RequestRoleViewModel mViewModel;
@Nullable
private PackageRemovalMonitor mPackageRemovalMonitor;
/**
* Create a new instance of this fragment.
*
* @param roleName the name of the requested role
* @param packageName the package name of the application requesting the role
*
* @return a new instance of this fragment
*/
public static RequestRoleFragment newInstance(@NonNull String roleName,
@NonNull String packageName) {
RequestRoleFragment fragment = new RequestRoleFragment();
Bundle arguments = new Bundle();
arguments.putString(Intent.EXTRA_ROLE_NAME, roleName);
arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
fragment.setArguments(arguments);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle arguments = getArguments();
mPackageName = arguments.getString(Intent.EXTRA_PACKAGE_NAME);
mRoleName = arguments.getString(Intent.EXTRA_ROLE_NAME);
mRole = Roles.get(requireContext()).get(mRoleName);
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(), getTheme());
Context context = builder.getContext();
RoleManager roleManager = context.getSystemService(RoleManager.class);
List<String> currentPackageNames = roleManager.getRoleHolders(mRoleName);
if (currentPackageNames.contains(mPackageName)) {
Log.i(LOG_TAG, "Application is already a role holder, role: " + mRoleName
+ ", package: " + mPackageName);
reportRequestResult(PermissionControllerStatsLog
.ROLE_REQUEST_RESULT_REPORTED__RESULT__IGNORED_ALREADY_GRANTED, null);
clearDeniedSetResultOkAndFinish();
return super.onCreateDialog(savedInstanceState);
}
ApplicationInfo applicationInfo = PackageUtils.getApplicationInfo(mPackageName, context);
if (applicationInfo == null) {
Log.w(LOG_TAG, "Unknown application: " + mPackageName);
reportRequestResult(
PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED__RESULT__IGNORED,
null);
finish();
return super.onCreateDialog(savedInstanceState);
}
Drawable icon = Utils.getBadgedIcon(context, applicationInfo);
String applicationLabel = Utils.getAppLabel(applicationInfo, context);
String title = getString(mRole.getRequestTitleResource(), applicationLabel);
LayoutInflater inflater = LayoutInflater.from(context);
View titleLayout = inflater.inflate(R.layout.request_role_title, null);
ImageView iconImage = titleLayout.findViewById(R.id.icon);
iconImage.setImageDrawable(icon);
TextView titleText = titleLayout.findViewById(R.id.title);
titleText.setText(title);
mAdapter = new Adapter(mRole);
if (savedInstanceState != null) {
mAdapter.onRestoreInstanceState(savedInstanceState);
}
View viewLayout = null;
if (UserDeniedManager.getInstance(context).isDeniedOnce(mRoleName, mPackageName)) {
viewLayout = inflater.inflate(R.layout.request_role_view, null);
mDontAskAgainCheck = viewLayout.findViewById(R.id.dont_ask_again);
mDontAskAgainCheck.setOnClickListener(view -> updateUi());
if (savedInstanceState != null) {
boolean dontAskAgain = savedInstanceState.getBoolean(STATE_DONT_ASK_AGAIN);
mDontAskAgainCheck.setChecked(dontAskAgain);
mAdapter.setDontAskAgain(dontAskAgain);
}
}
AlertDialog dialog = builder
.setCustomTitle(titleLayout)
.setSingleChoiceItems(mAdapter, AdapterView.INVALID_POSITION, (dialog2, which) ->
onItemClicked(which))
.setView(viewLayout)
// Set the positive button listener later to avoid the automatic dismiss behavior.
.setPositiveButton(R.string.request_role_set_as_default, null)
// The default behavior for a null listener is to dismiss the dialog, not cancel.
.setNegativeButton(android.R.string.cancel, (dialog2, which) -> dialog2.cancel())
.create();
dialog.getWindow().addSystemFlags(
WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
dialog.setOnShowListener(dialog2 -> dialog.getButton(Dialog.BUTTON_POSITIVE)
.setOnClickListener(view -> onSetAsDefault()));
return dialog;
}
@Override
public AlertDialog getDialog() {
return (AlertDialog) super.getDialog();
}
@Override
public void onStart() {
super.onStart();
Context context = requireContext();
if (PackageUtils.getApplicationInfo(mPackageName, context) == null) {
Log.w(LOG_TAG, "Unknown application: " + mPackageName);
reportRequestResult(
PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED__RESULT__IGNORED,
null);
finish();
return;
}
mPackageRemovalMonitor = new PackageRemovalMonitor(context, mPackageName) {
@Override
protected void onPackageRemoved() {
Log.w(LOG_TAG, "Application is uninstalled, role: " + mRoleName + ", package: "
+ mPackageName);
reportRequestResult(
PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED__RESULT__IGNORED,
null);
finish();
}
};
mPackageRemovalMonitor.register();
mAdapter.setListView(getDialog().getListView());
// Postponed to onStart() so that the list view in dialog is created.
mViewModel = ViewModelProviders.of(this, new RequestRoleViewModel.Factory(mRole,
requireActivity().getApplication())).get(RequestRoleViewModel.class);
mViewModel.getRoleLiveData().observe(this, mAdapter::replace);
mViewModel.getManageRoleHolderStateLiveData().observe(this,
this::onManageRoleHolderStateChanged);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
mAdapter.onSaveInstanceState(outState);
if (mDontAskAgainCheck != null) {
outState.putBoolean(STATE_DONT_ASK_AGAIN, mDontAskAgainCheck.isChecked());
}
}
@Override
public void onStop() {
super.onStop();
if (mPackageRemovalMonitor != null) {
mPackageRemovalMonitor.unregister();
mPackageRemovalMonitor = null;
}
}
@Override
public void onCancel(@NonNull DialogInterface dialog) {
super.onCancel(dialog);
Log.i(LOG_TAG, "Dialog cancelled, role: " + mRoleName + ", package: " + mPackageName);
reportRequestResult(
PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED,
null);
setDeniedOnceAndFinish();
}
private void onItemClicked(int position) {
mAdapter.onItemClicked(position);
updateUi();
}
private void onSetAsDefault() {
if (mDontAskAgainCheck != null && mDontAskAgainCheck.isChecked()) {
Log.i(LOG_TAG, "Request denied with don't ask again, role: " + mRoleName + ", package: "
+ mPackageName);
reportRequestResult(PermissionControllerStatsLog
.ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_ALWAYS, null);
setDeniedAlwaysAndFinish();
} else {
setRoleHolder();
}
}
private void setRoleHolder() {
String packageName = mAdapter.getCheckedPackageName();
Context context = requireContext();
UserHandle user = Process.myUserHandle();
if (packageName == null) {
reportRequestResult(PermissionControllerStatsLog
.ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_GRANTED_ANOTHER,
null);
mRole.onNoneHolderSelectedAsUser(user, context);
mViewModel.getManageRoleHolderStateLiveData().clearRoleHoldersAsUser(mRoleName, 0, user,
context);
} else {
boolean isRequestingApplication = Objects.equals(packageName, mPackageName);
if (isRequestingApplication) {
reportRequestResult(PermissionControllerStatsLog
.ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED, null);
} else {
reportRequestResult(PermissionControllerStatsLog
.ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_GRANTED_ANOTHER,
packageName);
}
int flags = isRequestingApplication ? RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP : 0;
mViewModel.getManageRoleHolderStateLiveData().setRoleHolderAsUser(mRoleName,
packageName, true, flags, user, context);
}
}
private void onManageRoleHolderStateChanged(int state) {
switch (state) {
case ManageRoleHolderStateLiveData.STATE_IDLE:
case ManageRoleHolderStateLiveData.STATE_WORKING:
updateUi();
break;
case ManageRoleHolderStateLiveData.STATE_SUCCESS: {
ManageRoleHolderStateLiveData liveData =
mViewModel.getManageRoleHolderStateLiveData();
String packageName = liveData.getLastPackageName();
if (packageName != null) {
mRole.onHolderSelectedAsUser(packageName, liveData.getLastUser(),
requireContext());
}
if (Objects.equals(packageName, mPackageName)) {
Log.i(LOG_TAG, "Application added as a role holder, role: " + mRoleName
+ ", package: " + mPackageName);
clearDeniedSetResultOkAndFinish();
} else {
Log.i(LOG_TAG, "Request denied with another application added as a role holder,"
+ " role: " + mRoleName + ", package: " + mPackageName);
setDeniedOnceAndFinish();
}
break;
}
case ManageRoleHolderStateLiveData.STATE_FAILURE:
finish();
break;
}
}
private void updateUi() {
AlertDialog dialog = getDialog();
boolean enabled = mViewModel.getManageRoleHolderStateLiveData().getValue()
== ManageRoleHolderStateLiveData.STATE_IDLE;
dialog.getListView().setEnabled(enabled);
boolean dontAskAgain = mDontAskAgainCheck != null && mDontAskAgainCheck.isChecked();
mAdapter.setDontAskAgain(dontAskAgain);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled && (dontAskAgain
|| !mAdapter.isHolderApplicationChecked()));
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(enabled);
}
private void clearDeniedSetResultOkAndFinish() {
UserDeniedManager.getInstance(requireContext()).clearDenied(mRoleName, mPackageName);
requireActivity().setResult(Activity.RESULT_OK);
finish();
}
private void setDeniedOnceAndFinish() {
UserDeniedManager.getInstance(requireContext()).setDeniedOnce(mRoleName, mPackageName);
finish();
}
private void setDeniedAlwaysAndFinish() {
UserDeniedManager.getInstance(requireContext()).setDeniedAlways(mRoleName, mPackageName);
finish();
}
private void finish() {
requireActivity().finish();
}
private void reportRequestResult(int result, @Nullable String grantedAnotherPackageName) {
String holderPackageName = getHolderPackageName();
reportRequestResult(getApplicationUid(mPackageName), mPackageName, mRoleName,
getQualifyingApplicationCount(), getQualifyingApplicationUid(holderPackageName),
holderPackageName, getQualifyingApplicationUid(grantedAnotherPackageName),
grantedAnotherPackageName, result);
}
private int getApplicationUid(@NonNull String packageName) {
int uid = getQualifyingApplicationUid(packageName);
if (uid != -1) {
return uid;
}
ApplicationInfo applicationInfo = PackageUtils.getApplicationInfo(packageName,
requireActivity());
if (applicationInfo == null) {
return -1;
}
return applicationInfo.uid;
}
private int getQualifyingApplicationUid(@Nullable String packageName) {
if (packageName == null || mAdapter == null) {
return -1;
}
int count = mAdapter.getCount();
for (int i = 0; i < count; i++) {
Pair<ApplicationInfo, Boolean> qualifyingApplication = mAdapter.getItem(i);
if (qualifyingApplication == null) {
// Skip the "None" item.
continue;
}
ApplicationInfo qualifyingApplicationInfo = qualifyingApplication.first;
if (Objects.equals(qualifyingApplicationInfo.packageName, packageName)) {
return qualifyingApplicationInfo.uid;
}
}
return -1;
}
private int getQualifyingApplicationCount() {
if (mAdapter == null) {
return -1;
}
int count = mAdapter.getCount();
if (count > 0 && mAdapter.getItem(0) == null) {
// Exclude the "None" item.
--count;
}
return count;
}
@Nullable
private String getHolderPackageName() {
if (mAdapter == null) {
return null;
}
int count = mAdapter.getCount();
for (int i = 0; i < count; i++) {
Pair<ApplicationInfo, Boolean> qualifyingApplication = mAdapter.getItem(i);
if (qualifyingApplication == null) {
// Skip the "None" item.
continue;
}
boolean isHolderApplication = qualifyingApplication.second;
if (isHolderApplication) {
return qualifyingApplication.first.packageName;
}
}
return null;
}
static void reportRequestResult(int requestingUid, String requestingPackageName,
String roleName, int qualifyingCount, int currentUid, String currentPackageName,
int grantedAnotherUid, String grantedAnotherPackageName, int result) {
Log.v(LOG_TAG, "Role request result"
+ " requestingUid=" + requestingUid
+ " requestingPackageName=" + requestingPackageName
+ " roleName=" + roleName
+ " qualifyingCount=" + qualifyingCount
+ " currentUid=" + currentUid
+ " currentPackageName=" + currentPackageName
+ " grantedAnotherUid=" + grantedAnotherUid
+ " grantedAnotherPackageName=" + grantedAnotherPackageName
+ " result=" + result);
PermissionControllerStatsLog.write(
PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED, requestingUid,
requestingPackageName, roleName, qualifyingCount, currentUid, currentPackageName,
grantedAnotherUid, grantedAnotherPackageName, result);
}
private static class Adapter extends BaseAdapter {
private static final String STATE_USER_CHECKED = Adapter.class.getName()
+ ".state.USER_CHECKED";
private static final String STATE_USER_CHECKED_PACKAGE_NAME = Adapter.class.getName()
+ ".state.USER_CHECKED_PACKAGE_NAME";
private static final int LAYOUT_TRANSITION_DURATION_MILLIS = 150;
@NonNull
private final Role mRole;
// We'll use a null to represent the "None" item.
@NonNull
private final List<Pair<ApplicationInfo, Boolean>> mQualifyingApplications =
new ArrayList<>();
private boolean mHasHolderApplication;
private ListView mListView;
private boolean mDontAskAgain;
// If user has ever clicked an item to mark it as checked, we no longer automatically mark
// the current holder as checked.
private boolean mUserChecked;
private boolean mPendingUserChecked;
// We may use a null to represent the "None" item.
@Nullable
private String mPendingUserCheckedPackageName;
Adapter(@NonNull Role role) {
mRole = role;
}
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(STATE_USER_CHECKED, mUserChecked);
if (mUserChecked) {
outState.putString(STATE_USER_CHECKED_PACKAGE_NAME, getCheckedPackageName());
}
}
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
mPendingUserChecked = savedInstanceState.getBoolean(STATE_USER_CHECKED);
if (mPendingUserChecked) {
mPendingUserCheckedPackageName = savedInstanceState.getString(
STATE_USER_CHECKED_PACKAGE_NAME);
}
}
public void setListView(@NonNull ListView listView) {
mListView = listView;
}
public void setDontAskAgain(boolean dontAskAgain) {
if (mDontAskAgain == dontAskAgain) {
return;
}
mDontAskAgain = dontAskAgain;
if (mDontAskAgain) {
mUserChecked = false;
updateItemChecked();
}
notifyDataSetChanged();
}
public void onItemClicked(int position) {
mUserChecked = true;
// We may need to change description based on checked state.
notifyDataSetChanged();
}
public void replace(@NonNull List<Pair<ApplicationInfo, Boolean>> qualifyingApplications) {
mQualifyingApplications.clear();
if (mRole.shouldShowNone()) {
mQualifyingApplications.add(0, null);
}
mQualifyingApplications.addAll(qualifyingApplications);
mHasHolderApplication = hasHolderApplication(qualifyingApplications);
notifyDataSetChanged();
if (mPendingUserChecked) {
restoreItemChecked();
mPendingUserChecked = false;
mPendingUserCheckedPackageName = null;
}
if (!mUserChecked) {
updateItemChecked();
}
}
private static boolean hasHolderApplication(
@NonNull List<Pair<ApplicationInfo, Boolean>> qualifyingApplications) {
int qualifyingApplicationsSize = qualifyingApplications.size();
for (int i = 0; i < qualifyingApplicationsSize; i++) {
Pair<ApplicationInfo, Boolean> qualifyingApplication = qualifyingApplications.get(
i);
boolean isHolderApplication = qualifyingApplication.second;
if (isHolderApplication) {
return true;
}
}
return false;
}
private void restoreItemChecked() {
if (mPendingUserCheckedPackageName == null) {
if (mRole.shouldShowNone()) {
mUserChecked = true;
mListView.setItemChecked(0, true);
}
} else {
int count = getCount();
for (int i = 0; i < count; i++) {
Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(i);
if (qualifyingApplication == null) {
continue;
}
String packageName = qualifyingApplication.first.packageName;
if (Objects.equals(packageName, mPendingUserCheckedPackageName)) {
mUserChecked = true;
mListView.setItemChecked(i, true);
break;
}
}
}
}
private void updateItemChecked() {
if (!mHasHolderApplication) {
if (mRole.shouldShowNone()) {
mListView.setItemChecked(0, true);
} else {
mListView.clearChoices();
}
} else {
int count = getCount();
for (int i = 0; i < count; i++) {
Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(i);
if (qualifyingApplication == null) {
continue;
}
boolean isHolderApplication = qualifyingApplication.second;
if (isHolderApplication) {
mListView.setItemChecked(i, true);
break;
}
}
}
}
@Nullable
public Pair<ApplicationInfo, Boolean> getCheckedItem() {
int position = mListView.getCheckedItemPosition();
return position != AdapterView.INVALID_POSITION ? getItem(position) : null;
}
@Nullable
public String getCheckedPackageName() {
Pair<ApplicationInfo, Boolean> qualifyingApplication = getCheckedItem();
return qualifyingApplication == null ? null : qualifyingApplication.first.packageName;
}
public boolean isHolderApplicationChecked() {
Pair<ApplicationInfo, Boolean> qualifyingApplication = getCheckedItem();
return qualifyingApplication == null ? !mHasHolderApplication
: qualifyingApplication.second;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public int getCount() {
return mQualifyingApplications.size();
}
@Nullable
@Override
public Pair<ApplicationInfo, Boolean> getItem(int position) {
return mQualifyingApplications.get(position);
}
@Override
public long getItemId(int position) {
Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(position);
return qualifyingApplication == null ? 0
: qualifyingApplication.first.packageName.hashCode();
}
@Override
public boolean isEnabled(int position) {
if (!mDontAskAgain) {
return true;
}
Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(position);
if (qualifyingApplication == null) {
return !mHasHolderApplication;
} else {
boolean isHolderApplication = qualifyingApplication.second;
return isHolderApplication;
}
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Context context = parent.getContext();
View view = convertView;
ViewHolder holder;
if (view != null) {
holder = (ViewHolder) view.getTag();
} else {
view = LayoutInflater.from(context).inflate(R.layout.request_role_item, parent,
false);
holder = new ViewHolder(view);
view.setTag(holder);
holder.titleAndSubtitleLayout.getLayoutTransition().setDuration(
LAYOUT_TRANSITION_DURATION_MILLIS);
}
view.setEnabled(isEnabled(position));
Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(position);
Drawable icon;
String title;
String subtitle;
if (qualifyingApplication == null) {
icon = AppCompatResources.getDrawable(context, R.drawable.ic_remove_circle);
title = context.getString(R.string.default_app_none);
subtitle = !mHasHolderApplication ? context.getString(
R.string.request_role_current_default) : null;
} else {
ApplicationInfo qualifyingApplicationInfo = qualifyingApplication.first;
icon = Utils.getBadgedIcon(context, qualifyingApplicationInfo);
title = Utils.getAppLabel(qualifyingApplicationInfo, context);
boolean isHolderApplication = qualifyingApplication.second;
subtitle = isHolderApplication
? context.getString(R.string.request_role_current_default)
: mListView.isItemChecked(position)
? context.getString(mRole.getRequestDescriptionResource()) : null;
}
holder.iconImage.setImageDrawable(icon);
holder.titleText.setText(title);
holder.subtitleText.setVisibility(!TextUtils.isEmpty(subtitle) ? View.VISIBLE
: View.GONE);
holder.subtitleText.setText(subtitle);
return view;
}
private static class ViewHolder {
@NonNull
public final ImageView iconImage;
@NonNull
public final ViewGroup titleAndSubtitleLayout;
@NonNull
public final TextView titleText;
@NonNull
public final TextView subtitleText;
ViewHolder(@NonNull View view) {
iconImage = Objects.requireNonNull(view.findViewById(R.id.icon));
titleAndSubtitleLayout = Objects.requireNonNull(view.findViewById(
R.id.title_and_subtitle));
titleText = Objects.requireNonNull(view.findViewById(R.id.title));
subtitleText = Objects.requireNonNull(view.findViewById(R.id.subtitle));
}
}
}
}