Android client source code

Простой клиент-сервер на Android (интернет-мессенджер)

Важно. Все написанное ниже не представляет собой какой либо ценности для профессионалов, но может служит полезным примером для начинающих Android разработчиков! В коде старался все действия комментировать и логировать.

Поехали. Многие мобильные приложения (и не только) используют архитектуру клиент-сервер. Общая схема, думаю, понятна.

Уделим внимание каждому элементу и отметим:

  • сервер — представляет собой некую программу, работающую на удаленном компьютере, и реализующую функционал «общения» с приложениями-клиентами (слушает запросы, распознает переданные параметры и значения, корректно отвечает на них);
  • клиент — в нашем случае, программа на мобильном устройстве, которая умеет формировать понятный серверу запрос и читать полученный ответ;
  • интерфейс взаимодействия — некий формат и способ передачи/получения запросов/ответов обеими сторонами.

Неважно, как реализован любой из этих элементов, все они в любом случае присутствуют. Давайте реализуем примитивный сервер и Android клиент, работающий с ним. Как пример, будем использовать любой популярный мобильный интернет-мессенджер (Viber, ICQ), а приложение условно назовем «интернет-чат».

Схема взаимодействия следующая:

Клиент, установленный на устройстве А, посылает сообщение для клиента, установленного на устройстве Б. И наоборот. Сервер играет роль связующего звена между устройством А и Б… С, Д… и т.д. Также он играет роль «накопителя» сообщений, для их восстановления, на случай удаления на одном из клиентских устройств.

Для хранения сообщений используем SQL БД как на сервере, так и на устройствах-клиентах (в принципе, вся работа клиентов интернет-мессенджеров и сводится к постоянной синхронизации локальной и удаленной БД с сообщениями). Дополнительно, наш интернет-чат будет уметь стартовать вместе с запуском устройства и работать в фоне. Взаимодействие будет происходить путем HTTP запросов и JSON ответов.

Более логично, если синхронизация происходит через порт/сокет, это с одной стороны упрощает задачу (не нужно циклично слать HTTP запросы на проверку новых сообщений, достаточно проверять состояние прослушиваемого сокета), но с другой стороны, это усложняет создание серверной части приложения.

Делаем сервер

Для реализации «сервера», нам нужно зарегистрироваться на любом хостинге, который дает возможность работы с SQL и PHP.

Создаем пустую SQL БД, в ней создаем таблицу.

  1. author — автор сообщения;
  2. client — получатель сообщения;
  3. data — время и дата получения сообщения на сервере;
  4. text — сообщение.

В двух следующих файлах необходимо изменить переменные, содержащие данные для доступа к БД, на свои, полученные Вами при регистрации Вашего«сервера».

Структура запросов к api:

  • обязательный атрибут action — может быть равен select (сервер ответит списком записей из своей БД), insert (сервер добавить новую запись в свою БД), delete (сервер очистит свою БД)
  • если action=insert, нам нужно будет передать дополнительные параметры: author (кто написал сообщение), client (кому адресовано сообщение), text (сообщение)
  • action=select может содержать дополнительный параметр data, в этом случае ответ сервера содержит не все сообщения из БД, а только те, у которых время создания позднее переданного

Примеры:

  • chat.php?action=delete – удалит все записи на сервере
  • chat.php?action=insert&author=Jon&client=Smith&text=Hello — добавит на сервере новую запись: автор Jon, получатель Smith, содержание Hello
  • chat.php?action=select&data=151351333 — вернет все записи, полученные после переданного времени в long формате

Клиентская часть

Теперь структура Android приложения:

В фоне работает FoneService.java, который, в отдельном потоке, каждые 15 секунд делает запрос на сервер. Если ответ сервера содержит новые сообщения, FoneService.java записывает их в локальную БД и отправляет сообщение ChatActivity.java о необходимости обновить ListView, с сообщениями. ChatActivity.java (если она в этот момент открыта) получает сообщение и обновляет содержимое ListView из локальной БД.

Отправка нового сообщения из ChatActivity.java происходит сразу на сервер, минуя FoneService.java. При этом наше сообщение НЕ записывается в локальную БД! Там оно появится только после получения его назад в виде ответа сервера. Такую реализацию я использовал в связи с важным нюансом работы любого интернет-чата — обязательной группировкой сообщений по времени. Если не использовать группировку по времени, будет нарушена последовательность сообщений. Учитывая, что клиентские приложения просто физически не могут быть синхронизированы с точностью до миллисекунд, а возможно будут работать даже в разных часовых поясах, логичнее всего будет использовать время сервера. Так мы и делаем.

Читайте также:  Авиа навигация для андроид

Создавая новое сообщение, мы передаем запросом на сервер: имя автора сообщения, имя получателя сообщения, текст сообщения. Получая эту запись назад, в виде ответа сервера, мы получаем то, что отправляли + четвертый параметр: время получения сообщения сервером.

Источник

Пишем клиент для Хабра под Android

Забегая вперед, вот что получилось:

12:56. Я буду делать это параллельно с написанием топика (так интересней). По ходу написания клиента поясняя все шаги. Итак, покурили, налили чай, подготовили плейлист и, пока чай остывает — проверяем не занято ли имя habrahabr в маркете. Отлично, переходим к созданию приложения.

13:02 Создаем новый проект.
Скриншот

API level равен 4, по той причине, что при меньшем значении — на планшетниках Samsung Galaxy Tab разрешение экрана будет некорректным и обладатели данных чудо-девайсов не преминут насовать вам кучу минусов в маркет (хотя в принципе, врятли это косяк разработчика).

13:08 Фиксим манифест.
Необходимо добавить две строчки:
— android:configChanges=«orientation», данная строка нужна для того, чтобы при смене ориентации экрана не разрушалось наше активити.
— , запрашиваем разрешение на доступ в интернет
AndroidManifest
* играет front242 — headhunter v3.0

13:13 Фиксим layout.
Стираем всё и добавляем один единственный элемент — webview — на весь экран

13:16 Иконка.
При помощи фаербага подрезаем абсолютную ссылку и при помощи фотошопа обрезаем до 48*48 px и кидаем в res/drawable…

13:27 С иконкой всё сложнее оказалось. Пятно логотипа после уменьшения превратилось в мутную хрень, пришлось нагуглить. Надеюсь автор не обидится.

Уф, самое сложное закончили, наконец то можно покодить.

13:39 Загружаем хабр

public class habr extends Activity <

private WebView wv;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) <
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

wv = (WebView) findViewById(R.id.wv);

WebSettings webSettings = wv.getSettings();
webSettings.setSavePassword( true );
webSettings.setSaveFormData( true );
webSettings.setJavaScriptEnabled( true );

* This source code was highlighted with Source Code Highlighter .

* Здесь мы просто натравили наше вью на habr, предварительно включив джаваскрипт и запоминалку форм/паролей. Выглядит пока уродливо, но уже работает. Перекур.

13:53 Продолжаем разговор.

public class habr extends Activity <

private WebView wv;
private String LASTURL = «» ;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) <
super.onCreate(savedInstanceState);
this .getWindow().requestFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.main);

wv = (WebView) findViewById(R.id.wv);

WebSettings webSettings = wv.getSettings();
webSettings.setSavePassword( true );
webSettings.setSaveFormData( true );
webSettings.setJavaScriptEnabled( true );

final Activity activity = this ;

wv.setWebChromeClient( new WebChromeClient() <
public void onProgressChanged(WebView view, int progress)
<
activity.setTitle( » » +LASTURL);
activity.setProgress(progress * 100);

if (progress == 100)
activity.setTitle( » » +LASTURL);
>
>);
wv.setWebViewClient( new WebViewClient() <
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) <
Toast.makeText(getApplicationContext(), «Error: » + description+ » » + failingUrl, Toast.LENGTH_LONG).show();
>

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url)
<
if (url.indexOf( «habrahabr» ) // the link is not for a page on my site, so launch another Activity that handles URLs
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
return true ;
>
return false ;
>

public void onPageStarted (WebView view, String url, Bitmap favicon) <
LASTURL = url;
>

public void onPageFinished (WebView view, String url) <

* This source code was highlighted with Source Code Highlighter .

Итак, нам нужен градусник загрузки.
1. Запрашиваем фичу: this.getWindow().requestFeature(Window.FEATURE_PROGRESS);
2. На прогрессчейндж — заполняем окно градусника
activity.setTitle(» «+LASTURL);
activity.setProgress(progress * 100);
if(progress == 100) activity.setTitle(» «+LASTURL);
3. На начало загрузки запоминаем url в переменной LASTURL = url;

Обрабатываем отвалившийся вайфай на онресивед еррор:
Toast.makeText(getApplicationContext(), «Error: » + description+ » » + failingUrl, Toast.LENGTH_LONG).show();
(всплывает сообщение для совсем уж дебилов, хотя итак всё будет на странице написано )

14:05 Фиксим вёрстку.
Немного теории. На загруженной странице можно выполнить джаваскрипт. Если например набрать в адресной строке браузера что то вроде javasсript:alert(document.body.innerHTML) — то мы увидим тело страницы (копипастерам — в примере выше — буква «с» — русская, чтобы парсер пропустил).
Ну а дальше — дерево ДОМ и т.д., твори что хочешь, хоть полностью страницу переделывай. Однако мы не зайдем так далеко (я надеюсь) и просто скроем сайдбар, для улучшения читабельности. А браузер уже сам растянет полезный контент по странице. Итак, пробуем добавить обработчик на окончание загрузки страницы:

Читайте также:  What is facebook com facebook katana android

* This source code was highlighted with Source Code Highlighter .

/* Отвлекли по работе */
Такс, сайдбар скрывается, но с неким скачком. Фиксим загрузку изображений:

14:34 Ускоряем загрузку
Для этого отключим картинки при старте страницы:
view.getSettings().setLoadsImagesAutomatically(false);
и включим на финише, после хака с джаваскриптом:
view.getSettings().setLoadsImagesAutomatically(true);

Такс, грузить контент стал ощутимо быстрее (на моей полуживой Йоте, в эмуляторе, по крайней мере)

15:23 Продолжаем разговор
Пока ходил на обед — заглянул в зону бесплатного вайфая, заодно и потестил. С сожалением узрел панель поиска, нелепо висящую в пустом правом углу. Попробуем с ней чтоть сделать.
Для начала тупо скроем.

* музыка: submatakana — the krypt (это что то с чем то)

* This source code was highlighted with Source Code Highlighter .

Мда, ломать не строить. Такс, попробуем «приаппендить её к списку блогов.

«var parent = document.getElementsByClassName(‘page-navigation’)[0];» +
«var panel = document.getElementsByClassName(‘panel-tools’)[0];» +
«var div = document.createElement(‘div’);» +
«div.innerHTML = panel.innerHTML;» +
«parent.appendChild(div);» +

* This source code was highlighted with Source Code Highlighter .

опс, теперь у нас две панели)

* This source code was highlighted with Source Code Highlighter .

15:57 Опс, одна кавычечка не там и весь скрипт рушится как карточный домик.
Надо так: div.style[‘margin-left’] = ’30px’;
Так попробую ещё вырубить рекламные блоки в меню, но нет так нет, что то я долго вожусь с грешной вёрсткой (ненавижу).

16:04 Так как данные элементы не поименованы — попробовал так:

«var urls=document.getElementsByTagName(‘a’);for(var i=0;i +

* This source code was highlighted with Source Code Highlighter .

16:16 Попробуем ударить по площадям:

«var imgs=document.getElementsByTagName(‘IMG’);for(var i=0;i +

* This source code was highlighted with Source Code Highlighter .

Картинки скрылись, но пустое место всё равно торчит( Ладно, пусть это останется на домашнее задание желающим. Пусть пока живут на радость рекламодателям.
Перекур.

16:45 Поиск, пожалуй вернем назад, немного ужав по ширине:

«var panel = document.getElementById(‘search’);» +
«panel.style[‘width’] = ’55px’;» +

* This source code was highlighted with Source Code Highlighter .

Ну и займемся наконец андроидом.
16:47 Перекрываем аппаратную кнопку назад.

  1. @Override
  2. public boolean onKeyDown( int keyCode, KeyEvent event ) <
  3. if ((keyCode == KeyEvent.KEYCODE_BACK) && wv.canGoBack()) <
  4. wv.goBack();
  5. return true ;
  6. >
  7. return super.onKeyDown(keyCode, event );
  8. >

* This source code was highlighted with Source Code Highlighter .

16:57 Создаем меню

  1. @Override
  2. public boolean onCreateOptionsMenu(Menu menu)
  3. <
  4. super.onCreateOptionsMenu(menu);
  5. this .myMenu = menu;
  6. MenuItem item = menu.add(0, 1, 0, «MAIN PAGE» );
  7. item.setIcon(R.drawable.home);
  8. MenuItem item2 = menu.add(0, 2, 0, «BACK» );
  9. item2.setIcon(R.drawable.arrowleft);
  10. MenuItem item3 = menu.add(0, 3, 0, «F5» );
  11. item3.setIcon(R.drawable.s);
  12. MenuItem item4 = menu.add(0, 4, 0, «CLEAR CACHE» );
  13. item4.setIcon(R.drawable.trash);
  14. MenuItem item5 = menu.add(0, 5, 0, «VOID» );
  15. item5.setIcon(R.drawable.vote);
  16. return true ;
  17. >
  18. @Override
  19. public boolean onOptionsItemSelected(MenuItem item) <
  20. switch (item.getItemId())
  21. <
  22. case 1:
  23. wv.loadUrl( «http://habrahabr.ru» );
  24. break ;
  25. case 2:
  26. if (wv.canGoBack()) <
  27. wv.goBack();
  28. >
  29. break ;
  30. case 3:
  31. wv.loadUrl(LASTURL);
  32. break ;
  33. case 4:
  34. wv.clearCache( true );
  35. break ;
  36. case 5:
  37. Intent marketIntent2 = new Intent(Intent.ACTION_VIEW, Uri.parse(
  38. «http://market.android.com/details?id=» + getPackageName()));
  39. startActivity(marketIntent2);
  40. break ;
  41. >
  42. return true ;
  43. >

* This source code was highlighted with Source Code Highlighter .

Здесь пояснять особо даже и ничего вроде…
Вебвью хранит данные в локальном изолированном кеше и функция clearCache — удаляет закешированные картики и т.п.

В маркет пользователя отправляем при помощи интента + старт активити, это стандартный механизм взаимодействия с внешними приложениями.

17:01 Такс, пожалуй, стоит сделать режим с картиками/без картинок
Фиксим меню:
menu.add(0, 6, 0, „IMG ON“);
menu.add(0, 7, 0, „IMG OFF“);

17:05 Мутим функции сохранения настроек

  1. private void saveSettings(Boolean val)
  2. <
  3. SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
  4. SharedPreferences.Editor editor = settings.edit();
  5. editor.putBoolean( «IMGMODE» , val);
  6. editor.commit();
  7. >

* This source code was highlighted with Source Code Highlighter .

В неё будем передавать настройки (грузить или нет картинки), а она пусть запихивает в переменную переданное значение.
(выше объявили константу PREFS_NAME — это как бы имя конфига)

Теперь просто вызываем её в обработчике меню:

  1. case 6:
  2. saveSettings( true );
  3. break ;
  4. case 7:
  5. saveSettings( false );
  6. break ;

* This source code was highlighted with Source Code Highlighter .

17:18 И читаем константу при создании приложения из нашего конфига

  1. SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
  2. imgOn = settings.getBoolean( «IMGMODE» , false );
  3. webSettings.setLoadsImagesAutomatically(imgOn);

* This source code was highlighted with Source Code Highlighter .

17:19 Тестируем ещё раз

Читайте также:  Vanced manager для android tv

17:25 Вроде косяков нет. Экспортируем проект.
В эклипсе это правая кнопка на проекте экспорт и запускается мастер, позволяющий создать/выбрать сертификат и упаковывающий проект. Далее топаем в маркет

17:38 Публикуем.
На самом деле не самая простая задача. Где то надо добыть кучу промографики определенного размера, поэтому не дизайнерам тут тяжко.

По параметрам.
— Чтобы установить основным языком приложения русский — надо сперва добавить русский, и только после этого появится возможность удалить английский.
— В полях дескрипшн и промотекст желательно упомянуть ключевые слова, по которым могут искать приложение.
— Если устновить цену free — потом сделать его платным — невозможно
— Не ставьте галку copy protection, приложение не будет ставиться на часть девайсов
— Лучше укажите все страны, даже если приложение только для русскоязычных, например.

Мда, с иконкой не очень красиво получилось( Почему то меня гложет эта мысль. Так что если кто-ть может изобразить чтоть 48*48 — буду премного благодарен.

Полностью, итоговый исходник:

  1. package ru.habrahabr.android;
  2. import android.app.Activity;
  3. import android.content.Intent;
  4. import android.content.SharedPreferences;
  5. import android.graphics.Bitmap;
  6. import android.net.Uri;
  7. import android.os.Bundle;
  8. import android.view.KeyEvent;
  9. import android.view.Menu;
  10. import android.view.MenuItem;
  11. import android.view.Window;
  12. import android.webkit.WebChromeClient;
  13. import android.webkit.WebSettings;
  14. import android.webkit.WebView;
  15. import android.webkit.WebViewClient;
  16. import android.widget.Toast;
  17. public class habr extends Activity <
  18. private WebView wv;
  19. private String LASTURL = «» ;
  20. Menu myMenu = null ;
  21. private static final String PREFS_NAME = «MyPrefs» ;
  22. private Boolean imgOn;
  23. /** Called when the activity is first created. */
  24. @Override
  25. public void onCreate(Bundle savedInstanceState) <
  26. super.onCreate(savedInstanceState);
  27. this .getWindow().requestFeature(Window.FEATURE_PROGRESS);
  28. setContentView(R.layout.main);
  29. wv = (WebView) findViewById(R.id.wv);
  30. WebSettings webSettings = wv.getSettings();
  31. webSettings.setSavePassword( true );
  32. webSettings.setSaveFormData( true );
  33. webSettings.setJavaScriptEnabled( true );
  34. SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
  35. imgOn = settings.getBoolean( «IMGMODE» , false );
  36. webSettings.setLoadsImagesAutomatically(imgOn);
  37. final Activity activity = this ;
  38. wv.setWebChromeClient( new WebChromeClient() <
  39. public void onProgressChanged(WebView view, int progress)
  40. <
  41. activity.setTitle( » » +LASTURL);
  42. activity.setProgress(progress * 100);
  43. if (progress == 100)
  44. activity.setTitle( » » +LASTURL);
  45. >
  46. >);
  47. wv.setWebViewClient( new WebViewClient() <
  48. public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) <
  49. Toast.makeText(getApplicationContext(), «Error: » + description+ » » + failingUrl, Toast.LENGTH_LONG).show();
  50. >
  51. @Override
  52. public boolean shouldOverrideUrlLoading(WebView view, String url)
  53. <
  54. if (url.indexOf( «habrahabr» ) // the link is not for a page on my site, so launch another Activity that handles URLs
  55. Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
  56. startActivity(intent);
  57. return true ;
  58. >
  59. return false ;
  60. >
  61. public void onPageStarted (WebView view, String url, Bitmap favicon) <
  62. LASTURL = url;
  63. view.getSettings().setLoadsImagesAutomatically( false );
  64. >
  65. public void onPageFinished (WebView view, String url) <
  66. view.loadUrl( «javascript:(function() < " +
  67. «hide(‘sidebar’);» +
  68. //»var parent = document.getElementsByClassName(‘page-navigation’)[0];»+
  69. //»var panel = document.getElementsByClassName(‘panel-tools’)[0];»+
  70. //»var div = document.createElement(‘div’);»+
  71. //»div.innerHTML = panel.innerHTML;»+
  72. //»parent.appendChild(div);»+
  73. //»panel.innerHTML = »;»+
  74. //»div.style[‘margin-left’] = ’31px’;»+
  75. «var panel = document.getElementById(‘search’);» +
  76. «panel.style[‘width’] = ’55px’;» +
  77. //»var imgs=document.getElementsByTagName(‘IMG’);for(var i=0;i
  78. //»var urls=document.getElementsByTagName(‘li’);for(var i=0;i
  79. //»hideByClass(‘panel-tools’);»+
  80. «function hide(id)>» +
  81. //»function hideByClass(c)
  82. «>)()» );
  83. if (imgOn) view.getSettings().setLoadsImagesAutomatically( true );
  84. >
  85. >);
  86. wv.loadUrl( «http://habrahabr.ru» );
  87. >
  88. @Override
  89. public boolean onKeyDown( int keyCode, KeyEvent event ) <
  90. if ((keyCode == KeyEvent.KEYCODE_BACK) && wv.canGoBack()) <
  91. wv.goBack();
  92. return true ;
  93. >
  94. return super.onKeyDown(keyCode, event );
  95. >
  96. @Override
  97. public boolean onCreateOptionsMenu(Menu menu)
  98. <
  99. super.onCreateOptionsMenu(menu);
  100. this .myMenu = menu;
  101. MenuItem item = menu.add(0, 1, 0, «MAIN PAGE» );
  102. item.setIcon(R.drawable.home);
  103. MenuItem item2 = menu.add(0, 2, 0, «BACK» );
  104. item2.setIcon(R.drawable.arrowleft);
  105. MenuItem item3 = menu.add(0, 3, 0, «F5» );
  106. item3.setIcon(R.drawable.s);
  107. MenuItem item4 = menu.add(0, 4, 0, «CLEAR CACHE» );
  108. item4.setIcon(R.drawable.trash);
  109. MenuItem item5 = menu.add(0, 5, 0, «VOID» );
  110. item5.setIcon(R.drawable.vote);
  111. menu.add(0, 6, 0, «IMG ON» );
  112. menu.add(0, 7, 0, «IMG OFF» );
  113. return true ;
  114. >
  115. @Override
  116. public boolean onOptionsItemSelected(MenuItem item) <
  117. switch (item.getItemId())
  118. <
  119. case 1:
  120. wv.loadUrl( «http://habrahabr.ru» );
  121. break ;
  122. case 2:
  123. if (wv.canGoBack()) <
  124. wv.goBack();
  125. >
  126. break ;
  127. case 3:
  128. wv.loadUrl(LASTURL);
  129. break ;
  130. case 4:
  131. wv.clearCache( true );
  132. break ;
  133. case 5:
  134. Intent marketIntent2 = new Intent(Intent.ACTION_VIEW, Uri.parse(
  135. «http://market.android.com/details?id=» + getPackageName()));
  136. startActivity(marketIntent2);
  137. break ;
  138. case 6:
  139. saveSettings( true );
  140. break ;
  141. case 7:
  142. saveSettings( false );
  143. break ;
  144. >
  145. return true ;
  146. >
  147. private void saveSettings(Boolean val)
  148. <
  149. SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
  150. SharedPreferences.Editor editor = settings.edit();
  151. editor.putBoolean( «IMGMODE» , val);
  152. editor.commit();
  153. >
  154. >

* This source code was highlighted with Source Code Highlighter .

UPD: Очень сильно переделал вёрстку.
За основу взял стиль разработанный almalexa.habrahabr.ru и существенно доработал его напильником под маленькое разрешение.
Получившийся стиль: userstyles.org/styles/46932/habr
На этом считаю разработку законченной. Клиент в маркете — обновлён.
Итого: на всё про всё ушли сутки.

Источник

Оцените статью