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
17package com.android.packageinstaller.permission.ui.wear;
18
19import android.Manifest;
20import android.app.Activity;
21import android.app.Fragment;
22import android.content.DialogInterface;
23import android.content.Intent;
24import android.content.pm.PackageInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.PermissionInfo;
27import android.os.Build;
28import android.os.Bundle;
29import android.os.UserHandle;
30import android.preference.Preference;
31import android.preference.PreferenceFragment;
32import android.preference.PreferenceScreen;
33import android.preference.SwitchPreference;
34import android.support.wearable.view.WearableDialogHelper;
35import android.util.Log;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.ViewGroup;
39import android.widget.Toast;
40
41import com.android.packageinstaller.R;
42import com.android.packageinstaller.permission.model.AppPermissionGroup;
43import com.android.packageinstaller.permission.model.AppPermissions;
44import com.android.packageinstaller.permission.model.Permission;
45import com.android.packageinstaller.permission.utils.ArrayUtils;
46import com.android.packageinstaller.permission.utils.LocationUtils;
47import com.android.packageinstaller.permission.utils.SafetyNetLogger;
48import com.android.packageinstaller.permission.utils.Utils;
49import com.android.settingslib.RestrictedLockUtils;
50import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
51
52import java.util.ArrayList;
53import java.util.List;
54
55public final class AppPermissionsFragmentWear extends PreferenceFragment {
56    private static final String LOG_TAG = "AppPermFragWear";
57
58    private static final String KEY_NO_PERMISSIONS = "no_permissions";
59
60    public static AppPermissionsFragmentWear newInstance(String packageName) {
61        return setPackageName(new AppPermissionsFragmentWear(), packageName);
62    }
63
64    private static <T extends Fragment> T setPackageName(T fragment, String packageName) {
65        Bundle arguments = new Bundle();
66        arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
67        fragment.setArguments(arguments);
68        return fragment;
69    }
70
71    private PackageManager mPackageManager;
72    private List<AppPermissionGroup> mToggledGroups;
73    private AppPermissions mAppPermissions;
74
75    private boolean mHasConfirmedRevoke;
76
77    /**
78     * Provides click behavior for disabled preferences.
79     * We can't use {@link PreferenceFragment#onPreferenceTreeClick}, as the base
80     * {@link SwitchPreference} doesn't delegate to that method if the preference is disabled.
81     */
82    private static class PermissionSwitchPreference extends SwitchPreference {
83
84        private final Activity mActivity;
85
86        public PermissionSwitchPreference(Activity activity) {
87            super(activity);
88            this.mActivity = activity;
89        }
90
91        @Override
92        public void performClick(PreferenceScreen preferenceScreen) {
93            super.performClick(preferenceScreen);
94            if (!isEnabled()) {
95                // If setting the permission is disabled, it must have been locked
96                // by the device or profile owner. So get that info and pass it to
97                // the support details dialog.
98                EnforcedAdmin deviceOrProfileOwner = RestrictedLockUtils.getProfileOrDeviceOwner(
99                    mActivity, UserHandle.myUserId());
100                RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
101                    mActivity, deviceOrProfileOwner);
102            }
103        }
104    }
105
106    @Override
107    public void onCreate(Bundle savedInstanceState) {
108        super.onCreate(savedInstanceState);
109
110        String packageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
111        Activity activity = getActivity();
112        mPackageManager = activity.getPackageManager();
113
114        PackageInfo packageInfo;
115
116        try {
117            packageInfo = mPackageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
118        } catch (PackageManager.NameNotFoundException e) {
119            Log.i(LOG_TAG, "No package:" + activity.getCallingPackage(), e);
120            packageInfo = null;
121        }
122
123        if (packageInfo == null) {
124            Toast.makeText(activity, R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show();
125            activity.finish();
126            return;
127        }
128
129        mAppPermissions = new AppPermissions(
130                activity, packageInfo, null, true, () -> getActivity().finish());
131
132        addPreferencesFromResource(R.xml.watch_permissions);
133        initializePermissionGroupList();
134    }
135
136    @Override
137    public void onResume() {
138        super.onResume();
139        mAppPermissions.refresh();
140
141        // Also refresh the UI
142        for (final AppPermissionGroup group : mAppPermissions.getPermissionGroups()) {
143            if (Utils.areGroupPermissionsIndividuallyControlled(getContext(), group.getName())) {
144                for (PermissionInfo perm : getPermissionInfosFromGroup(group)) {
145                    setPreferenceCheckedIfPresent(perm.name,
146                            group.areRuntimePermissionsGranted(new String[]{ perm.name }));
147                }
148            } else {
149                setPreferenceCheckedIfPresent(group.getName(), group.areRuntimePermissionsGranted());
150            }
151        }
152    }
153
154    @Override
155    public void onPause() {
156        super.onPause();
157        logAndClearToggledGroups();
158    }
159
160    private void initializePermissionGroupList() {
161        final String packageName = mAppPermissions.getPackageInfo().packageName;
162        List<AppPermissionGroup> groups = mAppPermissions.getPermissionGroups();
163        List<SwitchPreference> nonSystemPreferences = new ArrayList<>();
164
165        if (!groups.isEmpty()) {
166            getPreferenceScreen().removePreference(findPreference(KEY_NO_PERMISSIONS));
167        }
168
169        for (final AppPermissionGroup group : groups) {
170            if (!Utils.shouldShowPermission(group, packageName)) {
171                continue;
172            }
173
174            boolean isPlatform = group.getDeclaringPackage().equals(Utils.OS_PKG);
175
176            if (Utils.areGroupPermissionsIndividuallyControlled(getContext(), group.getName())) {
177                // If permission is controlled individually, we show all requested permission
178                // inside this group.
179                for (PermissionInfo perm : getPermissionInfosFromGroup(group)) {
180                    final SwitchPreference pref = createSwitchPreferenceForPermission(group, perm);
181                    showOrAddToNonSystemPreferences(pref, nonSystemPreferences, isPlatform);
182                }
183            } else {
184                final SwitchPreference pref = createSwitchPreferenceForGroup(group);
185                showOrAddToNonSystemPreferences(pref, nonSystemPreferences, isPlatform);
186            }
187        }
188
189        // Now add the non-system settings to the end of the list
190        for (SwitchPreference nonSystemPreference : nonSystemPreferences) {
191            getPreferenceScreen().addPreference(nonSystemPreference);
192        }
193    }
194
195    private void showOrAddToNonSystemPreferences(SwitchPreference pref,
196            List<SwitchPreference> nonSystemPreferences, // Mutate
197            boolean isPlatform) {
198        // The UI shows System settings first, then non-system settings
199        if (isPlatform) {
200            getPreferenceScreen().addPreference(pref);
201        } else {
202            nonSystemPreferences.add(pref);
203        }
204    }
205
206    private SwitchPreference createSwitchPreferenceForPermission(AppPermissionGroup group,
207            PermissionInfo perm) {
208        final SwitchPreference pref = new PermissionSwitchPreference(getActivity());
209        pref.setKey(perm.name);
210        pref.setTitle(perm.loadLabel(mPackageManager));
211        pref.setChecked(group.areRuntimePermissionsGranted(new String[]{ perm.name }));
212        pref.setOnPreferenceChangeListener((p, newVal) -> {
213            if((Boolean) newVal) {
214                group.grantRuntimePermissions(false, new String[]{ perm.name });
215
216                if (Utils.areGroupPermissionsIndividuallyControlled(getContext(), group.getName())
217                        && group.doesSupportRuntimePermissions()) {
218                    // We are granting a permission from a group but since this is an
219                    // individual permission control other permissions in the group may
220                    // be revoked, hence we need to mark them user fixed to prevent the
221                    // app from requesting a non-granted permission and it being granted
222                    // because another permission in the group is granted. This applies
223                    // only to apps that support runtime permissions.
224                    String[] revokedPermissionsToFix = null;
225                    final int permissionCount = group.getPermissions().size();
226
227                    for (int i = 0; i < permissionCount; i++) {
228                        Permission current = group.getPermissions().get(i);
229                        if (!current.isGranted() && !current.isUserFixed()) {
230                            revokedPermissionsToFix = ArrayUtils.appendString(
231                                    revokedPermissionsToFix, current.getName());
232                        }
233                    }
234
235                    if (revokedPermissionsToFix != null) {
236                        // If some permissions were not granted then they should be fixed.
237                        group.revokeRuntimePermissions(true, revokedPermissionsToFix);
238                    }
239                }
240            } else {
241                final Permission appPerm = getPermissionFromGroup(group, perm.name);
242                if (appPerm == null) {
243                    return false;
244                }
245
246                final boolean grantedByDefault = appPerm.isGrantedByDefault();
247                if (grantedByDefault
248                        || (!group.doesSupportRuntimePermissions() && !mHasConfirmedRevoke)) {
249                    showRevocationWarningDialog(
250                            (dialog, which) -> {
251                                revokePermissionInGroup(group, perm.name);
252                                pref.setChecked(false);
253                                if (!appPerm.isGrantedByDefault()) {
254                                    mHasConfirmedRevoke = true;
255                                }
256                            },
257                            grantedByDefault
258                                    ? R.string.system_warning
259                                    : R.string.old_sdk_deny_warning);
260                    return false;
261                } else {
262                    revokePermissionInGroup(group, perm.name);
263                }
264            }
265
266            return true;
267        });
268        return pref;
269    }
270
271    private void showRevocationWarningDialog(
272            DialogInterface.OnClickListener confirmListener,
273            int warningMessageId) {
274        new WearableDialogHelper.DialogBuilder(getContext())
275                .setNegativeIcon(R.drawable.confirm_button)
276                .setPositiveIcon(R.drawable.cancel_button)
277                .setNegativeButton(R.string.grant_dialog_button_deny_anyway, confirmListener)
278                .setPositiveButton(R.string.cancel, null)
279                .setMessage(warningMessageId)
280                .show();
281    }
282
283    private static Permission getPermissionFromGroup(AppPermissionGroup group, String permName) {
284        final int permissionCount = group.getPermissions().size();
285
286        for (int i = 0; i < permissionCount; i++) {
287            Permission currentPerm = group.getPermissions().get(i);
288            if(currentPerm.getName().equals(permName)) {
289                return currentPerm;
290            };
291        }
292
293        if ("user".equals(Build.TYPE)) {
294            Log.e(LOG_TAG, String.format("The impossible happens, permission %s is not in group %s.",
295                    permName, group.getName()));
296            return null;
297        } else {
298            // This is impossible, throw a fatal error in non-user build.
299            throw new IllegalArgumentException(
300                    String.format("Permission %s is not in group %s", permName, group.getName()));
301        }
302    }
303
304    private void revokePermissionInGroup(AppPermissionGroup group, String permName) {
305        group.revokeRuntimePermissions(true, new String[]{ permName });
306
307        if (Utils.areGroupPermissionsIndividuallyControlled(getContext(), group.getName())
308                && group.doesSupportRuntimePermissions()
309                && !group.areRuntimePermissionsGranted()) {
310            // If we just revoked the last permission we need to clear
311            // the user fixed state as now the app should be able to
312            // request them at runtime if supported.
313            group.revokeRuntimePermissions(false);
314        }
315    }
316
317    private SwitchPreference createSwitchPreferenceForGroup(AppPermissionGroup group) {
318        final SwitchPreference pref = new PermissionSwitchPreference(getActivity());
319
320        pref.setKey(group.getName());
321        pref.setTitle(group.getLabel());
322        pref.setChecked(group.areRuntimePermissionsGranted());
323
324        if (group.isPolicyFixed()) {
325            pref.setEnabled(false);
326        } else {
327            pref.setOnPreferenceChangeListener((p, newVal) -> {
328                if (LocationUtils.isLocationGroupAndProvider(
329                        group.getName(), group.getApp().packageName)) {
330                    LocationUtils.showLocationDialog(
331                            getContext(), mAppPermissions.getAppLabel());
332                    return false;
333                }
334
335                if ((Boolean) newVal) {
336                    setPermission(group, pref, true);
337                } else {
338                    final boolean grantedByDefault = group.hasGrantedByDefaultPermission();
339                    if (grantedByDefault
340                            || (!group.doesSupportRuntimePermissions() && !mHasConfirmedRevoke)) {
341                        showRevocationWarningDialog(
342                                (dialog, which) -> {
343                                    setPermission(group, pref, false);
344                                    if (!group.hasGrantedByDefaultPermission()) {
345                                        mHasConfirmedRevoke = true;
346                                    }
347                                },
348                                grantedByDefault
349                                        ? R.string.system_warning
350                                        : R.string.old_sdk_deny_warning);
351                        return false;
352                    } else {
353                        setPermission(group, pref, false);
354                    }
355                }
356
357                return true;
358            });
359        }
360        return pref;
361    }
362
363    private void setPermission(AppPermissionGroup group, SwitchPreference pref, boolean grant) {
364        if (grant) {
365            group.grantRuntimePermissions(false);
366        } else {
367            group.revokeRuntimePermissions(false);
368        }
369        addToggledGroup(group);
370        pref.setChecked(grant);
371    }
372
373    private void addToggledGroup(AppPermissionGroup group) {
374        if (mToggledGroups == null) {
375            mToggledGroups = new ArrayList<>();
376        }
377        // Double toggle is back to initial state.
378        if (mToggledGroups.contains(group)) {
379            mToggledGroups.remove(group);
380        } else {
381            mToggledGroups.add(group);
382        }
383    }
384
385    private void logAndClearToggledGroups() {
386        if (mToggledGroups != null) {
387            String packageName = mAppPermissions.getPackageInfo().packageName;
388            SafetyNetLogger.logPermissionsToggled(packageName, mToggledGroups);
389            mToggledGroups = null;
390        }
391    }
392
393    private List<PermissionInfo> getPermissionInfosFromGroup(AppPermissionGroup group) {
394        ArrayList<PermissionInfo> permInfos = new ArrayList<>(group.getPermissions().size());
395        for(Permission perm : group.getPermissions()) {
396            try {
397                permInfos.add(mPackageManager.getPermissionInfo(perm.getName(), 0));
398            } catch (PackageManager.NameNotFoundException e) {
399                Log.w(LOG_TAG, "No permission:" + perm.getName());
400            }
401        }
402        return permInfos;
403    }
404
405    private void setPreferenceCheckedIfPresent(String preferenceKey, boolean checked) {
406        Preference pref = findPreference(preferenceKey);
407        if (pref instanceof SwitchPreference) {
408            ((SwitchPreference) pref).setChecked(checked);
409        }
410    }
411}
412