1 /*
2  * Copyright (C) 2018 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.role.ui;
18 
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.app.role.RoleManager;
23 import android.content.Context;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.content.pm.ApplicationInfo;
27 import android.graphics.drawable.Drawable;
28 import android.os.Bundle;
29 import android.os.Process;
30 import android.os.UserHandle;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.util.Pair;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.WindowManager;
38 import android.widget.AdapterView;
39 import android.widget.BaseAdapter;
40 import android.widget.CheckBox;
41 import android.widget.ImageView;
42 import android.widget.ListView;
43 import android.widget.TextView;
44 
45 import androidx.annotation.NonNull;
46 import androidx.annotation.Nullable;
47 import androidx.appcompat.content.res.AppCompatResources;
48 import androidx.fragment.app.DialogFragment;
49 import androidx.lifecycle.ViewModelProviders;
50 
51 import com.android.permissioncontroller.PermissionControllerStatsLog;
52 import com.android.permissioncontroller.R;
53 import com.android.permissioncontroller.permission.utils.PackageRemovalMonitor;
54 import com.android.permissioncontroller.permission.utils.Utils;
55 import com.android.permissioncontroller.role.model.Role;
56 import com.android.permissioncontroller.role.model.Roles;
57 import com.android.permissioncontroller.role.model.UserDeniedManager;
58 import com.android.permissioncontroller.role.utils.PackageUtils;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 import java.util.Objects;
63 
64 /**
65  * {@code Fragment} for a role request.
66  */
67 public class RequestRoleFragment extends DialogFragment {
68 
69     private static final String LOG_TAG = RequestRoleFragment.class.getSimpleName();
70 
71     private static final String STATE_DONT_ASK_AGAIN = RequestRoleFragment.class.getName()
72             + ".state.DONT_ASK_AGAIN";
73 
74     private String mRoleName;
75     private String mPackageName;
76 
77     private Role mRole;
78 
79     private ListView mListView;
80     private Adapter mAdapter;
81     @Nullable
82     private CheckBox mDontAskAgainCheck;
83 
84     private RequestRoleViewModel mViewModel;
85 
86     @Nullable
87     private PackageRemovalMonitor mPackageRemovalMonitor;
88 
89     /**
90      * Create a new instance of this fragment.
91      *
92      * @param roleName the name of the requested role
93      * @param packageName the package name of the application requesting the role
94      *
95      * @return a new instance of this fragment
96      */
newInstance(@onNull String roleName, @NonNull String packageName)97     public static RequestRoleFragment newInstance(@NonNull String roleName,
98             @NonNull String packageName) {
99         RequestRoleFragment fragment = new RequestRoleFragment();
100         Bundle arguments = new Bundle();
101         arguments.putString(Intent.EXTRA_ROLE_NAME, roleName);
102         arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
103         fragment.setArguments(arguments);
104         return fragment;
105     }
106 
107     @Override
onCreate(@ullable Bundle savedInstanceState)108     public void onCreate(@Nullable Bundle savedInstanceState) {
109         super.onCreate(savedInstanceState);
110 
111         Bundle arguments = getArguments();
112         mPackageName = arguments.getString(Intent.EXTRA_PACKAGE_NAME);
113         mRoleName = arguments.getString(Intent.EXTRA_ROLE_NAME);
114 
115         mRole = Roles.get(requireContext()).get(mRoleName);
116     }
117 
118     @NonNull
119     @Override
onCreateDialog(@ullable Bundle savedInstanceState)120     public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
121         AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(), getTheme());
122         Context context = builder.getContext();
123 
124         RoleManager roleManager = context.getSystemService(RoleManager.class);
125         List<String> currentPackageNames = roleManager.getRoleHolders(mRoleName);
126         if (currentPackageNames.contains(mPackageName)) {
127             Log.i(LOG_TAG, "Application is already a role holder, role: " + mRoleName
128                     + ", package: " + mPackageName);
129             reportRequestResult(PermissionControllerStatsLog
130                     .ROLE_REQUEST_RESULT_REPORTED__RESULT__IGNORED_ALREADY_GRANTED, null);
131             clearDeniedSetResultOkAndFinish();
132             return super.onCreateDialog(savedInstanceState);
133         }
134 
135         ApplicationInfo applicationInfo = PackageUtils.getApplicationInfo(mPackageName, context);
136         if (applicationInfo == null) {
137             Log.w(LOG_TAG, "Unknown application: " + mPackageName);
138             reportRequestResult(
139                     PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED__RESULT__IGNORED,
140                     null);
141             finish();
142             return super.onCreateDialog(savedInstanceState);
143         }
144         Drawable icon = Utils.getBadgedIcon(context, applicationInfo);
145         String applicationLabel = Utils.getAppLabel(applicationInfo, context);
146         String title = getString(mRole.getRequestTitleResource(), applicationLabel);
147 
148         LayoutInflater inflater = LayoutInflater.from(context);
149         View titleLayout = inflater.inflate(R.layout.request_role_title, null);
150         ImageView iconImage = titleLayout.requireViewById(R.id.icon);
151         iconImage.setImageDrawable(icon);
152         TextView titleText = titleLayout.requireViewById(R.id.title);
153         titleText.setText(title);
154 
155         View viewLayout = inflater.inflate(R.layout.request_role_view, null);
156         mListView = viewLayout.requireViewById(R.id.list);
157         mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
158         mListView.setOnItemClickListener((parent, view, position, id) -> onItemClicked(position));
159         mAdapter = new Adapter(mListView, mRole);
160         if (savedInstanceState != null) {
161             mAdapter.onRestoreInstanceState(savedInstanceState);
162         }
163         mListView.setAdapter(mAdapter);
164 
165         CheckBox dontAskAgainCheck = viewLayout.requireViewById(R.id.dont_ask_again);
166         boolean isDeniedOnce = UserDeniedManager.getInstance(context).isDeniedOnce(mRoleName,
167                 mPackageName);
168         dontAskAgainCheck.setVisibility(isDeniedOnce ? View.VISIBLE : View.GONE);
169         if (isDeniedOnce) {
170             mDontAskAgainCheck = dontAskAgainCheck;
171             mDontAskAgainCheck.setOnClickListener(view -> updateUi());
172             if (savedInstanceState != null) {
173                 boolean dontAskAgain = savedInstanceState.getBoolean(STATE_DONT_ASK_AGAIN);
174                 mDontAskAgainCheck.setChecked(dontAskAgain);
175                 mAdapter.setDontAskAgain(dontAskAgain);
176             }
177         }
178 
179         AlertDialog dialog = builder
180                 .setCustomTitle(titleLayout)
181                 .setView(viewLayout)
182                 // Set the positive button listener later to avoid the automatic dismiss behavior.
183                 .setPositiveButton(R.string.request_role_set_as_default, null)
184                 // The default behavior for a null listener is to dismiss the dialog, not cancel.
185                 .setNegativeButton(android.R.string.cancel, (dialog2, which) -> dialog2.cancel())
186                 .create();
187         dialog.getWindow().addSystemFlags(
188                 WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
189         dialog.setOnShowListener(dialog2 -> dialog.getButton(Dialog.BUTTON_POSITIVE)
190                 .setOnClickListener(view -> onSetAsDefault()));
191         return dialog;
192     }
193 
194     @Override
getDialog()195     public AlertDialog getDialog() {
196         return (AlertDialog) super.getDialog();
197     }
198 
199     @Override
onStart()200     public void onStart() {
201         super.onStart();
202 
203         Context context = requireContext();
204         if (PackageUtils.getApplicationInfo(mPackageName, context) == null) {
205             Log.w(LOG_TAG, "Unknown application: " + mPackageName);
206             reportRequestResult(
207                     PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED__RESULT__IGNORED,
208                     null);
209             finish();
210             return;
211         }
212 
213         mPackageRemovalMonitor = new PackageRemovalMonitor(context, mPackageName) {
214             @Override
215             protected void onPackageRemoved() {
216                 Log.w(LOG_TAG, "Application is uninstalled, role: " + mRoleName + ", package: "
217                         + mPackageName);
218                 reportRequestResult(
219                         PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED__RESULT__IGNORED,
220                         null);
221                 finish();
222             }
223         };
224         mPackageRemovalMonitor.register();
225 
226         // Postponed to onStart() so that the list view in dialog is created.
227         mViewModel = ViewModelProviders.of(this, new RequestRoleViewModel.Factory(mRole,
228                 requireActivity().getApplication())).get(RequestRoleViewModel.class);
229         mViewModel.getRoleLiveData().observe(this, this::onRoleDataChanged);
230         mViewModel.getManageRoleHolderStateLiveData().observe(this,
231                 this::onManageRoleHolderStateChanged);
232     }
233 
234     @Override
onSaveInstanceState(@onNull Bundle outState)235     public void onSaveInstanceState(@NonNull Bundle outState) {
236         super.onSaveInstanceState(outState);
237 
238         mAdapter.onSaveInstanceState(outState);
239         if (mDontAskAgainCheck != null) {
240             outState.putBoolean(STATE_DONT_ASK_AGAIN, mDontAskAgainCheck.isChecked());
241         }
242     }
243 
244     @Override
onStop()245     public void onStop() {
246         super.onStop();
247 
248         if (mPackageRemovalMonitor != null) {
249             mPackageRemovalMonitor.unregister();
250             mPackageRemovalMonitor = null;
251         }
252     }
253 
254     @Override
onCancel(@onNull DialogInterface dialog)255     public void onCancel(@NonNull DialogInterface dialog) {
256         super.onCancel(dialog);
257 
258         Log.i(LOG_TAG, "Dialog cancelled, role: " + mRoleName + ", package: " + mPackageName);
259         reportRequestResult(
260                 PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED,
261                 null);
262         setDeniedOnceAndFinish();
263     }
264 
onRoleDataChanged( @onNull List<Pair<ApplicationInfo, Boolean>> qualifyingApplications)265     private void onRoleDataChanged(
266             @NonNull List<Pair<ApplicationInfo, Boolean>> qualifyingApplications) {
267         mAdapter.replace(qualifyingApplications);
268         updateUi();
269     }
270 
onItemClicked(int position)271     private void onItemClicked(int position) {
272         mAdapter.onItemClicked(position);
273         updateUi();
274     }
275 
onSetAsDefault()276     private void onSetAsDefault() {
277         if (mDontAskAgainCheck != null && mDontAskAgainCheck.isChecked()) {
278             Log.i(LOG_TAG, "Request denied with don't ask again, role: " + mRoleName + ", package: "
279                     + mPackageName);
280             reportRequestResult(PermissionControllerStatsLog
281                     .ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_WITH_ALWAYS, null);
282             setDeniedAlwaysAndFinish();
283         } else {
284             setRoleHolder();
285         }
286     }
287 
setRoleHolder()288     private void setRoleHolder() {
289         String packageName = mAdapter.getCheckedPackageName();
290         Context context = requireContext();
291         UserHandle user = Process.myUserHandle();
292         if (packageName == null) {
293             reportRequestResult(PermissionControllerStatsLog
294                             .ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_GRANTED_ANOTHER,
295                     null);
296             mRole.onNoneHolderSelectedAsUser(user, context);
297             mViewModel.getManageRoleHolderStateLiveData().clearRoleHoldersAsUser(mRoleName, 0, user,
298                     context);
299         } else {
300             boolean isRequestingApplication = Objects.equals(packageName, mPackageName);
301             if (isRequestingApplication) {
302                 reportRequestResult(PermissionControllerStatsLog
303                         .ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_GRANTED, null);
304             } else {
305                 reportRequestResult(PermissionControllerStatsLog
306                         .ROLE_REQUEST_RESULT_REPORTED__RESULT__USER_DENIED_GRANTED_ANOTHER,
307                         packageName);
308             }
309             int flags = isRequestingApplication ? RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP : 0;
310             mViewModel.getManageRoleHolderStateLiveData().setRoleHolderAsUser(mRoleName,
311                     packageName, true, flags, user, context);
312         }
313     }
314 
onManageRoleHolderStateChanged(int state)315     private void onManageRoleHolderStateChanged(int state) {
316         switch (state) {
317             case ManageRoleHolderStateLiveData.STATE_IDLE:
318             case ManageRoleHolderStateLiveData.STATE_WORKING:
319                 updateUi();
320                 break;
321             case ManageRoleHolderStateLiveData.STATE_SUCCESS: {
322                 ManageRoleHolderStateLiveData liveData =
323                         mViewModel.getManageRoleHolderStateLiveData();
324                 String packageName = liveData.getLastPackageName();
325                 if (packageName != null) {
326                     mRole.onHolderSelectedAsUser(packageName, liveData.getLastUser(),
327                             requireContext());
328                 }
329                 if (Objects.equals(packageName, mPackageName)) {
330                     Log.i(LOG_TAG, "Application added as a role holder, role: " + mRoleName
331                             + ", package: " + mPackageName);
332                     clearDeniedSetResultOkAndFinish();
333                 } else {
334                     Log.i(LOG_TAG, "Request denied with another application added as a role holder,"
335                             + " role: " + mRoleName + ", package: " + mPackageName);
336                     setDeniedOnceAndFinish();
337                 }
338                 break;
339             }
340             case ManageRoleHolderStateLiveData.STATE_FAILURE:
341                 finish();
342                 break;
343         }
344     }
345 
updateUi()346     private void updateUi() {
347         boolean enabled = mViewModel.getManageRoleHolderStateLiveData().getValue()
348                 == ManageRoleHolderStateLiveData.STATE_IDLE;
349         mListView.setEnabled(enabled);
350         boolean dontAskAgain = mDontAskAgainCheck != null && mDontAskAgainCheck.isChecked();
351         mAdapter.setDontAskAgain(dontAskAgain);
352         AlertDialog dialog = getDialog();
353         boolean hasRoleData = mViewModel.getRoleLiveData().getValue() != null;
354         dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled && hasRoleData
355                 && (dontAskAgain || !mAdapter.isHolderApplicationChecked()));
356         dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(enabled);
357     }
358 
clearDeniedSetResultOkAndFinish()359     private void clearDeniedSetResultOkAndFinish() {
360         UserDeniedManager.getInstance(requireContext()).clearDenied(mRoleName, mPackageName);
361         requireActivity().setResult(Activity.RESULT_OK);
362         finish();
363     }
364 
setDeniedOnceAndFinish()365     private void setDeniedOnceAndFinish() {
366         UserDeniedManager.getInstance(requireContext()).setDeniedOnce(mRoleName, mPackageName);
367         finish();
368     }
369 
setDeniedAlwaysAndFinish()370     private void setDeniedAlwaysAndFinish() {
371         UserDeniedManager.getInstance(requireContext()).setDeniedAlways(mRoleName, mPackageName);
372         finish();
373     }
374 
finish()375     private void finish() {
376         requireActivity().finish();
377     }
378 
reportRequestResult(int result, @Nullable String grantedAnotherPackageName)379     private void reportRequestResult(int result, @Nullable String grantedAnotherPackageName) {
380         String holderPackageName = getHolderPackageName();
381         reportRequestResult(getApplicationUid(mPackageName), mPackageName, mRoleName,
382                 getQualifyingApplicationCount(), getQualifyingApplicationUid(holderPackageName),
383                 holderPackageName, getQualifyingApplicationUid(grantedAnotherPackageName),
384                 grantedAnotherPackageName, result);
385     }
386 
getApplicationUid(@onNull String packageName)387     private int getApplicationUid(@NonNull String packageName) {
388         int uid = getQualifyingApplicationUid(packageName);
389         if (uid != -1) {
390             return uid;
391         }
392         ApplicationInfo applicationInfo = PackageUtils.getApplicationInfo(packageName,
393                 requireActivity());
394         if (applicationInfo == null) {
395             return -1;
396         }
397         return applicationInfo.uid;
398     }
399 
getQualifyingApplicationUid(@ullable String packageName)400     private int getQualifyingApplicationUid(@Nullable String packageName) {
401         if (packageName == null || mAdapter == null) {
402             return -1;
403         }
404         int count = mAdapter.getCount();
405         for (int i = 0; i < count; i++) {
406             Pair<ApplicationInfo, Boolean> qualifyingApplication = mAdapter.getItem(i);
407             if (qualifyingApplication == null) {
408                 // Skip the "None" item.
409                 continue;
410             }
411             ApplicationInfo qualifyingApplicationInfo = qualifyingApplication.first;
412             if (Objects.equals(qualifyingApplicationInfo.packageName, packageName)) {
413                 return qualifyingApplicationInfo.uid;
414             }
415         }
416         return -1;
417     }
418 
getQualifyingApplicationCount()419     private int getQualifyingApplicationCount() {
420         if (mAdapter == null) {
421             return -1;
422         }
423         int count = mAdapter.getCount();
424         if (count > 0 && mAdapter.getItem(0) == null) {
425             // Exclude the "None" item.
426             --count;
427         }
428         return count;
429     }
430 
431     @Nullable
getHolderPackageName()432     private String getHolderPackageName() {
433         if (mAdapter == null) {
434             return null;
435         }
436         int count = mAdapter.getCount();
437         for (int i = 0; i < count; i++) {
438             Pair<ApplicationInfo, Boolean> qualifyingApplication = mAdapter.getItem(i);
439             if (qualifyingApplication == null) {
440                 // Skip the "None" item.
441                 continue;
442             }
443             boolean isHolderApplication = qualifyingApplication.second;
444             if (isHolderApplication) {
445                 return qualifyingApplication.first.packageName;
446             }
447         }
448         return null;
449     }
450 
reportRequestResult(int requestingUid, String requestingPackageName, String roleName, int qualifyingCount, int currentUid, String currentPackageName, int grantedAnotherUid, String grantedAnotherPackageName, int result)451     static void reportRequestResult(int requestingUid, String requestingPackageName,
452             String roleName, int qualifyingCount, int currentUid, String currentPackageName,
453             int grantedAnotherUid, String grantedAnotherPackageName, int result) {
454         Log.v(LOG_TAG, "Role request result"
455                 + " requestingUid=" + requestingUid
456                 + " requestingPackageName=" + requestingPackageName
457                 + " roleName=" + roleName
458                 + " qualifyingCount=" + qualifyingCount
459                 + " currentUid=" + currentUid
460                 + " currentPackageName=" + currentPackageName
461                 + " grantedAnotherUid=" + grantedAnotherUid
462                 + " grantedAnotherPackageName=" + grantedAnotherPackageName
463                 + " result=" + result);
464         PermissionControllerStatsLog.write(
465                 PermissionControllerStatsLog.ROLE_REQUEST_RESULT_REPORTED, requestingUid,
466                 requestingPackageName, roleName, qualifyingCount, currentUid, currentPackageName,
467                 grantedAnotherUid, grantedAnotherPackageName, result);
468     }
469 
470     private static class Adapter extends BaseAdapter {
471 
472         private static final String STATE_USER_CHECKED = Adapter.class.getName()
473                 + ".state.USER_CHECKED";
474         private static final String STATE_USER_CHECKED_PACKAGE_NAME = Adapter.class.getName()
475                 + ".state.USER_CHECKED_PACKAGE_NAME";
476 
477         private static final int LAYOUT_TRANSITION_DURATION_MILLIS = 150;
478 
479         @NonNull
480         private final ListView mListView;
481 
482         @NonNull
483         private final Role mRole;
484 
485         // We'll use a null to represent the "None" item.
486         @NonNull
487         private final List<Pair<ApplicationInfo, Boolean>> mQualifyingApplications =
488                 new ArrayList<>();
489 
490         private boolean mHasHolderApplication;
491 
492         private boolean mDontAskAgain;
493 
494         // If user has ever clicked an item to mark it as checked, we no longer automatically mark
495         // the current holder as checked.
496         private boolean mUserChecked;
497 
498         private boolean mPendingUserChecked;
499         // We may use a null to represent the "None" item.
500         @Nullable
501         private String mPendingUserCheckedPackageName;
502 
Adapter(@onNull ListView listView, @NonNull Role role)503         Adapter(@NonNull ListView listView, @NonNull Role role) {
504             mListView = listView;
505             mRole = role;
506         }
507 
onSaveInstanceState(@onNull Bundle outState)508         public void onSaveInstanceState(@NonNull Bundle outState) {
509             outState.putBoolean(STATE_USER_CHECKED, mUserChecked);
510             if (mUserChecked) {
511                 outState.putString(STATE_USER_CHECKED_PACKAGE_NAME, getCheckedPackageName());
512             }
513         }
514 
onRestoreInstanceState(@onNull Bundle savedInstanceState)515         public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
516             mPendingUserChecked = savedInstanceState.getBoolean(STATE_USER_CHECKED);
517             if (mPendingUserChecked) {
518                 mPendingUserCheckedPackageName = savedInstanceState.getString(
519                         STATE_USER_CHECKED_PACKAGE_NAME);
520             }
521         }
522 
setDontAskAgain(boolean dontAskAgain)523         public void setDontAskAgain(boolean dontAskAgain) {
524             if (mDontAskAgain == dontAskAgain) {
525                 return;
526             }
527             mDontAskAgain = dontAskAgain;
528             if (mDontAskAgain) {
529                 mUserChecked = false;
530                 updateItemChecked();
531             }
532             notifyDataSetChanged();
533         }
534 
onItemClicked(int position)535         public void onItemClicked(int position) {
536             mUserChecked = true;
537             // We may need to change description based on checked state.
538             notifyDataSetChanged();
539         }
540 
replace(@onNull List<Pair<ApplicationInfo, Boolean>> qualifyingApplications)541         public void replace(@NonNull List<Pair<ApplicationInfo, Boolean>> qualifyingApplications) {
542             mQualifyingApplications.clear();
543             if (mRole.shouldShowNone()) {
544                 mQualifyingApplications.add(0, null);
545             }
546             mQualifyingApplications.addAll(qualifyingApplications);
547             mHasHolderApplication = hasHolderApplication(qualifyingApplications);
548             notifyDataSetChanged();
549 
550             if (mPendingUserChecked) {
551                 restoreItemChecked();
552                 mPendingUserChecked = false;
553                 mPendingUserCheckedPackageName = null;
554             }
555 
556             if (!mUserChecked) {
557                 updateItemChecked();
558             }
559         }
560 
hasHolderApplication( @onNull List<Pair<ApplicationInfo, Boolean>> qualifyingApplications)561         private static boolean hasHolderApplication(
562                 @NonNull List<Pair<ApplicationInfo, Boolean>> qualifyingApplications) {
563             int qualifyingApplicationsSize = qualifyingApplications.size();
564             for (int i = 0; i < qualifyingApplicationsSize; i++) {
565                 Pair<ApplicationInfo, Boolean> qualifyingApplication = qualifyingApplications.get(
566                         i);
567                 boolean isHolderApplication = qualifyingApplication.second;
568 
569                 if (isHolderApplication) {
570                     return true;
571                 }
572             }
573             return false;
574         }
575 
restoreItemChecked()576         private void restoreItemChecked() {
577             if (mPendingUserCheckedPackageName == null) {
578                 if (mRole.shouldShowNone()) {
579                     mUserChecked = true;
580                     mListView.setItemChecked(0, true);
581                 }
582             } else {
583                 int count = getCount();
584                 for (int i = 0; i < count; i++) {
585                     Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(i);
586                     if (qualifyingApplication == null) {
587                         continue;
588                     }
589                     String packageName = qualifyingApplication.first.packageName;
590 
591                     if (Objects.equals(packageName, mPendingUserCheckedPackageName)) {
592                         mUserChecked = true;
593                         mListView.setItemChecked(i, true);
594                         break;
595                     }
596                 }
597             }
598         }
599 
updateItemChecked()600         private void updateItemChecked() {
601             if (!mHasHolderApplication) {
602                 if (mRole.shouldShowNone()) {
603                     mListView.setItemChecked(0, true);
604                 } else {
605                     mListView.clearChoices();
606                 }
607             } else {
608                 int count = getCount();
609                 for (int i = 0; i < count; i++) {
610                     Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(i);
611                     if (qualifyingApplication == null) {
612                         continue;
613                     }
614                     boolean isHolderApplication = qualifyingApplication.second;
615 
616                     if (isHolderApplication) {
617                         mListView.setItemChecked(i, true);
618                         break;
619                     }
620                 }
621             }
622         }
623 
624         @Nullable
getCheckedItem()625         public Pair<ApplicationInfo, Boolean> getCheckedItem() {
626             int position = mListView.getCheckedItemPosition();
627             return position != AdapterView.INVALID_POSITION ? getItem(position) : null;
628         }
629 
630         @Nullable
getCheckedPackageName()631         public String getCheckedPackageName() {
632             Pair<ApplicationInfo, Boolean> qualifyingApplication = getCheckedItem();
633             return qualifyingApplication == null ? null : qualifyingApplication.first.packageName;
634         }
635 
isHolderApplicationChecked()636         public boolean isHolderApplicationChecked() {
637             Pair<ApplicationInfo, Boolean> qualifyingApplication = getCheckedItem();
638             return qualifyingApplication == null ? !mHasHolderApplication
639                     : qualifyingApplication.second;
640         }
641 
642         @Override
hasStableIds()643         public boolean hasStableIds() {
644             return true;
645         }
646 
647         @Override
areAllItemsEnabled()648         public boolean areAllItemsEnabled() {
649             return false;
650         }
651 
652         @Override
getCount()653         public int getCount() {
654             return mQualifyingApplications.size();
655         }
656 
657         @Nullable
658         @Override
getItem(int position)659         public Pair<ApplicationInfo, Boolean> getItem(int position) {
660             return mQualifyingApplications.get(position);
661         }
662 
663         @Override
getItemId(int position)664         public long getItemId(int position) {
665             if (position >= getCount()) {
666                 // Work around AbsListView.confirmCheckedPositionsById() not respecting our count.
667                 return ListView.INVALID_ROW_ID;
668             }
669             Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(position);
670             return qualifyingApplication == null ? 0
671                     : qualifyingApplication.first.packageName.hashCode();
672         }
673 
674         @Override
isEnabled(int position)675         public boolean isEnabled(int position) {
676             if (!mDontAskAgain) {
677                 return true;
678             }
679             Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(position);
680             if (qualifyingApplication == null) {
681                 return !mHasHolderApplication;
682             } else {
683                 boolean isHolderApplication = qualifyingApplication.second;
684                 return isHolderApplication;
685             }
686         }
687 
688         @NonNull
689         @Override
getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)690         public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
691             Context context = parent.getContext();
692             View view = convertView;
693             ViewHolder holder;
694             if (view != null) {
695                 holder = (ViewHolder) view.getTag();
696             } else {
697                 view = LayoutInflater.from(context).inflate(R.layout.request_role_item, parent,
698                         false);
699                 holder = new ViewHolder(view);
700                 view.setTag(holder);
701 
702                 holder.titleAndSubtitleLayout.getLayoutTransition().setDuration(
703                         LAYOUT_TRANSITION_DURATION_MILLIS);
704             }
705 
706             view.setEnabled(isEnabled(position));
707 
708             Pair<ApplicationInfo, Boolean> qualifyingApplication = getItem(position);
709             Drawable icon;
710             String title;
711             String subtitle;
712             if (qualifyingApplication == null) {
713                 icon = AppCompatResources.getDrawable(context, R.drawable.ic_remove_circle);
714                 title = context.getString(R.string.default_app_none);
715                 subtitle = !mHasHolderApplication ? context.getString(
716                         R.string.request_role_current_default) : null;
717             } else {
718                 ApplicationInfo qualifyingApplicationInfo = qualifyingApplication.first;
719                 icon = Utils.getBadgedIcon(context, qualifyingApplicationInfo);
720                 title = Utils.getAppLabel(qualifyingApplicationInfo, context);
721                 boolean isHolderApplication = qualifyingApplication.second;
722                 subtitle = isHolderApplication
723                         ? context.getString(R.string.request_role_current_default)
724                         : mListView.isItemChecked(position)
725                                 ? context.getString(mRole.getRequestDescriptionResource()) : null;
726             }
727 
728             holder.iconImage.setImageDrawable(icon);
729             holder.titleText.setText(title);
730             holder.subtitleText.setVisibility(!TextUtils.isEmpty(subtitle) ? View.VISIBLE
731                     : View.GONE);
732             holder.subtitleText.setText(subtitle);
733 
734             return view;
735         }
736 
737         private static class ViewHolder {
738 
739             @NonNull
740             public final ImageView iconImage;
741             @NonNull
742             public final ViewGroup titleAndSubtitleLayout;
743             @NonNull
744             public final TextView titleText;
745             @NonNull
746             public final TextView subtitleText;
747 
ViewHolder(@onNull View view)748             ViewHolder(@NonNull View view) {
749                 iconImage = view.requireViewById(R.id.icon);
750                 titleAndSubtitleLayout = view.requireViewById(R.id.title_and_subtitle);
751                 titleText = view.requireViewById(R.id.title);
752                 subtitleText = view.requireViewById(R.id.subtitle);
753             }
754         }
755     }
756 }
757