1 /*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 
17 package com.android.permissioncontroller.permission.ui.television;
18 
19 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
20 import static com.android.permissioncontroller.hibernation.HibernationPolicyKt.isHibernationEnabled;
21 
22 import android.app.ActionBar;
23 import android.app.Activity;
24 import android.app.Application;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager;
29 import android.graphics.drawable.Drawable;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.os.UserHandle;
33 import android.provider.Settings;
34 import android.text.BidiFormatter;
35 import android.util.ArraySet;
36 import android.util.Log;
37 import android.view.Menu;
38 import android.view.MenuInflater;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.widget.Toast;
42 
43 import androidx.lifecycle.ViewModelProvider;
44 import androidx.preference.Preference;
45 import androidx.preference.Preference.OnPreferenceClickListener;
46 import androidx.preference.PreferenceCategory;
47 import androidx.preference.PreferenceScreen;
48 import androidx.preference.PreferenceViewHolder;
49 import androidx.preference.SwitchPreference;
50 
51 import com.android.permissioncontroller.R;
52 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
53 import com.android.permissioncontroller.permission.model.AppPermissions;
54 import com.android.permissioncontroller.permission.model.livedatatypes.HibernationSettingState;
55 import com.android.permissioncontroller.permission.ui.ReviewPermissionsActivity;
56 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel;
57 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModelFactory;
58 import com.android.permissioncontroller.permission.utils.KotlinUtils;
59 import com.android.permissioncontroller.permission.utils.LocationUtils;
60 import com.android.permissioncontroller.permission.utils.SafetyNetLogger;
61 import com.android.permissioncontroller.permission.utils.Utils;
62 
63 public final class AppPermissionsFragment extends SettingsWithHeader
64         implements OnPreferenceClickListener {
65 
66     private static final String LOG_TAG = "ManagePermsFragment";
67 
68     static final String EXTRA_HIDE_INFO_BUTTON = "hideInfoButton";
69     private static final String AUTO_REVOKE_SWITCH_KEY = "_AUTO_REVOKE_SWITCH_KEY";
70     private static final String UNUSED_APPS_KEY = "_UNUSED_APPS_KEY";
71 
72     private static final int MENU_ALL_PERMS = 0;
73 
74     private ArraySet<AppPermissionGroup> mToggledGroups;
75     private AppPermissionGroupsViewModel mViewModel;
76     private AppPermissions mAppPermissions;
77     private PreferenceScreen mExtraScreen;
78 
79     private boolean mHasConfirmedRevoke;
80 
newInstance(String packageName, UserHandle user)81     public static AppPermissionsFragment newInstance(String packageName, UserHandle user) {
82         return setPackage(new AppPermissionsFragment(), packageName, user);
83     }
84 
setPackage( T fragment, String packageName, UserHandle user)85     private static <T extends PermissionsFrameFragment> T setPackage(
86             T fragment, String packageName, UserHandle user) {
87         Bundle arguments = new Bundle();
88         arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
89         arguments.putParcelable(Intent.EXTRA_USER, user);
90         fragment.setArguments(arguments);
91         return fragment;
92     }
93 
94     @Override
onCreate(Bundle savedInstanceState)95     public void onCreate(Bundle savedInstanceState) {
96         super.onCreate(savedInstanceState);
97         setLoading(true /* loading */, false /* animate */);
98         setHasOptionsMenu(true);
99         final ActionBar ab = getActivity().getActionBar();
100         if (ab != null) {
101             ab.setDisplayHomeAsUpEnabled(true);
102         }
103 
104         final String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
105         final UserHandle user = getArguments().getParcelable(Intent.EXTRA_USER);
106 
107         Activity activity = getActivity();
108         PackageInfo packageInfo = getPackageInfo(activity, packageName);
109         if (packageName == null) {
110             Toast.makeText(activity, R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show();
111             getActivity().finish();
112             return;
113         }
114 
115         mAppPermissions = new AppPermissions(activity, packageInfo, true,
116                 () -> getActivity().finish());
117 
118         if (mAppPermissions.isReviewRequired()) {
119             Intent intent = new Intent(getActivity(), ReviewPermissionsActivity.class);
120             intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
121             intent.putExtra(Intent.EXTRA_USER, user);
122             startActivity(intent);
123             getActivity().finish();
124             return;
125         }
126     }
127 
128     @Override
onResume()129     public void onResume() {
130         super.onResume();
131         final String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
132         final UserHandle user = getArguments().getParcelable(Intent.EXTRA_USER);
133 
134         AppPermissionGroupsViewModelFactory factory =
135                 new AppPermissionGroupsViewModelFactory(packageName, user, 0);
136         mViewModel = new ViewModelProvider(this, factory).get(AppPermissionGroupsViewModel.class);
137         mViewModel.getAutoRevokeLiveData().observe(this, this::setAutoRevokeToggleState);
138 
139         mAppPermissions.refresh();
140         loadPreferences();
141         setPreferencesCheckedState();
142     }
143 
144     @Override
onOptionsItemSelected(MenuItem item)145     public boolean onOptionsItemSelected(MenuItem item) {
146         switch (item.getItemId()) {
147             case android.R.id.home: {
148                 getActivity().finish();
149                 return true;
150             }
151 
152             case MENU_ALL_PERMS: {
153                 PermissionsFrameFragment frag =
154                         AllAppPermissionsFragment.newInstance(
155                                 getArguments().getString(Intent.EXTRA_PACKAGE_NAME));
156                 getFragmentManager().beginTransaction()
157                         .replace(android.R.id.content, frag)
158                         .addToBackStack("AllPerms")
159                         .commit();
160                 return true;
161             }
162         }
163         return super.onOptionsItemSelected(item);
164     }
165 
166     @Override
onViewCreated(View view, Bundle savedInstanceState)167     public void onViewCreated(View view, Bundle savedInstanceState) {
168         super.onViewCreated(view, savedInstanceState);
169         if (mAppPermissions != null) {
170             bindUi(this,
171                 getArguments().getString(Intent.EXTRA_PACKAGE_NAME),
172                 getArguments().getParcelable(Intent.EXTRA_USER),
173                 R.string.app_permissions_decor_title);
174         }
175     }
176 
177     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)178     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
179         super.onCreateOptionsMenu(menu, inflater);
180         menu.add(Menu.NONE, MENU_ALL_PERMS, Menu.NONE, R.string.all_permissions);
181     }
182 
bindUi(SettingsWithHeader fragment, String packageName, UserHandle user, int decorTitleStringResId)183     static void bindUi(SettingsWithHeader fragment, String packageName,
184             UserHandle user, int decorTitleStringResId) {
185         final Activity activity = fragment.getActivity();
186         final Application application = activity.getApplication();
187 
188         CharSequence label = BidiFormatter.getInstance().unicodeWrap(
189                 KotlinUtils.INSTANCE.getPackageLabel(application, packageName, user));
190         Drawable icon= KotlinUtils.INSTANCE.getBadgedPackageIcon(application, packageName, user);
191 
192         Intent infoIntent = null;
193         if (!activity.getIntent().getBooleanExtra(EXTRA_HIDE_INFO_BUTTON, false)) {
194             infoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
195                     .setData(Uri.fromParts("package", packageName, null));
196         }
197 
198         fragment.setHeader(icon, label, infoIntent, fragment.getString(
199                 R.string.additional_permissions_decor_title));
200     }
201 
loadPreferences()202     private void loadPreferences() {
203         Context context = getPreferenceManager().getContext();
204         if (context == null) {
205             return;
206         }
207 
208         PreferenceScreen screen = getPreferenceScreen();
209         screen.removeAll();
210         screen.addPreference(createHeaderLineTwoPreference(context));
211 
212         if (mExtraScreen != null) {
213             mExtraScreen.removeAll();
214             mExtraScreen = null;
215         }
216 
217         final Preference extraPerms = new Preference(context);
218         extraPerms.setIcon(R.drawable.ic_toc);
219         extraPerms.setTitle(R.string.additional_permissions);
220 
221         for (AppPermissionGroup group : mAppPermissions.getPermissionGroups()) {
222             if (!Utils.shouldShowPermission(getContext(), group)) {
223                 continue;
224             }
225 
226             boolean isPlatform = group.getDeclaringPackage().equals(Utils.OS_PKG);
227 
228             Preference preference = new Preference(context);
229             preference.setOnPreferenceClickListener(this);
230             preference.setKey(group.getName());
231             Drawable icon = Utils.loadDrawable(context.getPackageManager(),
232                     group.getIconPkg(), group.getIconResId());
233             preference.setIcon(Utils.applyTint(getContext(), icon,
234                     android.R.attr.colorControlNormal));
235             preference.setTitle(group.getLabel());
236             if (group.isSystemFixed()) {
237                 preference.setSummary(getString(R.string.permission_summary_enabled_system_fixed));
238             } else if (group.isPolicyFixed()) {
239                 preference.setSummary(getString(R.string.permission_summary_enforced_by_policy));
240             }
241             preference.setPersistent(false);
242             preference.setEnabled(!group.isSystemFixed() && !group.isPolicyFixed());
243 
244             if (isPlatform) {
245                 screen.addPreference(preference);
246             } else {
247                 if (mExtraScreen == null) {
248                     mExtraScreen = getPreferenceManager().createPreferenceScreen(context);
249                     mExtraScreen.addPreference(createHeaderLineTwoPreference(context));
250                 }
251                 mExtraScreen.addPreference(preference);
252             }
253         }
254 
255         final String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
256         final UserHandle user = getArguments().getParcelable(Intent.EXTRA_USER);
257 
258         if (mExtraScreen != null) {
259             extraPerms.setOnPreferenceClickListener(preference -> {
260                 AdditionalPermissionsFragment frag = new AdditionalPermissionsFragment();
261                 setPackage(frag, packageName, user);
262                 frag.setTargetFragment(AppPermissionsFragment.this, 0);
263                 getFragmentManager().beginTransaction()
264                         .replace(android.R.id.content, frag)
265                         .addToBackStack(null)
266                         .commit();
267                 return true;
268             });
269             int count = mExtraScreen.getPreferenceCount() - 1;
270             extraPerms.setSummary(getResources().getQuantityString(
271                     R.plurals.additional_permissions_more, count, count));
272             screen.addPreference(extraPerms);
273         }
274 
275         addAutoRevokePreferences(getPreferenceScreen());
276 
277         setLoading(false /* loading */, true /* animate */);
278     }
279 
280     /**
281      * Creates a heading below decor_title and above the rest of the preferences. This heading
282      * displays the app name and banner icon. It's used in both system and additional permissions
283      * fragments for each app. The styling used is the same as a leanback preference with a
284      * customized background color
285      * @param context The context the preferences created on
286      * @return The preference header to be inserted as the first preference in the list.
287      */
createHeaderLineTwoPreference(Context context)288     private Preference createHeaderLineTwoPreference(Context context) {
289         Preference headerLineTwo = new Preference(context) {
290             @Override
291             public void onBindViewHolder(PreferenceViewHolder holder) {
292                 super.onBindViewHolder(holder);
293                 holder.itemView.setBackgroundColor(
294                         getResources().getColor(R.color.lb_header_banner_color));
295             }
296         };
297         headerLineTwo.setKey(HEADER_PREFERENCE_KEY);
298         headerLineTwo.setSelectable(false);
299         headerLineTwo.setTitle(mLabel);
300         headerLineTwo.setIcon(mIcon);
301         return headerLineTwo;
302     }
303 
304     @Override
onPreferenceClick(final Preference preference)305     public boolean onPreferenceClick(final Preference preference) {
306         String groupName = preference.getKey();
307         final AppPermissionGroup group = mAppPermissions.getPermissionGroup(groupName);
308 
309         if (group == null) {
310             return false;
311         }
312 
313         addToggledGroup(group);
314 
315         if (LocationUtils.isLocationGroupAndProvider(getContext(), group.getName(),
316                 group.getApp().packageName)) {
317             LocationUtils.showLocationDialog(getContext(), mAppPermissions.getAppLabel());
318             return false;
319         }
320 
321         AppPermissionFragment frag = new AppPermissionFragment();
322 
323         frag.setArguments(AppPermissionFragment.createArgs(
324                     /* packageName= */ group.getApp().packageName,
325                     /* permName= */ null,
326                     /* groupName= */ group.getName(),
327                     /* userHandle= */ group.getUser(),
328                     /* caller= */ null,
329                     /* sessionId= */ INVALID_SESSION_ID,
330                     /* grantCategory= */ null));
331         frag.setTargetFragment(AppPermissionsFragment.this, 0);
332         getFragmentManager().beginTransaction()
333                 .replace(android.R.id.content, frag)
334                 .addToBackStack(null)
335                 .commit();
336 
337         return true;
338     }
339 
340     @Override
onPause()341     public void onPause() {
342         mViewModel.getAutoRevokeLiveData().removeObservers(this);
343         super.onPause();
344         logToggledGroups();
345     }
346 
addToggledGroup(AppPermissionGroup group)347     private void addToggledGroup(AppPermissionGroup group) {
348         if (mToggledGroups == null) {
349             mToggledGroups = new ArraySet<>();
350         }
351         mToggledGroups.add(group);
352     }
353 
logToggledGroups()354     private void logToggledGroups() {
355         if (mToggledGroups != null) {
356             SafetyNetLogger.logPermissionsToggled(mToggledGroups);
357             mToggledGroups = null;
358         }
359     }
360 
setPreferencesCheckedState()361     private void setPreferencesCheckedState() {
362         setPreferencesCheckedState(getPreferenceScreen());
363         if (mExtraScreen != null) {
364             setPreferencesCheckedState(mExtraScreen);
365         }
366         setAutoRevokeToggleState(mViewModel.getAutoRevokeLiveData().getValue());
367     }
368 
setPreferencesCheckedState(PreferenceScreen screen)369     private void setPreferencesCheckedState(PreferenceScreen screen) {
370         int preferenceCount = screen.getPreferenceCount();
371         for (int i = 0; i < preferenceCount; i++) {
372             Preference preference = screen.getPreference(i);
373             if (preference.getKey() == null) {
374                 continue;
375             }
376             AppPermissionGroup group = mAppPermissions.getPermissionGroup(preference.getKey());
377             if (group == null) {
378                 continue;
379             }
380             AppPermissionGroup backgroundGroup = group.getBackgroundPermissions();
381 
382             if (group.areRuntimePermissionsGranted()) {
383                 if (backgroundGroup == null) {
384                     preference.setSummary(R.string.app_permission_button_allow);
385                 } else {
386                     if (backgroundGroup.areRuntimePermissionsGranted()) {
387                         preference.setSummary(R.string.permission_access_always);
388                     } else {
389                         preference.setSummary(R.string.permission_access_only_foreground);
390                     }
391                 }
392             } else {
393                 if (group.isOneTime()) {
394                     preference.setSummary(R.string.app_permission_button_ask);
395                 } else {
396                     preference.setSummary(R.string.permission_access_never);
397                 }
398             }
399         }
400     }
401 
402 
addAutoRevokePreferences(PreferenceScreen screen)403     private void addAutoRevokePreferences(PreferenceScreen screen) {
404         SwitchPreference autoRevokeSwitch =
405                 new SwitchPreference(screen.getPreferenceManager().getContext());
406         autoRevokeSwitch.setLayoutResource(R.layout.preference_permissions_revoke);
407         autoRevokeSwitch.setOnPreferenceClickListener((preference) -> {
408             mViewModel.setAutoRevoke(autoRevokeSwitch.isChecked());
409             android.util.Log.w(LOG_TAG, "setAutoRevoke " + autoRevokeSwitch.isChecked());
410             return true;
411         });
412         autoRevokeSwitch.setTitle(isHibernationEnabled() ? R.string.unused_apps_label
413                 : R.string.auto_revoke_label);
414         autoRevokeSwitch.setSummary(R.string.auto_revoke_summary);
415         autoRevokeSwitch.setKey(AUTO_REVOKE_SWITCH_KEY);
416         if (isHibernationEnabled()) {
417             PreferenceCategory unusedAppsCategory = new PreferenceCategory(
418                     screen.getPreferenceManager().getContext());
419             unusedAppsCategory.setKey(UNUSED_APPS_KEY);
420             unusedAppsCategory.setTitle(R.string.unused_apps);
421             unusedAppsCategory.addPreference(autoRevokeSwitch);
422             screen.addPreference(unusedAppsCategory);
423         } else {
424             screen.addPreference(autoRevokeSwitch);
425         }
426     }
427 
setAutoRevokeToggleState(HibernationSettingState state)428     private void setAutoRevokeToggleState(HibernationSettingState state) {
429         SwitchPreference autoRevokeSwitch = getPreferenceScreen().findPreference(
430                 AUTO_REVOKE_SWITCH_KEY);
431         if (state == null || autoRevokeSwitch == null) {
432             return;
433         }
434         if (!state.isEnabledGlobal() || state.getRevocableGroupNames().isEmpty()) {
435             if (isHibernationEnabled()) {
436                 getPreferenceScreen().findPreference(UNUSED_APPS_KEY).setVisible(false);
437             }
438             autoRevokeSwitch.setVisible(false);
439             return;
440         }
441         if (isHibernationEnabled()) {
442             getPreferenceScreen().findPreference(UNUSED_APPS_KEY).setVisible(true);
443         }
444         autoRevokeSwitch.setVisible(true);
445         autoRevokeSwitch.setChecked(state.isEnabledForApp());
446     }
447 
getPackageInfo(Activity activity, String packageName)448     private static PackageInfo getPackageInfo(Activity activity, String packageName) {
449         try {
450             return activity.getPackageManager().getPackageInfo(
451                     packageName, PackageManager.GET_PERMISSIONS);
452         } catch (PackageManager.NameNotFoundException e) {
453             Log.i(LOG_TAG, "No package:" + activity.getCallingPackage(), e);
454             return null;
455         }
456     }
457 
458     public static class AdditionalPermissionsFragment extends SettingsWithHeader {
459         AppPermissionsFragment mOuterFragment;
460 
461         @Override
onCreate(Bundle savedInstanceState)462         public void onCreate(Bundle savedInstanceState) {
463             mOuterFragment = (AppPermissionsFragment) getTargetFragment();
464             super.onCreate(savedInstanceState);
465             setHasOptionsMenu(true);
466         }
467 
468         @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)469         public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
470             setPreferenceScreen(mOuterFragment.mExtraScreen);
471         }
472 
473         @Override
onViewCreated(View view, Bundle savedInstanceState)474         public void onViewCreated(View view, Bundle savedInstanceState) {
475             super.onViewCreated(view, savedInstanceState);
476             bindUi(this,
477                 getArguments().getString(Intent.EXTRA_PACKAGE_NAME),
478                 getArguments().getParcelable(Intent.EXTRA_USER),
479                 R.string.additional_permissions_decor_title);
480         }
481 
482         @Override
onOptionsItemSelected(MenuItem item)483         public boolean onOptionsItemSelected(MenuItem item) {
484             switch (item.getItemId()) {
485             case android.R.id.home:
486                 getFragmentManager().popBackStack();
487                 return true;
488             }
489             return super.onOptionsItemSelected(item);
490         }
491     }
492 }
493