• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.settings.applications;
2 
3 import android.app.Activity;
4 import android.app.ActivityManager;
5 import android.app.AlertDialog;
6 import android.app.ApplicationErrorReport;
7 import android.app.Dialog;
8 import android.app.DialogFragment;
9 import android.app.Fragment;
10 import android.app.PendingIntent;
11 import android.content.ActivityNotFoundException;
12 import android.content.ComponentName;
13 import android.content.Context;
14 import android.content.DialogInterface;
15 import android.content.Intent;
16 import android.content.IntentSender;
17 import android.content.pm.ApplicationInfo;
18 import android.content.pm.PackageManager;
19 import android.content.pm.PackageManager.NameNotFoundException;
20 import android.content.pm.ProviderInfo;
21 import android.content.pm.ServiceInfo;
22 import android.content.res.Resources;
23 import android.os.Bundle;
24 import android.os.Debug;
25 import android.os.Handler;
26 import android.os.SystemClock;
27 import android.os.UserHandle;
28 import android.provider.Settings;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.widget.Button;
34 import android.widget.TextView;
35 
36 import com.android.settings.R;
37 import com.android.settings.Utils;
38 
39 import java.io.File;
40 import java.io.FileInputStream;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 import java.util.ArrayList;
44 import java.util.Collections;
45 
46 public class RunningServiceDetails extends Fragment
47         implements RunningState.OnRefreshUiListener {
48     static final String TAG = "RunningServicesDetails";
49 
50     static final String KEY_UID = "uid";
51     static final String KEY_USER_ID = "user_id";
52     static final String KEY_PROCESS = "process";
53     static final String KEY_BACKGROUND = "background";
54 
55     static final int DIALOG_CONFIRM_STOP = 1;
56 
57     ActivityManager mAm;
58     LayoutInflater mInflater;
59 
60     RunningState mState;
61     boolean mHaveData;
62 
63     int mUid;
64     int mUserId;
65     String mProcessName;
66     boolean mShowBackground;
67 
68     RunningState.MergedItem mMergedItem;
69 
70     View mRootView;
71     ViewGroup mAllDetails;
72     ViewGroup mSnippet;
73     RunningProcessesView.ActiveItem mSnippetActiveItem;
74     RunningProcessesView.ViewHolder mSnippetViewHolder;
75 
76     int mNumServices, mNumProcesses;
77 
78     TextView mServicesHeader;
79     TextView mProcessesHeader;
80     final ArrayList<ActiveDetail> mActiveDetails = new ArrayList<ActiveDetail>();
81 
82     class ActiveDetail implements View.OnClickListener {
83         View mRootView;
84         Button mStopButton;
85         Button mReportButton;
86         RunningState.ServiceItem mServiceItem;
87         RunningProcessesView.ActiveItem mActiveItem;
88         RunningProcessesView.ViewHolder mViewHolder;
89         PendingIntent mManageIntent;
90         ComponentName mInstaller;
91 
stopActiveService(boolean confirmed)92         void stopActiveService(boolean confirmed) {
93             RunningState.ServiceItem si = mServiceItem;
94             if (!confirmed) {
95                 if ((si.mServiceInfo.applicationInfo.flags&ApplicationInfo.FLAG_SYSTEM) != 0) {
96                     showConfirmStopDialog(si.mRunningService.service);
97                     return;
98                 }
99             }
100             getActivity().stopService(new Intent().setComponent(si.mRunningService.service));
101             if (mMergedItem == null) {
102                 // If this is gone, we are gone.
103                 mState.updateNow();
104                 finish();
105             } else if (!mShowBackground && mMergedItem.mServices.size() <= 1) {
106                 // If there was only one service, we are finishing it,
107                 // so no reason for the UI to stick around.
108                 mState.updateNow();
109                 finish();
110             } else {
111                 mState.updateNow();
112             }
113         }
114 
onClick(View v)115         public void onClick(View v) {
116             if (v == mReportButton) {
117                 ApplicationErrorReport report = new ApplicationErrorReport();
118                 report.type = ApplicationErrorReport.TYPE_RUNNING_SERVICE;
119                 report.packageName = mServiceItem.mServiceInfo.packageName;
120                 report.installerPackageName = mInstaller.getPackageName();
121                 report.processName = mServiceItem.mRunningService.process;
122                 report.time = System.currentTimeMillis();
123                 report.systemApp = (mServiceItem.mServiceInfo.applicationInfo.flags
124                         & ApplicationInfo.FLAG_SYSTEM) != 0;
125                 ApplicationErrorReport.RunningServiceInfo info
126                         = new ApplicationErrorReport.RunningServiceInfo();
127                 if (mActiveItem.mFirstRunTime >= 0) {
128                     info.durationMillis = SystemClock.elapsedRealtime()-mActiveItem.mFirstRunTime;
129                 } else {
130                     info.durationMillis = -1;
131                 }
132                 ComponentName comp = new ComponentName(mServiceItem.mServiceInfo.packageName,
133                         mServiceItem.mServiceInfo.name);
134                 File filename = getActivity().getFileStreamPath("service_dump.txt");
135                 FileOutputStream output = null;
136                 try {
137                     output = new FileOutputStream(filename);
138                     Debug.dumpService("activity", output.getFD(),
139                             new String[] { "-a", "service", comp.flattenToString() });
140                 } catch (IOException e) {
141                     Log.w(TAG, "Can't dump service: " + comp, e);
142                 } finally {
143                     if (output != null) try { output.close(); } catch (IOException e) {}
144                 }
145                 FileInputStream input = null;
146                 try {
147                     input = new FileInputStream(filename);
148                     byte[] buffer = new byte[(int) filename.length()];
149                     input.read(buffer);
150                     info.serviceDetails = new String(buffer);
151                 } catch (IOException e) {
152                     Log.w(TAG, "Can't read service dump: " + comp, e);
153                 } finally {
154                     if (input != null) try { input.close(); } catch (IOException e) {}
155                 }
156                 filename.delete();
157                 Log.i(TAG, "Details: " + info.serviceDetails);
158                 report.runningServiceInfo = info;
159                 Intent result = new Intent(Intent.ACTION_APP_ERROR);
160                 result.setComponent(mInstaller);
161                 result.putExtra(Intent.EXTRA_BUG_REPORT, report);
162                 result.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
163                 startActivity(result);
164                 return;
165             }
166 
167             if (mManageIntent != null) {
168                 try {
169                     getActivity().startIntentSender(mManageIntent.getIntentSender(), null,
170                             Intent.FLAG_ACTIVITY_NEW_TASK
171                                     | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET,
172                             Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET, 0);
173                 } catch (IntentSender.SendIntentException e) {
174                     Log.w(TAG, e);
175                 } catch (IllegalArgumentException e) {
176                     Log.w(TAG, e);
177                 } catch (ActivityNotFoundException e) {
178                     Log.w(TAG, e);
179                 }
180             } else if (mServiceItem != null) {
181                 stopActiveService(false);
182             } else if (mActiveItem.mItem.mBackground) {
183                 // Background process.  Just kill it.
184                 mAm.killBackgroundProcesses(mActiveItem.mItem.mPackageInfo.packageName);
185                 finish();
186             } else {
187                 // Heavy-weight process.  We'll do a force-stop on it.
188                 mAm.forceStopPackage(mActiveItem.mItem.mPackageInfo.packageName);
189                 finish();
190             }
191         }
192     }
193 
194     StringBuilder mBuilder = new StringBuilder(128);
195 
findMergedItem()196     boolean findMergedItem() {
197         RunningState.MergedItem item = null;
198         ArrayList<RunningState.MergedItem> newItems = mShowBackground
199                 ? mState.getCurrentBackgroundItems() : mState.getCurrentMergedItems();
200         if (newItems != null) {
201             for (int i=0; i<newItems.size(); i++) {
202                 RunningState.MergedItem mi = newItems.get(i);
203                 if (mi.mUserId != mUserId) {
204                     continue;
205                 }
206                 if (mUid >= 0 && mi.mProcess != null && mi.mProcess.mUid != mUid) {
207                     continue;
208                 }
209                 if (mProcessName == null || (mi.mProcess != null
210                         && mProcessName.equals(mi.mProcess.mProcessName))) {
211                     item = mi;
212                     break;
213                 }
214             }
215         }
216 
217         if (mMergedItem != item) {
218             mMergedItem = item;
219             return true;
220         }
221         return false;
222     }
223 
addServicesHeader()224     void addServicesHeader() {
225         if (mNumServices == 0) {
226             mServicesHeader = (TextView)mInflater.inflate(R.layout.separator_label,
227                     mAllDetails, false);
228             mServicesHeader.setText(R.string.runningservicedetails_services_title);
229             mAllDetails.addView(mServicesHeader);
230         }
231         mNumServices++;
232     }
233 
addProcessesHeader()234     void addProcessesHeader() {
235         if (mNumProcesses == 0) {
236             mProcessesHeader = (TextView)mInflater.inflate(R.layout.separator_label,
237                     mAllDetails, false);
238             mProcessesHeader.setText(R.string.runningservicedetails_processes_title);
239             mAllDetails.addView(mProcessesHeader);
240         }
241         mNumProcesses++;
242     }
243 
addServiceDetailsView(RunningState.ServiceItem si, RunningState.MergedItem mi, boolean isService, boolean inclDetails)244     void addServiceDetailsView(RunningState.ServiceItem si, RunningState.MergedItem mi,
245             boolean isService, boolean inclDetails) {
246         if (isService) {
247             addServicesHeader();
248         } else if (mi.mUserId != UserHandle.myUserId()) {
249             // This is being called for another user, and is not a service...
250             // That is, it is a background processes, being added for the
251             // details of a user.  In this case we want a header for processes,
252             // since the top subject line is for the user.
253             addProcessesHeader();
254         }
255 
256         RunningState.BaseItem bi = si != null ? si : mi;
257 
258         ActiveDetail detail = new ActiveDetail();
259         View root = mInflater.inflate(R.layout.running_service_details_service,
260                 mAllDetails, false);
261         mAllDetails.addView(root);
262         detail.mRootView = root;
263         detail.mServiceItem = si;
264         detail.mViewHolder = new RunningProcessesView.ViewHolder(root);
265         detail.mActiveItem = detail.mViewHolder.bind(mState, bi, mBuilder);
266 
267         if (!inclDetails) {
268             root.findViewById(R.id.service).setVisibility(View.GONE);
269         }
270 
271         if (si != null && si.mRunningService.clientLabel != 0) {
272             detail.mManageIntent = mAm.getRunningServiceControlPanel(
273                     si.mRunningService.service);
274         }
275 
276         TextView description = (TextView)root.findViewById(R.id.comp_description);
277         detail.mStopButton = (Button)root.findViewById(R.id.left_button);
278         detail.mReportButton = (Button)root.findViewById(R.id.right_button);
279 
280         if (isService && mi.mUserId != UserHandle.myUserId()) {
281             // For services from other users, we don't show any description or
282             // controls, because the current user can not perform
283             // actions on them.
284             description.setVisibility(View.GONE);
285             root.findViewById(R.id.control_buttons_panel).setVisibility(View.GONE);
286         } else {
287             if (si != null && si.mServiceInfo.descriptionRes != 0) {
288                 description.setText(getActivity().getPackageManager().getText(
289                         si.mServiceInfo.packageName, si.mServiceInfo.descriptionRes,
290                         si.mServiceInfo.applicationInfo));
291             } else {
292                 if (mi.mBackground) {
293                     description.setText(R.string.background_process_stop_description);
294                 } else if (detail.mManageIntent != null) {
295                     try {
296                         Resources clientr = getActivity().getPackageManager().getResourcesForApplication(
297                                 si.mRunningService.clientPackage);
298                         String label = clientr.getString(si.mRunningService.clientLabel);
299                         description.setText(getActivity().getString(R.string.service_manage_description,
300                                 label));
301                     } catch (PackageManager.NameNotFoundException e) {
302                     }
303                 } else {
304                     description.setText(getActivity().getText(si != null
305                             ? R.string.service_stop_description
306                             : R.string.heavy_weight_stop_description));
307                 }
308             }
309 
310             detail.mStopButton.setOnClickListener(detail);
311             detail.mStopButton.setText(getActivity().getText(detail.mManageIntent != null
312                     ? R.string.service_manage : R.string.service_stop));
313             detail.mReportButton.setOnClickListener(detail);
314             detail.mReportButton.setText(com.android.internal.R.string.report);
315             // check if error reporting is enabled in secure settings
316             int enabled = Settings.Global.getInt(getActivity().getContentResolver(),
317                     Settings.Global.SEND_ACTION_APP_ERROR, 0);
318             if (enabled != 0 && si != null) {
319                 detail.mInstaller = ApplicationErrorReport.getErrorReportReceiver(
320                         getActivity(), si.mServiceInfo.packageName,
321                         si.mServiceInfo.applicationInfo.flags);
322                 detail.mReportButton.setEnabled(detail.mInstaller != null);
323             } else {
324                 detail.mReportButton.setEnabled(false);
325             }
326         }
327 
328         mActiveDetails.add(detail);
329     }
330 
addProcessDetailsView(RunningState.ProcessItem pi, boolean isMain)331     void addProcessDetailsView(RunningState.ProcessItem pi, boolean isMain) {
332         addProcessesHeader();
333 
334         ActiveDetail detail = new ActiveDetail();
335         View root = mInflater.inflate(R.layout.running_service_details_process,
336                 mAllDetails, false);
337         mAllDetails.addView(root);
338         detail.mRootView = root;
339         detail.mViewHolder = new RunningProcessesView.ViewHolder(root);
340         detail.mActiveItem = detail.mViewHolder.bind(mState, pi, mBuilder);
341 
342         TextView description = (TextView)root.findViewById(R.id.comp_description);
343         if (pi.mUserId != UserHandle.myUserId()) {
344             // Processes for another user are all shown batched together; there is
345             // no reason to have a description.
346             description.setVisibility(View.GONE);
347         } else if (isMain) {
348             description.setText(R.string.main_running_process_description);
349         } else {
350             int textid = 0;
351             CharSequence label = null;
352             ActivityManager.RunningAppProcessInfo rpi = pi.mRunningProcessInfo;
353             final ComponentName comp = rpi.importanceReasonComponent;
354             //Log.i(TAG, "Secondary proc: code=" + rpi.importanceReasonCode
355             //        + " pid=" + rpi.importanceReasonPid + " comp=" + comp);
356             switch (rpi.importanceReasonCode) {
357                 case ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE:
358                     textid = R.string.process_provider_in_use_description;
359                     if (rpi.importanceReasonComponent != null) {
360                         try {
361                             ProviderInfo prov = getActivity().getPackageManager().getProviderInfo(
362                                     rpi.importanceReasonComponent, 0);
363                             label = RunningState.makeLabel(getActivity().getPackageManager(),
364                                     prov.name, prov);
365                         } catch (NameNotFoundException e) {
366                         }
367                     }
368                     break;
369                 case ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE:
370                     textid = R.string.process_service_in_use_description;
371                     if (rpi.importanceReasonComponent != null) {
372                         try {
373                             ServiceInfo serv = getActivity().getPackageManager().getServiceInfo(
374                                     rpi.importanceReasonComponent, 0);
375                             label = RunningState.makeLabel(getActivity().getPackageManager(),
376                                     serv.name, serv);
377                         } catch (NameNotFoundException e) {
378                         }
379                     }
380                     break;
381             }
382             if (textid != 0 && label != null) {
383                 description.setText(getActivity().getString(textid, label));
384             }
385         }
386 
387         mActiveDetails.add(detail);
388     }
389 
addDetailsViews(RunningState.MergedItem item, boolean inclServices, boolean inclProcesses)390     void addDetailsViews(RunningState.MergedItem item, boolean inclServices,
391             boolean inclProcesses) {
392         if (item != null) {
393             if (inclServices) {
394                 for (int i=0; i<item.mServices.size(); i++) {
395                     addServiceDetailsView(item.mServices.get(i), item, true, true);
396                 }
397             }
398 
399             if (inclProcesses) {
400                 if (item.mServices.size() <= 0) {
401                     // This item does not have any services, so it must be
402                     // another interesting process...  we will put a fake service
403                     // entry for it, to allow the user to "stop" it.
404                     addServiceDetailsView(null, item, false, item.mUserId != UserHandle.myUserId());
405                 } else {
406                     // This screen is actually showing services, so also show
407                     // the process details.
408                     for (int i=-1; i<item.mOtherProcesses.size(); i++) {
409                         RunningState.ProcessItem pi = i < 0 ? item.mProcess
410                                 : item.mOtherProcesses.get(i);
411                         if (pi != null && pi.mPid <= 0) {
412                             continue;
413                         }
414 
415                         addProcessDetailsView(pi, i < 0);
416                     }
417                 }
418             }
419         }
420     }
421 
422     void addDetailViews() {
423         for (int i=mActiveDetails.size()-1; i>=0; i--) {
424             mAllDetails.removeView(mActiveDetails.get(i).mRootView);
425         }
426         mActiveDetails.clear();
427 
428         if (mServicesHeader != null) {
429             mAllDetails.removeView(mServicesHeader);
430             mServicesHeader = null;
431         }
432 
433         if (mProcessesHeader != null) {
434             mAllDetails.removeView(mProcessesHeader);
435             mProcessesHeader = null;
436         }
437 
438         mNumServices = mNumProcesses = 0;
439 
440         if (mMergedItem != null) {
441             if (mMergedItem.mUser != null) {
442                 ArrayList<RunningState.MergedItem> items;
443                 if (mShowBackground) {
444                     items = new ArrayList<RunningState.MergedItem>(mMergedItem.mChildren);
445                     Collections.sort(items, mState.mBackgroundComparator);
446                 } else {
447                     items = mMergedItem.mChildren;
448                 }
449                 for (int i=0; i<items.size(); i++) {
450                     addDetailsViews(items.get(i), true, false);
451                 }
452                 for (int i=0; i<items.size(); i++) {
453                     addDetailsViews(items.get(i), false, true);
454                 }
455             } else {
456                 addDetailsViews(mMergedItem, true, true);
457             }
458         }
459     }
460 
461     void refreshUi(boolean dataChanged) {
462         if (findMergedItem()) {
463             dataChanged = true;
464         }
465         if (dataChanged) {
466             if (mMergedItem != null) {
467                 mSnippetActiveItem = mSnippetViewHolder.bind(mState,
468                         mMergedItem, mBuilder);
469             } else if (mSnippetActiveItem != null) {
470                 // Clear whatever is currently being shown.
471                 mSnippetActiveItem.mHolder.size.setText("");
472                 mSnippetActiveItem.mHolder.uptime.setText("");
473                 mSnippetActiveItem.mHolder.description.setText(R.string.no_services);
474             } else {
475                 // No merged item, never had one.  Nothing to do.
476                 finish();
477                 return;
478             }
479             addDetailViews();
480         }
481     }
482 
483     private void finish() {
484         (new Handler()).post(new Runnable() {
485             @Override
486             public void run() {
487                 Activity a = getActivity();
488                 if (a != null) {
489                     a.onBackPressed();
490                 }
491             }
492         });
493     }
494 
495     @Override
496     public void onCreate(Bundle savedInstanceState) {
497         super.onCreate(savedInstanceState);
498 
499         mUid = getArguments().getInt(KEY_UID, -1);
500         mUserId = getArguments().getInt(KEY_USER_ID, 0);
501         mProcessName = getArguments().getString(KEY_PROCESS, null);
502         mShowBackground = getArguments().getBoolean(KEY_BACKGROUND, false);
503 
504         mAm = (ActivityManager)getActivity().getSystemService(Context.ACTIVITY_SERVICE);
505         mInflater = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
506 
507         mState = RunningState.getInstance(getActivity());
508     }
509 
510     @Override
511     public View onCreateView(
512             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
513         final View view = inflater.inflate(R.layout.running_service_details, container, false);
514         Utils.prepareCustomPreferencesList(container, view, view, false);
515 
516         mRootView = view;
517         mAllDetails = (ViewGroup)view.findViewById(R.id.all_details);
518         mSnippet = (ViewGroup)view.findViewById(R.id.snippet);
519         mSnippetViewHolder = new RunningProcessesView.ViewHolder(mSnippet);
520 
521         // We want to retrieve the data right now, so any active managed
522         // dialog that gets created can find it.
523         ensureData();
524 
525         return view;
526     }
527 
528     @Override
529     public void onPause() {
530         super.onPause();
531         mHaveData = false;
532         mState.pause();
533     }
534 
535     @Override
536     public void onResume() {
537         super.onResume();
538         ensureData();
539     }
540 
541     ActiveDetail activeDetailForService(ComponentName comp) {
542         for (int i=0; i<mActiveDetails.size(); i++) {
543             ActiveDetail ad = mActiveDetails.get(i);
544             if (ad.mServiceItem != null && ad.mServiceItem.mRunningService != null
545                     && comp.equals(ad.mServiceItem.mRunningService.service)) {
546                 return ad;
547             }
548         }
549         return null;
550     }
551 
552     private void showConfirmStopDialog(ComponentName comp) {
553         DialogFragment newFragment = MyAlertDialogFragment.newConfirmStop(
554                 DIALOG_CONFIRM_STOP, comp);
555         newFragment.setTargetFragment(this, 0);
556         newFragment.show(getFragmentManager(), "confirmstop");
557     }
558 
559     public static class MyAlertDialogFragment extends DialogFragment {
560 
561         public static MyAlertDialogFragment newConfirmStop(int id, ComponentName comp) {
562             MyAlertDialogFragment frag = new MyAlertDialogFragment();
563             Bundle args = new Bundle();
564             args.putInt("id", id);
565             args.putParcelable("comp", comp);
566             frag.setArguments(args);
567             return frag;
568         }
569 
570         RunningServiceDetails getOwner() {
571             return (RunningServiceDetails)getTargetFragment();
572         }
573 
574         @Override
575         public Dialog onCreateDialog(Bundle savedInstanceState) {
576             int id = getArguments().getInt("id");
577             switch (id) {
578                 case DIALOG_CONFIRM_STOP: {
579                     final ComponentName comp = (ComponentName)getArguments().getParcelable("comp");
580                     if (getOwner().activeDetailForService(comp) == null) {
581                         return null;
582                     }
583 
584                     return new AlertDialog.Builder(getActivity())
585                             .setTitle(getActivity().getString(R.string.runningservicedetails_stop_dlg_title))
586                             .setIconAttribute(android.R.attr.alertDialogIcon)
587                             .setMessage(getActivity().getString(R.string.runningservicedetails_stop_dlg_text))
588                             .setPositiveButton(R.string.dlg_ok,
589                                     new DialogInterface.OnClickListener() {
590                                 public void onClick(DialogInterface dialog, int which) {
591                                     ActiveDetail ad = getOwner().activeDetailForService(comp);
592                                     if (ad != null) {
593                                         ad.stopActiveService(true);
594                                     }
595                                 }
596                             })
597                             .setNegativeButton(R.string.dlg_cancel, null)
598                             .create();
599                 }
600             }
601             throw new IllegalArgumentException("unknown id " + id);
602         }
603     }
604 
605     void ensureData() {
606         if (!mHaveData) {
607             mHaveData = true;
608             mState.resume(this);
609 
610             // We want to go away if the service being shown no longer exists,
611             // so we need to ensure we have done the initial data retrieval before
612             // showing our ui.
613             mState.waitForData();
614 
615             // And since we know we have the data, let's show the UI right away
616             // to avoid flicker.
617             refreshUi(true);
618         }
619     }
620 
621     void updateTimes() {
622         if (mSnippetActiveItem != null) {
623             mSnippetActiveItem.updateTime(getActivity(), mBuilder);
624         }
625         for (int i=0; i<mActiveDetails.size(); i++) {
626             mActiveDetails.get(i).mActiveItem.updateTime(getActivity(), mBuilder);
627         }
628     }
629 
630     @Override
631     public void onRefreshUi(int what) {
632         if (getActivity() == null) return;
633         switch (what) {
634             case REFRESH_TIME:
635                 updateTimes();
636                 break;
637             case REFRESH_DATA:
638                 refreshUi(false);
639                 updateTimes();
640                 break;
641             case REFRESH_STRUCTURE:
642                 refreshUi(true);
643                 updateTimes();
644                 break;
645         }
646     }
647 }
648