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  */
16 package com.android.car.settings.applications;
17 
18 import android.app.Activity;
19 import android.app.ActivityManager;
20 import android.app.admin.DevicePolicyManager;
21 import android.content.ActivityNotFoundException;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.PackageInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ResolveInfo;
29 import android.content.res.Resources;
30 import android.icu.text.ListFormatter;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.UserHandle;
34 import android.util.Log;
35 import android.view.View;
36 import android.view.View.OnClickListener;
37 import android.widget.Button;
38 import android.widget.ImageView;
39 import android.widget.TextView;
40 
41 import com.android.car.settings.common.CarSettingActivity;
42 import com.android.car.settings.R;
43 
44 import com.android.settingslib.Utils;
45 import com.android.settingslib.applications.PermissionsSummaryHelper;
46 import com.android.settingslib.applications.PermissionsSummaryHelper.PermissionsResultCallback;
47 
48 import java.text.MessageFormat;
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 /**
53  * Shows details about an application and action associated with that application,
54  * like uninstall, forceStop.
55  */
56 public class ApplicationDetailActivity extends CarSettingActivity {
57     private static final String TAG = "AppDetailActivity";
58     public static final String APPLICATION_INFO_KEY = "APPLICATION_INFO_KEY";
59 
60     private ResolveInfo mResolveInfo;
61     private TextView mPermissionDetailView;
62     private View mPermissionContainer;
63     private Button mDisableToggle;
64     private Button mForceStopButton;
65     private DevicePolicyManager mDpm;
66 
67     @Override
onCreate(Bundle savedInstanceState)68     protected void onCreate(Bundle savedInstanceState) {
69         super.onCreate(savedInstanceState);
70         setContentView(R.layout.application_details);
71         if (getIntent() != null && getIntent().getExtras() != null) {
72             mResolveInfo = getIntent().getExtras().getParcelable(APPLICATION_INFO_KEY);
73         }
74         if (mResolveInfo == null) {
75             Log.w(TAG, "No application info set.");
76             return;
77         }
78 
79         mDisableToggle = (Button) findViewById(R.id.disable_toggle);
80         mForceStopButton = (Button) findViewById(R.id.force_stop);
81 
82         mPermissionDetailView = (TextView) findViewById(R.id.permission_detail);
83         mPermissionContainer = findViewById(R.id.permission_container);
84 
85         ImageView icon = (ImageView) findViewById(R.id.icon);
86         icon.setImageDrawable(mResolveInfo.loadIcon(getPackageManager()));
87 
88         TextView appName = (TextView) findViewById(R.id.title);
89         appName.setText(mResolveInfo.loadLabel(getPackageManager()));
90 
91         PermissionsSummaryHelper.getPermissionSummary(this /* context */,
92                 mResolveInfo.activityInfo.packageName, mPermissionCallback);
93         mDpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
94         updateForceStopButton();
95         mForceStopButton.setOnClickListener(v -> {
96             forceStopPackage(mResolveInfo.activityInfo.packageName);
97         });
98         updateDisableable();
99     }
100 
101     // fetch the latest ApplicationInfo instead of caching it so it reflects the current state.
getAppInfo()102     private ApplicationInfo getAppInfo() {
103         try {
104             return getPackageManager().getApplicationInfo(
105                     mResolveInfo.activityInfo.packageName, 0 /* flag */);
106         } catch (PackageManager.NameNotFoundException e) {
107             Log.e(TAG, "incorrect packagename: " + mResolveInfo.activityInfo.packageName, e);
108             throw new IllegalArgumentException(e);
109         }
110     }
111 
getPackageInfo()112     private PackageInfo getPackageInfo() {
113         try {
114             return getPackageManager().getPackageInfo(
115                     mResolveInfo.activityInfo.packageName, 0 /* flag */);
116         } catch (PackageManager.NameNotFoundException e) {
117             Log.e(TAG, "incorrect packagename: " + mResolveInfo.activityInfo.packageName, e);
118             throw new IllegalArgumentException(e);
119         }
120     }
121 
updateDisableable()122     private void updateDisableable() {
123         boolean disableable = false;
124         boolean disabled = false;
125         // Try to prevent the user from bricking their phone
126         // by not allowing disabling of apps in the system.
127         if (Utils.isSystemPackage(getResources(), getPackageManager(), getPackageInfo())) {
128             // Disable button for core system applications.
129             mDisableToggle.setText(R.string.disable_text);
130             disabled = false;
131         } else if (getAppInfo().enabled && !isDisabledUntilUsed()) {
132             mDisableToggle.setText(R.string.disable_text);
133             disableable = true;
134             disabled = false;
135         } else {
136             mDisableToggle.setText(R.string.enable_text);
137             disableable = true;
138             disabled = true;
139         }
140         mDisableToggle.setEnabled(disableable);
141         final int enableState = disabled
142                 ? PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
143                 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
144         mDisableToggle.setOnClickListener(v -> {
145             getPackageManager().setApplicationEnabledSetting(
146                     mResolveInfo.activityInfo.packageName,
147                     enableState,
148                     0);
149             updateDisableable();
150         });
151     }
152 
isDisabledUntilUsed()153     private boolean isDisabledUntilUsed() {
154         return getAppInfo().enabledSetting
155                 == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED;
156     }
157 
forceStopPackage(String pkgName)158     private void forceStopPackage(String pkgName) {
159         ActivityManager am = (ActivityManager) getSystemService(
160                 Context.ACTIVITY_SERVICE);
161         Log.d(TAG, "Stopping package " + pkgName);
162         am.forceStopPackage(pkgName);
163         updateForceStopButton();
164     }
165 
166     // enable or disable the force stop button:
167     // - disabled if it's a device admin
168     // - if the application is stopped unexplicitly, enabled the button
169     // - if there's a reason for the system to restart the application, that indicates the app
170     //   can be force stopped.
updateForceStopButton()171     private void updateForceStopButton() {
172         if (mDpm.packageHasActiveAdmins(mResolveInfo.activityInfo.packageName)) {
173             // User can't force stop device admin.
174             if (Log.isLoggable(TAG, Log.DEBUG)) {
175                 Log.d(TAG, "Disabling button, user can't force stop device admin");
176             }
177             mForceStopButton.setEnabled(false);
178         } else if ((getAppInfo().flags & ApplicationInfo.FLAG_STOPPED) == 0) {
179             // If the app isn't explicitly stopped, then always show the
180             // force stop button.
181             if (Log.isLoggable(TAG, Log.WARN)) {
182                 Log.w(TAG, "App is not explicitly stopped");
183             }
184             mForceStopButton.setEnabled(true);
185         } else {
186             Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART,
187                     Uri.fromParts("package", mResolveInfo.activityInfo.packageName, null));
188             intent.putExtra(Intent.EXTRA_PACKAGES, new String[]{
189                     mResolveInfo.activityInfo.packageName
190             });
191 
192             if (Log.isLoggable(TAG, Log.DEBUG)) {
193                 Log.d(TAG, "Sending broadcast to query restart for "
194                         + mResolveInfo.activityInfo.packageName);
195             }
196             sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null,
197                     mCheckKillProcessesReceiver, null, Activity.RESULT_CANCELED, null, null);
198         }
199     }
200 
201     private final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() {
202         @Override
203         public void onReceive(Context context, Intent intent) {
204             final boolean enabled = getResultCode() != Activity.RESULT_CANCELED;
205             if (Log.isLoggable(TAG, Log.DEBUG)) {
206                 Log.d(TAG,
207                         MessageFormat.format("Got broadcast response: Restart status for {0} {1}",
208                                 mResolveInfo.activityInfo.packageName, enabled));
209             }
210             mForceStopButton.setEnabled(enabled);
211         }
212     };
213 
214     private final PermissionsResultCallback mPermissionCallback
215             = new PermissionsResultCallback() {
216         @Override
217         public void onPermissionSummaryResult(int standardGrantedPermissionCount,
218                 int requestedPermissionCount, int additionalGrantedPermissionCount,
219                 List<CharSequence> grantedGroupLabels) {
220             Resources res = getResources();
221             CharSequence summary = null;
222 
223             if (requestedPermissionCount == 0) {
224                 summary = res.getString(
225                         R.string.runtime_permissions_summary_no_permissions_requested);
226                 mPermissionContainer.setEnabled(false);
227                 mPermissionContainer.setOnClickListener(null);
228             } else {
229                 ArrayList<CharSequence> list = new ArrayList<>(grantedGroupLabels);
230                 if (additionalGrantedPermissionCount > 0) {
231                     // N additional permissions.
232                     list.add(res.getQuantityString(
233                             R.plurals.runtime_permissions_additional_count,
234                             additionalGrantedPermissionCount, additionalGrantedPermissionCount));
235                 }
236                 if (list.size() == 0) {
237                     summary = res.getString(
238                             R.string.runtime_permissions_summary_no_permissions_granted);
239                 } else {
240                     summary = ListFormatter.getInstance().format(list);
241                 }
242                 mPermissionContainer.setEnabled(true);
243                 mPermissionContainer.setOnClickListener(mPermissionClickedListener);
244             }
245             mPermissionDetailView.setText(summary);
246         }
247     };
248 
249     private OnClickListener mPermissionClickedListener = new OnClickListener() {
250         @Override
251         public void onClick(View v) {
252             // start new activity to manage app permissions
253             Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
254             intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mResolveInfo.activityInfo.packageName);
255             try {
256                 startActivity(intent);
257             } catch (ActivityNotFoundException e) {
258                 Log.w(TAG, "No app can handle android.intent.action.MANAGE_APP_PERMISSIONS");
259             }
260         }
261     };
262 }
263