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