comparison app/src/main/java/com/five_ten_sg/connectbot/HostListActivity.java @ 438:d29cce60f393

migrate from Eclipse to Android Studio
author Carl Byington <carl@five-ten-sg.com>
date Thu, 03 Dec 2015 11:23:55 -0800
parents src/com/five_ten_sg/connectbot/HostListActivity.java@fe0fbcf55ed9
children 1c9d043c2c03
comparison
equal deleted inserted replaced
437:208b31032318 438:d29cce60f393
1 /*
2 * ConnectBot: simple, powerful, open-source SSH client for Android
3 * Copyright 2007 Kenny Root, Jeffrey Sharkey
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package com.five_ten_sg.connectbot;
19
20
21 import com.five_ten_sg.connectbot.bean.HostBean;
22 import com.five_ten_sg.connectbot.service.TerminalBridge;
23 import com.five_ten_sg.connectbot.service.TerminalManager;
24 import com.five_ten_sg.connectbot.transport.TransportFactory;
25 import com.five_ten_sg.connectbot.util.HostDatabase;
26 import com.five_ten_sg.connectbot.util.PreferenceConstants;
27
28 import java.io.BufferedReader;
29 import java.io.FileReader;
30 import java.io.File;
31 import java.util.HashMap;
32 import java.util.List;
33
34 import android.app.Activity;
35 import android.app.AlertDialog;
36 import android.app.ListActivity;
37 import android.content.ComponentName;
38 import android.content.ContentValues;
39 import android.content.Context;
40 import android.content.DialogInterface;
41 import android.content.Intent;
42 import android.content.Intent.ShortcutIconResource;
43 import android.content.ServiceConnection;
44 import android.content.SharedPreferences;
45 import android.content.SharedPreferences.Editor;
46 import android.content.res.ColorStateList;
47 import android.database.sqlite.SQLiteDatabase;
48 import android.net.Uri;
49 import android.os.Build;
50 import android.os.Bundle;
51 import android.os.Environment;
52 import android.os.Handler;
53 import android.os.IBinder;
54 import android.os.Message;
55 import android.preference.PreferenceManager;
56 import android.util.Log;
57 import android.view.ContextMenu;
58 import android.view.KeyEvent;
59 import android.view.LayoutInflater;
60 import android.view.Menu;
61 import android.view.MenuItem;
62 import android.view.MenuItem.OnMenuItemClickListener;
63 import android.view.View;
64 import android.view.View.OnKeyListener;
65 import android.view.ViewGroup;
66 import android.widget.AdapterView;
67 import android.widget.AdapterView.OnItemClickListener;
68 import android.widget.ArrayAdapter;
69 import android.widget.ImageView;
70 import android.widget.ListView;
71 import android.widget.Spinner;
72 import android.widget.TextView;
73
74 public class HostListActivity extends ListActivity {
75 protected static final String TAG = "ConnectBot.HostListActivity";
76 public final static int REQUEST_EDIT = 1;
77 public final static int REQUEST_EULA = 2;
78
79 protected TerminalManager bound = null;
80
81 protected HostDatabase hostdb;
82 private List<HostBean> hosts;
83 protected LayoutInflater inflater = null;
84
85 protected boolean sortedByColor = false;
86
87 private MenuItem sortcolor;
88
89 private MenuItem sortlast;
90
91 private Spinner transportSpinner;
92 private TextView quickconnect;
93
94 private SharedPreferences prefs = null;
95
96 protected boolean makingShortcut = false;
97
98 protected Handler updateHandler = new Handler() {
99 @Override
100 public void handleMessage(Message msg) {
101 HostListActivity.this.updateList();
102 }
103 };
104
105 private ServiceConnection connection = new ServiceConnection() {
106 public void onServiceConnected(ComponentName className, IBinder service) {
107 bound = ((TerminalManager.TerminalBinder) service).getService();
108 // update our listview binder to find the service
109 updateList();
110 }
111 public void onServiceDisconnected(ComponentName className) {
112 bound = null;
113 updateList();
114 }
115 };
116
117 @Override
118 public void onStart() {
119 super.onStart();
120 // start the terminal manager service
121 this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
122
123 if (this.hostdb == null)
124 this.hostdb = new HostDatabase(this);
125 }
126
127 @Override
128 public void onStop() {
129 super.onStop();
130 this.unbindService(connection);
131
132 if (this.hostdb != null) {
133 this.hostdb.close();
134 this.hostdb = null;
135 }
136 }
137
138 @Override
139 public void onResume() {
140 super.onResume();
141 }
142
143 @Override
144 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
145 if (requestCode == REQUEST_EULA) {
146 if (resultCode == Activity.RESULT_OK) {
147 // yay they agreed, so store that info
148 Editor editor = prefs.edit();
149 editor.putBoolean(PreferenceConstants.EULA, true);
150 editor.commit();
151 }
152 else {
153 // user didnt agree, so close
154 this.finish();
155 }
156 }
157 else if (requestCode == REQUEST_EDIT) {
158 updateList();
159 }
160 }
161
162 @Override
163 public void onCreate(Bundle icicle) {
164 super.onCreate(icicle);
165 setContentView(R.layout.act_hostlist);
166 this.setTitle(String.format("%s: %s",
167 getResources().getText(R.string.app_name),
168 getResources().getText(R.string.title_hosts_list)));
169 this.prefs = PreferenceManager.getDefaultSharedPreferences(this);
170
171 // detect HTC Dream and apply special preferences
172 if (Build.MANUFACTURER.equals("HTC") && Build.DEVICE.equals("dream")) {
173 if (!prefs.contains(PreferenceConstants.SHIFT_FKEYS) &&
174 !prefs.contains(PreferenceConstants.CTRL_FKEYS)) {
175 Editor editor = prefs.edit();
176 editor.putBoolean(PreferenceConstants.SHIFT_FKEYS, true);
177 editor.putBoolean(PreferenceConstants.CTRL_FKEYS, true);
178 editor.commit();
179 }
180 }
181
182 makingShortcut = Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())
183 || Intent.ACTION_PICK.equals(getIntent().getAction());
184
185 // connect with hosts database, read deployment file, and populate list
186 hostdb = new HostDatabase(this);
187 createDeploymentHosts(); // build hosts from a deployment text file
188 updateList();
189
190 // check for eula agreement, which might be set from the deployment file
191 boolean agreed = prefs.getBoolean(PreferenceConstants.EULA, false);
192 if (!agreed) {
193 startActivityForResult(new Intent(this, WizardActivity.class), REQUEST_EULA);
194 }
195
196 // show the list
197 sortedByColor = prefs.getBoolean(PreferenceConstants.SORT_BY_COLOR, false);
198 registerForContextMenu(getListView());
199 getListView().setOnItemClickListener(new OnItemClickListener() {
200
201 public synchronized void onItemClick(AdapterView<?> parent, View view, int position, long id) {
202 // launch off to console details
203 HostBean host = (HostBean) getListView().getItemAtPosition(position);
204 Uri uri = host.getUri();
205 Intent contents = new Intent(Intent.ACTION_VIEW, uri);
206 contents.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
207
208 if (makingShortcut) {
209 // create shortcut if requested
210 ShortcutIconResource icon = Intent.ShortcutIconResource.fromContext(HostListActivity.this, R.drawable.icon);
211 Intent intent = new Intent();
212 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, contents);
213 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, host.getNickname());
214 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);
215 setResult(RESULT_OK, intent);
216 finish();
217 }
218 else {
219 // otherwise just launch activity to show this host
220 startActivity(contents);
221 }
222 }
223 });
224 quickconnect = (TextView) this.findViewById(R.id.front_quickconnect);
225 quickconnect.setVisibility(makingShortcut ? View.GONE : View.VISIBLE);
226 quickconnect.setOnKeyListener(new OnKeyListener() {
227 public boolean onKey(View v, int keyCode, KeyEvent event) {
228 if (event.getAction() == KeyEvent.ACTION_UP) return false;
229
230 if (keyCode != KeyEvent.KEYCODE_ENTER) return false;
231
232 return startConsoleActivity();
233 }
234 });
235 transportSpinner = (Spinner)findViewById(R.id.transport_selection);
236 transportSpinner.setVisibility(makingShortcut ? View.GONE : View.VISIBLE);
237 ArrayAdapter<String> transportSelection = new ArrayAdapter<String> (this,
238 android.R.layout.simple_spinner_item, TransportFactory.getTransportNames());
239 transportSelection.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
240 transportSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
241 public void onItemSelected(AdapterView<?> arg0, View view, int position, long id) {
242 String formatHint = TransportFactory.getFormatHint(
243 (String) transportSpinner.getSelectedItem(),
244 HostListActivity.this);
245 quickconnect.setHint(formatHint);
246 quickconnect.setError(null);
247 quickconnect.requestFocus();
248 }
249 public void onNothingSelected(AdapterView<?> arg0) { }
250 });
251 transportSpinner.setAdapter(transportSelection);
252 this.inflater = LayoutInflater.from(this);
253 }
254
255 @Override
256 public boolean onPrepareOptionsMenu(Menu menu) {
257 super.onPrepareOptionsMenu(menu);
258
259 // don't offer menus when creating shortcut
260 if (makingShortcut) return true;
261
262 sortcolor.setVisible(!sortedByColor);
263 sortlast.setVisible(sortedByColor);
264 return true;
265 }
266
267 @Override
268 public boolean onCreateOptionsMenu(Menu menu) {
269 super.onCreateOptionsMenu(menu);
270
271 // don't offer menus when creating shortcut
272 if (makingShortcut) return true;
273
274 // add host, ssh keys, about
275 sortcolor = menu.add(R.string.list_menu_sortcolor);
276 sortcolor.setIcon(android.R.drawable.ic_menu_share);
277 sortcolor.setOnMenuItemClickListener(new OnMenuItemClickListener() {
278 public boolean onMenuItemClick(MenuItem item) {
279 sortedByColor = true;
280 updateList();
281 return true;
282 }
283 });
284 sortlast = menu.add(R.string.list_menu_sortname);
285 sortlast.setIcon(android.R.drawable.ic_menu_share);
286 sortlast.setOnMenuItemClickListener(new OnMenuItemClickListener() {
287 public boolean onMenuItemClick(MenuItem item) {
288 sortedByColor = false;
289 updateList();
290 return true;
291 }
292 });
293 MenuItem keys = menu.add(R.string.list_menu_pubkeys);
294 keys.setIcon(android.R.drawable.ic_lock_lock);
295 keys.setIntent(new Intent(HostListActivity.this, PubkeyListActivity.class));
296 MenuItem colors = menu.add(R.string.title_colors);
297 colors.setIcon(android.R.drawable.ic_menu_slideshow);
298 colors.setIntent(new Intent(HostListActivity.this, ColorsActivity.class));
299 MenuItem settings = menu.add(R.string.list_menu_settings);
300 settings.setIcon(android.R.drawable.ic_menu_preferences);
301 settings.setIntent(new Intent(HostListActivity.this, SettingsActivity.class));
302 MenuItem help = menu.add(R.string.title_help);
303 help.setIcon(android.R.drawable.ic_menu_help);
304 help.setIntent(new Intent(HostListActivity.this, HelpActivity.class));
305 return true;
306 }
307
308
309 @Override
310 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
311 // create menu to handle hosts
312 // create menu to handle deleting and sharing lists
313 AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
314 final HostBean host = (HostBean) this.getListView().getItemAtPosition(info.position);
315 menu.setHeaderTitle(host.getNickname());
316 // edit, disconnect, delete
317 MenuItem connect = menu.add(R.string.list_host_disconnect);
318 final TerminalBridge bridge = bound.getConnectedBridge(host);
319 connect.setEnabled((bridge != null));
320 connect.setOnMenuItemClickListener(new OnMenuItemClickListener() {
321 public boolean onMenuItemClick(MenuItem item) {
322 bridge.dispatchDisconnect(true);
323 updateHandler.sendEmptyMessage(-1);
324 return true;
325 }
326 });
327 MenuItem edit = menu.add(R.string.list_host_edit);
328 edit.setOnMenuItemClickListener(new OnMenuItemClickListener() {
329 public boolean onMenuItemClick(MenuItem item) {
330 Intent intent = new Intent(HostListActivity.this, HostEditorActivity.class);
331 intent.putExtra(Intent.EXTRA_TITLE, host.getId());
332 HostListActivity.this.startActivityForResult(intent, REQUEST_EDIT);
333 return true;
334 }
335 });
336 MenuItem portForwards = menu.add(R.string.list_host_portforwards);
337 portForwards.setOnMenuItemClickListener(new OnMenuItemClickListener() {
338 public boolean onMenuItemClick(MenuItem item) {
339 Intent intent = new Intent(HostListActivity.this, PortForwardListActivity.class);
340 intent.putExtra(Intent.EXTRA_TITLE, host.getId());
341 HostListActivity.this.startActivityForResult(intent, REQUEST_EDIT);
342 return true;
343 }
344 });
345
346 if (!TransportFactory.canForwardPorts(host.getProtocol()))
347 portForwards.setEnabled(false);
348
349 MenuItem delete = menu.add(R.string.list_host_delete);
350 delete.setOnMenuItemClickListener(new OnMenuItemClickListener() {
351 public boolean onMenuItemClick(MenuItem item) {
352 // prompt user to make sure they really want this
353 new AlertDialog.Builder(HostListActivity.this)
354 .setMessage(getString(R.string.delete_message, host.getNickname()))
355 .setPositiveButton(R.string.delete_pos, new DialogInterface.OnClickListener() {
356 public void onClick(DialogInterface dialog, int which) {
357 // make sure we disconnect
358 if (bridge != null)
359 bridge.dispatchDisconnect(true);
360
361 hostdb.deleteHost(host);
362 updateHandler.sendEmptyMessage(-1);
363 }
364 })
365 .setNegativeButton(R.string.delete_neg, null).create().show();
366 return true;
367 }
368 });
369 }
370
371 /**
372 * @param text
373 * @return
374 */
375 private boolean startConsoleActivity() {
376 Uri uri = TransportFactory.getUri((String) transportSpinner
377 .getSelectedItem(), quickconnect.getText().toString());
378
379 if (uri == null) {
380 quickconnect.setError(getString(R.string.list_format_error,
381 TransportFactory.getFormatHint(
382 (String) transportSpinner.getSelectedItem(),
383 HostListActivity.this)));
384 return false;
385 }
386
387 HostBean host = TransportFactory.findHost(hostdb, uri);
388
389 if (host == null) {
390 host = TransportFactory.getTransport(uri.getScheme()).createHost(uri);
391 host.setColor(HostDatabase.COLOR_GRAY);
392 host.setPubkeyId(HostDatabase.PUBKEYID_ANY);
393 hostdb.saveHost(host);
394 }
395
396 Intent intent = new Intent(HostListActivity.this, ConsoleActivity.class);
397 intent.setData(uri);
398 startActivity(intent);
399 return true;
400 }
401
402 private void createDeploymentHosts() {
403 try {
404 String fn = Environment.getExternalStorageDirectory().getAbsolutePath() +
405 File.separator + "deployment.connections";
406 BufferedReader reader = new BufferedReader(new FileReader(fn));
407 String line = null;
408
409 while ((line = reader.readLine()) != null) {
410 if (line.length() == 0) continue; // empty
411
412 if (line.substring(0, 1).equals("#")) continue; // comment
413
414 if (!line.contains("://")) continue; // invalid uri
415
416 Uri uri = Uri.parse(line);
417 ContentValues values = null;
418
419 while ((line = reader.readLine()).length() > 0) {
420 String [] parts = line.split("=");
421
422 if (parts.length != 2) continue;
423
424 if (values == null) values = new ContentValues();
425
426 values.put(parts[0].trim(), parts[1].trim());
427 }
428
429 if (uri.getScheme().equals("global")) {
430 Editor editor = prefs.edit();
431 HashMap<String, String> types = new HashMap<String, String>();
432 types.put("memkeys", "boolean");
433 types.put("connPersist", "boolean");
434 types.put("emulation", "string");
435 types.put("scrollback", "string");
436 types.put("rotation", "string");
437 types.put("shiftfkeys", "boolean");
438 types.put("ctrlfkeys", "boolean");
439 types.put("keymode", "string");
440 types.put("camera", "string");
441 types.put("volup", "string");
442 types.put("voldn", "string");
443 types.put("search", "string");
444 types.put("ptt", "string");
445 types.put("keepalive", "boolean");
446 types.put("wifilock", "boolean");
447 types.put("bumpyarrows", "boolean");
448 types.put("eula", "boolean");
449 types.put("extended_longpress", "boolean");
450 types.put("ctrl_string", "string");
451 types.put("picker_string", "string");
452 types.put("picker_keep_open", "boolean");
453 types.put("list_custom_keymap", "string");
454 types.put("bell", "boolean");
455 types.put("bellVolume", "float");
456 types.put("bellVibrate", "boolean");
457 types.put("bellNotification", "boolean");
458 types.put("screen_capture_folder", "string");
459 types.put("screen_capture_popup", "boolean");
460 types.put("file_dialog", "string");
461 types.put("download_folder", "string");
462 types.put("remote_upload_folder", "string");
463 types.put("upload_dest_prompt", "boolean");
464 types.put("background_file_transfer", "boolean");
465 types.put("debug_keycodes", "boolean");
466
467 for (String key : values.keySet()) {
468 if (types.containsKey(key)) {
469 String t = types.get(key);
470 String sv = values.getAsString(key);
471 if (t.equals("float")) {
472 editor.putFloat(key, Float.parseFloat(sv));
473 }
474 else if (t.equals("boolean")) {
475 editor.putBoolean(key, Boolean.parseBoolean(sv));
476 }
477 else if (t.equals("string")) {
478 editor.putString(key, sv);
479 }
480 }
481 }
482
483 editor.commit();
484 }
485 else {
486 HostBean host = TransportFactory.findHost(hostdb, uri);
487
488 if (host == null) {
489 host = TransportFactory.getTransport(uri.getScheme()).createHost(uri);
490 host.setColor(HostDatabase.COLOR_GRAY);
491 host.setPubkeyId(HostDatabase.PUBKEYID_ANY);
492 hostdb.saveHost(host);
493 }
494
495 host = TransportFactory.findHost(hostdb, uri);
496
497 if (host == null) continue;
498
499 if (values == null) continue;
500
501 SQLiteDatabase db = hostdb.getWritableDatabase();
502 db.update(HostDatabase.TABLE_HOSTS, values, "_id = ?", new String[] { String.valueOf(host.getId()) });
503 db.close();
504 }
505 }
506
507 reader.close();
508 (new File(fn)).delete();
509 }
510 catch (Exception e) {
511 Log.d(TAG, "Deployment scan failed.", e);
512 }
513 }
514
515 protected void updateList() {
516 if (prefs.getBoolean(PreferenceConstants.SORT_BY_COLOR, false) != sortedByColor) {
517 Editor editor = prefs.edit();
518 editor.putBoolean(PreferenceConstants.SORT_BY_COLOR, sortedByColor);
519 editor.commit();
520 }
521
522 if (hostdb == null)
523 hostdb = new HostDatabase(this);
524
525 hosts = hostdb.getHosts(sortedByColor);
526
527 // Don't lose hosts that are connected via shortcuts but not in the database.
528 if (bound != null) {
529 for (TerminalBridge bridge : bound.bridges) {
530 if (!hosts.contains(bridge.host))
531 hosts.add(0, bridge.host);
532 }
533 }
534
535 HostAdapter adapter = new HostAdapter(this, hosts, bound);
536 this.setListAdapter(adapter);
537 }
538
539 class HostAdapter extends ArrayAdapter<HostBean> {
540 private List<HostBean> hosts;
541 private final TerminalManager manager;
542 private final ColorStateList red, green, blue;
543
544 public final static int STATE_UNKNOWN = 1, STATE_CONNECTED = 2, STATE_DISCONNECTED = 3;
545
546 class ViewHolder {
547 public TextView nickname;
548 public TextView caption;
549 public ImageView icon;
550 }
551
552 public HostAdapter(Context context, List<HostBean> hosts, TerminalManager manager) {
553 super(context, R.layout.item_host, hosts);
554 this.hosts = hosts;
555 this.manager = manager;
556 red = context.getResources().getColorStateList(R.color.red);
557 green = context.getResources().getColorStateList(R.color.green);
558 blue = context.getResources().getColorStateList(R.color.blue);
559 }
560
561 /**
562 * Check if we're connected to a terminal with the given host.
563 */
564 private int getConnectedState(HostBean host) {
565 // always disconnected if we dont have backend service
566 if (this.manager == null)
567 return STATE_UNKNOWN;
568
569 if (manager.getConnectedBridge(host) != null)
570 return STATE_CONNECTED;
571
572 if (manager.disconnected.contains(host))
573 return STATE_DISCONNECTED;
574
575 return STATE_UNKNOWN;
576 }
577
578 @Override
579 public View getView(int position, View convertView, ViewGroup parent) {
580 ViewHolder holder;
581
582 if (convertView == null) {
583 convertView = inflater.inflate(R.layout.item_host, null, false);
584 holder = new ViewHolder();
585 holder.nickname = (TextView)convertView.findViewById(android.R.id.text1);
586 holder.caption = (TextView)convertView.findViewById(android.R.id.text2);
587 holder.icon = (ImageView)convertView.findViewById(android.R.id.icon);
588 convertView.setTag(holder);
589 }
590 else
591 holder = (ViewHolder) convertView.getTag();
592
593 HostBean host = hosts.get(position);
594
595 if (host == null) {
596 // Well, something bad happened. We can't continue.
597 Log.e("HostAdapter", "Host bean is null!");
598 holder.nickname.setText("Error during lookup");
599 holder.caption.setText("see 'adb logcat' for more");
600 return convertView;
601 }
602
603 holder.nickname.setText(host.getNickname());
604
605 switch (this.getConnectedState(host)) {
606 case STATE_UNKNOWN:
607 holder.icon.setImageState(new int[] { }, true);
608 break;
609
610 case STATE_CONNECTED:
611 holder.icon.setImageState(new int[] { android.R.attr.state_checked }, true);
612 break;
613
614 case STATE_DISCONNECTED:
615 holder.icon.setImageState(new int[] { android.R.attr.state_expanded }, true);
616 break;
617 }
618
619 ColorStateList chosen = null;
620
621 if (HostDatabase.COLOR_RED.equals(host.getColor()))
622 chosen = this.red;
623 else if (HostDatabase.COLOR_GREEN.equals(host.getColor()))
624 chosen = this.green;
625 else if (HostDatabase.COLOR_BLUE.equals(host.getColor()))
626 chosen = this.blue;
627
628 Context context = convertView.getContext();
629
630 if (chosen != null) {
631 // set color normally if not selected
632 holder.nickname.setTextColor(chosen);
633 holder.caption.setTextColor(chosen);
634 }
635 else {
636 // selected, so revert back to default black text
637 holder.nickname.setTextAppearance(context, android.R.attr.textAppearanceLarge);
638 holder.caption.setTextAppearance(context, android.R.attr.textAppearanceSmall);
639 }
640
641 long now = System.currentTimeMillis() / 1000;
642 String nice = context.getString(R.string.bind_never);
643
644 if (host.getLastConnect() > 0) {
645 int minutes = (int)((now - host.getLastConnect()) / 60);
646
647 if (minutes >= 60) {
648 int hours = (minutes / 60);
649
650 if (hours >= 24) {
651 int days = (hours / 24);
652 nice = context.getString(R.string.bind_days, days);
653 }
654 else
655 nice = context.getString(R.string.bind_hours, hours);
656 }
657 else
658 nice = context.getString(R.string.bind_minutes, minutes);
659 }
660
661 holder.caption.setText(nice);
662 return convertView;
663 }
664 }
665 }