| package com.android.settings.applications; |
| |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.app.ApplicationErrorReport; |
| import android.app.Dialog; |
| import android.app.PendingIntent; |
| import android.app.settings.SettingsEnums; |
| import android.content.ActivityNotFoundException; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.pm.ProviderInfo; |
| import android.content.pm.ServiceInfo; |
| import android.content.res.Resources; |
| import android.os.Bundle; |
| import android.os.Debug; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.Button; |
| import android.widget.TextView; |
| |
| import androidx.appcompat.app.AlertDialog; |
| import androidx.fragment.app.DialogFragment; |
| |
| import com.android.settings.R; |
| import com.android.settings.Utils; |
| import com.android.settings.core.InstrumentedFragment; |
| import com.android.settings.core.instrumentation.InstrumentedDialogFragment; |
| import com.android.settingslib.utils.ThreadUtils; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| |
| public class RunningServiceDetails extends InstrumentedFragment |
| implements RunningState.OnRefreshUiListener { |
| static final String TAG = "RunningServicesDetails"; |
| |
| static final String KEY_UID = "uid"; |
| static final String KEY_USER_ID = "user_id"; |
| static final String KEY_PROCESS = "process"; |
| static final String KEY_BACKGROUND = "background"; |
| |
| static final int DIALOG_CONFIRM_STOP = 1; |
| |
| ActivityManager mAm; |
| LayoutInflater mInflater; |
| |
| RunningState mState; |
| boolean mHaveData; |
| |
| int mUid; |
| int mUserId; |
| String mProcessName; |
| boolean mShowBackground; |
| |
| RunningState.MergedItem mMergedItem; |
| |
| View mRootView; |
| ViewGroup mAllDetails; |
| ViewGroup mSnippet; |
| RunningProcessesView.ActiveItem mSnippetActiveItem; |
| RunningProcessesView.ViewHolder mSnippetViewHolder; |
| |
| int mNumServices, mNumProcesses; |
| |
| TextView mServicesHeader; |
| TextView mProcessesHeader; |
| final ArrayList<ActiveDetail> mActiveDetails = new ArrayList<ActiveDetail>(); |
| |
| class ActiveDetail implements View.OnClickListener { |
| View mRootView; |
| Button mStopButton; |
| Button mReportButton; |
| RunningState.ServiceItem mServiceItem; |
| RunningProcessesView.ActiveItem mActiveItem; |
| RunningProcessesView.ViewHolder mViewHolder; |
| PendingIntent mManageIntent; |
| ComponentName mInstaller; |
| |
| void stopActiveService(boolean confirmed) { |
| RunningState.ServiceItem si = mServiceItem; |
| if (!confirmed) { |
| if ((si.mServiceInfo.applicationInfo.flags&ApplicationInfo.FLAG_SYSTEM) != 0) { |
| showConfirmStopDialog(si.mRunningService.service); |
| return; |
| } |
| } |
| getActivity().stopService(new Intent().setComponent(si.mRunningService.service)); |
| if (mMergedItem == null) { |
| // If this is gone, we are gone. |
| mState.updateNow(); |
| finish(); |
| } else if (!mShowBackground && mMergedItem.mServices.size() <= 1) { |
| // If there was only one service, we are finishing it, |
| // so no reason for the UI to stick around. |
| mState.updateNow(); |
| finish(); |
| } else { |
| mState.updateNow(); |
| } |
| } |
| |
| public void onClick(View v) { |
| if (v == mReportButton) { |
| ApplicationErrorReport report = new ApplicationErrorReport(); |
| report.type = ApplicationErrorReport.TYPE_RUNNING_SERVICE; |
| report.packageName = mServiceItem.mServiceInfo.packageName; |
| report.installerPackageName = mInstaller.getPackageName(); |
| report.processName = mServiceItem.mRunningService.process; |
| report.time = System.currentTimeMillis(); |
| report.systemApp = (mServiceItem.mServiceInfo.applicationInfo.flags |
| & ApplicationInfo.FLAG_SYSTEM) != 0; |
| ApplicationErrorReport.RunningServiceInfo info |
| = new ApplicationErrorReport.RunningServiceInfo(); |
| if (mActiveItem.mFirstRunTime >= 0) { |
| info.durationMillis = SystemClock.elapsedRealtime()-mActiveItem.mFirstRunTime; |
| } else { |
| info.durationMillis = -1; |
| } |
| ComponentName comp = new ComponentName(mServiceItem.mServiceInfo.packageName, |
| mServiceItem.mServiceInfo.name); |
| File filename = getActivity().getFileStreamPath("service_dump.txt"); |
| FileOutputStream output = null; |
| try { |
| output = new FileOutputStream(filename); |
| Debug.dumpService("activity", output.getFD(), |
| new String[] { "-a", "service", comp.flattenToString() }); |
| } catch (IOException e) { |
| Log.w(TAG, "Can't dump service: " + comp, e); |
| } finally { |
| if (output != null) try { output.close(); } catch (IOException e) {} |
| } |
| FileInputStream input = null; |
| try { |
| input = new FileInputStream(filename); |
| byte[] buffer = new byte[(int) filename.length()]; |
| input.read(buffer); |
| info.serviceDetails = new String(buffer); |
| } catch (IOException e) { |
| Log.w(TAG, "Can't read service dump: " + comp, e); |
| } finally { |
| if (input != null) try { input.close(); } catch (IOException e) {} |
| } |
| filename.delete(); |
| Log.i(TAG, "Details: " + info.serviceDetails); |
| report.runningServiceInfo = info; |
| Intent result = new Intent(Intent.ACTION_APP_ERROR); |
| result.setComponent(mInstaller); |
| result.putExtra(Intent.EXTRA_BUG_REPORT, report); |
| result.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| startActivity(result); |
| return; |
| } |
| |
| if (mManageIntent != null) { |
| try { |
| getActivity().startIntentSender(mManageIntent.getIntentSender(), null, |
| Intent.FLAG_ACTIVITY_NEW_TASK |
| | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET, |
| Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET, 0); |
| } catch (IntentSender.SendIntentException e) { |
| Log.w(TAG, e); |
| } catch (IllegalArgumentException e) { |
| Log.w(TAG, e); |
| } catch (ActivityNotFoundException e) { |
| Log.w(TAG, e); |
| } |
| } else if (mServiceItem != null) { |
| stopActiveService(false); |
| } else if (mActiveItem.mItem.mBackground) { |
| // Background process. Just kill it. |
| mAm.killBackgroundProcesses(mActiveItem.mItem.mPackageInfo.packageName); |
| finish(); |
| } else { |
| // Heavy-weight process. We'll do a force-stop on it. |
| mAm.forceStopPackage(mActiveItem.mItem.mPackageInfo.packageName); |
| finish(); |
| } |
| } |
| } |
| |
| StringBuilder mBuilder = new StringBuilder(128); |
| |
| boolean findMergedItem() { |
| RunningState.MergedItem item = null; |
| ArrayList<RunningState.MergedItem> newItems = mShowBackground |
| ? mState.getCurrentBackgroundItems() : mState.getCurrentMergedItems(); |
| if (newItems != null) { |
| for (int i=0; i<newItems.size(); i++) { |
| RunningState.MergedItem mi = newItems.get(i); |
| if (mi.mUserId != mUserId) { |
| continue; |
| } |
| if (mUid >= 0 && mi.mProcess != null && mi.mProcess.mUid != mUid) { |
| continue; |
| } |
| if (mProcessName == null || (mi.mProcess != null |
| && mProcessName.equals(mi.mProcess.mProcessName))) { |
| item = mi; |
| break; |
| } |
| } |
| } |
| |
| if (mMergedItem != item) { |
| mMergedItem = item; |
| return true; |
| } |
| return false; |
| } |
| |
| void addServicesHeader() { |
| if (mNumServices == 0) { |
| mServicesHeader = (TextView) mInflater.inflate( |
| androidx.preference.R.layout.preference_category, mAllDetails, false); |
| mServicesHeader.setText(R.string.runningservicedetails_services_title); |
| mAllDetails.addView(mServicesHeader); |
| } |
| mNumServices++; |
| } |
| |
| void addProcessesHeader() { |
| if (mNumProcesses == 0) { |
| mProcessesHeader = (TextView) mInflater.inflate( |
| androidx.preference.R.layout.preference_category, mAllDetails, false); |
| mProcessesHeader.setText(R.string.runningservicedetails_processes_title); |
| mAllDetails.addView(mProcessesHeader); |
| } |
| mNumProcesses++; |
| } |
| |
| void addServiceDetailsView(RunningState.ServiceItem si, RunningState.MergedItem mi, |
| boolean isService, boolean inclDetails) { |
| if (isService) { |
| addServicesHeader(); |
| } else if (mi.mUserId != UserHandle.myUserId()) { |
| // This is being called for another user, and is not a service... |
| // That is, it is a background processes, being added for the |
| // details of a user. In this case we want a header for processes, |
| // since the top subject line is for the user. |
| addProcessesHeader(); |
| } |
| |
| RunningState.BaseItem bi = si != null ? si : mi; |
| |
| ActiveDetail detail = new ActiveDetail(); |
| View root = mInflater.inflate(R.layout.running_service_details_service, |
| mAllDetails, false); |
| mAllDetails.addView(root); |
| detail.mRootView = root; |
| detail.mServiceItem = si; |
| detail.mViewHolder = new RunningProcessesView.ViewHolder(root); |
| detail.mActiveItem = detail.mViewHolder.bind(mState, bi, mBuilder); |
| |
| if (!inclDetails) { |
| root.findViewById(R.id.service).setVisibility(View.GONE); |
| } |
| |
| if (si != null && si.mRunningService.clientLabel != 0) { |
| detail.mManageIntent = mAm.getRunningServiceControlPanel( |
| si.mRunningService.service); |
| } |
| |
| TextView description = (TextView)root.findViewById(R.id.comp_description); |
| detail.mStopButton = (Button)root.findViewById(R.id.left_button); |
| detail.mReportButton = (Button)root.findViewById(R.id.right_button); |
| |
| if (isService && mi.mUserId != UserHandle.myUserId()) { |
| // For services from other users, we don't show any description or |
| // controls, because the current user can not perform |
| // actions on them. |
| description.setVisibility(View.GONE); |
| root.findViewById(R.id.control_buttons_panel).setVisibility(View.GONE); |
| } else { |
| if (si != null && si.mServiceInfo.descriptionRes != 0) { |
| description.setText(getActivity().getPackageManager().getText( |
| si.mServiceInfo.packageName, si.mServiceInfo.descriptionRes, |
| si.mServiceInfo.applicationInfo)); |
| } else { |
| if (mi.mBackground) { |
| description.setText(R.string.background_process_stop_description); |
| } else if (detail.mManageIntent != null) { |
| try { |
| Resources clientr = getActivity().getPackageManager().getResourcesForApplication( |
| si.mRunningService.clientPackage); |
| String label = clientr.getString(si.mRunningService.clientLabel); |
| description.setText(getActivity().getString(R.string.service_manage_description, |
| label)); |
| } catch (PackageManager.NameNotFoundException e) { |
| } |
| } else { |
| description.setText(getActivity().getText(si != null |
| ? R.string.service_stop_description |
| : R.string.heavy_weight_stop_description)); |
| } |
| } |
| |
| detail.mStopButton.setOnClickListener(detail); |
| detail.mStopButton.setText(getActivity().getText(detail.mManageIntent != null |
| ? R.string.service_manage : R.string.service_stop)); |
| detail.mReportButton.setOnClickListener(detail); |
| detail.mReportButton.setText(com.android.internal.R.string.report); |
| // check if error reporting is enabled in secure settings |
| int enabled = Settings.Global.getInt(getActivity().getContentResolver(), |
| Settings.Global.SEND_ACTION_APP_ERROR, 0); |
| if (enabled != 0 && si != null) { |
| detail.mInstaller = ApplicationErrorReport.getErrorReportReceiver( |
| getActivity(), si.mServiceInfo.packageName, |
| si.mServiceInfo.applicationInfo.flags); |
| detail.mReportButton.setEnabled(detail.mInstaller != null); |
| } else { |
| detail.mReportButton.setEnabled(false); |
| } |
| } |
| |
| mActiveDetails.add(detail); |
| } |
| |
| void addProcessDetailsView(RunningState.ProcessItem pi, boolean isMain) { |
| addProcessesHeader(); |
| |
| ActiveDetail detail = new ActiveDetail(); |
| View root = mInflater.inflate(R.layout.running_service_details_process, |
| mAllDetails, false); |
| mAllDetails.addView(root); |
| detail.mRootView = root; |
| detail.mViewHolder = new RunningProcessesView.ViewHolder(root); |
| detail.mActiveItem = detail.mViewHolder.bind(mState, pi, mBuilder); |
| |
| TextView description = (TextView)root.findViewById(R.id.comp_description); |
| if (pi.mUserId != UserHandle.myUserId()) { |
| // Processes for another user are all shown batched together; there is |
| // no reason to have a description. |
| description.setVisibility(View.GONE); |
| } else if (isMain) { |
| description.setText(R.string.main_running_process_description); |
| } else { |
| int textid = 0; |
| CharSequence label = null; |
| ActivityManager.RunningAppProcessInfo rpi = pi.mRunningProcessInfo; |
| final ComponentName comp = rpi.importanceReasonComponent; |
| //Log.i(TAG, "Secondary proc: code=" + rpi.importanceReasonCode |
| // + " pid=" + rpi.importanceReasonPid + " comp=" + comp); |
| switch (rpi.importanceReasonCode) { |
| case ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE: |
| textid = R.string.process_provider_in_use_description; |
| if (rpi.importanceReasonComponent != null) { |
| try { |
| ProviderInfo prov = getActivity().getPackageManager().getProviderInfo( |
| rpi.importanceReasonComponent, 0); |
| label = RunningState.makeLabel(getActivity().getPackageManager(), |
| prov.name, prov); |
| } catch (NameNotFoundException e) { |
| } |
| } |
| break; |
| case ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE: |
| textid = R.string.process_service_in_use_description; |
| if (rpi.importanceReasonComponent != null) { |
| try { |
| ServiceInfo serv = getActivity().getPackageManager().getServiceInfo( |
| rpi.importanceReasonComponent, 0); |
| label = RunningState.makeLabel(getActivity().getPackageManager(), |
| serv.name, serv); |
| } catch (NameNotFoundException e) { |
| } |
| } |
| break; |
| } |
| if (textid != 0 && label != null) { |
| description.setText(getActivity().getString(textid, label)); |
| } |
| } |
| |
| mActiveDetails.add(detail); |
| } |
| |
| void addDetailsViews(RunningState.MergedItem item, boolean inclServices, |
| boolean inclProcesses) { |
| if (item != null) { |
| if (inclServices) { |
| for (int i=0; i<item.mServices.size(); i++) { |
| addServiceDetailsView(item.mServices.get(i), item, true, true); |
| } |
| } |
| |
| if (inclProcesses) { |
| if (item.mServices.size() <= 0) { |
| // This item does not have any services, so it must be |
| // another interesting process... we will put a fake service |
| // entry for it, to allow the user to "stop" it. |
| addServiceDetailsView(null, item, false, item.mUserId != UserHandle.myUserId()); |
| } else { |
| // This screen is actually showing services, so also show |
| // the process details. |
| for (int i=-1; i<item.mOtherProcesses.size(); i++) { |
| RunningState.ProcessItem pi = i < 0 ? item.mProcess |
| : item.mOtherProcesses.get(i); |
| if (pi != null && pi.mPid <= 0) { |
| continue; |
| } |
| |
| addProcessDetailsView(pi, i < 0); |
| } |
| } |
| } |
| } |
| } |
| |
| void addDetailViews() { |
| for (int i=mActiveDetails.size()-1; i>=0; i--) { |
| mAllDetails.removeView(mActiveDetails.get(i).mRootView); |
| } |
| mActiveDetails.clear(); |
| |
| if (mServicesHeader != null) { |
| mAllDetails.removeView(mServicesHeader); |
| mServicesHeader = null; |
| } |
| |
| if (mProcessesHeader != null) { |
| mAllDetails.removeView(mProcessesHeader); |
| mProcessesHeader = null; |
| } |
| |
| mNumServices = mNumProcesses = 0; |
| |
| if (mMergedItem != null) { |
| if (mMergedItem.mUser != null) { |
| ArrayList<RunningState.MergedItem> items; |
| if (mShowBackground) { |
| items = new ArrayList<RunningState.MergedItem>(mMergedItem.mChildren); |
| Collections.sort(items, mState.mBackgroundComparator); |
| } else { |
| items = mMergedItem.mChildren; |
| } |
| for (int i=0; i<items.size(); i++) { |
| addDetailsViews(items.get(i), true, false); |
| } |
| for (int i=0; i<items.size(); i++) { |
| addDetailsViews(items.get(i), false, true); |
| } |
| } else { |
| addDetailsViews(mMergedItem, true, true); |
| } |
| } |
| } |
| |
| void refreshUi(boolean dataChanged) { |
| if (findMergedItem()) { |
| dataChanged = true; |
| } |
| if (dataChanged) { |
| if (mMergedItem != null) { |
| mSnippetActiveItem = mSnippetViewHolder.bind(mState, |
| mMergedItem, mBuilder); |
| } else if (mSnippetActiveItem != null) { |
| // Clear whatever is currently being shown. |
| mSnippetActiveItem.mHolder.size.setText(""); |
| mSnippetActiveItem.mHolder.uptime.setText(""); |
| mSnippetActiveItem.mHolder.description.setText(R.string.no_services); |
| } else { |
| // No merged item, never had one. Nothing to do. |
| finish(); |
| return; |
| } |
| addDetailViews(); |
| } |
| } |
| |
| private void finish() { |
| ThreadUtils.postOnMainThread(() -> { |
| final Activity a = getActivity(); |
| if (a != null) { |
| a.onBackPressed(); |
| } |
| }); |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setHasOptionsMenu(true); |
| mUid = getArguments().getInt(KEY_UID, -1); |
| mUserId = getArguments().getInt(KEY_USER_ID, 0); |
| mProcessName = getArguments().getString(KEY_PROCESS, null); |
| mShowBackground = getArguments().getBoolean(KEY_BACKGROUND, false); |
| |
| mAm = (ActivityManager) getActivity().getSystemService(Context.ACTIVITY_SERVICE); |
| mInflater = (LayoutInflater) getActivity().getSystemService( |
| Context.LAYOUT_INFLATER_SERVICE); |
| |
| mState = RunningState.getInstance(getActivity()); |
| } |
| |
| @Override |
| public View onCreateView( |
| LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
| final View view = inflater.inflate(R.layout.running_service_details, container, false); |
| Utils.prepareCustomPreferencesList(container, view, view, false); |
| |
| mRootView = view; |
| mAllDetails = (ViewGroup)view.findViewById(R.id.all_details); |
| mSnippet = (ViewGroup)view.findViewById(R.id.snippet); |
| mSnippetViewHolder = new RunningProcessesView.ViewHolder(mSnippet); |
| |
| // We want to retrieve the data right now, so any active managed |
| // dialog that gets created can find it. |
| ensureData(); |
| |
| return view; |
| } |
| |
| @Override |
| public void onPause() { |
| super.onPause(); |
| mHaveData = false; |
| mState.pause(); |
| } |
| |
| @Override |
| public int getMetricsCategory() { |
| return SettingsEnums.RUNNING_SERVICE_DETAILS; |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| ensureData(); |
| } |
| |
| ActiveDetail activeDetailForService(ComponentName comp) { |
| for (int i=0; i<mActiveDetails.size(); i++) { |
| ActiveDetail ad = mActiveDetails.get(i); |
| if (ad.mServiceItem != null && ad.mServiceItem.mRunningService != null |
| && comp.equals(ad.mServiceItem.mRunningService.service)) { |
| return ad; |
| } |
| } |
| return null; |
| } |
| |
| private void showConfirmStopDialog(ComponentName comp) { |
| DialogFragment newFragment = MyAlertDialogFragment.newConfirmStop( |
| DIALOG_CONFIRM_STOP, comp); |
| newFragment.setTargetFragment(this, 0); |
| newFragment.show(getFragmentManager(), "confirmstop"); |
| } |
| |
| public static class MyAlertDialogFragment extends InstrumentedDialogFragment { |
| |
| public static MyAlertDialogFragment newConfirmStop(int id, ComponentName comp) { |
| MyAlertDialogFragment frag = new MyAlertDialogFragment(); |
| Bundle args = new Bundle(); |
| args.putInt("id", id); |
| args.putParcelable("comp", comp); |
| frag.setArguments(args); |
| return frag; |
| } |
| |
| RunningServiceDetails getOwner() { |
| return (RunningServiceDetails)getTargetFragment(); |
| } |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| int id = getArguments().getInt("id"); |
| switch (id) { |
| case DIALOG_CONFIRM_STOP: { |
| final ComponentName comp = (ComponentName)getArguments().getParcelable("comp"); |
| if (getOwner().activeDetailForService(comp) == null) { |
| return null; |
| } |
| |
| return new AlertDialog.Builder(getActivity()) |
| .setTitle(getActivity().getString(R.string.runningservicedetails_stop_dlg_title)) |
| .setMessage(getActivity().getString(R.string.runningservicedetails_stop_dlg_text)) |
| .setPositiveButton(R.string.dlg_ok, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| ActiveDetail ad = getOwner().activeDetailForService(comp); |
| if (ad != null) { |
| ad.stopActiveService(true); |
| } |
| } |
| }) |
| .setNegativeButton(R.string.dlg_cancel, null) |
| .create(); |
| } |
| } |
| throw new IllegalArgumentException("unknown id " + id); |
| } |
| |
| @Override |
| public int getMetricsCategory() { |
| return SettingsEnums.DIALOG_RUNNIGN_SERVICE; |
| } |
| } |
| |
| void ensureData() { |
| if (!mHaveData) { |
| mHaveData = true; |
| mState.resume(this); |
| |
| // We want to go away if the service being shown no longer exists, |
| // so we need to ensure we have done the initial data retrieval before |
| // showing our ui. |
| mState.waitForData(); |
| |
| // And since we know we have the data, let's show the UI right away |
| // to avoid flicker. |
| refreshUi(true); |
| } |
| } |
| |
| void updateTimes() { |
| if (mSnippetActiveItem != null) { |
| mSnippetActiveItem.updateTime(getActivity(), mBuilder); |
| } |
| for (int i=0; i<mActiveDetails.size(); i++) { |
| mActiveDetails.get(i).mActiveItem.updateTime(getActivity(), mBuilder); |
| } |
| } |
| |
| @Override |
| public void onRefreshUi(int what) { |
| if (getActivity() == null) return; |
| switch (what) { |
| case REFRESH_TIME: |
| updateTimes(); |
| break; |
| case REFRESH_DATA: |
| refreshUi(false); |
| updateTimes(); |
| break; |
| case REFRESH_STRUCTURE: |
| refreshUi(true); |
| updateTimes(); |
| break; |
| } |
| } |
| } |