1/*
2 * Copyright (C) 2017 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 */
16package com.android.car.settings.applications;
17
18import android.app.Activity;
19import android.app.ActivityManager;
20import android.app.admin.DevicePolicyManager;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.ApplicationInfo;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.ResolveInfo;
28import android.icu.text.ListFormatter;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.UserHandle;
32import android.util.Log;
33import android.view.View;
34import android.widget.TextView;
35
36import com.android.car.settings.R;
37
38import com.android.car.settings.common.ListSettingsFragment;
39import com.android.car.settings.common.SingleTextLineItem;
40import com.android.car.settings.common.TypedPagedListAdapter;
41import com.android.settingslib.Utils;
42import com.android.settingslib.applications.PermissionsSummaryHelper;
43import com.android.settingslib.applications.PermissionsSummaryHelper.PermissionsResultCallback;
44
45import java.text.MessageFormat;
46import java.util.ArrayList;
47import java.util.List;
48
49/**
50 * Shows details about an application and action associated with that application,
51 * like uninstall, forceStop.
52 */
53public class ApplicationDetailFragment extends ListSettingsFragment {
54    private static final String TAG = "AppDetailActivity";
55    public static final String EXTRA_RESOLVE_INFO = "extra_resolve_info";
56
57    private ResolveInfo mResolveInfo;
58    private PackageInfo mPackageInfo;
59
60    private TextView mDisableToggle;
61    private TextView mForceStopButton;
62    private DevicePolicyManager mDpm;
63
64    public static ApplicationDetailFragment getInstance(ResolveInfo resolveInfo) {
65        ApplicationDetailFragment applicationDetailFragment = new ApplicationDetailFragment();
66        Bundle bundle = ListSettingsFragment.getBundle();
67        bundle.putParcelable(EXTRA_RESOLVE_INFO, resolveInfo);
68        bundle.putInt(EXTRA_TITLE_ID, R.string.applications_settings);
69        bundle.putInt(EXTRA_ACTION_BAR_LAYOUT, R.layout.action_bar_with_button);
70        applicationDetailFragment.setArguments(bundle);
71        return applicationDetailFragment;
72    }
73
74    @Override
75    public void onCreate(Bundle savedInstanceState) {
76        super.onCreate(savedInstanceState);
77        mResolveInfo = getArguments().getParcelable(EXTRA_RESOLVE_INFO);
78    }
79
80    @Override
81    public void onActivityCreated(Bundle savedInstanceState) {
82        mPackageInfo = getPackageInfo();
83        super.onActivityCreated(savedInstanceState);
84        if (mResolveInfo == null) {
85            Log.w(TAG, "No application info set.");
86            return;
87        }
88
89        mDisableToggle = (TextView) getActivity().findViewById(R.id.action_button1);
90        mForceStopButton = (TextView) getActivity().findViewById(R.id.action_button2);
91        mForceStopButton.setText(R.string.force_stop);
92        mForceStopButton.setVisibility(View.VISIBLE);
93
94        mDpm = (DevicePolicyManager) getContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
95        updateForceStopButton();
96        mForceStopButton.setOnClickListener(
97                v -> forceStopPackage(mResolveInfo.activityInfo.packageName));
98    }
99
100    @Override
101    public void onStart() {
102        super.onStart();
103        updateForceStopButton();
104        updateDisableable();
105    }
106
107    @Override
108    public ArrayList<TypedPagedListAdapter.LineItem> getLineItems() {
109        ArrayList<TypedPagedListAdapter.LineItem> items = new ArrayList<>();
110        items.add(new ApplicationLineItem(
111                getContext(),
112                getContext().getPackageManager(),
113                mResolveInfo,
114                null /* fragmentController */,
115                false));
116        items.add(new ApplicationPermissionLineItem(getContext(), mResolveInfo));
117        items.add(new SingleTextLineItem(getContext().getString(
118                R.string.application_version_label, mPackageInfo.versionName)));
119        return items;
120    }
121
122    // fetch the latest ApplicationInfo instead of caching it so it reflects the current state.
123    private ApplicationInfo getAppInfo() {
124        try {
125            return getContext().getPackageManager().getApplicationInfo(
126                    mResolveInfo.activityInfo.packageName, 0 /* flag */);
127        } catch (PackageManager.NameNotFoundException e) {
128            Log.e(TAG, "incorrect packagename: " + mResolveInfo.activityInfo.packageName, e);
129            throw new IllegalArgumentException(e);
130        }
131    }
132
133    private PackageInfo getPackageInfo() {
134        try {
135            return getContext().getPackageManager().getPackageInfo(
136                    mResolveInfo.activityInfo.packageName, 0 /* flag */);
137        } catch (PackageManager.NameNotFoundException e) {
138            Log.e(TAG, "incorrect packagename: " + mResolveInfo.activityInfo.packageName, e);
139            throw new IllegalArgumentException(e);
140        }
141    }
142
143    private void updateDisableable() {
144        boolean disableable = false;
145        boolean disabled = false;
146        // Try to prevent the user from bricking their phone
147        // by not allowing disabling of apps in the system.
148        if (Utils.isSystemPackage(
149                getResources(), getContext().getPackageManager(), mPackageInfo)) {
150            // Disable button for core system applications.
151            mDisableToggle.setText(R.string.disable_text);
152            disabled = false;
153        } else if (getAppInfo().enabled && !isDisabledUntilUsed()) {
154            mDisableToggle.setText(R.string.disable_text);
155            disableable = true;
156            disabled = false;
157        } else {
158            mDisableToggle.setText(R.string.enable_text);
159            disableable = true;
160            disabled = true;
161        }
162        mDisableToggle.setEnabled(disableable);
163        final int enableState = disabled
164                ? PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
165                : PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
166        mDisableToggle.setOnClickListener(v -> {
167            getContext().getPackageManager().setApplicationEnabledSetting(
168                    mResolveInfo.activityInfo.packageName,
169                    enableState,
170                    0);
171            updateDisableable();
172        });
173    }
174
175    private boolean isDisabledUntilUsed() {
176        return getAppInfo().enabledSetting
177                == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED;
178    }
179
180    private void forceStopPackage(String pkgName) {
181        ActivityManager am = (ActivityManager) getContext().getSystemService(
182                Context.ACTIVITY_SERVICE);
183        Log.d(TAG, "Stopping package " + pkgName);
184        am.forceStopPackage(pkgName);
185        updateForceStopButton();
186    }
187
188    // enable or disable the force stop button:
189    // - disabled if it's a device admin
190    // - if the application is stopped unexplicitly, enabled the button
191    // - if there's a reason for the system to restart the application, that indicates the app
192    //   can be force stopped.
193    private void updateForceStopButton() {
194        if (mDpm.packageHasActiveAdmins(mResolveInfo.activityInfo.packageName)) {
195            // User can't force stop device admin.
196            if (Log.isLoggable(TAG, Log.DEBUG)) {
197                Log.d(TAG, "Disabling button, user can't force stop device admin");
198            }
199            mForceStopButton.setEnabled(false);
200        } else if ((getAppInfo().flags & ApplicationInfo.FLAG_STOPPED) == 0) {
201            // If the app isn't explicitly stopped, then always show the
202            // force stop button.
203            if (Log.isLoggable(TAG, Log.WARN)) {
204                Log.w(TAG, "App is not explicitly stopped");
205            }
206            mForceStopButton.setEnabled(true);
207        } else {
208            Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART,
209                    Uri.fromParts("package", mResolveInfo.activityInfo.packageName, null));
210            intent.putExtra(Intent.EXTRA_PACKAGES, new String[]{
211                    mResolveInfo.activityInfo.packageName
212            });
213
214            if (Log.isLoggable(TAG, Log.DEBUG)) {
215                Log.d(TAG, "Sending broadcast to query restart for "
216                        + mResolveInfo.activityInfo.packageName);
217            }
218            getActivity().sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null,
219                    mCheckKillProcessesReceiver, null, Activity.RESULT_CANCELED, null, null);
220        }
221    }
222
223    private final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() {
224        @Override
225        public void onReceive(Context context, Intent intent) {
226            final boolean enabled = getResultCode() != Activity.RESULT_CANCELED;
227            if (Log.isLoggable(TAG, Log.DEBUG)) {
228                Log.d(TAG,
229                        MessageFormat.format("Got broadcast response: Restart status for {0} {1}",
230                                mResolveInfo.activityInfo.packageName, enabled));
231            }
232            mForceStopButton.setEnabled(enabled);
233        }
234    };
235}
236